Compare commits

..

44 Commits

Author SHA1 Message Date
J. Nick Koston
b2474c6de9 [nfc] Use StaticVector for NFC UID storage to eliminate heap allocation (#13507) 2026-01-26 19:43:52 -10:00
J. Nick Koston
3aaf10b6a8 [web_server_base] Update ESPAsyncWebServer to 3.9.5 (#13467) 2026-01-27 04:18:57 +00:00
J. Nick Koston
33f545a8e3 [factory_reset] Store reset reason comparison strings in flash on ESP8266 (#13547)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-01-27 03:50:49 +00:00
J. Nick Koston
d056e1040b [mqtt] Store command comparison strings in flash on ESP8266 (#13546)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-01-27 03:48:06 +00:00
J. Nick Koston
75a78b2bf3 [core] Encapsulate entity preference creation to prepare for hash migration (#13505) 2026-01-26 17:35:45 -10:00
J. Nick Koston
cd6314dc96 [socket] ESP8266: call delay(0) instead of esp_delay(0, cb) for zero timeout (#13530) 2026-01-26 17:34:55 -10:00
J. Nick Koston
f91bffff9a [wifi] Avoid heap allocation when building AP SSID (#13474) 2026-01-26 17:32:58 -10:00
J. Nick Koston
5cbe9af485 [rp2040] Use SmallBufferWithHeapFallback for preferences (#13501) 2026-01-26 17:32:03 -10:00
J. Nick Koston
a7fbecb25c [ci] Soft-deprecate str_sprintf/str_snprintf to prevent hidden heap allocations (#13227) 2026-01-26 17:28:07 -10:00
J. Nick Koston
bf92d94863 [mqtt] Use stack buffers for publish_state() topic building (#13434)
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>
2026-01-26 17:25:02 -10:00
J. Nick Koston
9c3817f544 [sml] Use constexpr std::array for START_SEQ constant (#13506) 2026-01-26 17:21:17 -10:00
J. Nick Koston
ee9e3315b6 [tm1638] Use member array instead of heap allocation for display buffer (#13504) 2026-01-26 17:21:05 -10:00
J. Nick Koston
67dea1e538 [light] Use member array instead of heap allocation in AddressableLightWrapper (#13503) 2026-01-26 17:20:49 -10:00
J. Nick Koston
003b9c6c3f [uln2003] Refactor step mode logging to use LogString (#13543) 2026-01-26 17:20:33 -10:00
J. Nick Koston
2f1a345905 [mhz19] Refactor detection range logging to use LogString (#13541) 2026-01-26 17:20:21 -10:00
J. Nick Koston
7ef933abec [libretiny] Bump to 1.11.0 (#13512) 2026-01-26 17:20:08 -10:00
J. Nick Koston
4ddd40bcfb [core] Add PROGMEM string comparison helpers and use in cover/valve/helpers (#13545) 2026-01-26 17:19:50 -10:00
J. Nick Koston
8ae901b3f1 [http_request] Use stack allocation for MD5 buffer in OTA (#13550) 2026-01-26 17:19:30 -10:00
J. Nick Koston
bc49174920 Add additional text_sensor filter tests (#13479) 2026-01-26 17:18:36 -10:00
J. Nick Koston
123ee02d39 [ota] Improve error message when device closes connection without responding (#13562) 2026-01-26 17:13:18 -10:00
Jonathan Swoboda
0cc8055757 [http_request] Add custom CA certificate support for ESP32 (#13552)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 22:07:27 -05:00
dependabot[bot]
27a212c14d Bump aioesphomeapi from 43.13.0 to 43.14.0 (#13557)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-26 15:43:40 -10:00
dependabot[bot]
65dc182526 Bump setuptools from 80.10.1 to 80.10.2 (#13558)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-26 15:43:27 -10:00
dependabot[bot]
dd91039ff1 Bump github/codeql-action from 4.31.11 to 4.32.0 (#13559)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-26 15:43:16 -10:00
sebcaps
1c9a9c7536 [mhz19] Fix Uninitialized var warning message (#13526) 2026-01-25 20:07:29 -10:00
Jonathan Swoboda
011407ea8b Merge branch 'release' into dev 2026-01-25 13:21:39 -05:00
Jonathan Swoboda
1141e83a7c Merge pull request #13529 from esphome/bump-2026.1.2
2026.1.2
2026-01-25 13:21:26 -05:00
Jonathan Swoboda
214ce95cf3 Bump version to 2026.1.2 2026-01-25 12:22:18 -05:00
J. Nick Koston
3a7b83ba93 [wifi] Fix scan flag race condition causing reconnect failure on ESP8266/LibreTiny (#13514) 2026-01-25 12:22:18 -05:00
Clyde Stubbs
cc2f3d85dc [wifi] Fix watchdog timeout on P4 WiFi scan (#13520) 2026-01-25 12:22:18 -05:00
Jonathan Swoboda
723f67d5e2 [i2c] Increase ESP-IDF I2C transaction timeout from 20ms to 100ms (#13483)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 12:22:18 -05:00
Jonathan Swoboda
70e45706d9 [modbus_controller] Fix YAML serialization error with custom_command (#13482)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 12:22:18 -05:00
Jas Strong
56a2a2269f [rd03d] Fix speed and resolution field order (#13495)
Co-authored-by: jas <jas@asspa.in>
2026-01-25 12:22:18 -05:00
Keith Burzinski
d6841ba33a [light] Fix cwww state restore (#13493) 2026-01-25 12:22:18 -05:00
Clyde Stubbs
10cbd0164a [lvgl] Fix setting empty text (#13494) 2026-01-25 12:22:18 -05:00
Big Mike
d285706b41 [sen5x] Fix store baseline functionality (#13469) 2026-01-25 12:22:18 -05:00
J. Nick Koston
ef469c20df [slow_pwm] Fix dump_summary deprecation warning (#13460) 2026-01-25 12:22:18 -05:00
Clyde Stubbs
6870d3dc50 [mipi_rgb] Add software reset command to st7701s init sequence (#13470) 2026-01-25 12:22:18 -05:00
Keith Burzinski
9cc39621a6 [ir_rf_proxy] Remove unnecessary headers, add tests (#13464) 2026-01-25 12:22:18 -05:00
J. Nick Koston
c4f7d09553 [rpi_dpi_rgb] Fix dump_summary deprecation warning (#13461) 2026-01-25 12:22:18 -05:00
J. Nick Koston
ab1661ef22 [mipi_rgb] Fix dump_summary deprecation warning (#13463) 2026-01-25 12:22:18 -05:00
J. Nick Koston
ccbf17d5ab [st7701s] Fix dump_summary deprecation warning (#13462) 2026-01-25 12:22:18 -05:00
J. Nick Koston
bac96086be [wifi] Fix scan flag race condition causing reconnect failure on ESP8266/LibreTiny (#13514) 2026-01-25 12:16:07 -05:00
Clyde Stubbs
c32e4bc65b [wifi] Fix watchdog timeout on P4 WiFi scan (#13520) 2026-01-26 03:52:23 +11:00
114 changed files with 850 additions and 335 deletions

View File

@@ -1 +1 @@
c0db7505713f2ebf5d18f274d35469cfbdaabe1e1def9fe2195594dc345f1a49
a172e2f65981e98354cc6b5ecf69bdb055dd13602226042ab2c7acd037a2bf41

View File

@@ -58,7 +58,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11
uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.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@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11
uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
with:
category: "/language:${{matrix.language}}"

View File

@@ -292,7 +292,7 @@ CONFIG_SCHEMA = cv.All(
CONF_MAX_CONNECTIONS,
esp8266=4, # ~40KB free RAM, each connection uses ~500-1000 bytes
esp32=8, # 520KB RAM available
rp2040=8, # 264KB RAM, plenty of heap available
rp2040=4, # 264KB RAM but LWIP constraints
bk72xx=8, # Moderate RAM
rtl87xx=8, # Moderate RAM
host=8, # Abundant resources

View File

@@ -38,10 +38,8 @@ async def to_code(config):
# https://github.com/ESP32Async/ESPAsyncTCP
cg.add_library("ESP32Async/ESPAsyncTCP", "2.0.0")
elif CORE.is_rp2040:
# https://github.com/ayushsharma82/RPAsyncTCP
# RPAsyncTCP is a drop-in replacement for AsyncTCP_RP2040W with better
# ESPAsyncWebServer compatibility
cg.add_library("ayushsharma82/RPAsyncTCP", "1.3.2")
# https://github.com/khoih-prog/AsyncTCP_RP2040W
cg.add_library("khoih-prog/AsyncTCP_RP2040W", "1.2.0")
# Other platforms (host, etc) use socket-based implementation

View File

@@ -8,8 +8,8 @@
// Use ESPAsyncTCP library for ESP8266 (always Arduino)
#include <ESPAsyncTCP.h>
#elif defined(USE_RP2040)
// Use RPAsyncTCP library for RP2040
#include <RPAsyncTCP.h>
// Use AsyncTCP_RP2040W library for RP2040
#include <AsyncTCP_RP2040W.h>
#else
// Use socket-based implementation for other platforms
#include "async_tcp_socket.h"

View File

@@ -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<float>(this->get_preference_hash());
this->pref_ = this->make_entity_preference<float>();
if (!this->pref_.load(&value)) {
value = 0.0f;
}

View File

@@ -13,7 +13,6 @@ from esphome.const import (
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_LN882X,
PLATFORM_RP2040,
PLATFORM_RTL87XX,
PlatformFramework,
)
@@ -54,7 +53,6 @@ CONFIG_SCHEMA = cv.All(
PLATFORM_ESP8266,
PLATFORM_BK72XX,
PLATFORM_LN882X,
PLATFORM_RP2040,
PLATFORM_RTL87XX,
]
),
@@ -108,8 +106,6 @@ async def to_code(config):
cg.add_library("DNSServer", None)
if CORE.is_libretiny:
cg.add_library("DNSServer", None)
if CORE.is_rp2040:
cg.add_library("DNSServer", None)
# Only compile the ESP-IDF DNS server when using ESP-IDF framework

View File

@@ -360,8 +360,7 @@ void Climate::add_on_control_callback(std::function<void(ClimateCall &)> &&callb
static const uint32_t RESTORE_STATE_VERSION = 0x848EA6ADUL;
optional<ClimateDeviceRestoreState> Climate::restore_state_() {
this->rtc_ = global_preferences->make_preference<ClimateDeviceRestoreState>(this->get_preference_hash() ^
RESTORE_STATE_VERSION);
this->rtc_ = this->make_entity_preference<ClimateDeviceRestoreState>(RESTORE_STATE_VERSION);
ClimateDeviceRestoreState recovered{};
if (!this->rtc_.load(&recovered))
return {};

View File

@@ -1,11 +1,11 @@
#include "cover.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include <strings.h>
#include "esphome/core/log.h"
namespace esphome::cover {
static const char *const TAG = "cover";
@@ -39,13 +39,13 @@ Cover::Cover() : position{COVER_OPEN} {}
CoverCall::CoverCall(Cover *parent) : parent_(parent) {}
CoverCall &CoverCall::set_command(const char *command) {
if (strcasecmp(command, "OPEN") == 0) {
if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("OPEN")) == 0) {
this->set_command_open();
} else if (strcasecmp(command, "CLOSE") == 0) {
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("CLOSE")) == 0) {
this->set_command_close();
} else if (strcasecmp(command, "STOP") == 0) {
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("STOP")) == 0) {
this->set_command_stop();
} else if (strcasecmp(command, "TOGGLE") == 0) {
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("TOGGLE")) == 0) {
this->set_command_toggle();
} else {
ESP_LOGW(TAG, "'%s' - Unrecognized command %s", this->parent_->get_name().c_str(), command);
@@ -187,7 +187,7 @@ void Cover::publish_state(bool save) {
}
}
optional<CoverRestoreState> Cover::restore_state_() {
this->rtc_ = global_preferences->make_preference<CoverRestoreState>(this->get_preference_hash());
this->rtc_ = this->make_entity_preference<CoverRestoreState>();
CoverRestoreState recovered{};
if (!this->rtc_.load(&recovered))
return {};

View File

@@ -41,7 +41,7 @@ void DutyTimeSensor::setup() {
uint32_t seconds = 0;
if (this->restore_) {
this->pref_ = global_preferences->make_preference<uint32_t>(this->get_preference_hash());
this->pref_ = this->make_entity_preference<uint32_t>();
this->pref_.load(&seconds);
}

View File

@@ -12,6 +12,7 @@ from esphome.const import (
KEY_FRAMEWORK_VERSION,
)
from esphome.core import CORE
from esphome.cpp_generator import add_define
CODEOWNERS = ["@swoboda1337"]
@@ -42,6 +43,7 @@ CONFIG_SCHEMA = cv.All(
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",

View File

@@ -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 <cinttypes>
@@ -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();

View File

@@ -227,8 +227,7 @@ void Fan::publish_state() {
constexpr uint32_t RESTORE_STATE_VERSION = 0x71700ABA;
optional<FanRestoreState> Fan::restore_state_() {
FanRestoreState recovered{};
this->rtc_ =
global_preferences->make_preference<FanRestoreState>(this->get_preference_hash() ^ RESTORE_STATE_VERSION);
this->rtc_ = this->make_entity_preference<FanRestoreState>(RESTORE_STATE_VERSION);
bool restored = this->rtc_.load(&recovered);
switch (this->restore_mode_) {

View File

@@ -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<HaierBaseSettings>(this->get_preference_hash() ^ restore_settings_version);
this->base_rtc_ = this->make_entity_preference<HaierBaseSettings>(restore_settings_version);
HaierBaseSettings recovered;
if (!this->base_rtc_.load(&recovered)) {
recovered = {false, true};

View File

@@ -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<HonSettings>(this->get_preference_hash() ^ restore_settings_version);
this->hon_rtc_ = this->make_entity_preference<HonSettings>(restore_settings_version);
HonSettings recovered;
if (this->hon_rtc_.load(&recovered)) {
this->settings_ = recovered;

View File

@@ -126,7 +126,7 @@ CONFIG_SCHEMA = cv.All(
),
cv.Optional(CONF_CA_CERTIFICATE_PATH): cv.All(
cv.file_,
cv.only_on(PLATFORM_HOST),
cv.Any(cv.only_on(PLATFORM_HOST), cv.only_on_esp32),
),
}
).extend(cv.COMPONENT_SCHEMA),
@@ -160,7 +160,14 @@ async def to_code(config):
cg.add(var.set_verify_ssl(config[CONF_VERIFY_SSL]))
if config.get(CONF_VERIFY_SSL):
esp32.add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True)
if ca_cert_path := config.get(CONF_CA_CERTIFICATE_PATH):
with open(ca_cert_path, encoding="utf-8") as f:
ca_cert_content = f.read()
cg.add(var.set_ca_certificate(ca_cert_content))
else:
esp32.add_idf_sdkconfig_option(
"CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True
)
esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_TLS_INSECURE",

View File

@@ -27,8 +27,9 @@ void HttpRequestIDF::dump_config() {
HttpRequestComponent::dump_config();
ESP_LOGCONFIG(TAG,
" Buffer Size RX: %u\n"
" Buffer Size TX: %u",
this->buffer_size_rx_, this->buffer_size_tx_);
" Buffer Size TX: %u\n"
" Custom CA Certificate: %s",
this->buffer_size_rx_, this->buffer_size_tx_, YESNO(this->ca_certificate_ != nullptr));
}
esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) {
@@ -88,11 +89,15 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
config.disable_auto_redirect = !this->follow_redirects_;
config.max_redirection_count = this->redirect_limit_;
config.auth_type = HTTP_AUTH_TYPE_BASIC;
#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
if (secure && this->verify_ssl_) {
config.crt_bundle_attach = esp_crt_bundle_attach;
}
if (this->ca_certificate_ != nullptr) {
config.cert_pem = this->ca_certificate_;
#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
} else {
config.crt_bundle_attach = esp_crt_bundle_attach;
#endif
}
}
if (this->useragent_ != nullptr) {
config.user_agent = this->useragent_;

View File

@@ -35,6 +35,7 @@ class HttpRequestIDF : public HttpRequestComponent {
void set_buffer_size_rx(uint16_t buffer_size_rx) { this->buffer_size_rx_ = buffer_size_rx; }
void set_buffer_size_tx(uint16_t buffer_size_tx) { this->buffer_size_tx_ = buffer_size_tx; }
void set_verify_ssl(bool verify_ssl) { this->verify_ssl_ = verify_ssl; }
void set_ca_certificate(const char *ca_certificate) { this->ca_certificate_ = ca_certificate; }
protected:
std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body,
@@ -44,6 +45,7 @@ class HttpRequestIDF : public HttpRequestComponent {
uint16_t buffer_size_rx_{};
uint16_t buffer_size_tx_{};
bool verify_ssl_{true};
const char *ca_certificate_{nullptr};
/// @brief Monitors the http client events to gather response headers
static esp_err_t http_event_handler(esp_http_client_event_t *evt);

View File

@@ -82,7 +82,7 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
uint32_t last_progress = 0;
uint32_t update_start_time = millis();
md5::MD5Digest md5_receive;
std::unique_ptr<char[]> md5_receive_str(new char[33]);
char md5_receive_str[33];
if (this->md5_expected_.empty() && !this->http_get_md5_()) {
return OTA_MD5_INVALID;
@@ -176,14 +176,14 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
// verify MD5 is as expected and act accordingly
md5_receive.calculate();
md5_receive.get_hex(md5_receive_str.get());
this->md5_computed_ = md5_receive_str.get();
md5_receive.get_hex(md5_receive_str);
this->md5_computed_ = md5_receive_str;
if (strncmp(this->md5_computed_.c_str(), this->md5_expected_.c_str(), MD5_SIZE) != 0) {
ESP_LOGE(TAG, "MD5 computed: %s - Aborting due to MD5 mismatch", this->md5_computed_.c_str());
this->cleanup_(std::move(backend), container);
return ota::OTA_RESPONSE_ERROR_MD5_MISMATCH;
} else {
backend->set_update_md5(md5_receive_str.get());
backend->set_update_md5(md5_receive_str);
}
container->end();

View File

@@ -10,7 +10,7 @@ static const char *const TAG = "integration";
void IntegrationSensor::setup() {
if (this->restore_) {
this->pref_ = global_preferences->make_preference<float>(this->get_preference_hash());
this->pref_ = this->make_entity_preference<float>();
float preference_value = 0;
this->pref_.load(&preference_value);
this->result_ = preference_value;

View File

@@ -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<float>(this->presence_timeout_number_->get_preference_hash());
this->pref_ = this->presence_timeout_number_->make_entity_preference<float>();
this->set_presence_timeout();
}
#endif

View File

@@ -191,10 +191,17 @@ def _notify_old_style(config):
# The dev and latest branches will be at *least* this version, which is what matters.
# Use GitHub releases directly to avoid PlatformIO moderation delays.
ARDUINO_VERSIONS = {
"dev": (cv.Version(1, 9, 2), "https://github.com/libretiny-eu/libretiny.git"),
"latest": (cv.Version(1, 9, 2), "libretiny"),
"recommended": (cv.Version(1, 9, 2), None),
"dev": (cv.Version(1, 11, 0), "https://github.com/libretiny-eu/libretiny.git"),
"latest": (
cv.Version(1, 11, 0),
"https://github.com/libretiny-eu/libretiny.git#v1.11.0",
),
"recommended": (
cv.Version(1, 11, 0),
"https://github.com/libretiny-eu/libretiny.git#v1.11.0",
),
}

View File

@@ -7,9 +7,7 @@ namespace esphome::light {
class AddressableLightWrapper : public light::AddressableLight {
public:
explicit AddressableLightWrapper(light::LightState *light_state) : light_state_(light_state) {
this->wrapper_state_ = new uint8_t[5]; // NOLINT(cppcoreguidelines-owning-memory)
}
explicit AddressableLightWrapper(light::LightState *light_state) : light_state_(light_state) {}
int32_t size() const override { return 1; }
@@ -118,7 +116,7 @@ class AddressableLightWrapper : public light::AddressableLight {
}
light::LightState *light_state_;
uint8_t *wrapper_state_;
mutable uint8_t wrapper_state_[5]{};
ColorMode color_mode_{ColorMode::UNKNOWN};
};

View File

@@ -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<LightStateRTCState>(this->get_preference_hash());
this->rtc_ = this->make_entity_preference<LightStateRTCState>();
// 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<LightStateRTCState>(this->get_preference_hash());
this->rtc_ = this->make_entity_preference<LightStateRTCState>();
this->rtc_.load(&recovered);
recovered.state = (this->restore_mode_ == LIGHT_RESTORE_AND_ON);
break;

View File

@@ -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<float>(this->get_preference_hash());
this->pref_ = this->make_entity_preference<float>();
if (this->pref_.load(&value)) {
this->control_lambda_(value);
}

View File

@@ -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<size_t>(this->get_preference_hash());
this->pref_ = this->make_entity_preference<size_t>();
if (this->pref_.load(&index))
this->widget_->set_selected_index(index, LV_ANIM_OFF);
}

View File

@@ -3,8 +3,7 @@
#include <cinttypes>
namespace esphome {
namespace mhz19 {
namespace esphome::mhz19 {
static const char *const TAG = "mhz19";
static const uint8_t MHZ19_REQUEST_LENGTH = 8;
@@ -17,6 +16,19 @@ 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};
static const LogString *detection_range_to_log_string(MHZ19DetectionRange range) {
switch (range) {
case MHZ19_DETECTION_RANGE_0_2000PPM:
return LOG_STR("0-2000 ppm");
case MHZ19_DETECTION_RANGE_0_5000PPM:
return LOG_STR("0-5000 ppm");
case MHZ19_DETECTION_RANGE_0_10000PPM:
return LOG_STR("0-10000 ppm");
default:
return LOG_STR("default");
}
}
uint8_t mhz19_checksum(const uint8_t *command) {
uint8_t sum = 0;
for (uint8_t i = 1; i < MHZ19_REQUEST_LENGTH; i++) {
@@ -91,24 +103,24 @@ void MHZ19Component::abc_disable() {
this->mhz19_write_command_(MHZ19_COMMAND_ABC_DISABLE, nullptr);
}
void MHZ19Component::range_set(MHZ19DetectionRange detection_ppm) {
switch (detection_ppm) {
case MHZ19_DETECTION_RANGE_DEFAULT:
ESP_LOGV(TAG, "Using previously set detection range (no change)");
break;
void MHZ19Component::range_set(MHZ19DetectionRange detection_range) {
const uint8_t *command;
switch (detection_range) {
case MHZ19_DETECTION_RANGE_0_2000PPM:
ESP_LOGD(TAG, "Setting detection range to 0 to 2000ppm");
this->mhz19_write_command_(MHZ19_COMMAND_DETECTION_RANGE_0_2000PPM, nullptr);
command = MHZ19_COMMAND_DETECTION_RANGE_0_2000PPM;
break;
case MHZ19_DETECTION_RANGE_0_5000PPM:
ESP_LOGD(TAG, "Setting detection range to 0 to 5000ppm");
this->mhz19_write_command_(MHZ19_COMMAND_DETECTION_RANGE_0_5000PPM, nullptr);
command = MHZ19_COMMAND_DETECTION_RANGE_0_5000PPM;
break;
case MHZ19_DETECTION_RANGE_0_10000PPM:
ESP_LOGD(TAG, "Setting detection range to 0 to 10000ppm");
this->mhz19_write_command_(MHZ19_COMMAND_DETECTION_RANGE_0_10000PPM, nullptr);
command = MHZ19_COMMAND_DETECTION_RANGE_0_10000PPM;
break;
default:
ESP_LOGV(TAG, "Using previously set detection range (no change)");
return;
}
ESP_LOGD(TAG, "Setting detection range to %s", LOG_STR_ARG(detection_range_to_log_string(detection_range)));
this->mhz19_write_command_(command, nullptr);
}
bool MHZ19Component::mhz19_write_command_(const uint8_t *command, uint8_t *response) {
@@ -140,24 +152,7 @@ void MHZ19Component::dump_config() {
}
ESP_LOGCONFIG(TAG, " Warmup time: %" PRIu32 " s", this->warmup_seconds_);
const char *range_str;
switch (this->detection_range_) {
case MHZ19_DETECTION_RANGE_DEFAULT:
range_str = "default";
break;
case MHZ19_DETECTION_RANGE_0_2000PPM:
range_str = "0 to 2000ppm";
break;
case MHZ19_DETECTION_RANGE_0_5000PPM:
range_str = "0 to 5000ppm";
break;
case MHZ19_DETECTION_RANGE_0_10000PPM:
range_str = "0 to 10000ppm";
break;
}
ESP_LOGCONFIG(TAG, " Detection range: %s", range_str);
ESP_LOGCONFIG(TAG, " Detection range: %s", LOG_STR_ARG(detection_range_to_log_string(this->detection_range_)));
}
} // namespace mhz19
} // namespace esphome
} // namespace esphome::mhz19

View File

@@ -5,8 +5,7 @@
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/uart/uart.h"
namespace esphome {
namespace mhz19 {
namespace esphome::mhz19 {
enum MHZ19ABCLogic {
MHZ19_ABC_NONE = 0,
@@ -32,7 +31,7 @@ class MHZ19Component : public PollingComponent, public uart::UARTDevice {
void calibrate_zero();
void abc_enable();
void abc_disable();
void range_set(MHZ19DetectionRange detection_ppm);
void range_set(MHZ19DetectionRange detection_range);
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
void set_co2_sensor(sensor::Sensor *co2_sensor) { co2_sensor_ = co2_sensor; }
@@ -74,5 +73,4 @@ template<typename... Ts> class MHZ19DetectionRangeSetAction : public Action<Ts..
void play(const Ts &...x) override { this->parent_->range_set(this->detection_range_.value(x...)); }
};
} // namespace mhz19
} // namespace esphome
} // namespace esphome::mhz19

View File

@@ -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());
@@ -119,7 +120,8 @@ bool MQTTAlarmControlPanelComponent::publish_state() {
default:
state_s = "unknown";
}
return this->publish(this->get_state_topic_(), state_s);
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
return this->publish(this->get_state_topic_to_(topic_buf), state_s);
}
} // namespace esphome::mqtt

View File

@@ -52,8 +52,9 @@ bool MQTTBinarySensorComponent::publish_state(bool state) {
if (this->binary_sensor_->is_status_binary_sensor())
return true;
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
const char *state_s = state ? "ON" : "OFF";
return this->publish(this->get_state_topic_(), state_s);
return this->publish(this->get_state_topic_to_(topic_buf), state_s);
}
} // namespace esphome::mqtt

View File

@@ -132,17 +132,29 @@ std::string MQTTComponent::get_command_topic_() const {
}
bool MQTTComponent::publish(const std::string &topic, const std::string &payload) {
return this->publish(topic, payload.data(), payload.size());
return this->publish(topic.c_str(), payload.data(), payload.size());
}
bool MQTTComponent::publish(const std::string &topic, const char *payload, size_t payload_length) {
if (topic.empty())
return this->publish(topic.c_str(), payload, payload_length);
}
bool MQTTComponent::publish(const char *topic, const char *payload, size_t payload_length) {
if (topic[0] == '\0')
return false;
return global_mqtt_client->publish(topic, payload, payload_length, this->qos_, this->retain_);
}
bool MQTTComponent::publish(const char *topic, const char *payload) {
return this->publish(topic, payload, strlen(payload));
}
bool MQTTComponent::publish_json(const std::string &topic, const json::json_build_t &f) {
if (topic.empty())
return this->publish_json(topic.c_str(), f);
}
bool MQTTComponent::publish_json(const char *topic, const json::json_build_t &f) {
if (topic[0] == '\0')
return false;
return global_mqtt_client->publish_json(topic, f, this->qos_, this->retain_);
}

View File

@@ -157,6 +157,38 @@ class MQTTComponent : public Component {
*/
bool publish(const std::string &topic, const char *payload, size_t payload_length);
/** Send a MQTT message (no heap allocation for topic).
*
* @param topic The topic as C string.
* @param payload The payload buffer.
* @param payload_length The length of the payload.
*/
bool publish(const char *topic, const char *payload, size_t payload_length);
/** Send a MQTT message (no heap allocation for topic).
*
* @param topic The topic as StringRef (for use with get_state_topic_to_()).
* @param payload The payload buffer.
* @param payload_length The length of the payload.
*/
bool publish(StringRef topic, const char *payload, size_t payload_length) {
return this->publish(topic.c_str(), payload, payload_length);
}
/** Send a MQTT message (no heap allocation for topic).
*
* @param topic The topic as C string.
* @param payload The null-terminated payload.
*/
bool publish(const char *topic, const char *payload);
/** Send a MQTT message (no heap allocation for topic).
*
* @param topic The topic as StringRef (for use with get_state_topic_to_()).
* @param payload The null-terminated payload.
*/
bool publish(StringRef topic, const char *payload) { return this->publish(topic.c_str(), payload); }
/** Construct and send a JSON MQTT message.
*
* @param topic The topic.
@@ -164,6 +196,20 @@ class MQTTComponent : public Component {
*/
bool publish_json(const std::string &topic, const json::json_build_t &f);
/** Construct and send a JSON MQTT message (no heap allocation for topic).
*
* @param topic The topic as C string.
* @param f The Json Message builder.
*/
bool publish_json(const char *topic, const json::json_build_t &f);
/** Construct and send a JSON MQTT message (no heap allocation for topic).
*
* @param topic The topic as StringRef (for use with get_state_topic_to_()).
* @param f The Json Message builder.
*/
bool publish_json(StringRef topic, const json::json_build_t &f) { return this->publish_json(topic.c_str(), f); }
/** Subscribe to a MQTT topic.
*
* @param topic The topic. Wildcards are currently not supported.

View File

@@ -115,7 +115,8 @@ bool MQTTCoverComponent::publish_state() {
: this->cover_->position == COVER_OPEN ? "open"
: traits.get_supports_position() ? "open"
: "unknown";
if (!this->publish(this->get_state_topic_(), state_s))
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
if (!this->publish(this->get_state_topic_to_(topic_buf), state_s))
success = false;
return success;
}

View File

@@ -53,7 +53,8 @@ bool MQTTDateComponent::send_initial_state() {
}
}
bool MQTTDateComponent::publish_state(uint16_t year, uint8_t month, uint8_t day) {
return this->publish_json(this->get_state_topic_(), [year, month, day](JsonObject root) {
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
return this->publish_json(this->get_state_topic_to_(topic_buf), [year, month, day](JsonObject root) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
root[ESPHOME_F("year")] = year;
root[ESPHOME_F("month")] = month;

View File

@@ -66,15 +66,17 @@ bool MQTTDateTimeComponent::send_initial_state() {
}
bool MQTTDateTimeComponent::publish_state(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute,
uint8_t second) {
return this->publish_json(this->get_state_topic_(), [year, month, day, hour, minute, second](JsonObject root) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
root[ESPHOME_F("year")] = year;
root[ESPHOME_F("month")] = month;
root[ESPHOME_F("day")] = day;
root[ESPHOME_F("hour")] = hour;
root[ESPHOME_F("minute")] = minute;
root[ESPHOME_F("second")] = second;
});
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
return this->publish_json(this->get_state_topic_to_(topic_buf),
[year, month, day, hour, minute, second](JsonObject root) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
root[ESPHOME_F("year")] = year;
root[ESPHOME_F("month")] = month;
root[ESPHOME_F("day")] = day;
root[ESPHOME_F("hour")] = hour;
root[ESPHOME_F("minute")] = minute;
root[ESPHOME_F("second")] = second;
});
}
} // namespace esphome::mqtt

View File

@@ -44,7 +44,8 @@ void MQTTEventComponent::dump_config() {
}
bool MQTTEventComponent::publish_event_(const std::string &event_type) {
return this->publish_json(this->get_state_topic_(), [event_type](JsonObject root) {
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
return this->publish_json(this->get_state_topic_to_(topic_buf), [event_type](JsonObject root) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
root[MQTT_EVENT_TYPE] = event_type;
});

View File

@@ -158,9 +158,10 @@ void MQTTFanComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig
}
}
bool MQTTFanComponent::publish_state() {
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
const char *state_s = this->state_->state ? "ON" : "OFF";
ESP_LOGD(TAG, "'%s' Sending state %s.", this->state_->get_name().c_str(), state_s);
this->publish(this->get_state_topic_(), state_s);
this->publish(this->get_state_topic_to_(topic_buf), state_s);
bool failed = false;
if (this->state_->get_traits().supports_direction()) {
bool success = this->publish(this->get_direction_state_topic(),

View File

@@ -34,7 +34,8 @@ void MQTTJSONLightComponent::on_light_remote_values_update() {
MQTTJSONLightComponent::MQTTJSONLightComponent(LightState *state) : state_(state) {}
bool MQTTJSONLightComponent::publish_state_() {
return this->publish_json(this->get_state_topic_(), [this](JsonObject root) {
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
return this->publish_json(this->get_state_topic_to_(topic_buf), [this](JsonObject root) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
LightJSONSchema::dump_json(*this->state_, root);
});

View File

@@ -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());
@@ -47,13 +48,14 @@ void MQTTLockComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfi
bool MQTTLockComponent::send_initial_state() { return this->publish_state(); }
bool MQTTLockComponent::publish_state() {
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
#ifdef USE_STORE_LOG_STR_IN_FLASH
char buf[LOCK_STATE_STR_SIZE];
strncpy_P(buf, (PGM_P) lock_state_to_string(this->lock_->state), sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
return this->publish(this->get_state_topic_(), buf);
return this->publish(this->get_state_topic_to_(topic_buf), buf);
#else
return this->publish(this->get_state_topic_(), LOG_STR_ARG(lock_state_to_string(this->lock_->state)));
return this->publish(this->get_state_topic_to_(topic_buf), LOG_STR_ARG(lock_state_to_string(this->lock_->state)));
#endif
}

View File

@@ -74,9 +74,10 @@ bool MQTTNumberComponent::send_initial_state() {
}
}
bool MQTTNumberComponent::publish_state(float value) {
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
char buffer[64];
buf_append_printf(buffer, sizeof(buffer), 0, "%f", value);
return this->publish(this->get_state_topic_(), buffer);
size_t len = buf_append_printf(buffer, sizeof(buffer), 0, "%f", value);
return this->publish(this->get_state_topic_to_(topic_buf), buffer, len);
}
} // namespace esphome::mqtt

View File

@@ -50,7 +50,8 @@ bool MQTTSelectComponent::send_initial_state() {
}
}
bool MQTTSelectComponent::publish_state(const std::string &value) {
return this->publish(this->get_state_topic_(), value);
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
return this->publish(this->get_state_topic_to_(topic_buf), value.data(), value.size());
}
} // namespace esphome::mqtt

View File

@@ -79,12 +79,13 @@ bool MQTTSensorComponent::send_initial_state() {
}
}
bool MQTTSensorComponent::publish_state(float value) {
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
if (mqtt::global_mqtt_client->is_publish_nan_as_none() && std::isnan(value))
return this->publish(this->get_state_topic_(), "None", 4);
return this->publish(this->get_state_topic_to_(topic_buf), "None", 4);
int8_t accuracy = this->sensor_->get_accuracy_decimals();
char buf[VALUE_ACCURACY_MAX_LEN];
size_t len = value_accuracy_to_buf(buf, value, accuracy);
return this->publish(this->get_state_topic_(), buf, len);
return this->publish(this->get_state_topic_to_(topic_buf), buf, len);
}
} // namespace esphome::mqtt

View File

@@ -52,8 +52,9 @@ void MQTTSwitchComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon
bool MQTTSwitchComponent::send_initial_state() { return this->publish_state(this->switch_->state); }
bool MQTTSwitchComponent::publish_state(bool state) {
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
const char *state_s = state ? "ON" : "OFF";
return this->publish(this->get_state_topic_(), state_s);
return this->publish(this->get_state_topic_to_(topic_buf), state_s);
}
} // namespace esphome::mqtt

View File

@@ -53,7 +53,8 @@ bool MQTTTextComponent::send_initial_state() {
}
}
bool MQTTTextComponent::publish_state(const std::string &value) {
return this->publish(this->get_state_topic_(), value);
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
return this->publish(this->get_state_topic_to_(topic_buf), value.data(), value.size());
}
} // namespace esphome::mqtt

View File

@@ -31,7 +31,10 @@ void MQTTTextSensor::dump_config() {
LOG_MQTT_COMPONENT(true, false);
}
bool MQTTTextSensor::publish_state(const std::string &value) { return this->publish(this->get_state_topic_(), value); }
bool MQTTTextSensor::publish_state(const std::string &value) {
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
return this->publish(this->get_state_topic_to_(topic_buf), value.data(), value.size());
}
bool MQTTTextSensor::send_initial_state() {
if (this->sensor_->has_state()) {
return this->publish_state(this->sensor_->state);

View File

@@ -53,7 +53,8 @@ bool MQTTTimeComponent::send_initial_state() {
}
}
bool MQTTTimeComponent::publish_state(uint8_t hour, uint8_t minute, uint8_t second) {
return this->publish_json(this->get_state_topic_(), [hour, minute, second](JsonObject root) {
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
return this->publish_json(this->get_state_topic_to_(topic_buf), [hour, minute, second](JsonObject root) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
root[ESPHOME_F("hour")] = hour;
root[ESPHOME_F("minute")] = minute;

View File

@@ -28,7 +28,8 @@ void MQTTUpdateComponent::setup() {
}
bool MQTTUpdateComponent::publish_state() {
return this->publish_json(this->get_state_topic_(), [this](JsonObject root) {
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
return this->publish_json(this->get_state_topic_to_(topic_buf), [this](JsonObject root) {
root[ESPHOME_F("installed_version")] = this->update_->update_info.current_version;
root[ESPHOME_F("latest_version")] = this->update_->update_info.latest_version;
root[ESPHOME_F("title")] = this->update_->update_info.title;

View File

@@ -84,7 +84,8 @@ bool MQTTValveComponent::publish_state() {
: this->valve_->position == VALVE_OPEN ? "open"
: traits.get_supports_position() ? "open"
: "unknown";
if (!this->publish(this->get_state_topic_(), state_s))
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
if (!this->publish(this->get_state_topic_to_(topic_buf), state_s))
success = false;
return success;
}

View File

@@ -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<uint8_t> &uid) { this->uid_ = uid; }
void NfcTagBinarySensor::set_uid(const NfcTagUid &uid) { this->uid_ = uid; }
bool NfcTagBinarySensor::tag_match_ndef_string(const std::shared_ptr<NdefMessage> &msg) {
for (const auto &record : msg->get_records()) {
@@ -63,7 +63,7 @@ bool NfcTagBinarySensor::tag_match_tag_name(const std::shared_ptr<NdefMessage> &
return false;
}
bool NfcTagBinarySensor::tag_match_uid(const std::vector<uint8_t> &data) {
bool NfcTagBinarySensor::tag_match_uid(const NfcTagUid &data) {
if (data.size() != this->uid_.size()) {
return false;
}

View File

@@ -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<uint8_t> &uid);
void set_uid(const NfcTagUid &uid);
bool tag_match_ndef_string(const std::shared_ptr<NdefMessage> &msg);
bool tag_match_tag_name(const std::shared_ptr<NdefMessage> &msg);
bool tag_match_uid(const std::vector<uint8_t> &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<uint8_t> uid_;
NfcTagUid uid_;
};
} // namespace nfc

View File

@@ -8,19 +8,23 @@ namespace nfc {
static const char *const TAG = "nfc";
char *format_uid_to(char *buffer, const std::vector<uint8_t> &uid) {
char *format_uid_to(char *buffer, std::span<const uint8_t> uid) {
return format_hex_pretty_to(buffer, FORMAT_UID_BUFFER_SIZE, uid.data(), uid.size(), '-');
}
char *format_bytes_to(char *buffer, const std::vector<uint8_t> &bytes) {
char *format_bytes_to(char *buffer, std::span<const uint8_t> 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<uint8_t> &uid) { return format_hex_pretty(uid, '-', false); } // NOLINT
std::string format_bytes(const std::vector<uint8_t> &bytes) { return format_hex_pretty(bytes, ' ', false); } // NOLINT
std::string format_uid(std::span<const uint8_t> uid) {
return format_hex_pretty(uid.data(), uid.size(), '-', false); // NOLINT
}
std::string format_bytes(std::span<const uint8_t> bytes) {
return format_hex_pretty(bytes.data(), bytes.size(), ' ', false); // NOLINT
}
#pragma GCC diagnostic pop
uint8_t guess_tag_type(uint8_t uid_length) {

View File

@@ -6,6 +6,7 @@
#include "ndef_record.h"
#include "nfc_tag.h"
#include <span>
#include <vector>
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<uint8_t> &uid);
char *format_uid_to(char *buffer, std::span<const uint8_t> 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<uint8_t> &bytes);
char *format_bytes_to(char *buffer, std::span<const uint8_t> 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<uint8_t> &uid);
std::string format_uid(std::span<const uint8_t> 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<uint8_t> &bytes);
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);

View File

@@ -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<uint8_t, NFC_UID_MAX_LENGTH>;
class NfcTag {
public:
NfcTag() {
this->uid_ = {};
this->tag_type_ = "Unknown";
};
NfcTag(std::vector<uint8_t> &uid) {
NfcTag() { this->tag_type_ = "Unknown"; };
NfcTag(const NfcTagUid &uid) {
this->uid_ = uid;
this->tag_type_ = "Unknown";
};
NfcTag(std::vector<uint8_t> &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<uint8_t> &uid, const std::string &tag_type, std::unique_ptr<nfc::NdefMessage> ndef_message) {
NfcTag(const NfcTagUid &uid, const std::string &tag_type, std::unique_ptr<nfc::NdefMessage> ndef_message) {
this->uid_ = uid;
this->tag_type_ = tag_type;
this->ndef_message_ = std::move(ndef_message);
};
NfcTag(std::vector<uint8_t> &uid, const std::string &tag_type, std::vector<uint8_t> &ndef_data) {
NfcTag(const NfcTagUid &uid, const std::string &tag_type, std::vector<uint8_t> &ndef_data) {
this->uid_ = uid;
this->tag_type_ = tag_type;
this->ndef_message_ = make_unique<NdefMessage>(ndef_data);
@@ -41,14 +42,14 @@ class NfcTag {
ndef_message_ = make_unique<NdefMessage>(*rhs.ndef_message_);
}
std::vector<uint8_t> &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<NdefMessage> &get_ndef_message() { return this->ndef_message_; };
void set_ndef_message(std::unique_ptr<NdefMessage> ndef_message) { this->ndef_message_ = std::move(ndef_message); };
protected:
std::vector<uint8_t> uid_;
NfcTagUid uid_;
std::string tag_type_;
std::shared_ptr<NdefMessage> ndef_message_;
};

View File

@@ -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<bool>(myhash);
this->rtc_ = this->parent_->make_entity_preference<bool>(hash.to);
bool initial_state;
if (this->rtc_.load(&initial_state)) {
this->previous_in_range_ = initial_state;

View File

@@ -17,7 +17,7 @@ void OpenthermNumber::setup() {
if (!this->restore_value_) {
value = this->initial_value_;
} else {
this->pref_ = global_preferences->make_preference<float>(this->get_preference_hash());
this->pref_ = this->make_entity_preference<float>();
if (!this->pref_.load(&value)) {
if (!std::isnan(this->initial_value_)) {
value = this->initial_value_;

View File

@@ -168,11 +168,11 @@ void PN532::loop() {
}
uint8_t nfcid_length = read[5];
std::vector<uint8_t> 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<nfc::NfcTag> PN532::read_tag_(std::vector<uint8_t> &uid) {
std::unique_ptr<nfc::NfcTag> 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<uint8_t> &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<uint8_t> &uid) {
return false;
}
bool PN532::format_tag_(std::vector<uint8_t> &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<uint8_t> &uid) {
return false;
}
bool PN532::write_tag_(std::vector<uint8_t> &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<uint8_t> &data) {
bool PN532BinarySensor::process(const nfc::NfcTagUid &data) {
if (data.size() != this->uid_.size())
return false;

View File

@@ -69,28 +69,28 @@ class PN532 : public PollingComponent {
virtual bool read_data(std::vector<uint8_t> &data, uint8_t len) = 0;
virtual bool read_response(uint8_t command, std::vector<uint8_t> &data) = 0;
std::unique_ptr<nfc::NfcTag> read_tag_(std::vector<uint8_t> &uid);
std::unique_ptr<nfc::NfcTag> read_tag_(nfc::NfcTagUid &uid);
bool format_tag_(std::vector<uint8_t> &uid);
bool clean_tag_(std::vector<uint8_t> &uid);
bool write_tag_(std::vector<uint8_t> &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<nfc::NfcTag> read_mifare_classic_tag_(std::vector<uint8_t> &uid);
std::unique_ptr<nfc::NfcTag> read_mifare_classic_tag_(nfc::NfcTagUid &uid);
bool read_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &data);
bool write_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t> &data);
bool auth_mifare_classic_block_(std::vector<uint8_t> &uid, uint8_t block_num, uint8_t key_num, const uint8_t *key);
bool format_mifare_classic_mifare_(std::vector<uint8_t> &uid);
bool format_mifare_classic_ndef_(std::vector<uint8_t> &uid);
bool write_mifare_classic_tag_(std::vector<uint8_t> &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<nfc::NfcTag> read_mifare_ultralight_tag_(std::vector<uint8_t> &uid);
std::unique_ptr<nfc::NfcTag> read_mifare_ultralight_tag_(nfc::NfcTagUid &uid);
bool read_mifare_ultralight_bytes_(uint8_t start_page, uint16_t num_bytes, std::vector<uint8_t> &data);
bool is_mifare_ultralight_formatted_(const std::vector<uint8_t> &page_3_to_6);
uint16_t read_mifare_ultralight_capacity_();
bool find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_to_6, uint8_t &message_length,
uint8_t &message_start_index);
bool write_mifare_ultralight_page_(uint8_t page_num, std::vector<uint8_t> &write_data);
bool write_mifare_ultralight_tag_(std::vector<uint8_t> &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<PN532BinarySensor *> binary_sensors_;
std::vector<nfc::NfcOnTagTrigger *> triggers_ontag_;
std::vector<nfc::NfcOnTagTrigger *> triggers_ontagremoved_;
std::vector<uint8_t> 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<uint8_t> &uid) { uid_ = uid; }
void set_uid(const nfc::NfcTagUid &uid) { uid_ = uid; }
bool process(std::vector<uint8_t> &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<uint8_t> uid_;
nfc::NfcTagUid uid_;
bool found_{false};
};

View File

@@ -8,7 +8,7 @@ namespace pn532 {
static const char *const TAG = "pn532.mifare_classic";
std::unique_ptr<nfc::NfcTag> PN532::read_mifare_classic_tag_(std::vector<uint8_t> &uid) {
std::unique_ptr<nfc::NfcTag> 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<uint8_t> &
return true;
}
bool PN532::auth_mifare_classic_block_(std::vector<uint8_t> &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<uint8_t> data({
PN532_COMMAND_INDATAEXCHANGE,
0x01, // One card
@@ -106,7 +105,7 @@ bool PN532::auth_mifare_classic_block_(std::vector<uint8_t> &uid, uint8_t block_
return true;
}
bool PN532::format_mifare_classic_mifare_(std::vector<uint8_t> &uid) {
bool PN532::format_mifare_classic_mifare_(nfc::NfcTagUid &uid) {
std::vector<uint8_t> blank_buffer(
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00});
std::vector<uint8_t> trailer_buffer(
@@ -141,7 +140,7 @@ bool PN532::format_mifare_classic_mifare_(std::vector<uint8_t> &uid) {
return !error;
}
bool PN532::format_mifare_classic_ndef_(std::vector<uint8_t> &uid) {
bool PN532::format_mifare_classic_ndef_(nfc::NfcTagUid &uid) {
std::vector<uint8_t> empty_ndef_message(
{0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00});
std::vector<uint8_t> blank_block(
@@ -216,7 +215,7 @@ bool PN532::write_mifare_classic_block_(uint8_t block_num, std::vector<uint8_t>
return true;
}
bool PN532::write_mifare_classic_tag_(std::vector<uint8_t> &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();

View File

@@ -8,7 +8,7 @@ namespace pn532 {
static const char *const TAG = "pn532.mifare_ultralight";
std::unique_ptr<nfc::NfcTag> PN532::read_mifare_ultralight_tag_(std::vector<uint8_t> &uid) {
std::unique_ptr<nfc::NfcTag> PN532::read_mifare_ultralight_tag_(nfc::NfcTagUid &uid) {
std::vector<uint8_t> 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<uint8_t> &page_3_to_6
return false;
}
bool PN532::write_mifare_ultralight_tag_(std::vector<uint8_t> &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();

View File

@@ -478,7 +478,7 @@ uint8_t PN7150::read_endpoint_data_(nfc::NfcTag &tag) {
return nfc::STATUS_FAILED;
}
uint8_t PN7150::clean_endpoint_(std::vector<uint8_t> &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<uint8_t> &uid) {
return nfc::STATUS_FAILED;
}
uint8_t PN7150::format_endpoint_(std::vector<uint8_t> &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<uint8_t> &uid) {
return nfc::STATUS_FAILED;
}
uint8_t PN7150::write_endpoint_(std::vector<uint8_t> &uid, std::shared_ptr<nfc::NdefMessage> &message) {
uint8_t PN7150::write_endpoint_(nfc::NfcTagUid &uid, std::shared_ptr<nfc::NdefMessage> &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<nfc::NfcTag> PN7150::build_tag_(const uint8_t mode_tech, const s
ESP_LOGE(TAG, "UID length cannot be zero");
return nullptr;
}
std::vector<uint8_t> 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<nfc::NfcTag>(uid, tag_type_str);
@@ -543,7 +543,7 @@ std::unique_ptr<nfc::NfcTag> PN7150::build_tag_(const uint8_t mode_tech, const s
return nullptr;
}
optional<size_t> PN7150::find_tag_uid_(const std::vector<uint8_t> &uid) {
optional<size_t> 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();

View File

@@ -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<uint8_t> &uid);
uint8_t format_endpoint_(std::vector<uint8_t> &uid);
uint8_t write_endpoint_(std::vector<uint8_t> &uid, std::shared_ptr<nfc::NdefMessage> &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<nfc::NdefMessage> &message);
std::unique_ptr<nfc::NfcTag> build_tag_(uint8_t mode_tech, const std::vector<uint8_t> &data);
optional<size_t> find_tag_uid_(const std::vector<uint8_t> &uid);
optional<size_t> 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<uint8_t> &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<uint8_t> &write_data);
uint8_t write_mifare_ultralight_tag_(std::vector<uint8_t> &uid, const std::shared_ptr<nfc::NdefMessage> &message);
uint8_t write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::shared_ptr<nfc::NdefMessage> &message);
uint8_t clean_mifare_ultralight_();
enum NfcTask : uint8_t {

View File

@@ -115,8 +115,7 @@ uint8_t PN7150::find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_
return nfc::STATUS_FAILED;
}
uint8_t PN7150::write_mifare_ultralight_tag_(std::vector<uint8_t> &uid,
const std::shared_ptr<nfc::NdefMessage> &message) {
uint8_t PN7150::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::shared_ptr<nfc::NdefMessage> &message) {
uint32_t capacity = this->read_mifare_ultralight_capacity_();
auto encoded = message->encode();

View File

@@ -506,7 +506,7 @@ uint8_t PN7160::read_endpoint_data_(nfc::NfcTag &tag) {
return nfc::STATUS_FAILED;
}
uint8_t PN7160::clean_endpoint_(std::vector<uint8_t> &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<uint8_t> &uid) {
return nfc::STATUS_FAILED;
}
uint8_t PN7160::format_endpoint_(std::vector<uint8_t> &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<uint8_t> &uid) {
return nfc::STATUS_FAILED;
}
uint8_t PN7160::write_endpoint_(std::vector<uint8_t> &uid, std::shared_ptr<nfc::NdefMessage> &message) {
uint8_t PN7160::write_endpoint_(nfc::NfcTagUid &uid, std::shared_ptr<nfc::NdefMessage> &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<nfc::NfcTag> PN7160::build_tag_(const uint8_t mode_tech, const s
ESP_LOGE(TAG, "UID length cannot be zero");
return nullptr;
}
std::vector<uint8_t> 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<nfc::NfcTag>(uid, tag_type_str);
@@ -571,7 +571,7 @@ std::unique_ptr<nfc::NfcTag> PN7160::build_tag_(const uint8_t mode_tech, const s
return nullptr;
}
optional<size_t> PN7160::find_tag_uid_(const std::vector<uint8_t> &uid) {
optional<size_t> 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();

View File

@@ -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<uint8_t> &uid);
uint8_t format_endpoint_(std::vector<uint8_t> &uid);
uint8_t write_endpoint_(std::vector<uint8_t> &uid, std::shared_ptr<nfc::NdefMessage> &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<nfc::NdefMessage> &message);
std::unique_ptr<nfc::NfcTag> build_tag_(uint8_t mode_tech, const std::vector<uint8_t> &data);
optional<size_t> find_tag_uid_(const std::vector<uint8_t> &uid);
optional<size_t> 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<uint8_t> &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<uint8_t> &write_data);
uint8_t write_mifare_ultralight_tag_(std::vector<uint8_t> &uid, const std::shared_ptr<nfc::NdefMessage> &message);
uint8_t write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::shared_ptr<nfc::NdefMessage> &message);
uint8_t clean_mifare_ultralight_();
enum NfcTask : uint8_t {

View File

@@ -115,8 +115,7 @@ uint8_t PN7160::find_mifare_ultralight_ndef_(const std::vector<uint8_t> &page_3_
return nfc::STATUS_FAILED;
}
uint8_t PN7160::write_mifare_ultralight_tag_(std::vector<uint8_t> &uid,
const std::shared_ptr<nfc::NdefMessage> &message) {
uint8_t PN7160::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, const std::shared_ptr<nfc::NdefMessage> &message) {
uint32_t capacity = this->read_mifare_ultralight_capacity_();
auto encoded = message->encode();

View File

@@ -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<int32_t>(this->get_preference_hash());
this->rtc_ = this->make_entity_preference<int32_t>();
if (!this->rtc_.load(&initial_value)) {
initial_value = 0;
}

View File

@@ -8,7 +8,6 @@
#include "preferences.h"
#include <cstring>
#include <vector>
#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<class It> 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<uint8_t> 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<PREF_BUFFER_SIZE> 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<uint8_t> buffer;
buffer.resize(len + 1);
const size_t buffer_size = len + 1;
SmallBufferWithHeapFallback<PREF_BUFFER_SIZE> 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;
}
};

View File

@@ -39,7 +39,7 @@ class ValueRangeTrigger : public Trigger<float>, public Component {
template<typename V> void set_max(V max) { this->max_ = max; }
void setup() override {
this->rtc_ = global_preferences->make_preference<bool>(this->parent_->get_preference_hash());
this->rtc_ = this->parent_->make_entity_preference<bool>();
bool initial_state;
if (this->rtc_.load(&initial_state)) {
this->previous_in_range_ = initial_state;

View File

@@ -1,5 +1,6 @@
#pragma once
#include <array>
#include <cstdint>
namespace esphome {
@@ -21,7 +22,7 @@ enum SmlMessageType : uint16_t { SML_PUBLIC_OPEN_RES = 0x0101, SML_GET_LIST_RES
const uint16_t START_MASK = 0x55aa; // 0x1b 1b 1b 1b 01 01 01 01
const uint16_t END_MASK = 0x0157; // 0x1b 1b 1b 1b 1a
const std::vector<uint8_t> START_SEQ = {0x1b, 0x1b, 0x1b, 0x1b, 0x01, 0x01, 0x01, 0x01};
constexpr std::array<uint8_t, 8> START_SEQ = {0x1b, 0x1b, 0x1b, 0x1b, 0x01, 0x01, 0x01, 0x01};
} // namespace sml
} // namespace esphome

View File

@@ -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; });
}

View File

@@ -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<VolumeRestoreState>(this->get_preference_hash());
this->pref_ = this->make_entity_preference<VolumeRestoreState>();
VolumeRestoreState volume_restore_state;
if (this->pref_.load(&volume_restore_state)) {

View File

@@ -16,7 +16,7 @@ void SprinklerControllerNumber::setup() {
if (!this->restore_value_) {
value = this->initial_value_;
} else {
this->pref_ = global_preferences->make_preference<float>(this->get_preference_hash());
this->pref_ = this->make_entity_preference<float>();
if (!this->pref_.load(&value)) {
if (!std::isnan(this->initial_value_)) {
value = this->initial_value_;

View File

@@ -34,7 +34,7 @@ optional<bool> Switch::get_initial_state() {
if (!(restore_mode & RESTORE_MODE_PERSISTENT_MASK))
return {};
this->rtc_ = global_preferences->make_preference<bool>(this->get_preference_hash());
this->rtc_ = this->make_entity_preference<bool>();
bool initial_state;
if (!this->rtc_.load(&initial_state))
return {};

View File

@@ -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<uint8_t>(this->get_preference_hash());
this->pref_ = this->make_entity_preference<uint8_t>();
if (this->pref_.load(&value)) {
this->current_state_ = static_cast<alarm_control_panel::AlarmControlPanelState>(value);
}

View File

@@ -18,8 +18,7 @@ void TemplateDate::setup() {
state = this->initial_value_;
} else {
datetime::DateEntityRestoreState temp;
this->pref_ =
global_preferences->make_preference<datetime::DateEntityRestoreState>(194434030U ^ this->get_preference_hash());
this->pref_ = this->make_entity_preference<datetime::DateEntityRestoreState>(194434030U);
if (this->pref_.load(&temp)) {
temp.apply(this);
return;

View File

@@ -18,8 +18,7 @@ void TemplateDateTime::setup() {
state = this->initial_value_;
} else {
datetime::DateTimeEntityRestoreState temp;
this->pref_ = global_preferences->make_preference<datetime::DateTimeEntityRestoreState>(
194434090U ^ this->get_preference_hash());
this->pref_ = this->make_entity_preference<datetime::DateTimeEntityRestoreState>(194434090U);
if (this->pref_.load(&temp)) {
temp.apply(this);
return;

View File

@@ -18,8 +18,7 @@ void TemplateTime::setup() {
state = this->initial_value_;
} else {
datetime::TimeEntityRestoreState temp;
this->pref_ =
global_preferences->make_preference<datetime::TimeEntityRestoreState>(194434060U ^ this->get_preference_hash());
this->pref_ = this->make_entity_preference<datetime::TimeEntityRestoreState>(194434060U);
if (this->pref_.load(&temp)) {
temp.apply(this);
return;

View File

@@ -13,7 +13,7 @@ void TemplateNumber::setup() {
if (!this->restore_value_) {
value = this->initial_value_;
} else {
this->pref_ = global_preferences->make_preference<float>(this->get_preference_hash());
this->pref_ = this->make_entity_preference<float>();
if (!this->pref_.load(&value)) {
if (!std::isnan(this->initial_value_)) {
value = this->initial_value_;

View File

@@ -11,7 +11,7 @@ void TemplateSelect::setup() {
size_t index = this->initial_option_index_;
if (this->restore_value_) {
this->pref_ = global_preferences->make_preference<size_t>(this->get_preference_hash());
this->pref_ = this->make_entity_preference<size_t>();
size_t restored_index;
if (this->pref_.load(&restored_index) && this->has_index(restored_index)) {
index = restored_index;

View File

@@ -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;

View File

@@ -35,9 +35,6 @@ void TM1638Component::setup() {
this->set_intensity(intensity_);
this->reset_(); // all LEDs off
for (uint8_t i = 0; i < 8; i++) // zero fill print buffer
this->buffer_[i] = 0;
}
void TM1638Component::dump_config() {

View File

@@ -70,7 +70,7 @@ class TM1638Component : public PollingComponent {
GPIOPin *clk_pin_;
GPIOPin *stb_pin_;
GPIOPin *dio_pin_;
uint8_t *buffer_ = new uint8_t[8];
uint8_t buffer_[8]{};
tm1638_writer_t writer_{};
std::vector<KeyListener *> listeners_{};
};

View File

@@ -10,7 +10,7 @@ void TotalDailyEnergy::setup() {
float initial_value = 0;
if (this->restore_) {
this->pref_ = global_preferences->make_preference<float>(this->get_preference_hash());
this->pref_ = this->make_entity_preference<float>();
this->pref_.load(&initial_value);
}
this->publish_state_and_save(initial_value);

View File

@@ -8,7 +8,7 @@ static const char *const TAG = "tuya.number";
void TuyaNumber::setup() {
if (this->restore_value_) {
this->pref_ = global_preferences->make_preference<float>(this->get_preference_hash());
this->pref_ = this->make_entity_preference<float>();
}
this->parent_->register_listener(this->number_id_, [this](const TuyaDatapoint &datapoint) {

View File

@@ -1,11 +1,23 @@
#include "uln2003.h"
#include "esphome/core/log.h"
namespace esphome {
namespace uln2003 {
namespace esphome::uln2003 {
static const char *const TAG = "uln2003.stepper";
static const LogString *step_mode_to_log_string(ULN2003StepMode mode) {
switch (mode) {
case ULN2003_STEP_MODE_FULL_STEP:
return LOG_STR("FULL STEP");
case ULN2003_STEP_MODE_HALF_STEP:
return LOG_STR("HALF STEP");
case ULN2003_STEP_MODE_WAVE_DRIVE:
return LOG_STR("WAVE DRIVE");
default:
return LOG_STR("UNKNOWN");
}
}
void ULN2003::setup() {
this->pin_a_->setup();
this->pin_b_->setup();
@@ -42,22 +54,7 @@ void ULN2003::dump_config() {
LOG_PIN(" Pin B: ", this->pin_b_);
LOG_PIN(" Pin C: ", this->pin_c_);
LOG_PIN(" Pin D: ", this->pin_d_);
const char *step_mode_s;
switch (this->step_mode_) {
case ULN2003_STEP_MODE_FULL_STEP:
step_mode_s = "FULL STEP";
break;
case ULN2003_STEP_MODE_HALF_STEP:
step_mode_s = "HALF STEP";
break;
case ULN2003_STEP_MODE_WAVE_DRIVE:
step_mode_s = "WAVE DRIVE";
break;
default:
step_mode_s = "UNKNOWN";
break;
}
ESP_LOGCONFIG(TAG, " Step Mode: %s", step_mode_s);
ESP_LOGCONFIG(TAG, " Step Mode: %s", LOG_STR_ARG(step_mode_to_log_string(this->step_mode_)));
}
void ULN2003::write_step_(int32_t step) {
int32_t n = this->step_mode_ == ULN2003_STEP_MODE_HALF_STEP ? 8 : 4;
@@ -90,5 +87,4 @@ void ULN2003::write_step_(int32_t step) {
this->pin_d_->digital_write((res >> 3) & 1);
}
} // namespace uln2003
} // namespace esphome
} // namespace esphome::uln2003

View File

@@ -4,8 +4,7 @@
#include "esphome/core/hal.h"
#include "esphome/components/stepper/stepper.h"
namespace esphome {
namespace uln2003 {
namespace esphome::uln2003 {
enum ULN2003StepMode {
ULN2003_STEP_MODE_FULL_STEP,
@@ -40,5 +39,4 @@ class ULN2003 : public stepper::Stepper, public Component {
int32_t current_uln_pos_{0};
};
} // namespace uln2003
} // namespace esphome
} // namespace esphome::uln2003

View File

@@ -2,6 +2,8 @@
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include <strings.h>
namespace esphome {
@@ -38,13 +40,13 @@ Valve::Valve() : position{VALVE_OPEN} {}
ValveCall::ValveCall(Valve *parent) : parent_(parent) {}
ValveCall &ValveCall::set_command(const char *command) {
if (strcasecmp(command, "OPEN") == 0) {
if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("OPEN")) == 0) {
this->set_command_open();
} else if (strcasecmp(command, "CLOSE") == 0) {
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("CLOSE")) == 0) {
this->set_command_close();
} else if (strcasecmp(command, "STOP") == 0) {
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("STOP")) == 0) {
this->set_command_stop();
} else if (strcasecmp(command, "TOGGLE") == 0) {
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("TOGGLE")) == 0) {
this->set_command_toggle();
} else {
ESP_LOGW(TAG, "'%s' - Unrecognized command %s", this->parent_->get_name().c_str(), command);
@@ -161,7 +163,7 @@ void Valve::publish_state(bool save) {
}
}
optional<ValveRestoreState> Valve::restore_state_() {
this->rtc_ = global_preferences->make_preference<ValveRestoreState>(this->get_preference_hash());
this->rtc_ = this->make_entity_preference<ValveRestoreState>();
ValveRestoreState recovered{};
if (!this->rtc_.load(&recovered))
return {};

View File

@@ -185,7 +185,7 @@ void WaterHeater::publish_state() {
}
optional<WaterHeaterCall> WaterHeater::restore_state_() {
this->pref_ = global_preferences->make_preference<SavedWaterHeaterState>(this->get_preference_hash());
this->pref_ = this->make_entity_preference<SavedWaterHeaterState>();
SavedWaterHeaterState recovered{};
if (!this->pref_.load(&recovered))
return {};

View File

@@ -31,7 +31,6 @@ from esphome.const import (
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_LN882X,
PLATFORM_RP2040,
PLATFORM_RTL87XX,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
@@ -214,7 +213,6 @@ CONFIG_SCHEMA = cv.All(
PLATFORM_ESP8266,
PLATFORM_BK72XX,
PLATFORM_LN882X,
PLATFORM_RP2040,
PLATFORM_RTL87XX,
]
),

View File

@@ -47,10 +47,5 @@ async def to_code(config):
cg.add_library("ESP8266WiFi", None)
if CORE.is_libretiny:
CORE.add_platformio_option("lib_ignore", ["ESPAsyncTCP", "RPAsyncTCP"])
if CORE.is_rp2040:
# Ignore bundled AsyncTCP libraries - we use RPAsyncTCP from async_tcp component
CORE.add_platformio_option(
"lib_ignore", ["ESPAsyncTCP", "AsyncTCP", "AsyncTCP_RP2040W"]
)
# https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json
cg.add_library("ESP32Async/ESPAsyncWebServer", "3.9.5")

View File

@@ -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_);

View File

@@ -698,6 +698,10 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
if (!this->wifi_mode_(true, {}))
return false;
// Reset scan_done_ before starting new scan to prevent stale flag from previous scan
// (e.g., roaming scan completed just before unexpected disconnect)
this->scan_done_ = false;
struct scan_config config {};
memset(&config, 0, sizeof(config));
config.ssid = nullptr;

View File

@@ -14,6 +14,7 @@
#include <algorithm>
#include <cinttypes>
#include <memory>
#include <utility>
#ifdef USE_WIFI_WPA2_EAP
#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1)
@@ -828,16 +829,29 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
uint16_t number = it.number;
scan_result_.init(number);
// Process one record at a time to avoid large buffer allocation
wifi_ap_record_t record;
#ifdef USE_ESP32_HOSTED
// getting records one at a time fails on P4 with hosted esp32 WiFi coprocessor
// Presumably an upstream bug, work-around by getting all records at once
auto records = std::make_unique<wifi_ap_record_t[]>(number);
err = esp_wifi_scan_get_ap_records(&number, records.get());
if (err != ESP_OK) {
esp_wifi_clear_ap_list();
ESP_LOGW(TAG, "esp_wifi_scan_get_ap_records failed: %s", esp_err_to_name(err));
return;
}
for (uint16_t i = 0; i < number; i++) {
wifi_ap_record_t &record = records[i];
#else
// Process one record at a time to avoid large buffer allocation
for (uint16_t i = 0; i < number; i++) {
wifi_ap_record_t record;
err = esp_wifi_scan_get_ap_record(&record);
if (err != ESP_OK) {
ESP_LOGW(TAG, "esp_wifi_scan_get_ap_record failed: %s", esp_err_to_name(err));
esp_wifi_clear_ap_list(); // Free remaining records not yet retrieved
break;
}
#endif // USE_ESP32_HOSTED
bssid_t bssid;
std::copy(record.bssid, record.bssid + 6, bssid.begin());
std::string ssid(reinterpret_cast<const char *>(record.ssid));

View File

@@ -649,6 +649,10 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
if (!this->wifi_mode_(true, {}))
return false;
// Reset scan_done_ before starting new scan to prevent stale flag from previous scan
// (e.g., roaming scan completed just before unexpected disconnect)
this->scan_done_ = false;
// need to use WiFi because of WiFiScanClass allocations :(
int16_t err = WiFi.scanNetworks(true, true, passive, 200);
if (err != WIFI_SCAN_RUNNING) {

View File

@@ -88,8 +88,6 @@ bool WiFiComponent::wifi_sta_pre_setup_() { return this->wifi_mode_(true, {}); }
bool WiFiComponent::wifi_sta_ip_config_(const optional<ManualIP> &manual_ip) {
if (!manual_ip.has_value()) {
// Explicitly enable DHCP by passing all zeros - this resets any previous static IP config
WiFi.config(IPAddress(0, 0, 0, 0), IPAddress(0, 0, 0, 0), IPAddress(0, 0, 0, 0), IPAddress(0, 0, 0, 0));
return true;
}
@@ -163,18 +161,19 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
#ifdef USE_WIFI_AP
bool WiFiComponent::wifi_ap_ip_config_(const optional<ManualIP> &manual_ip) {
IPAddress ip_address, gateway, subnet;
esphome::network::IPAddress ip_address, gateway, subnet, dns;
if (manual_ip.has_value()) {
ip_address = IPAddress(manual_ip->static_ip);
gateway = IPAddress(manual_ip->gateway);
subnet = IPAddress(manual_ip->subnet);
ip_address = manual_ip->static_ip;
gateway = manual_ip->gateway;
subnet = manual_ip->subnet;
dns = manual_ip->static_ip;
} else {
ip_address = IPAddress(192, 168, 4, 1);
gateway = IPAddress(192, 168, 4, 1);
subnet = IPAddress(255, 255, 255, 0);
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);
}
// Use softAPConfig for AP mode - WiFi.config() would disable DHCP for STA mode
WiFi.softAPConfig(ip_address, gateway, subnet);
WiFi.config(ip_address, dns, gateway, subnet);
return true;
}
@@ -193,16 +192,12 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
}
#endif
const char *password = ap.get_password().empty() ? nullptr : ap.get_password().c_str();
WiFi.beginAP(ap.get_ssid().c_str(), password, ap.has_channel() ? ap.get_channel() : 1);
WiFi.beginAP(ap.get_ssid().c_str(), ap.get_password().c_str(), ap.has_channel() ? ap.get_channel() : 1);
return true;
}
network::IPAddress WiFiComponent::wifi_soft_ap_ip() {
struct netif *ap_netif = &cyw43_state.netif[CYW43_ITF_AP];
return {&ap_netif->ip_addr};
}
network::IPAddress WiFiComponent::wifi_soft_ap_ip() { return {(const ip_addr_t *) WiFi.localIP()}; }
#endif // USE_WIFI_AP
bool WiFiComponent::wifi_disconnect_() {
@@ -234,29 +229,20 @@ network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() {
network::IPAddresses addresses;
uint8_t index = 0;
for (auto addr : addrList) {
// Only include addresses from the STA interface (CYW43_ITF_STA = 0)
if (addr.ifnumber() == CYW43_ITF_STA) {
addresses[index++] = addr.ipFromNetifNum();
}
addresses[index++] = addr.ipFromNetifNum();
}
return addresses;
}
network::IPAddress WiFiComponent::wifi_subnet_mask_() {
struct netif *sta_netif = &cyw43_state.netif[CYW43_ITF_STA];
return {&sta_netif->netmask};
}
network::IPAddress WiFiComponent::wifi_gateway_ip_() {
struct netif *sta_netif = &cyw43_state.netif[CYW43_ITF_STA];
return {&sta_netif->gw};
}
network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {(const ip_addr_t *) WiFi.subnetMask()}; }
network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {(const ip_addr_t *) WiFi.gatewayIP()}; }
network::IPAddress WiFiComponent::wifi_dns_ip_(int num) {
const ip_addr_t *dns_ip = dns_getserver(num);
return network::IPAddress(dns_ip);
}
void WiFiComponent::wifi_loop_() {
// Handle scan completion - only process once when scan finishes
if (this->state_ == WIFI_COMPONENT_STATE_STA_SCANNING && !this->scan_done_ && !cyw43_wifi_scan_active(&cyw43_state)) {
// Handle scan completion
if (this->state_ == WIFI_COMPONENT_STATE_STA_SCANNING && !cyw43_wifi_scan_active(&cyw43_state)) {
this->scan_done_ = true;
ESP_LOGV(TAG, "Scan done");
#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS

View File

@@ -42,6 +42,7 @@
#define USE_DEVICES
#define USE_DISPLAY
#define USE_ENTITY_ICON
#define USE_ESP32_HOSTED
#define USE_ESP32_IMPROV_STATE_CALLBACK
#define USE_EVENT
#define USE_FAN

View File

@@ -92,6 +92,48 @@ StringRef EntityBase::get_object_id_to(std::span<char, OBJECT_ID_MAX_LEN> 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 "";

View File

@@ -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<T>() instead, or preferences won't be migrated.
* See https://github.com/esphome/backlog/issues/85
*/
ESPDEPRECATED("Use make_entity_preference<T>() 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<typename T> ESPPreferenceObject make_entity_preference(uint32_t version = 0) {
static_assert(std::is_trivially_copyable<T>::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_;

View File

@@ -3,6 +3,7 @@
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include "esphome/core/string_ref.h"
#include <strings.h>
@@ -451,15 +452,15 @@ std::string format_bin(const uint8_t *data, size_t length) {
}
ParseOnOffState parse_on_off(const char *str, const char *on, const char *off) {
if (on == nullptr && strcasecmp(str, "on") == 0)
if (on == nullptr && ESPHOME_strcasecmp_P(str, ESPHOME_PSTR("on")) == 0)
return PARSE_ON;
if (on != nullptr && strcasecmp(str, on) == 0)
return PARSE_ON;
if (off == nullptr && strcasecmp(str, "off") == 0)
if (off == nullptr && ESPHOME_strcasecmp_P(str, ESPHOME_PSTR("off")) == 0)
return PARSE_OFF;
if (off != nullptr && strcasecmp(str, off) == 0)
return PARSE_OFF;
if (strcasecmp(str, "toggle") == 0)
if (ESPHOME_strcasecmp_P(str, ESPHOME_PSTR("toggle")) == 0)
return PARSE_TOGGLE;
return PARSE_NONE;

View File

@@ -148,6 +148,25 @@ template<typename T, size_t N> class StaticVector {
size_t count_{0};
public:
// Default constructor
StaticVector() = default;
// Iterator range constructor
template<typename InputIt> StaticVector(InputIt first, InputIt last) {
while (first != last && count_ < N) {
data_[count_++] = *first++;
}
}
// Initializer list constructor
StaticVector(std::initializer_list<T> 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<typename T, size_t N> class StaticVector {
}
}
// Clear all elements
void clear() { count_ = 0; }
// Assign from iterator range
template<typename InputIt> 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<typename T, size_t N> 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<T>() { return std::span<T>(data_.data(), count_); }
operator std::span<const T>() const { return std::span<const T>(data_.data(), count_); }
};
/// Fixed-capacity vector - allocates once at runtime, never reallocates
@@ -655,9 +689,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

Some files were not shown because too many files have changed in this diff Show More