From a7fbecb25c03c2584acfd85d56504db253106681 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 26 Jan 2026 17:28:07 -1000 Subject: [PATCH 1/9] [ci] Soft-deprecate str_sprintf/str_snprintf to prevent hidden heap allocations (#13227) --- esphome/core/helpers.h | 2 ++ script/ci-custom.py | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 1aa29fa3f7..81397668e8 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -655,9 +655,11 @@ inline uint32_t fnv1_hash_object_id(const char *str, size_t len) { } /// 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, ...); #ifdef USE_ESP8266 diff --git a/script/ci-custom.py b/script/ci-custom.py index 01e197057a..b146c9867e 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -692,6 +692,8 @@ HEAP_ALLOCATING_HELPERS = { "str_truncate": "removal (function is unused)", "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", } @@ -710,7 +712,9 @@ HEAP_ALLOCATING_HELPERS = { r"str_sanitize(?!_)|" r"str_truncate|" r"str_upper_case|" - r"str_snake_case" + r"str_snake_case|" + r"str_sprintf|" + r"str_snprintf" r")\s*\(" + CPP_RE_EOL, include=cpp_include, exclude=[ From 5cbe9af48504f7bc84772eb5c1a32d2310c63dde Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 26 Jan 2026 17:32:03 -1000 Subject: [PATCH 2/9] [rp2040] Use SmallBufferWithHeapFallback for preferences (#13501) --- esphome/components/rp2040/preferences.cpp | 29 +++++++++++++---------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/esphome/components/rp2040/preferences.cpp b/esphome/components/rp2040/preferences.cpp index cbf2b00641..e84033bc52 100644 --- a/esphome/components/rp2040/preferences.cpp +++ b/esphome/components/rp2040/preferences.cpp @@ -8,7 +8,6 @@ #include "preferences.h" #include -#include #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -25,6 +24,9 @@ static bool s_flash_dirty = false; // NOLINT(cppcoreguidelines-avoid-no static const uint32_t RP2040_FLASH_STORAGE_SIZE = 512; +// Stack buffer size for preferences - covers virtually all real-world preferences without heap allocation +static constexpr size_t PREF_BUFFER_SIZE = 64; + extern "C" uint8_t _EEPROM_start; template uint8_t calculate_crc(It first, It last, uint32_t type) { @@ -42,12 +44,14 @@ class RP2040PreferenceBackend : public ESPPreferenceBackend { uint32_t type = 0; bool save(const uint8_t *data, size_t len) override { - std::vector buffer; - buffer.resize(len + 1); - memcpy(buffer.data(), data, len); - buffer[buffer.size() - 1] = calculate_crc(buffer.begin(), buffer.end() - 1, type); + const size_t buffer_size = len + 1; + SmallBufferWithHeapFallback buffer_alloc(buffer_size); + uint8_t *buffer = buffer_alloc.get(); - for (uint32_t i = 0; i < len + 1; i++) { + memcpy(buffer, data, len); + buffer[len] = calculate_crc(buffer, buffer + len, type); + + for (size_t i = 0; i < buffer_size; i++) { uint32_t j = offset + i; if (j >= RP2040_FLASH_STORAGE_SIZE) return false; @@ -60,22 +64,23 @@ class RP2040PreferenceBackend : public ESPPreferenceBackend { return true; } bool load(uint8_t *data, size_t len) override { - std::vector buffer; - buffer.resize(len + 1); + const size_t buffer_size = len + 1; + SmallBufferWithHeapFallback buffer_alloc(buffer_size); + uint8_t *buffer = buffer_alloc.get(); - for (size_t i = 0; i < len + 1; i++) { + for (size_t i = 0; i < buffer_size; i++) { uint32_t j = offset + i; if (j >= RP2040_FLASH_STORAGE_SIZE) return false; buffer[i] = s_flash_storage[j]; } - uint8_t crc = calculate_crc(buffer.begin(), buffer.end() - 1, type); - if (buffer[buffer.size() - 1] != crc) { + uint8_t crc = calculate_crc(buffer, buffer + len, type); + if (buffer[len] != crc) { return false; } - memcpy(data, buffer.data(), len); + memcpy(data, buffer, len); return true; } }; From f91bffff9aa50f7ad6e8a56ce02fcefc7687c07b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 26 Jan 2026 17:32:58 -1000 Subject: [PATCH 3/9] [wifi] Avoid heap allocation when building AP SSID (#13474) --- esphome/components/wifi/wifi_component.cpp | 26 +++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 52d9b2b442..ec9978da79 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -746,16 +746,32 @@ void WiFiComponent::setup_ap_config_() { return; if (this->ap_.get_ssid().empty()) { - std::string name = App.get_name(); - if (name.length() > 32) { + // Build AP SSID from app name without heap allocation + // WiFi SSID max is 32 bytes, with MAC suffix we keep first 25 + last 7 + static constexpr size_t AP_SSID_MAX_LEN = 32; + 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 char *name_ptr = app_name.c_str(); + size_t name_len = app_name.length(); + + if (name_len <= AP_SSID_MAX_LEN) { + // Name fits, use directly + this->ap_.set_ssid(name_ptr); + } else { + // Name too long, need to truncate into stack buffer + char ssid_buf[AP_SSID_MAX_LEN + 1]; if (App.is_name_add_mac_suffix_enabled()) { // Keep first 25 chars and last 7 chars (MAC suffix), remove middle - name.erase(25, name.length() - 32); + memcpy(ssid_buf, name_ptr, AP_SSID_PREFIX_LEN); + memcpy(ssid_buf + AP_SSID_PREFIX_LEN, name_ptr + name_len - AP_SSID_SUFFIX_LEN, AP_SSID_SUFFIX_LEN); } else { - name.resize(32); + memcpy(ssid_buf, name_ptr, AP_SSID_MAX_LEN); } + ssid_buf[AP_SSID_MAX_LEN] = '\0'; + this->ap_.set_ssid(ssid_buf); } - this->ap_.set_ssid(name); } this->ap_setup_ = this->wifi_start_ap_(this->ap_); From cd6314dc96d9d13c66ac7935144c9cf089badf1d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 26 Jan 2026 17:34:55 -1000 Subject: [PATCH 4/9] [socket] ESP8266: call delay(0) instead of esp_delay(0, cb) for zero timeout (#13530) --- esphome/components/socket/lwip_raw_tcp_impl.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index 429f59ceca..a9c2eda4e8 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -29,6 +29,14 @@ 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; }); } From 75a78b2bf3eaa4c8790194beab88b75b59d543e4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 26 Jan 2026 17:35:45 -1000 Subject: [PATCH 5/9] [core] Encapsulate entity preference creation to prepare for hash migration (#13505) --- .../bl0940/number/calibration_number.cpp | 2 +- esphome/components/climate/climate.cpp | 3 +- esphome/components/cover/cover.cpp | 2 +- .../components/duty_time/duty_time_sensor.cpp | 2 +- esphome/components/fan/fan.cpp | 3 +- esphome/components/haier/haier_base.cpp | 3 +- esphome/components/haier/hon_climate.cpp | 3 +- .../integration/integration_sensor.cpp | 2 +- esphome/components/ld2450/ld2450.cpp | 2 +- esphome/components/light/light_state.cpp | 4 +- esphome/components/lvgl/number/lvgl_number.h | 2 +- esphome/components/lvgl/select/lvgl_select.h | 2 +- esphome/components/number/automation.cpp | 3 +- .../opentherm/number/opentherm_number.cpp | 2 +- .../rotary_encoder/rotary_encoder.cpp | 2 +- esphome/components/sensor/automation.h | 2 +- .../media_player/speaker_media_player.cpp | 2 +- esphome/components/sprinkler/sprinkler.cpp | 2 +- esphome/components/switch/switch.cpp | 2 +- .../template_alarm_control_panel.cpp | 2 +- .../template/datetime/template_date.cpp | 3 +- .../template/datetime/template_datetime.cpp | 3 +- .../template/datetime/template_time.cpp | 3 +- .../template/number/template_number.cpp | 2 +- .../template/select/template_select.cpp | 2 +- .../template/text/template_text.cpp | 7 ++++ .../total_daily_energy/total_daily_energy.cpp | 2 +- .../components/tuya/number/tuya_number.cpp | 2 +- esphome/components/valve/valve.cpp | 2 +- .../components/water_heater/water_heater.cpp | 2 +- esphome/core/entity_base.cpp | 42 +++++++++++++++++++ esphome/core/entity_base.h | 18 ++++++++ 32 files changed, 97 insertions(+), 38 deletions(-) diff --git a/esphome/components/bl0940/number/calibration_number.cpp b/esphome/components/bl0940/number/calibration_number.cpp index e83c3add1f..5e775004bd 100644 --- a/esphome/components/bl0940/number/calibration_number.cpp +++ b/esphome/components/bl0940/number/calibration_number.cpp @@ -9,7 +9,7 @@ static const char *const TAG = "bl0940.number"; void CalibrationNumber::setup() { float value = 0.0f; if (this->restore_value_) { - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + this->pref_ = this->make_entity_preference(); if (!this->pref_.load(&value)) { value = 0.0f; } diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 816bd5dfcb..ba6de4ff61 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -360,8 +360,7 @@ void Climate::add_on_control_callback(std::function &&callb static const uint32_t RESTORE_STATE_VERSION = 0x848EA6ADUL; optional Climate::restore_state_() { - this->rtc_ = global_preferences->make_preference(this->get_preference_hash() ^ - RESTORE_STATE_VERSION); + this->rtc_ = this->make_entity_preference(RESTORE_STATE_VERSION); ClimateDeviceRestoreState recovered{}; if (!this->rtc_.load(&recovered)) return {}; diff --git a/esphome/components/cover/cover.cpp b/esphome/components/cover/cover.cpp index 68688794d7..0d9e7e8ffb 100644 --- a/esphome/components/cover/cover.cpp +++ b/esphome/components/cover/cover.cpp @@ -187,7 +187,7 @@ void Cover::publish_state(bool save) { } } optional Cover::restore_state_() { - this->rtc_ = global_preferences->make_preference(this->get_preference_hash()); + this->rtc_ = this->make_entity_preference(); CoverRestoreState recovered{}; if (!this->rtc_.load(&recovered)) return {}; diff --git a/esphome/components/duty_time/duty_time_sensor.cpp b/esphome/components/duty_time/duty_time_sensor.cpp index f77f1fcf53..561040623d 100644 --- a/esphome/components/duty_time/duty_time_sensor.cpp +++ b/esphome/components/duty_time/duty_time_sensor.cpp @@ -41,7 +41,7 @@ void DutyTimeSensor::setup() { uint32_t seconds = 0; if (this->restore_) { - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + this->pref_ = this->make_entity_preference(); this->pref_.load(&seconds); } diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index 02fde730eb..a983babe1c 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -227,8 +227,7 @@ void Fan::publish_state() { constexpr uint32_t RESTORE_STATE_VERSION = 0x71700ABA; optional Fan::restore_state_() { FanRestoreState recovered{}; - this->rtc_ = - global_preferences->make_preference(this->get_preference_hash() ^ RESTORE_STATE_VERSION); + this->rtc_ = this->make_entity_preference(RESTORE_STATE_VERSION); bool restored = this->rtc_.load(&recovered); switch (this->restore_mode_) { diff --git a/esphome/components/haier/haier_base.cpp b/esphome/components/haier/haier_base.cpp index cd2673a272..1882aa439e 100644 --- a/esphome/components/haier/haier_base.cpp +++ b/esphome/components/haier/haier_base.cpp @@ -350,8 +350,7 @@ ClimateTraits HaierClimateBase::traits() { return traits_; } void HaierClimateBase::initialization() { constexpr uint32_t restore_settings_version = 0xA77D21EF; - this->base_rtc_ = - global_preferences->make_preference(this->get_preference_hash() ^ restore_settings_version); + this->base_rtc_ = this->make_entity_preference(restore_settings_version); HaierBaseSettings recovered; if (!this->base_rtc_.load(&recovered)) { recovered = {false, true}; diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index 23d28bfd47..d98d273957 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -515,8 +515,7 @@ haier_protocol::HaierMessage HonClimate::get_power_message(bool state) { void HonClimate::initialization() { HaierClimateBase::initialization(); constexpr uint32_t restore_settings_version = 0x57EB59DDUL; - this->hon_rtc_ = - global_preferences->make_preference(this->get_preference_hash() ^ restore_settings_version); + this->hon_rtc_ = this->make_entity_preference(restore_settings_version); HonSettings recovered; if (this->hon_rtc_.load(&recovered)) { this->settings_ = recovered; diff --git a/esphome/components/integration/integration_sensor.cpp b/esphome/components/integration/integration_sensor.cpp index 80c718dc8d..b084801d3b 100644 --- a/esphome/components/integration/integration_sensor.cpp +++ b/esphome/components/integration/integration_sensor.cpp @@ -10,7 +10,7 @@ static const char *const TAG = "integration"; void IntegrationSensor::setup() { if (this->restore_) { - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + this->pref_ = this->make_entity_preference(); float preference_value = 0; this->pref_.load(&preference_value); this->result_ = preference_value; diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 58d469b2a7..07809023cd 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -184,7 +184,7 @@ static inline bool validate_header_footer(const uint8_t *header_footer, const ui void LD2450Component::setup() { #ifdef USE_NUMBER if (this->presence_timeout_number_ != nullptr) { - this->pref_ = global_preferences->make_preference(this->presence_timeout_number_->get_preference_hash()); + this->pref_ = this->presence_timeout_number_->make_entity_preference(); this->set_presence_timeout(); } #endif diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 91bb2e2f1f..ed86bf58da 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -44,7 +44,7 @@ void LightState::setup() { case LIGHT_RESTORE_DEFAULT_ON: case LIGHT_RESTORE_INVERTED_DEFAULT_OFF: case LIGHT_RESTORE_INVERTED_DEFAULT_ON: - this->rtc_ = global_preferences->make_preference(this->get_preference_hash()); + this->rtc_ = this->make_entity_preference(); // Attempt to load from preferences, else fall back to default values if (!this->rtc_.load(&recovered)) { recovered.state = (this->restore_mode_ == LIGHT_RESTORE_DEFAULT_ON || @@ -57,7 +57,7 @@ void LightState::setup() { break; case LIGHT_RESTORE_AND_OFF: case LIGHT_RESTORE_AND_ON: - this->rtc_ = global_preferences->make_preference(this->get_preference_hash()); + this->rtc_ = this->make_entity_preference(); this->rtc_.load(&recovered); recovered.state = (this->restore_mode_ == LIGHT_RESTORE_AND_ON); break; diff --git a/esphome/components/lvgl/number/lvgl_number.h b/esphome/components/lvgl/number/lvgl_number.h index d9885bc7fb..44409a0ad5 100644 --- a/esphome/components/lvgl/number/lvgl_number.h +++ b/esphome/components/lvgl/number/lvgl_number.h @@ -21,7 +21,7 @@ class LVGLNumber : public number::Number, public Component { void setup() override { float value = this->value_lambda_(); if (this->restore_) { - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + this->pref_ = this->make_entity_preference(); if (this->pref_.load(&value)) { this->control_lambda_(value); } diff --git a/esphome/components/lvgl/select/lvgl_select.h b/esphome/components/lvgl/select/lvgl_select.h index 70bb3e7bcb..ba03920a88 100644 --- a/esphome/components/lvgl/select/lvgl_select.h +++ b/esphome/components/lvgl/select/lvgl_select.h @@ -20,7 +20,7 @@ class LVGLSelect : public select::Select, public Component { this->set_options_(); if (this->restore_) { size_t index; - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + this->pref_ = this->make_entity_preference(); if (this->pref_.load(&index)) this->widget_->set_selected_index(index, LV_ANIM_OFF); } diff --git a/esphome/components/number/automation.cpp b/esphome/components/number/automation.cpp index 78ffc255fe..a3d49a6ff6 100644 --- a/esphome/components/number/automation.cpp +++ b/esphome/components/number/automation.cpp @@ -14,8 +14,7 @@ void ValueRangeTrigger::setup() { float local_min = this->min_.value(0.0); float local_max = this->max_.value(0.0); convert hash = {.from = (local_max - local_min)}; - uint32_t myhash = hash.to ^ this->parent_->get_preference_hash(); - this->rtc_ = global_preferences->make_preference(myhash); + this->rtc_ = this->parent_->make_entity_preference(hash.to); bool initial_state; if (this->rtc_.load(&initial_state)) { this->previous_in_range_ = initial_state; diff --git a/esphome/components/opentherm/number/opentherm_number.cpp b/esphome/components/opentherm/number/opentherm_number.cpp index f0c69144c8..bdb02a605c 100644 --- a/esphome/components/opentherm/number/opentherm_number.cpp +++ b/esphome/components/opentherm/number/opentherm_number.cpp @@ -17,7 +17,7 @@ void OpenthermNumber::setup() { if (!this->restore_value_) { value = this->initial_value_; } else { - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + this->pref_ = this->make_entity_preference(); if (!this->pref_.load(&value)) { if (!std::isnan(this->initial_value_)) { value = this->initial_value_; diff --git a/esphome/components/rotary_encoder/rotary_encoder.cpp b/esphome/components/rotary_encoder/rotary_encoder.cpp index 26e20664f2..c652944120 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.cpp +++ b/esphome/components/rotary_encoder/rotary_encoder.cpp @@ -132,7 +132,7 @@ void RotaryEncoderSensor::setup() { int32_t initial_value = 0; switch (this->restore_mode_) { case ROTARY_ENCODER_RESTORE_DEFAULT_ZERO: - this->rtc_ = global_preferences->make_preference(this->get_preference_hash()); + this->rtc_ = this->make_entity_preference(); if (!this->rtc_.load(&initial_value)) { initial_value = 0; } diff --git a/esphome/components/sensor/automation.h b/esphome/components/sensor/automation.h index 996c7fc9b5..b4de712727 100644 --- a/esphome/components/sensor/automation.h +++ b/esphome/components/sensor/automation.h @@ -39,7 +39,7 @@ class ValueRangeTrigger : public Trigger, public Component { template void set_max(V max) { this->max_ = max; } void setup() override { - this->rtc_ = global_preferences->make_preference(this->parent_->get_preference_hash()); + this->rtc_ = this->parent_->make_entity_preference(); bool initial_state; if (this->rtc_.load(&initial_state)) { this->previous_in_range_ = initial_state; diff --git a/esphome/components/speaker/media_player/speaker_media_player.cpp b/esphome/components/speaker/media_player/speaker_media_player.cpp index 9a3a47bac8..172bc980a8 100644 --- a/esphome/components/speaker/media_player/speaker_media_player.cpp +++ b/esphome/components/speaker/media_player/speaker_media_player.cpp @@ -55,7 +55,7 @@ void SpeakerMediaPlayer::setup() { this->media_control_command_queue_ = xQueueCreate(MEDIA_CONTROLS_QUEUE_LENGTH, sizeof(MediaCallCommand)); - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + this->pref_ = this->make_entity_preference(); VolumeRestoreState volume_restore_state; if (this->pref_.load(&volume_restore_state)) { diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index 369ee5e6ff..2a60eb042b 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -16,7 +16,7 @@ void SprinklerControllerNumber::setup() { if (!this->restore_value_) { value = this->initial_value_; } else { - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + this->pref_ = this->make_entity_preference(); if (!this->pref_.load(&value)) { if (!std::isnan(this->initial_value_)) { value = this->initial_value_; diff --git a/esphome/components/switch/switch.cpp b/esphome/components/switch/switch.cpp index 069533fa78..d880139c5f 100644 --- a/esphome/components/switch/switch.cpp +++ b/esphome/components/switch/switch.cpp @@ -34,7 +34,7 @@ optional Switch::get_initial_state() { if (!(restore_mode & RESTORE_MODE_PERSISTENT_MASK)) return {}; - this->rtc_ = global_preferences->make_preference(this->get_preference_hash()); + this->rtc_ = this->make_entity_preference(); bool initial_state; if (!this->rtc_.load(&initial_state)) return {}; 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 028d6f0879..1a5aef6b8d 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 @@ -82,7 +82,7 @@ void TemplateAlarmControlPanel::setup() { this->current_state_ = ACP_STATE_DISARMED; if (this->restore_mode_ == ALARM_CONTROL_PANEL_RESTORE_DEFAULT_DISARMED) { uint8_t value; - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + this->pref_ = this->make_entity_preference(); if (this->pref_.load(&value)) { this->current_state_ = static_cast(value); } diff --git a/esphome/components/template/datetime/template_date.cpp b/esphome/components/template/datetime/template_date.cpp index 303d5ae2b0..be1d875a7e 100644 --- a/esphome/components/template/datetime/template_date.cpp +++ b/esphome/components/template/datetime/template_date.cpp @@ -18,8 +18,7 @@ void TemplateDate::setup() { state = this->initial_value_; } else { datetime::DateEntityRestoreState temp; - this->pref_ = - global_preferences->make_preference(194434030U ^ this->get_preference_hash()); + this->pref_ = this->make_entity_preference(194434030U); if (this->pref_.load(&temp)) { temp.apply(this); return; diff --git a/esphome/components/template/datetime/template_datetime.cpp b/esphome/components/template/datetime/template_datetime.cpp index 81a823f53e..e134f2b654 100644 --- a/esphome/components/template/datetime/template_datetime.cpp +++ b/esphome/components/template/datetime/template_datetime.cpp @@ -18,8 +18,7 @@ void TemplateDateTime::setup() { state = this->initial_value_; } else { datetime::DateTimeEntityRestoreState temp; - this->pref_ = global_preferences->make_preference( - 194434090U ^ this->get_preference_hash()); + this->pref_ = this->make_entity_preference(194434090U); if (this->pref_.load(&temp)) { temp.apply(this); return; diff --git a/esphome/components/template/datetime/template_time.cpp b/esphome/components/template/datetime/template_time.cpp index 21f843dcc7..586e126e3b 100644 --- a/esphome/components/template/datetime/template_time.cpp +++ b/esphome/components/template/datetime/template_time.cpp @@ -18,8 +18,7 @@ void TemplateTime::setup() { state = this->initial_value_; } else { datetime::TimeEntityRestoreState temp; - this->pref_ = - global_preferences->make_preference(194434060U ^ this->get_preference_hash()); + this->pref_ = this->make_entity_preference(194434060U); if (this->pref_.load(&temp)) { temp.apply(this); return; diff --git a/esphome/components/template/number/template_number.cpp b/esphome/components/template/number/template_number.cpp index 76fef82225..885265cf5d 100644 --- a/esphome/components/template/number/template_number.cpp +++ b/esphome/components/template/number/template_number.cpp @@ -13,7 +13,7 @@ void TemplateNumber::setup() { if (!this->restore_value_) { value = this->initial_value_; } else { - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + this->pref_ = this->make_entity_preference(); if (!this->pref_.load(&value)) { if (!std::isnan(this->initial_value_)) { value = this->initial_value_; diff --git a/esphome/components/template/select/template_select.cpp b/esphome/components/template/select/template_select.cpp index 818abfc1d7..fa34aa9fa7 100644 --- a/esphome/components/template/select/template_select.cpp +++ b/esphome/components/template/select/template_select.cpp @@ -11,7 +11,7 @@ void TemplateSelect::setup() { size_t index = this->initial_option_index_; if (this->restore_value_) { - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + this->pref_ = this->make_entity_preference(); size_t restored_index; if (this->pref_.load(&restored_index) && this->has_index(restored_index)) { index = restored_index; diff --git a/esphome/components/template/text/template_text.cpp b/esphome/components/template/text/template_text.cpp index 5acbb6e15a..70b8dce312 100644 --- a/esphome/components/template/text/template_text.cpp +++ b/esphome/components/template/text/template_text.cpp @@ -20,7 +20,14 @@ void TemplateText::setup() { // Need std::string for pref_->setup() to fill from flash std::string value{this->initial_value_ != nullptr ? this->initial_value_ : ""}; + // For future hash migration: use migrate_entity_preference_() with: + // old_key = get_preference_hash() + extra + // new_key = get_preference_hash_v2() + extra + // See: https://github.com/esphome/backlog/issues/85 +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" uint32_t key = this->get_preference_hash(); +#pragma GCC diagnostic pop key += this->traits.get_min_length() << 2; key += this->traits.get_max_length() << 4; key += fnv1_hash(this->traits.get_pattern_c_str()) << 6; diff --git a/esphome/components/total_daily_energy/total_daily_energy.cpp b/esphome/components/total_daily_energy/total_daily_energy.cpp index 818696f99b..e7a45a5edf 100644 --- a/esphome/components/total_daily_energy/total_daily_energy.cpp +++ b/esphome/components/total_daily_energy/total_daily_energy.cpp @@ -10,7 +10,7 @@ void TotalDailyEnergy::setup() { float initial_value = 0; if (this->restore_) { - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + this->pref_ = this->make_entity_preference(); this->pref_.load(&initial_value); } this->publish_state_and_save(initial_value); diff --git a/esphome/components/tuya/number/tuya_number.cpp b/esphome/components/tuya/number/tuya_number.cpp index 44b22167de..fd22e642c6 100644 --- a/esphome/components/tuya/number/tuya_number.cpp +++ b/esphome/components/tuya/number/tuya_number.cpp @@ -8,7 +8,7 @@ static const char *const TAG = "tuya.number"; void TuyaNumber::setup() { if (this->restore_value_) { - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + this->pref_ = this->make_entity_preference(); } this->parent_->register_listener(this->number_id_, [this](const TuyaDatapoint &datapoint) { diff --git a/esphome/components/valve/valve.cpp b/esphome/components/valve/valve.cpp index 3a7c3cbf88..607f614ef7 100644 --- a/esphome/components/valve/valve.cpp +++ b/esphome/components/valve/valve.cpp @@ -163,7 +163,7 @@ void Valve::publish_state(bool save) { } } optional Valve::restore_state_() { - this->rtc_ = global_preferences->make_preference(this->get_preference_hash()); + this->rtc_ = this->make_entity_preference(); ValveRestoreState recovered{}; if (!this->rtc_.load(&recovered)) return {}; diff --git a/esphome/components/water_heater/water_heater.cpp b/esphome/components/water_heater/water_heater.cpp index fbb4181209..7266294d84 100644 --- a/esphome/components/water_heater/water_heater.cpp +++ b/esphome/components/water_heater/water_heater.cpp @@ -185,7 +185,7 @@ void WaterHeater::publish_state() { } optional WaterHeater::restore_state_() { - this->pref_ = global_preferences->make_preference(this->get_preference_hash()); + this->pref_ = this->make_entity_preference(); SavedWaterHeaterState recovered{}; if (!this->pref_.load(&recovered)) return {}; diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 8508b93411..7d7878f53a 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -92,6 +92,48 @@ StringRef EntityBase::get_object_id_to(std::span buf) c 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 +// +// FUTURE IMPLEMENTATION: +// This will require raw load/save methods on ESPPreferenceObject that take uint8_t* and size. +// void EntityBase::migrate_entity_preference_(size_t size, uint32_t old_key, uint32_t new_key) { +// if (old_key == new_key) +// return; +// auto old_pref = global_preferences->make_preference(size, old_key); +// auto new_pref = global_preferences->make_preference(size, new_key); +// SmallBufferWithHeapFallback<64> buffer(size); +// if (old_pref.load(buffer.data(), size)) { +// new_pref.save(buffer.data(), size); +// } +// } + +ESPPreferenceObject EntityBase::make_entity_preference_(size_t size, uint32_t version) { + // This helper centralizes preference creation to enable fixing hash collisions. + // See: https://github.com/esphome/backlog/issues/85 + // + // COLLISION PROBLEM: get_preference_hash() uses fnv1_hash on sanitized object_id. + // Multiple entity names can sanitize to the same object_id: + // - "Living Room" and "living_room" both become "living_room" + // - UTF-8 names like "温度" and "湿度" both become "__" (underscores) + // This causes entities to overwrite each other's stored preferences. + // + // FUTURE MIGRATION: When implementing get_preference_hash_v2() that hashes + // the original entity name (not sanitized object_id): + // + // uint32_t old_key = this->get_preference_hash() ^ version; + // uint32_t new_key = this->get_preference_hash_v2() ^ version; + // this->migrate_entity_preference_(size, old_key, new_key); + // return global_preferences->make_preference(size, new_key); + // +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + uint32_t key = this->get_preference_hash() ^ version; +#pragma GCC diagnostic pop + return global_preferences->make_preference(size, key); +} + std::string EntityBase_DeviceClass::get_device_class() { if (this->device_class_ == nullptr) { return ""; diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index f91bd9b20c..0b75b25817 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -6,6 +6,7 @@ #include "string_ref.h" #include "helpers.h" #include "log.h" +#include "preferences.h" #ifdef USE_DEVICES #include "device.h" @@ -138,7 +139,12 @@ class EntityBase { * from previous versions, so existing single-device configurations will continue to work. * * @return uint32_t The unique hash for preferences, including device_id if available. + * @deprecated Use make_entity_preference() instead, or preferences won't be migrated. + * See https://github.com/esphome/backlog/issues/85 */ + ESPDEPRECATED("Use make_entity_preference() instead, or preferences won't be migrated. " + "See https://github.com/esphome/backlog/issues/85. Will be removed in 2027.1.0.", + "2026.7.0") uint32_t get_preference_hash() { #ifdef USE_DEVICES // Combine object_id_hash with device_id to ensure uniqueness across devices @@ -151,7 +157,19 @@ class EntityBase { #endif } + /// Create a preference object for storing this entity's state/settings. + /// @tparam T The type of data to store (must be trivially copyable) + /// @param version Optional version hash XORed with preference key (change when struct layout changes) + template ESPPreferenceObject make_entity_preference(uint32_t version = 0) { + static_assert(std::is_trivially_copyable::value, "T must be trivially copyable"); + return this->make_entity_preference_(sizeof(T), version); + } + protected: + /// 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); + void calc_object_id_(); StringRef name_; From d056e1040b4469ca9a1296a97ff554711b072412 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 26 Jan 2026 17:48:06 -1000 Subject: [PATCH 6/9] [mqtt] Store command comparison strings in flash on ESP8266 (#13546) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- .../mqtt/mqtt_alarm_control_panel.cpp | 17 +++++++++-------- esphome/components/mqtt/mqtt_lock.cpp | 7 ++++--- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp index fbdc6dce23..263e554778 100644 --- a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp +++ b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp @@ -1,5 +1,6 @@ #include "mqtt_alarm_control_panel.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include "mqtt_const.h" @@ -18,21 +19,21 @@ 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 (strcasecmp(payload.c_str(), "ARM_AWAY") == 0) { + if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("ARM_AWAY")) == 0) { call.arm_away(); - } else if (strcasecmp(payload.c_str(), "ARM_HOME") == 0) { + } else if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("ARM_HOME")) == 0) { call.arm_home(); - } else if (strcasecmp(payload.c_str(), "ARM_NIGHT") == 0) { + } else if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("ARM_NIGHT")) == 0) { call.arm_night(); - } else if (strcasecmp(payload.c_str(), "ARM_VACATION") == 0) { + } else if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("ARM_VACATION")) == 0) { call.arm_vacation(); - } else if (strcasecmp(payload.c_str(), "ARM_CUSTOM_BYPASS") == 0) { + } else if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("ARM_CUSTOM_BYPASS")) == 0) { call.arm_custom_bypass(); - } else if (strcasecmp(payload.c_str(), "DISARM") == 0) { + } else if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("DISARM")) == 0) { call.disarm(); - } else if (strcasecmp(payload.c_str(), "PENDING") == 0) { + } else if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("PENDING")) == 0) { call.pending(); - } else if (strcasecmp(payload.c_str(), "TRIGGERED") == 0) { + } else if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("TRIGGERED")) == 0) { call.triggered(); } else { ESP_LOGW(TAG, "'%s': Received unknown command payload %s", this->friendly_name_().c_str(), payload.c_str()); diff --git a/esphome/components/mqtt/mqtt_lock.cpp b/esphome/components/mqtt/mqtt_lock.cpp index 96c9397da8..45d8e4698f 100644 --- a/esphome/components/mqtt/mqtt_lock.cpp +++ b/esphome/components/mqtt/mqtt_lock.cpp @@ -1,5 +1,6 @@ #include "mqtt_lock.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include "mqtt_const.h" @@ -16,11 +17,11 @@ MQTTLockComponent::MQTTLockComponent(lock::Lock *a_lock) : lock_(a_lock) {} void MQTTLockComponent::setup() { this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) { - if (strcasecmp(payload.c_str(), "LOCK") == 0) { + if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("LOCK")) == 0) { this->lock_->lock(); - } else if (strcasecmp(payload.c_str(), "UNLOCK") == 0) { + } else if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("UNLOCK")) == 0) { this->lock_->unlock(); - } else if (strcasecmp(payload.c_str(), "OPEN") == 0) { + } else if (ESPHOME_strcasecmp_P(payload.c_str(), ESPHOME_PSTR("OPEN")) == 0) { this->lock_->open(); } else { ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name_().c_str(), payload.c_str()); From 33f545a8e36762c417e2bf26bc4b62b1f58529a6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 26 Jan 2026 17:50:49 -1000 Subject: [PATCH 7/9] [factory_reset] Store reset reason comparison strings in flash on ESP8266 (#13547) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/factory_reset/factory_reset.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/factory_reset/factory_reset.cpp b/esphome/components/factory_reset/factory_reset.cpp index 2e3f802343..cd4134e9ae 100644 --- a/esphome/components/factory_reset/factory_reset.cpp +++ b/esphome/components/factory_reset/factory_reset.cpp @@ -3,6 +3,7 @@ #include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include @@ -19,7 +20,8 @@ static bool was_power_cycled() { #endif #ifdef USE_ESP8266 auto reset_reason = EspClass::getResetReason(); - return strcasecmp(reset_reason.c_str(), "power On") == 0 || strcasecmp(reset_reason.c_str(), "external system") == 0; + return ESPHOME_strcasecmp_P(reset_reason.c_str(), ESPHOME_PSTR("power On")) == 0 || + ESPHOME_strcasecmp_P(reset_reason.c_str(), ESPHOME_PSTR("external system")) == 0; #endif #ifdef USE_LIBRETINY auto reason = lt_get_reboot_reason(); From 3aaf10b6a88bdf30b2615b94a7474ed891c451ed Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 26 Jan 2026 18:18:57 -1000 Subject: [PATCH 8/9] [web_server_base] Update ESPAsyncWebServer to 3.9.5 (#13467) --- .clang-tidy.hash | 2 +- esphome/components/web_server_base/__init__.py | 2 +- platformio.ini | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 009f9db388..55239f961c 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -d565b0589e35e692b5f2fc0c14723a99595b4828a3a3ef96c442e86a23176c00 +a172e2f65981e98354cc6b5ecf69bdb055dd13602226042ab2c7acd037a2bf41 diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index d5d75b395d..e0eec7dedb 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -48,4 +48,4 @@ async def to_code(config): if CORE.is_libretiny: CORE.add_platformio_option("lib_ignore", ["ESPAsyncTCP", "RPAsyncTCP"]) # https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json - cg.add_library("ESP32Async/ESPAsyncWebServer", "3.7.10") + cg.add_library("ESP32Async/ESPAsyncWebServer", "3.9.5") diff --git a/platformio.ini b/platformio.ini index 9de72cd622..accc40ecf2 100644 --- a/platformio.ini +++ b/platformio.ini @@ -114,7 +114,7 @@ lib_deps = ESP8266WiFi ; wifi (Arduino built-in) Update ; ota (Arduino built-in) ESP32Async/ESPAsyncTCP@2.0.0 ; async_tcp - ESP32Async/ESPAsyncWebServer@3.7.8 ; web_server_base + ESP32Async/ESPAsyncWebServer@3.9.5 ; web_server_base makuna/NeoPixelBus@2.7.3 ; neopixelbus ESP8266HTTPClient ; http_request (Arduino built-in) ESP8266mDNS ; mdns (Arduino built-in) @@ -201,7 +201,7 @@ framework = arduino lib_deps = ${common:arduino.lib_deps} bblanchon/ArduinoJson@7.4.2 ; json - ESP32Async/ESPAsyncWebServer@3.7.8 ; web_server_base + ESP32Async/ESPAsyncWebServer@3.9.5 ; web_server_base build_flags = ${common:arduino.build_flags} -DUSE_RP2040 @@ -217,7 +217,7 @@ framework = arduino lib_compat_mode = soft lib_deps = bblanchon/ArduinoJson@7.4.2 ; json - ESP32Async/ESPAsyncWebServer@3.7.8 ; web_server_base + ESP32Async/ESPAsyncWebServer@3.9.5 ; web_server_base droscy/esp_wireguard@0.4.2 ; wireguard build_flags = ${common:arduino.build_flags} From b2474c6de9099ea8f36e84192d3fbfe49c32a7c4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 26 Jan 2026 19:43:52 -1000 Subject: [PATCH 9/9] [nfc] Use StaticVector for NFC UID storage to eliminate heap allocation (#13507) --- .../nfc/binary_sensor/nfc_binary_sensor.cpp | 4 +-- .../nfc/binary_sensor/nfc_binary_sensor.h | 6 ++-- esphome/components/nfc/nfc.cpp | 12 ++++--- esphome/components/nfc/nfc.h | 9 ++--- esphome/components/nfc/nfc_tag.h | 21 ++++++------ esphome/components/pn532/pn532.cpp | 14 ++++---- esphome/components/pn532/pn532.h | 30 ++++++++-------- .../components/pn532/pn532_mifare_classic.cpp | 11 +++--- .../pn532/pn532_mifare_ultralight.cpp | 4 +-- esphome/components/pn7150/pn7150.cpp | 10 +++--- esphome/components/pn7150/pn7150.h | 10 +++--- .../pn7150/pn7150_mifare_ultralight.cpp | 3 +- esphome/components/pn7160/pn7160.cpp | 10 +++--- esphome/components/pn7160/pn7160.h | 10 +++--- .../pn7160/pn7160_mifare_ultralight.cpp | 3 +- esphome/core/helpers.h | 34 +++++++++++++++++++ 16 files changed, 114 insertions(+), 77 deletions(-) diff --git a/esphome/components/nfc/binary_sensor/nfc_binary_sensor.cpp b/esphome/components/nfc/binary_sensor/nfc_binary_sensor.cpp index b62b243cc6..524ad5a413 100644 --- a/esphome/components/nfc/binary_sensor/nfc_binary_sensor.cpp +++ b/esphome/components/nfc/binary_sensor/nfc_binary_sensor.cpp @@ -40,7 +40,7 @@ void NfcTagBinarySensor::set_tag_name(const std::string &str) { this->match_tag_name_ = true; } -void NfcTagBinarySensor::set_uid(const std::vector &uid) { this->uid_ = uid; } +void NfcTagBinarySensor::set_uid(const NfcTagUid &uid) { this->uid_ = uid; } bool NfcTagBinarySensor::tag_match_ndef_string(const std::shared_ptr &msg) { for (const auto &record : msg->get_records()) { @@ -63,7 +63,7 @@ bool NfcTagBinarySensor::tag_match_tag_name(const std::shared_ptr & return false; } -bool NfcTagBinarySensor::tag_match_uid(const std::vector &data) { +bool NfcTagBinarySensor::tag_match_uid(const NfcTagUid &data) { if (data.size() != this->uid_.size()) { return false; } diff --git a/esphome/components/nfc/binary_sensor/nfc_binary_sensor.h b/esphome/components/nfc/binary_sensor/nfc_binary_sensor.h index cc313c2f2b..0a7ca0ca76 100644 --- a/esphome/components/nfc/binary_sensor/nfc_binary_sensor.h +++ b/esphome/components/nfc/binary_sensor/nfc_binary_sensor.h @@ -19,11 +19,11 @@ class NfcTagBinarySensor : public binary_sensor::BinarySensor, void set_ndef_match_string(const std::string &str); void set_tag_name(const std::string &str); - void set_uid(const std::vector &uid); + void set_uid(const NfcTagUid &uid); bool tag_match_ndef_string(const std::shared_ptr &msg); bool tag_match_tag_name(const std::shared_ptr &msg); - bool tag_match_uid(const std::vector &data); + bool tag_match_uid(const NfcTagUid &data); void tag_off(NfcTag &tag) override; void tag_on(NfcTag &tag) override; @@ -31,7 +31,7 @@ class NfcTagBinarySensor : public binary_sensor::BinarySensor, protected: bool match_tag_name_{false}; std::string match_string_; - std::vector uid_; + NfcTagUid uid_; }; } // namespace nfc diff --git a/esphome/components/nfc/nfc.cpp b/esphome/components/nfc/nfc.cpp index f60d2671cd..8567b0969a 100644 --- a/esphome/components/nfc/nfc.cpp +++ b/esphome/components/nfc/nfc.cpp @@ -8,19 +8,23 @@ namespace nfc { static const char *const TAG = "nfc"; -char *format_uid_to(char *buffer, const std::vector &uid) { +char *format_uid_to(char *buffer, std::span uid) { return format_hex_pretty_to(buffer, FORMAT_UID_BUFFER_SIZE, uid.data(), uid.size(), '-'); } -char *format_bytes_to(char *buffer, const std::vector &bytes) { +char *format_bytes_to(char *buffer, std::span bytes) { return format_hex_pretty_to(buffer, FORMAT_BYTES_BUFFER_SIZE, bytes.data(), bytes.size(), ' '); } #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" // Deprecated wrappers intentionally use heap-allocating version for backward compatibility -std::string format_uid(const std::vector &uid) { return format_hex_pretty(uid, '-', false); } // NOLINT -std::string format_bytes(const std::vector &bytes) { return format_hex_pretty(bytes, ' ', false); } // NOLINT +std::string format_uid(std::span uid) { + return format_hex_pretty(uid.data(), uid.size(), '-', false); // NOLINT +} +std::string format_bytes(std::span bytes) { + return format_hex_pretty(bytes.data(), bytes.size(), ' ', false); // NOLINT +} #pragma GCC diagnostic pop uint8_t guess_tag_type(uint8_t uid_length) { diff --git a/esphome/components/nfc/nfc.h b/esphome/components/nfc/nfc.h index 6568c60a85..5191904833 100644 --- a/esphome/components/nfc/nfc.h +++ b/esphome/components/nfc/nfc.h @@ -6,6 +6,7 @@ #include "ndef_record.h" #include "nfc_tag.h" +#include #include namespace esphome { @@ -56,19 +57,19 @@ static const 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; /// Format UID to buffer with '-' separator (e.g., "04-11-22-33"). Returns buffer for inline use. -char *format_uid_to(char *buffer, const std::vector &uid); +char *format_uid_to(char *buffer, std::span uid); /// Buffer size for format_bytes_to (64 bytes max = 192 chars with space separator) static constexpr size_t FORMAT_BYTES_BUFFER_SIZE = 192; /// Format bytes to buffer with ' ' separator (e.g., "04 11 22 33"). Returns buffer for inline use. -char *format_bytes_to(char *buffer, const std::vector &bytes); +char *format_bytes_to(char *buffer, std::span bytes); // Remove before 2026.6.0 ESPDEPRECATED("Use format_uid_to() with stack buffer instead. Removed in 2026.6.0", "2025.12.0") -std::string format_uid(const std::vector &uid); +std::string format_uid(std::span uid); // Remove before 2026.6.0 ESPDEPRECATED("Use format_bytes_to() with stack buffer instead. Removed in 2026.6.0", "2025.12.0") -std::string format_bytes(const std::vector &bytes); +std::string format_bytes(std::span bytes); uint8_t guess_tag_type(uint8_t uid_length); uint8_t get_mifare_classic_ndef_start_index(std::vector &data); diff --git a/esphome/components/nfc/nfc_tag.h b/esphome/components/nfc/nfc_tag.h index 55600c3bd9..0ded4cd6ee 100644 --- a/esphome/components/nfc/nfc_tag.h +++ b/esphome/components/nfc/nfc_tag.h @@ -10,26 +10,27 @@ namespace esphome { namespace nfc { +// NFC UIDs are 4, 7, or 10 bytes depending on tag type +static constexpr size_t NFC_UID_MAX_LENGTH = 10; +using NfcTagUid = StaticVector; + class NfcTag { public: - NfcTag() { - this->uid_ = {}; - this->tag_type_ = "Unknown"; - }; - NfcTag(std::vector &uid) { + NfcTag() { this->tag_type_ = "Unknown"; }; + NfcTag(const NfcTagUid &uid) { this->uid_ = uid; this->tag_type_ = "Unknown"; }; - NfcTag(std::vector &uid, const std::string &tag_type) { + NfcTag(const NfcTagUid &uid, const std::string &tag_type) { this->uid_ = uid; this->tag_type_ = tag_type; }; - NfcTag(std::vector &uid, const std::string &tag_type, std::unique_ptr ndef_message) { + NfcTag(const NfcTagUid &uid, const std::string &tag_type, std::unique_ptr ndef_message) { this->uid_ = uid; this->tag_type_ = tag_type; this->ndef_message_ = std::move(ndef_message); }; - NfcTag(std::vector &uid, const std::string &tag_type, std::vector &ndef_data) { + NfcTag(const NfcTagUid &uid, const std::string &tag_type, std::vector &ndef_data) { this->uid_ = uid; this->tag_type_ = tag_type; this->ndef_message_ = make_unique(ndef_data); @@ -41,14 +42,14 @@ class NfcTag { ndef_message_ = make_unique(*rhs.ndef_message_); } - std::vector &get_uid() { return this->uid_; }; + NfcTagUid &get_uid() { return this->uid_; }; const std::string &get_tag_type() { return this->tag_type_; }; bool has_ndef_message() { return this->ndef_message_ != nullptr; }; const std::shared_ptr &get_ndef_message() { return this->ndef_message_; }; void set_ndef_message(std::unique_ptr ndef_message) { this->ndef_message_ = std::move(ndef_message); }; protected: - std::vector uid_; + NfcTagUid uid_; std::string tag_type_; std::shared_ptr ndef_message_; }; diff --git a/esphome/components/pn532/pn532.cpp b/esphome/components/pn532/pn532.cpp index 8f0c5581d4..733810c242 100644 --- a/esphome/components/pn532/pn532.cpp +++ b/esphome/components/pn532/pn532.cpp @@ -168,11 +168,11 @@ void PN532::loop() { } uint8_t nfcid_length = read[5]; - std::vector nfcid(read.begin() + 6, read.begin() + 6 + nfcid_length); - if (read.size() < 6U + nfcid_length) { + if (nfcid_length > nfc::NFC_UID_MAX_LENGTH || read.size() < 6U + nfcid_length) { // oops, pn532 returned invalid data return; } + nfc::NfcTagUid nfcid(read.begin() + 6, read.begin() + 6 + nfcid_length); bool report = true; for (auto *bin_sens : this->binary_sensors_) { @@ -358,7 +358,7 @@ void PN532::turn_off_rf_() { }); } -std::unique_ptr PN532::read_tag_(std::vector &uid) { +std::unique_ptr PN532::read_tag_(nfc::NfcTagUid &uid) { uint8_t type = nfc::guess_tag_type(uid.size()); if (type == nfc::TAG_TYPE_MIFARE_CLASSIC) { @@ -393,7 +393,7 @@ void PN532::write_mode(nfc::NdefMessage *message) { ESP_LOGD(TAG, "Waiting to write next tag"); } -bool PN532::clean_tag_(std::vector &uid) { +bool PN532::clean_tag_(nfc::NfcTagUid &uid) { uint8_t type = nfc::guess_tag_type(uid.size()); if (type == nfc::TAG_TYPE_MIFARE_CLASSIC) { return this->format_mifare_classic_mifare_(uid); @@ -404,7 +404,7 @@ bool PN532::clean_tag_(std::vector &uid) { return false; } -bool PN532::format_tag_(std::vector &uid) { +bool PN532::format_tag_(nfc::NfcTagUid &uid) { uint8_t type = nfc::guess_tag_type(uid.size()); if (type == nfc::TAG_TYPE_MIFARE_CLASSIC) { return this->format_mifare_classic_ndef_(uid); @@ -415,7 +415,7 @@ bool PN532::format_tag_(std::vector &uid) { return false; } -bool PN532::write_tag_(std::vector &uid, nfc::NdefMessage *message) { +bool PN532::write_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message) { uint8_t type = nfc::guess_tag_type(uid.size()); if (type == nfc::TAG_TYPE_MIFARE_CLASSIC) { return this->write_mifare_classic_tag_(uid, message); @@ -448,7 +448,7 @@ void PN532::dump_config() { } } -bool PN532BinarySensor::process(std::vector &data) { +bool PN532BinarySensor::process(const nfc::NfcTagUid &data) { if (data.size() != this->uid_.size()) return false; diff --git a/esphome/components/pn532/pn532.h b/esphome/components/pn532/pn532.h index eeb15648fb..488ec4af3b 100644 --- a/esphome/components/pn532/pn532.h +++ b/esphome/components/pn532/pn532.h @@ -69,28 +69,28 @@ class PN532 : public PollingComponent { virtual bool read_data(std::vector &data, uint8_t len) = 0; virtual bool read_response(uint8_t command, std::vector &data) = 0; - std::unique_ptr read_tag_(std::vector &uid); + std::unique_ptr read_tag_(nfc::NfcTagUid &uid); - bool format_tag_(std::vector &uid); - bool clean_tag_(std::vector &uid); - bool write_tag_(std::vector &uid, nfc::NdefMessage *message); + bool format_tag_(nfc::NfcTagUid &uid); + bool clean_tag_(nfc::NfcTagUid &uid); + bool write_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message); - std::unique_ptr read_mifare_classic_tag_(std::vector &uid); + std::unique_ptr read_mifare_classic_tag_(nfc::NfcTagUid &uid); bool read_mifare_classic_block_(uint8_t block_num, std::vector &data); bool write_mifare_classic_block_(uint8_t block_num, std::vector &data); - bool auth_mifare_classic_block_(std::vector &uid, uint8_t block_num, uint8_t key_num, const uint8_t *key); - bool format_mifare_classic_mifare_(std::vector &uid); - bool format_mifare_classic_ndef_(std::vector &uid); - bool write_mifare_classic_tag_(std::vector &uid, nfc::NdefMessage *message); + bool auth_mifare_classic_block_(nfc::NfcTagUid &uid, uint8_t block_num, uint8_t key_num, const uint8_t *key); + bool format_mifare_classic_mifare_(nfc::NfcTagUid &uid); + bool format_mifare_classic_ndef_(nfc::NfcTagUid &uid); + bool write_mifare_classic_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message); - std::unique_ptr read_mifare_ultralight_tag_(std::vector &uid); + std::unique_ptr read_mifare_ultralight_tag_(nfc::NfcTagUid &uid); bool read_mifare_ultralight_bytes_(uint8_t start_page, uint16_t num_bytes, std::vector &data); bool is_mifare_ultralight_formatted_(const std::vector &page_3_to_6); uint16_t read_mifare_ultralight_capacity_(); bool find_mifare_ultralight_ndef_(const std::vector &page_3_to_6, uint8_t &message_length, uint8_t &message_start_index); bool write_mifare_ultralight_page_(uint8_t page_num, std::vector &write_data); - bool write_mifare_ultralight_tag_(std::vector &uid, nfc::NdefMessage *message); + bool write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message); bool clean_mifare_ultralight_(); bool updates_enabled_{true}; @@ -98,7 +98,7 @@ class PN532 : public PollingComponent { std::vector binary_sensors_; std::vector triggers_ontag_; std::vector triggers_ontagremoved_; - std::vector current_uid_; + nfc::NfcTagUid current_uid_; nfc::NdefMessage *next_task_message_to_write_; uint32_t rd_start_time_{0}; enum PN532ReadReady rd_ready_ { WOULDBLOCK }; @@ -118,9 +118,9 @@ class PN532 : public PollingComponent { class PN532BinarySensor : public binary_sensor::BinarySensor { public: - void set_uid(const std::vector &uid) { uid_ = uid; } + void set_uid(const nfc::NfcTagUid &uid) { uid_ = uid; } - bool process(std::vector &data); + bool process(const nfc::NfcTagUid &data); void on_scan_end() { if (!this->found_) { @@ -130,7 +130,7 @@ class PN532BinarySensor : public binary_sensor::BinarySensor { } protected: - std::vector uid_; + nfc::NfcTagUid uid_; bool found_{false}; }; diff --git a/esphome/components/pn532/pn532_mifare_classic.cpp b/esphome/components/pn532/pn532_mifare_classic.cpp index 28ab22e160..b762d5d936 100644 --- a/esphome/components/pn532/pn532_mifare_classic.cpp +++ b/esphome/components/pn532/pn532_mifare_classic.cpp @@ -8,7 +8,7 @@ namespace pn532 { static const char *const TAG = "pn532.mifare_classic"; -std::unique_ptr PN532::read_mifare_classic_tag_(std::vector &uid) { +std::unique_ptr PN532::read_mifare_classic_tag_(nfc::NfcTagUid &uid) { uint8_t current_block = 4; uint8_t message_start_index = 0; uint32_t message_length = 0; @@ -82,8 +82,7 @@ bool PN532::read_mifare_classic_block_(uint8_t block_num, std::vector & return true; } -bool PN532::auth_mifare_classic_block_(std::vector &uid, uint8_t block_num, uint8_t key_num, - const uint8_t *key) { +bool PN532::auth_mifare_classic_block_(nfc::NfcTagUid &uid, uint8_t block_num, uint8_t key_num, const uint8_t *key) { std::vector data({ PN532_COMMAND_INDATAEXCHANGE, 0x01, // One card @@ -106,7 +105,7 @@ bool PN532::auth_mifare_classic_block_(std::vector &uid, uint8_t block_ return true; } -bool PN532::format_mifare_classic_mifare_(std::vector &uid) { +bool PN532::format_mifare_classic_mifare_(nfc::NfcTagUid &uid) { std::vector blank_buffer( {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); std::vector trailer_buffer( @@ -141,7 +140,7 @@ bool PN532::format_mifare_classic_mifare_(std::vector &uid) { return !error; } -bool PN532::format_mifare_classic_ndef_(std::vector &uid) { +bool PN532::format_mifare_classic_ndef_(nfc::NfcTagUid &uid) { std::vector empty_ndef_message( {0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); std::vector blank_block( @@ -216,7 +215,7 @@ bool PN532::write_mifare_classic_block_(uint8_t block_num, std::vector return true; } -bool PN532::write_mifare_classic_tag_(std::vector &uid, nfc::NdefMessage *message) { +bool PN532::write_mifare_classic_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message) { auto encoded = message->encode(); uint32_t message_length = encoded.size(); diff --git a/esphome/components/pn532/pn532_mifare_ultralight.cpp b/esphome/components/pn532/pn532_mifare_ultralight.cpp index 0221ba31c5..01e41df5c0 100644 --- a/esphome/components/pn532/pn532_mifare_ultralight.cpp +++ b/esphome/components/pn532/pn532_mifare_ultralight.cpp @@ -8,7 +8,7 @@ namespace pn532 { static const char *const TAG = "pn532.mifare_ultralight"; -std::unique_ptr PN532::read_mifare_ultralight_tag_(std::vector &uid) { +std::unique_ptr PN532::read_mifare_ultralight_tag_(nfc::NfcTagUid &uid) { std::vector data; // pages 3 to 6 contain various info we are interested in -- do one read to grab it all if (!this->read_mifare_ultralight_bytes_(3, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE * nfc::MIFARE_ULTRALIGHT_READ_SIZE, @@ -114,7 +114,7 @@ bool PN532::find_mifare_ultralight_ndef_(const std::vector &page_3_to_6 return false; } -bool PN532::write_mifare_ultralight_tag_(std::vector &uid, nfc::NdefMessage *message) { +bool PN532::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message) { uint32_t capacity = this->read_mifare_ultralight_capacity_(); auto encoded = message->encode(); diff --git a/esphome/components/pn7150/pn7150.cpp b/esphome/components/pn7150/pn7150.cpp index e1ba3761d4..7bec1e08a9 100644 --- a/esphome/components/pn7150/pn7150.cpp +++ b/esphome/components/pn7150/pn7150.cpp @@ -478,7 +478,7 @@ uint8_t PN7150::read_endpoint_data_(nfc::NfcTag &tag) { return nfc::STATUS_FAILED; } -uint8_t PN7150::clean_endpoint_(std::vector &uid) { +uint8_t PN7150::clean_endpoint_(nfc::NfcTagUid &uid) { uint8_t type = nfc::guess_tag_type(uid.size()); switch (type) { case nfc::TAG_TYPE_MIFARE_CLASSIC: @@ -494,7 +494,7 @@ uint8_t PN7150::clean_endpoint_(std::vector &uid) { return nfc::STATUS_FAILED; } -uint8_t PN7150::format_endpoint_(std::vector &uid) { +uint8_t PN7150::format_endpoint_(nfc::NfcTagUid &uid) { uint8_t type = nfc::guess_tag_type(uid.size()); switch (type) { case nfc::TAG_TYPE_MIFARE_CLASSIC: @@ -510,7 +510,7 @@ uint8_t PN7150::format_endpoint_(std::vector &uid) { return nfc::STATUS_FAILED; } -uint8_t PN7150::write_endpoint_(std::vector &uid, std::shared_ptr &message) { +uint8_t PN7150::write_endpoint_(nfc::NfcTagUid &uid, std::shared_ptr &message) { uint8_t type = nfc::guess_tag_type(uid.size()); switch (type) { case nfc::TAG_TYPE_MIFARE_CLASSIC: @@ -534,7 +534,7 @@ std::unique_ptr PN7150::build_tag_(const uint8_t mode_tech, const s ESP_LOGE(TAG, "UID length cannot be zero"); return nullptr; } - std::vector uid(data.begin() + 3, data.begin() + 3 + uid_length); + nfc::NfcTagUid uid(data.begin() + 3, data.begin() + 3 + uid_length); const auto *tag_type_str = nfc::guess_tag_type(uid_length) == nfc::TAG_TYPE_MIFARE_CLASSIC ? nfc::MIFARE_CLASSIC : nfc::NFC_FORUM_TYPE_2; return make_unique(uid, tag_type_str); @@ -543,7 +543,7 @@ std::unique_ptr PN7150::build_tag_(const uint8_t mode_tech, const s return nullptr; } -optional PN7150::find_tag_uid_(const std::vector &uid) { +optional PN7150::find_tag_uid_(const nfc::NfcTagUid &uid) { if (!this->discovered_endpoint_.empty()) { for (size_t i = 0; i < this->discovered_endpoint_.size(); i++) { auto existing_tag_uid = this->discovered_endpoint_[i].tag->get_uid(); diff --git a/esphome/components/pn7150/pn7150.h b/esphome/components/pn7150/pn7150.h index 42cd7a6ef7..a5dcef9f99 100644 --- a/esphome/components/pn7150/pn7150.h +++ b/esphome/components/pn7150/pn7150.h @@ -203,12 +203,12 @@ class PN7150 : public nfc::Nfcc, public Component { void select_endpoint_(); uint8_t read_endpoint_data_(nfc::NfcTag &tag); - uint8_t clean_endpoint_(std::vector &uid); - uint8_t format_endpoint_(std::vector &uid); - uint8_t write_endpoint_(std::vector &uid, std::shared_ptr &message); + uint8_t clean_endpoint_(nfc::NfcTagUid &uid); + uint8_t format_endpoint_(nfc::NfcTagUid &uid); + uint8_t write_endpoint_(nfc::NfcTagUid &uid, std::shared_ptr &message); std::unique_ptr build_tag_(uint8_t mode_tech, const std::vector &data); - optional find_tag_uid_(const std::vector &uid); + optional find_tag_uid_(const nfc::NfcTagUid &uid); void purge_old_tags_(); void erase_tag_(uint8_t tag_index); @@ -251,7 +251,7 @@ class PN7150 : public nfc::Nfcc, public Component { uint8_t find_mifare_ultralight_ndef_(const std::vector &page_3_to_6, uint8_t &message_length, uint8_t &message_start_index); uint8_t write_mifare_ultralight_page_(uint8_t page_num, std::vector &write_data); - uint8_t write_mifare_ultralight_tag_(std::vector &uid, const std::shared_ptr &message); + uint8_t write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::shared_ptr &message); uint8_t clean_mifare_ultralight_(); enum NfcTask : uint8_t { diff --git a/esphome/components/pn7150/pn7150_mifare_ultralight.cpp b/esphome/components/pn7150/pn7150_mifare_ultralight.cpp index ac15475bad..166065f6c1 100644 --- a/esphome/components/pn7150/pn7150_mifare_ultralight.cpp +++ b/esphome/components/pn7150/pn7150_mifare_ultralight.cpp @@ -115,8 +115,7 @@ uint8_t PN7150::find_mifare_ultralight_ndef_(const std::vector &page_3_ return nfc::STATUS_FAILED; } -uint8_t PN7150::write_mifare_ultralight_tag_(std::vector &uid, - const std::shared_ptr &message) { +uint8_t PN7150::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::shared_ptr &message) { uint32_t capacity = this->read_mifare_ultralight_capacity_(); auto encoded = message->encode(); diff --git a/esphome/components/pn7160/pn7160.cpp b/esphome/components/pn7160/pn7160.cpp index 1a38dce5fd..28907b8e30 100644 --- a/esphome/components/pn7160/pn7160.cpp +++ b/esphome/components/pn7160/pn7160.cpp @@ -506,7 +506,7 @@ uint8_t PN7160::read_endpoint_data_(nfc::NfcTag &tag) { return nfc::STATUS_FAILED; } -uint8_t PN7160::clean_endpoint_(std::vector &uid) { +uint8_t PN7160::clean_endpoint_(nfc::NfcTagUid &uid) { uint8_t type = nfc::guess_tag_type(uid.size()); switch (type) { case nfc::TAG_TYPE_MIFARE_CLASSIC: @@ -522,7 +522,7 @@ uint8_t PN7160::clean_endpoint_(std::vector &uid) { return nfc::STATUS_FAILED; } -uint8_t PN7160::format_endpoint_(std::vector &uid) { +uint8_t PN7160::format_endpoint_(nfc::NfcTagUid &uid) { uint8_t type = nfc::guess_tag_type(uid.size()); switch (type) { case nfc::TAG_TYPE_MIFARE_CLASSIC: @@ -538,7 +538,7 @@ uint8_t PN7160::format_endpoint_(std::vector &uid) { return nfc::STATUS_FAILED; } -uint8_t PN7160::write_endpoint_(std::vector &uid, std::shared_ptr &message) { +uint8_t PN7160::write_endpoint_(nfc::NfcTagUid &uid, std::shared_ptr &message) { uint8_t type = nfc::guess_tag_type(uid.size()); switch (type) { case nfc::TAG_TYPE_MIFARE_CLASSIC: @@ -562,7 +562,7 @@ std::unique_ptr PN7160::build_tag_(const uint8_t mode_tech, const s ESP_LOGE(TAG, "UID length cannot be zero"); return nullptr; } - std::vector uid(data.begin() + 3, data.begin() + 3 + uid_length); + nfc::NfcTagUid uid(data.begin() + 3, data.begin() + 3 + uid_length); const auto *tag_type_str = nfc::guess_tag_type(uid_length) == nfc::TAG_TYPE_MIFARE_CLASSIC ? nfc::MIFARE_CLASSIC : nfc::NFC_FORUM_TYPE_2; return make_unique(uid, tag_type_str); @@ -571,7 +571,7 @@ std::unique_ptr PN7160::build_tag_(const uint8_t mode_tech, const s return nullptr; } -optional PN7160::find_tag_uid_(const std::vector &uid) { +optional PN7160::find_tag_uid_(const nfc::NfcTagUid &uid) { if (!this->discovered_endpoint_.empty()) { for (size_t i = 0; i < this->discovered_endpoint_.size(); i++) { auto existing_tag_uid = this->discovered_endpoint_[i].tag->get_uid(); diff --git a/esphome/components/pn7160/pn7160.h b/esphome/components/pn7160/pn7160.h index fc00296a71..572fab3351 100644 --- a/esphome/components/pn7160/pn7160.h +++ b/esphome/components/pn7160/pn7160.h @@ -220,12 +220,12 @@ class PN7160 : public nfc::Nfcc, public Component { void select_endpoint_(); uint8_t read_endpoint_data_(nfc::NfcTag &tag); - uint8_t clean_endpoint_(std::vector &uid); - uint8_t format_endpoint_(std::vector &uid); - uint8_t write_endpoint_(std::vector &uid, std::shared_ptr &message); + uint8_t clean_endpoint_(nfc::NfcTagUid &uid); + uint8_t format_endpoint_(nfc::NfcTagUid &uid); + uint8_t write_endpoint_(nfc::NfcTagUid &uid, std::shared_ptr &message); std::unique_ptr build_tag_(uint8_t mode_tech, const std::vector &data); - optional find_tag_uid_(const std::vector &uid); + optional find_tag_uid_(const nfc::NfcTagUid &uid); void purge_old_tags_(); void erase_tag_(uint8_t tag_index); @@ -268,7 +268,7 @@ class PN7160 : public nfc::Nfcc, public Component { uint8_t find_mifare_ultralight_ndef_(const std::vector &page_3_to_6, uint8_t &message_length, uint8_t &message_start_index); uint8_t write_mifare_ultralight_page_(uint8_t page_num, std::vector &write_data); - uint8_t write_mifare_ultralight_tag_(std::vector &uid, const std::shared_ptr &message); + uint8_t write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::shared_ptr &message); uint8_t clean_mifare_ultralight_(); enum NfcTask : uint8_t { diff --git a/esphome/components/pn7160/pn7160_mifare_ultralight.cpp b/esphome/components/pn7160/pn7160_mifare_ultralight.cpp index 584385f113..c473ff48d9 100644 --- a/esphome/components/pn7160/pn7160_mifare_ultralight.cpp +++ b/esphome/components/pn7160/pn7160_mifare_ultralight.cpp @@ -115,8 +115,7 @@ uint8_t PN7160::find_mifare_ultralight_ndef_(const std::vector &page_3_ return nfc::STATUS_FAILED; } -uint8_t PN7160::write_mifare_ultralight_tag_(std::vector &uid, - const std::shared_ptr &message) { +uint8_t PN7160::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::shared_ptr &message) { uint32_t capacity = this->read_mifare_ultralight_capacity_(); auto encoded = message->encode(); diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 81397668e8..6c5904ef25 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -148,6 +148,25 @@ template class StaticVector { size_t count_{0}; public: + // Default constructor + StaticVector() = default; + + // Iterator range constructor + template StaticVector(InputIt first, InputIt last) { + while (first != last && count_ < N) { + data_[count_++] = *first++; + } + } + + // Initializer list constructor + StaticVector(std::initializer_list init) { + for (const auto &val : init) { + if (count_ >= N) + break; + data_[count_++] = val; + } + } + // Minimal vector-compatible interface - only what we actually use void push_back(const T &value) { if (count_ < N) { @@ -155,6 +174,17 @@ template class StaticVector { } } + // Clear all elements + void clear() { count_ = 0; } + + // Assign from iterator range + template void assign(InputIt first, InputIt last) { + count_ = 0; + while (first != last && count_ < N) { + data_[count_++] = *first++; + } + } + // Return reference to next element and increment count (with bounds checking) T &emplace_next() { if (count_ >= N) { @@ -186,6 +216,10 @@ template class StaticVector { reverse_iterator rend() { return reverse_iterator(begin()); } const_reverse_iterator rbegin() const { return const_reverse_iterator(end()); } const_reverse_iterator rend() const { return const_reverse_iterator(begin()); } + + // Conversion to std::span for compatibility with span-based APIs + operator std::span() { return std::span(data_.data(), count_); } + operator std::span() const { return std::span(data_.data(), count_); } }; /// Fixed-capacity vector - allocates once at runtime, never reallocates