1
0
mirror of https://github.com/esphome/esphome.git synced 2026-02-10 01:32:06 +00:00

Compare commits

...

28 Commits

Author SHA1 Message Date
J. Nick Koston
2590636e48 Merge branch 'dev' into api-flash-string-progmem 2026-02-09 12:42:08 -06:00
J. Nick Koston
cf7da8e86d fix 2026-02-09 12:34:16 -06:00
J. Nick Koston
e4ea016d1e [ci] Block new std::to_string() usage, suggest snprintf alternatives (#13369) 2026-02-09 12:26:19 -06:00
J. Nick Koston
41a9588d81 [i2c] Replace switch with if-else to avoid CSWTCH table in RAM (#13815) 2026-02-09 12:26:06 -06:00
J. Nick Koston
cd55eb927d [modbus] Batch UART reads to reduce loop overhead (#13822) 2026-02-09 12:21:15 -06:00
J. Nick Koston
4a9ff48f02 [nextion] Batch UART reads to reduce loop overhead (#13823)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-09 12:20:50 -06:00
J. Nick Koston
8fffe7453d [seeed_mr24hpc1/mr60fda2/mr60bha2] Batch UART reads to reduce per-loop overhead (#13825) 2026-02-09 12:18:12 -06:00
J. Nick Koston
a5ee451043 [tuya] Batch UART reads to reduce per-loop overhead (#13827) 2026-02-09 12:17:58 -06:00
J. Nick Koston
e176cf50ab [dfplayer] Batch UART reads to reduce per-loop overhead (#13832) 2026-02-09 12:15:28 -06:00
J. Nick Koston
e7a900fbaa [rf_bridge] Batch UART reads to reduce per-loop overhead (#13831) 2026-02-09 12:15:15 -06:00
J. Nick Koston
623f33c9f9 [rd03d] Batch UART reads to reduce per-loop overhead (#13830) 2026-02-09 12:15:04 -06:00
J. Nick Koston
8b24112be5 [pipsolar] Batch UART reads to reduce per-loop overhead (#13829) 2026-02-09 12:14:48 -06:00
J. Nick Koston
d33f23dc43 [ld2410] Batch UART reads to reduce loop overhead (#13820) 2026-02-09 12:07:55 -06:00
J. Nick Koston
c43d3889b0 [modbus] Use stack buffer instead of heap vector in send() (#13853) 2026-02-09 12:07:42 -06:00
J. Nick Koston
50fe8e51f9 [ld2412] Batch UART reads to reduce loop overhead (#13819) 2026-02-09 12:07:28 -06:00
J. Nick Koston
c7883cb5ae [ld2450] Batch UART reads to reduce loop overhead (#13818) 2026-02-09 12:06:38 -06:00
J. Nick Koston
3b0df145b7 [cse7766] Batch UART reads to reduce loop overhead (#13817) 2026-02-09 12:05:59 -06:00
J. Nick Koston
2383b6b8b4 [core] Deprecate set_retry, cancel_retry, and RetryResult (#13845) 2026-02-09 12:05:32 -06:00
J. Nick Koston
c658d7b57f [api] Merge auth check into base read_message, eliminate APIServerConnection (#13873) 2026-02-09 12:02:02 -06:00
J. Nick Koston
dbef2e24b3 Fix variables returning non-string types, use identity check for std_string
- Revert variables templatable output_type back to None since variable
  lambdas can return any type (float, int, etc.), not just strings.
- Use lazy import identity check (output_type is std_string) instead of
  brittle string comparison in templatable().
- Clarify ESP8266 populate_service_map comment explaining why STATIC_STRING
  fast path is not needed (all codegen strings are FLASH_STRING on ESP8266).
2026-02-09 11:58:55 -06:00
J. Nick Koston
9440138ac7 fix 2026-02-09 11:58:54 -06:00
Jonathan Swoboda
04a6238c7b [esp32] Set UV_CACHE_DIR inside data dir so Clean All clears it (#13888)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 17:49:58 +00:00
J. Nick Koston
919afa1553 [web_server_base] Fix RP2040 compilation when Crypto-no-arduino is present (#13887) 2026-02-09 12:47:59 -05:00
J. Nick Koston
5cf27a8927 Merge branch 'dev' into api-flash-string-progmem 2026-02-09 11:26:46 -06:00
J. Nick Koston
5d5344cf91 Add tests for cg.templatable() auto FlashStringLiteral wrapping
Cover the new automatic ESPHOME_F() wrapping behavior: static strings
with std::string output_type, non-string values, None output_type,
to_exp callable/dict, and lambda passthrough.
2026-02-09 10:56:56 -06:00
J. Nick Koston
b0cf94c409 Auto-wrap static strings in ESPHOME_F() via templatable()
Move FlashStringLiteral wrapping from per-component manual code into
cg.templatable() itself. When output_type is std::string and the value
is a static string (not a lambda), it is automatically wrapped in
ESPHOME_F() for PROGMEM storage on ESP8266. On other platforms
ESPHOME_F() is a no-op returning const char*.

This makes all ~50 existing cg.templatable(..., cg.std_string) call
sites across every component benefit automatically, with no
per-component changes needed.

Simplify api/__init__.py by switching from output_type=None to
cg.std_string and removing the manual isinstance/FlashStringLiteral
checks that are now redundant.
2026-02-09 10:38:41 -06:00
J. Nick Koston
c990da265a Add unit tests for FlashStringLiteral
Cover the three lines reported uncovered by codecov in
cpp_generator.py (FlashStringLiteral.__init__ and __str__).
2026-02-09 07:45:03 -06:00
J. Nick Koston
91487e7f14 [api] Store HomeAssistant action strings in PROGMEM on ESP8266
On ESP8266, .rodata is copied to RAM at boot. Every string literal in
HomeAssistantServiceCallAction (service names, data keys, data values)
permanently consumes RAM even though the action may rarely fire.

Add FLASH_STRING type to TemplatableValue that stores PROGMEM pointers
on ESP8266 via the existing __FlashStringHelper* type. At play() time,
strings are copied from flash to temporary std::string storage — safe
because service calls are not in the hot path.

Add FlashStringLiteral codegen helper (cg.FlashStringLiteral) that wraps
strings in ESPHOME_F() — expands to F() on ESP8266 (PROGMEM), plain
string on other platforms (no-op). This helper can be adopted by other
components incrementally.

On non-ESP8266 platforms, FLASH_STRING is never set and all existing
code paths are unchanged.
2026-02-09 07:39:06 -06:00
36 changed files with 892 additions and 471 deletions

View File

@@ -11,6 +11,7 @@
from esphome.cpp_generator import ( # noqa: F401
ArrayInitializer,
Expression,
FlashStringLiteral,
LineComment,
LogStringLiteral,
MockObj,

View File

@@ -524,24 +524,31 @@ async def homeassistant_service_to_code(
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
serv = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, serv, False)
templ = await cg.templatable(config[CONF_ACTION], args, None)
templ = await cg.templatable(config[CONF_ACTION], args, cg.std_string)
cg.add(var.set_service(templ))
# Initialize FixedVectors with exact sizes from config
cg.add(var.init_data(len(config[CONF_DATA])))
for key, value in config[CONF_DATA].items():
# output_type=None because lambdas can return non-string types (int,
# float, char*) that TemplatableStringValue converts via to_string.
# Static strings are manually wrapped for PROGMEM on ESP8266.
templ = await cg.templatable(value, args, None)
cg.add(var.add_data(key, templ))
if isinstance(templ, str):
templ = cg.FlashStringLiteral(templ)
cg.add(var.add_data(cg.FlashStringLiteral(key), templ))
cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE])))
for key, value in config[CONF_DATA_TEMPLATE].items():
templ = await cg.templatable(value, args, None)
cg.add(var.add_data_template(key, templ))
if isinstance(templ, str):
templ = cg.FlashStringLiteral(templ)
cg.add(var.add_data_template(cg.FlashStringLiteral(key), templ))
cg.add(var.init_variables(len(config[CONF_VARIABLES])))
for key, value in config[CONF_VARIABLES].items():
templ = await cg.templatable(value, args, None)
cg.add(var.add_variable(key, templ))
cg.add(var.add_variable(cg.FlashStringLiteral(key), templ))
if on_error := config.get(CONF_ON_ERROR):
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES")
@@ -609,24 +616,31 @@ async def homeassistant_event_to_code(config, action_id, template_arg, args):
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
serv = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, serv, True)
templ = await cg.templatable(config[CONF_EVENT], args, None)
templ = await cg.templatable(config[CONF_EVENT], args, cg.std_string)
cg.add(var.set_service(templ))
# Initialize FixedVectors with exact sizes from config
cg.add(var.init_data(len(config[CONF_DATA])))
for key, value in config[CONF_DATA].items():
# output_type=None because lambdas can return non-string types (int,
# float, char*) that TemplatableStringValue converts via to_string.
# Static strings are manually wrapped for PROGMEM on ESP8266.
templ = await cg.templatable(value, args, None)
cg.add(var.add_data(key, templ))
if isinstance(templ, str):
templ = cg.FlashStringLiteral(templ)
cg.add(var.add_data(cg.FlashStringLiteral(key), templ))
cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE])))
for key, value in config[CONF_DATA_TEMPLATE].items():
templ = await cg.templatable(value, args, None)
cg.add(var.add_data_template(key, templ))
if isinstance(templ, str):
templ = cg.FlashStringLiteral(templ)
cg.add(var.add_data_template(cg.FlashStringLiteral(key), templ))
cg.add(var.init_variables(len(config[CONF_VARIABLES])))
for key, value in config[CONF_VARIABLES].items():
templ = await cg.templatable(value, args, None)
cg.add(var.add_variable(key, templ))
cg.add(var.add_variable(cg.FlashStringLiteral(key), templ))
return var
@@ -649,11 +663,11 @@ async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, arg
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
serv = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, serv, True)
cg.add(var.set_service("esphome.tag_scanned"))
cg.add(var.set_service(cg.FlashStringLiteral("esphome.tag_scanned")))
# Initialize FixedVector with exact size (1 data field)
cg.add(var.init_data(1))
templ = await cg.templatable(config[CONF_TAG], args, cg.std_string)
cg.add(var.add_data("tag_id", templ))
cg.add(var.add_data(cg.FlashStringLiteral("tag_id"), templ))
return var

View File

@@ -28,7 +28,7 @@ static constexpr size_t MAX_INITIAL_PER_BATCH = 34; // For clients >= AP
static_assert(MAX_MESSAGES_PER_BATCH >= MAX_INITIAL_PER_BATCH,
"MAX_MESSAGES_PER_BATCH must be >= MAX_INITIAL_PER_BATCH");
class APIConnection final : public APIServerConnection {
class APIConnection final : public APIServerConnectionBase {
public:
friend class APIServer;
friend class ListEntitiesIterator;

View File

@@ -21,6 +21,23 @@ void APIServerConnectionBase::log_receive_message_(const LogString *name) {
#endif
void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {
// Check authentication/connection requirements
switch (msg_type) {
case HelloRequest::MESSAGE_TYPE: // No setup required
case DisconnectRequest::MESSAGE_TYPE: // No setup required
case PingRequest::MESSAGE_TYPE: // No setup required
break;
case DeviceInfoRequest::MESSAGE_TYPE: // Connection setup only
if (!this->check_connection_setup_()) {
return;
}
break;
default:
if (!this->check_authenticated_()) {
return;
}
break;
}
switch (msg_type) {
case HelloRequest::MESSAGE_TYPE: {
HelloRequest msg;
@@ -623,28 +640,4 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
}
}
void APIServerConnection::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {
// Check authentication/connection requirements for messages
switch (msg_type) {
case HelloRequest::MESSAGE_TYPE: // No setup required
case DisconnectRequest::MESSAGE_TYPE: // No setup required
case PingRequest::MESSAGE_TYPE: // No setup required
break; // Skip all checks for these messages
case DeviceInfoRequest::MESSAGE_TYPE: // Connection setup only
if (!this->check_connection_setup_()) {
return; // Connection not setup
}
break;
default:
// All other messages require authentication (which includes connection check)
if (!this->check_authenticated_()) {
return; // Authentication failed
}
break;
}
// Call base implementation to process the message
APIServerConnectionBase::read_message(msg_size, msg_type, msg_data);
}
} // namespace esphome::api

View File

@@ -228,9 +228,4 @@ class APIServerConnectionBase : public ProtoService {
void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;
};
class APIServerConnection : public APIServerConnectionBase {
protected:
void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;
};
} // namespace esphome::api

View File

@@ -25,7 +25,9 @@ template<typename... X> class TemplatableStringValue : public TemplatableValue<s
private:
// Helper to convert value to string - handles the case where value is already a string
template<typename T> static std::string value_to_string(T &&val) { return to_string(std::forward<T>(val)); }
template<typename T> static std::string value_to_string(T &&val) {
return to_string(std::forward<T>(val)); // NOLINT
}
// Overloads for string types - needed because std::to_string doesn't support them
static std::string value_to_string(char *val) {
@@ -126,6 +128,20 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
this->add_kv_(this->variables_, key, std::forward<V>(value));
}
#ifdef USE_ESP8266
// On ESP8266, ESPHOME_F() returns __FlashStringHelper* (PROGMEM pointer).
// Store as const char* — populate_service_map copies from PROGMEM at play() time.
template<typename V> void add_data(const __FlashStringHelper *key, V &&value) {
this->add_kv_(this->data_, reinterpret_cast<const char *>(key), std::forward<V>(value));
}
template<typename V> void add_data_template(const __FlashStringHelper *key, V &&value) {
this->add_kv_(this->data_template_, reinterpret_cast<const char *>(key), std::forward<V>(value));
}
template<typename V> void add_variable(const __FlashStringHelper *key, V &&value) {
this->add_kv_(this->variables_, reinterpret_cast<const char *>(key), std::forward<V>(value));
}
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
template<typename T> void set_response_template(T response_template) {
this->response_template_ = response_template;
@@ -217,7 +233,32 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
Ts... x) {
dest.init(source.size());
// Count non-static strings to allocate exact storage needed
#ifdef USE_ESP8266
// On ESP8266, all static strings from codegen are FLASH_STRING (PROGMEM),
// so is_static_string() is always false — the zero-copy STATIC_STRING fast
// path from the non-ESP8266 branch cannot trigger. We copy all keys and
// values unconditionally: keys via _P functions (may be in PROGMEM), values
// via value() which handles FLASH_STRING internally.
value_storage.init(source.size() * 2);
for (auto &it : source) {
auto &kv = dest.emplace_back();
// Key: copy from possible PROGMEM
{
size_t key_len = strlen_P(it.key);
value_storage.push_back(std::string(key_len, '\0'));
memcpy_P(value_storage.back().data(), it.key, key_len);
kv.key = StringRef(value_storage.back());
}
// Value: value() handles FLASH_STRING via _P functions internally
value_storage.push_back(it.value.value(x...));
kv.value = StringRef(value_storage.back());
}
#else
// On non-ESP8266, strings are directly readable from flash-mapped memory.
// Count non-static strings to allocate exact storage needed.
size_t lambda_count = 0;
for (const auto &it : source) {
if (!it.value.is_static_string()) {
@@ -231,14 +272,15 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
kv.key = StringRef(it.key);
if (it.value.is_static_string()) {
// Static string from YAML - zero allocation
// Static string — pointer directly readable, zero allocation
kv.value = StringRef(it.value.get_static_string());
} else {
// Lambda evaluation - store result, reference it
// Lambda evaluate and store result
value_storage.push_back(it.value.value(x...));
kv.value = StringRef(value_storage.back());
}
}
#endif
}
APIServer *parent_;

View File

@@ -7,7 +7,6 @@ namespace esphome {
namespace cse7766 {
static const char *const TAG = "cse7766";
static constexpr size_t CSE7766_RAW_DATA_SIZE = 24;
void CSE7766Component::loop() {
const uint32_t now = App.get_loop_component_start_time();
@@ -16,25 +15,39 @@ void CSE7766Component::loop() {
this->raw_data_index_ = 0;
}
if (this->available() == 0) {
// Early return prevents updating last_transmission_ when no data is available.
int avail = this->available();
if (avail <= 0) {
return;
}
this->last_transmission_ = now;
while (this->available() != 0) {
this->read_byte(&this->raw_data_[this->raw_data_index_]);
if (!this->check_byte_()) {
this->raw_data_index_ = 0;
this->status_set_warning();
continue;
}
if (this->raw_data_index_ == 23) {
this->parse_data_();
this->status_clear_warning();
// Read all available bytes in batches to reduce UART call overhead.
// At 4800 baud (~480 bytes/sec) with ~122 Hz loop rate, typically ~4 bytes per call.
uint8_t buf[CSE7766_RAW_DATA_SIZE];
while (avail > 0) {
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}
avail -= to_read;
this->raw_data_index_ = (this->raw_data_index_ + 1) % 24;
for (size_t i = 0; i < to_read; i++) {
this->raw_data_[this->raw_data_index_] = buf[i];
if (!this->check_byte_()) {
this->raw_data_index_ = 0;
this->status_set_warning();
continue;
}
if (this->raw_data_index_ == CSE7766_RAW_DATA_SIZE - 1) {
this->parse_data_();
this->status_clear_warning();
}
this->raw_data_index_ = (this->raw_data_index_ + 1) % CSE7766_RAW_DATA_SIZE;
}
}
}
@@ -53,14 +66,15 @@ bool CSE7766Component::check_byte_() {
return true;
}
if (index == 23) {
if (index == CSE7766_RAW_DATA_SIZE - 1) {
uint8_t checksum = 0;
for (uint8_t i = 2; i < 23; i++) {
for (uint8_t i = 2; i < CSE7766_RAW_DATA_SIZE - 1; i++) {
checksum += this->raw_data_[i];
}
if (checksum != this->raw_data_[23]) {
ESP_LOGW(TAG, "Invalid checksum from CSE7766: 0x%02X != 0x%02X", checksum, this->raw_data_[23]);
if (checksum != this->raw_data_[CSE7766_RAW_DATA_SIZE - 1]) {
ESP_LOGW(TAG, "Invalid checksum from CSE7766: 0x%02X != 0x%02X", checksum,
this->raw_data_[CSE7766_RAW_DATA_SIZE - 1]);
return false;
}
return true;

View File

@@ -8,6 +8,8 @@
namespace esphome {
namespace cse7766 {
static constexpr size_t CSE7766_RAW_DATA_SIZE = 24;
class CSE7766Component : public Component, public uart::UARTDevice {
public:
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; }
@@ -33,7 +35,7 @@ class CSE7766Component : public Component, public uart::UARTDevice {
this->raw_data_[start_index + 2]);
}
uint8_t raw_data_[24];
uint8_t raw_data_[CSE7766_RAW_DATA_SIZE];
uint8_t raw_data_index_{0};
uint32_t last_transmission_{0};
sensor::Sensor *voltage_sensor_{nullptr};

View File

@@ -1,4 +1,5 @@
#include "dfplayer.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
@@ -131,140 +132,149 @@ void DFPlayer::send_cmd_(uint8_t cmd, uint16_t argument) {
}
void DFPlayer::loop() {
// Read message
while (this->available()) {
uint8_t byte;
this->read_byte(&byte);
if (this->read_pos_ == DFPLAYER_READ_BUFFER_LENGTH)
this->read_pos_ = 0;
switch (this->read_pos_) {
case 0: // Start mark
if (byte != 0x7E)
continue;
break;
case 1: // Version
if (byte != 0xFF) {
ESP_LOGW(TAG, "Expected Version 0xFF, got %#02x", byte);
this->read_pos_ = 0;
continue;
}
break;
case 2: // Buffer length
if (byte != 0x06) {
ESP_LOGW(TAG, "Expected Buffer length 0x06, got %#02x", byte);
this->read_pos_ = 0;
continue;
}
break;
case 9: // End byte
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
char byte_sequence[100];
byte_sequence[0] = '\0';
for (size_t i = 0; i < this->read_pos_ + 1; ++i) {
snprintf(byte_sequence + strlen(byte_sequence), sizeof(byte_sequence) - strlen(byte_sequence), "%02X ",
this->read_buffer_[i]);
}
ESP_LOGVV(TAG, "Received byte sequence: %s", byte_sequence);
#endif
if (byte != 0xEF) {
ESP_LOGW(TAG, "Expected end byte 0xEF, got %#02x", byte);
this->read_pos_ = 0;
continue;
}
// Parse valid received command
uint8_t cmd = this->read_buffer_[3];
uint16_t argument = (this->read_buffer_[5] << 8) | this->read_buffer_[6];
ESP_LOGV(TAG, "Received message cmd: %#02x arg %#04x", cmd, argument);
switch (cmd) {
case 0x3A:
if (argument == 1) {
ESP_LOGI(TAG, "USB loaded");
} else if (argument == 2) {
ESP_LOGI(TAG, "TF Card loaded");
}
break;
case 0x3B:
if (argument == 1) {
ESP_LOGI(TAG, "USB unloaded");
} else if (argument == 2) {
ESP_LOGI(TAG, "TF Card unloaded");
}
break;
case 0x3F:
if (argument == 1) {
ESP_LOGI(TAG, "USB available");
} else if (argument == 2) {
ESP_LOGI(TAG, "TF Card available");
} else if (argument == 3) {
ESP_LOGI(TAG, "USB, TF Card available");
}
break;
case 0x40:
ESP_LOGV(TAG, "Nack");
this->ack_set_is_playing_ = false;
this->ack_reset_is_playing_ = false;
switch (argument) {
case 0x01:
ESP_LOGE(TAG, "Module is busy or uninitialized");
break;
case 0x02:
ESP_LOGE(TAG, "Module is in sleep mode");
break;
case 0x03:
ESP_LOGE(TAG, "Serial receive error");
break;
case 0x04:
ESP_LOGE(TAG, "Checksum incorrect");
break;
case 0x05:
ESP_LOGE(TAG, "Specified track is out of current track scope");
this->is_playing_ = false;
break;
case 0x06:
ESP_LOGE(TAG, "Specified track is not found");
this->is_playing_ = false;
break;
case 0x07:
ESP_LOGE(TAG, "Insertion error (an inserting operation only can be done when a track is being played)");
break;
case 0x08:
ESP_LOGE(TAG, "SD card reading failed (SD card pulled out or damaged)");
break;
case 0x09:
ESP_LOGE(TAG, "Entered into sleep mode");
this->is_playing_ = false;
break;
}
break;
case 0x41:
ESP_LOGV(TAG, "Ack ok");
this->is_playing_ |= this->ack_set_is_playing_;
this->is_playing_ &= !this->ack_reset_is_playing_;
this->ack_set_is_playing_ = false;
this->ack_reset_is_playing_ = false;
break;
case 0x3C:
ESP_LOGV(TAG, "Playback finished (USB drive)");
this->is_playing_ = false;
this->on_finished_playback_callback_.call();
case 0x3D:
ESP_LOGV(TAG, "Playback finished (SD card)");
this->is_playing_ = false;
this->on_finished_playback_callback_.call();
break;
default:
ESP_LOGE(TAG, "Received unknown cmd %#02x arg %#04x", cmd, argument);
}
this->sent_cmd_ = 0;
this->read_pos_ = 0;
continue;
// Read all available bytes in batches to reduce UART call overhead.
int avail = this->available();
uint8_t buf[64];
while (avail > 0) {
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}
avail -= to_read;
for (size_t bi = 0; bi < to_read; bi++) {
uint8_t byte = buf[bi];
if (this->read_pos_ == DFPLAYER_READ_BUFFER_LENGTH)
this->read_pos_ = 0;
switch (this->read_pos_) {
case 0: // Start mark
if (byte != 0x7E)
continue;
break;
case 1: // Version
if (byte != 0xFF) {
ESP_LOGW(TAG, "Expected Version 0xFF, got %#02x", byte);
this->read_pos_ = 0;
continue;
}
break;
case 2: // Buffer length
if (byte != 0x06) {
ESP_LOGW(TAG, "Expected Buffer length 0x06, got %#02x", byte);
this->read_pos_ = 0;
continue;
}
break;
case 9: // End byte
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
char byte_sequence[100];
byte_sequence[0] = '\0';
for (size_t i = 0; i < this->read_pos_ + 1; ++i) {
snprintf(byte_sequence + strlen(byte_sequence), sizeof(byte_sequence) - strlen(byte_sequence), "%02X ",
this->read_buffer_[i]);
}
ESP_LOGVV(TAG, "Received byte sequence: %s", byte_sequence);
#endif
if (byte != 0xEF) {
ESP_LOGW(TAG, "Expected end byte 0xEF, got %#02x", byte);
this->read_pos_ = 0;
continue;
}
// Parse valid received command
uint8_t cmd = this->read_buffer_[3];
uint16_t argument = (this->read_buffer_[5] << 8) | this->read_buffer_[6];
ESP_LOGV(TAG, "Received message cmd: %#02x arg %#04x", cmd, argument);
switch (cmd) {
case 0x3A:
if (argument == 1) {
ESP_LOGI(TAG, "USB loaded");
} else if (argument == 2) {
ESP_LOGI(TAG, "TF Card loaded");
}
break;
case 0x3B:
if (argument == 1) {
ESP_LOGI(TAG, "USB unloaded");
} else if (argument == 2) {
ESP_LOGI(TAG, "TF Card unloaded");
}
break;
case 0x3F:
if (argument == 1) {
ESP_LOGI(TAG, "USB available");
} else if (argument == 2) {
ESP_LOGI(TAG, "TF Card available");
} else if (argument == 3) {
ESP_LOGI(TAG, "USB, TF Card available");
}
break;
case 0x40:
ESP_LOGV(TAG, "Nack");
this->ack_set_is_playing_ = false;
this->ack_reset_is_playing_ = false;
switch (argument) {
case 0x01:
ESP_LOGE(TAG, "Module is busy or uninitialized");
break;
case 0x02:
ESP_LOGE(TAG, "Module is in sleep mode");
break;
case 0x03:
ESP_LOGE(TAG, "Serial receive error");
break;
case 0x04:
ESP_LOGE(TAG, "Checksum incorrect");
break;
case 0x05:
ESP_LOGE(TAG, "Specified track is out of current track scope");
this->is_playing_ = false;
break;
case 0x06:
ESP_LOGE(TAG, "Specified track is not found");
this->is_playing_ = false;
break;
case 0x07:
ESP_LOGE(TAG,
"Insertion error (an inserting operation only can be done when a track is being played)");
break;
case 0x08:
ESP_LOGE(TAG, "SD card reading failed (SD card pulled out or damaged)");
break;
case 0x09:
ESP_LOGE(TAG, "Entered into sleep mode");
this->is_playing_ = false;
break;
}
break;
case 0x41:
ESP_LOGV(TAG, "Ack ok");
this->is_playing_ |= this->ack_set_is_playing_;
this->is_playing_ &= !this->ack_reset_is_playing_;
this->ack_set_is_playing_ = false;
this->ack_reset_is_playing_ = false;
break;
case 0x3C:
ESP_LOGV(TAG, "Playback finished (USB drive)");
this->is_playing_ = false;
this->on_finished_playback_callback_.call();
case 0x3D:
ESP_LOGV(TAG, "Playback finished (SD card)");
this->is_playing_ = false;
this->on_finished_playback_callback_.call();
break;
default:
ESP_LOGE(TAG, "Received unknown cmd %#02x arg %#04x", cmd, argument);
}
this->sent_cmd_ = 0;
this->read_pos_ = 0;
continue;
}
this->read_buffer_[this->read_pos_] = byte;
this->read_pos_++;
}
this->read_buffer_[this->read_pos_] = byte;
this->read_pos_++;
}
}
void DFPlayer::dump_config() {

View File

@@ -1435,6 +1435,10 @@ async def to_code(config):
CORE.relative_internal_path(".espressif")
)
# Set the uv cache inside the data dir so "Clean All" clears it.
# Avoids persistent corrupted cache from mid-stream download failures.
os.environ["UV_CACHE_DIR"] = str(CORE.relative_internal_path(".uv_cache"))
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
cg.add_build_flag("-DUSE_ESP_IDF")
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF")

View File

@@ -48,7 +48,7 @@ class ESPBTUUID {
// Remove before 2026.8.0
ESPDEPRECATED("Use to_str() instead. Removed in 2026.8.0", "2026.2.0")
std::string to_string() const;
std::string to_string() const; // NOLINT
const char *to_str(std::span<char, UUID_STR_LEN> output) const;
protected:

View File

@@ -134,25 +134,23 @@ ErrorCode ArduinoI2CBus::write_readv(uint8_t address, const uint8_t *write_buffe
for (size_t j = 0; j != read_count; j++)
read_buffer[j] = wire_->read();
}
switch (status) {
case 0:
return ERROR_OK;
case 1:
// transmit buffer not large enough
ESP_LOGVV(TAG, "TX failed: buffer not large enough");
return ERROR_UNKNOWN;
case 2:
case 3:
ESP_LOGVV(TAG, "TX failed: not acknowledged: %d", status);
return ERROR_NOT_ACKNOWLEDGED;
case 5:
ESP_LOGVV(TAG, "TX failed: timeout");
return ERROR_UNKNOWN;
case 4:
default:
ESP_LOGVV(TAG, "TX failed: unknown error %u", status);
return ERROR_UNKNOWN;
// Avoid switch to prevent compiler-generated lookup table in RAM on ESP8266
if (status == 0)
return ERROR_OK;
if (status == 1) {
ESP_LOGVV(TAG, "TX failed: buffer not large enough");
return ERROR_UNKNOWN;
}
if (status == 2 || status == 3) {
ESP_LOGVV(TAG, "TX failed: not acknowledged: %u", status);
return ERROR_NOT_ACKNOWLEDGED;
}
if (status == 5) {
ESP_LOGVV(TAG, "TX failed: timeout");
return ERROR_UNKNOWN;
}
ESP_LOGVV(TAG, "TX failed: unknown error %u", status);
return ERROR_UNKNOWN;
}
/// Perform I2C bus recovery, see:

View File

@@ -275,8 +275,19 @@ void LD2410Component::restart_and_read_all_info() {
}
void LD2410Component::loop() {
while (this->available()) {
this->readline_(this->read());
// Read all available bytes in batches to reduce UART call overhead.
int avail = this->available();
uint8_t buf[MAX_LINE_LENGTH];
while (avail > 0) {
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}
avail -= to_read;
for (size_t i = 0; i < to_read; i++) {
this->readline_(buf[i]);
}
}
}

View File

@@ -310,8 +310,19 @@ void LD2412Component::restart_and_read_all_info() {
}
void LD2412Component::loop() {
while (this->available()) {
this->readline_(this->read());
// Read all available bytes in batches to reduce UART call overhead.
int avail = this->available();
uint8_t buf[MAX_LINE_LENGTH];
while (avail > 0) {
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}
avail -= to_read;
for (size_t i = 0; i < to_read; i++) {
this->readline_(buf[i]);
}
}
}

View File

@@ -276,8 +276,19 @@ void LD2450Component::dump_config() {
}
void LD2450Component::loop() {
while (this->available()) {
this->readline_(this->read());
// Read all available bytes in batches to reduce UART call overhead.
int avail = this->available();
uint8_t buf[MAX_LINE_LENGTH];
while (avail > 0) {
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}
avail -= to_read;
for (size_t i = 0; i < to_read; i++) {
this->readline_(buf[i]);
}
}
}

View File

@@ -19,16 +19,25 @@ void Modbus::setup() {
void Modbus::loop() {
const uint32_t now = App.get_loop_component_start_time();
while (this->available()) {
uint8_t byte;
this->read_byte(&byte);
if (this->parse_modbus_byte_(byte)) {
this->last_modbus_byte_ = now;
} else {
size_t at = this->rx_buffer_.size();
if (at > 0) {
ESP_LOGV(TAG, "Clearing buffer of %d bytes - parse failed", at);
this->rx_buffer_.clear();
// Read all available bytes in batches to reduce UART call overhead.
int avail = this->available();
uint8_t buf[64];
while (avail > 0) {
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}
avail -= to_read;
for (size_t i = 0; i < to_read; i++) {
if (this->parse_modbus_byte_(buf[i])) {
this->last_modbus_byte_ = now;
} else {
size_t at = this->rx_buffer_.size();
if (at > 0) {
ESP_LOGV(TAG, "Clearing buffer of %d bytes - parse failed", at);
this->rx_buffer_.clear();
}
}
}
}
@@ -219,39 +228,50 @@ void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address
return;
}
std::vector<uint8_t> data;
data.push_back(address);
data.push_back(function_code);
static constexpr size_t ADDR_SIZE = 1;
static constexpr size_t FC_SIZE = 1;
static constexpr size_t START_ADDR_SIZE = 2;
static constexpr size_t NUM_ENTITIES_SIZE = 2;
static constexpr size_t BYTE_COUNT_SIZE = 1;
static constexpr size_t MAX_PAYLOAD_SIZE = std::numeric_limits<uint8_t>::max();
static constexpr size_t CRC_SIZE = 2;
static constexpr size_t MAX_FRAME_SIZE =
ADDR_SIZE + FC_SIZE + START_ADDR_SIZE + NUM_ENTITIES_SIZE + BYTE_COUNT_SIZE + MAX_PAYLOAD_SIZE + CRC_SIZE;
uint8_t data[MAX_FRAME_SIZE];
size_t pos = 0;
data[pos++] = address;
data[pos++] = function_code;
if (this->role == ModbusRole::CLIENT) {
data.push_back(start_address >> 8);
data.push_back(start_address >> 0);
data[pos++] = start_address >> 8;
data[pos++] = start_address >> 0;
if (function_code != ModbusFunctionCode::WRITE_SINGLE_COIL &&
function_code != ModbusFunctionCode::WRITE_SINGLE_REGISTER) {
data.push_back(number_of_entities >> 8);
data.push_back(number_of_entities >> 0);
data[pos++] = number_of_entities >> 8;
data[pos++] = number_of_entities >> 0;
}
}
if (payload != nullptr) {
if (this->role == ModbusRole::SERVER || function_code == ModbusFunctionCode::WRITE_MULTIPLE_COILS ||
function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) { // Write multiple
data.push_back(payload_len); // Byte count is required for write
data[pos++] = payload_len; // Byte count is required for write
} else {
payload_len = 2; // Write single register or coil
}
for (int i = 0; i < payload_len; i++) {
data.push_back(payload[i]);
data[pos++] = payload[i];
}
}
auto crc = crc16(data.data(), data.size());
data.push_back(crc >> 0);
data.push_back(crc >> 8);
auto crc = crc16(data, pos);
data[pos++] = crc >> 0;
data[pos++] = crc >> 8;
if (this->flow_control_pin_ != nullptr)
this->flow_control_pin_->digital_write(true);
this->write_array(data);
this->write_array(data, pos);
this->flush();
if (this->flow_control_pin_ != nullptr)
@@ -261,7 +281,7 @@ void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)];
#endif
ESP_LOGV(TAG, "Modbus write: %s", format_hex_pretty_to(hex_buf, data.data(), data.size()));
ESP_LOGV(TAG, "Modbus write: %s", format_hex_pretty_to(hex_buf, data, pos));
}
// Helper function for lambdas

View File

@@ -397,11 +397,17 @@ bool Nextion::remove_from_q_(bool report_empty) {
}
void Nextion::process_serial_() {
uint8_t d;
// Read all available bytes in batches to reduce UART call overhead.
int avail = this->available();
uint8_t buf[64];
while (avail > 0) {
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}
avail -= to_read;
while (this->available()) {
read_byte(&d);
this->command_data_ += d;
this->command_data_.append(reinterpret_cast<const char *>(buf), to_read);
}
}
// nextion.tech/instruction-set/

View File

@@ -13,9 +13,12 @@ void Pipsolar::setup() {
}
void Pipsolar::empty_uart_buffer_() {
uint8_t byte;
while (this->available()) {
this->read_byte(&byte);
uint8_t buf[64];
int avail;
while ((avail = this->available()) > 0) {
if (!this->read_array(buf, std::min(static_cast<size_t>(avail), sizeof(buf)))) {
break;
}
}
}
@@ -94,32 +97,47 @@ void Pipsolar::loop() {
}
if (this->state_ == STATE_COMMAND || this->state_ == STATE_POLL) {
while (this->available()) {
uint8_t byte;
this->read_byte(&byte);
// make sure data and null terminator fit in buffer
if (this->read_pos_ >= PIPSOLAR_READ_BUFFER_LENGTH - 1) {
this->read_pos_ = 0;
this->empty_uart_buffer_();
ESP_LOGW(TAG, "response data too long, discarding.");
int avail = this->available();
while (avail > 0) {
uint8_t buf[64];
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}
this->read_buffer_[this->read_pos_] = byte;
this->read_pos_++;
avail -= to_read;
bool done = false;
for (size_t i = 0; i < to_read; i++) {
uint8_t byte = buf[i];
// end of answer
if (byte == 0x0D) {
this->read_buffer_[this->read_pos_] = 0;
this->empty_uart_buffer_();
if (this->state_ == STATE_POLL) {
this->state_ = STATE_POLL_COMPLETE;
// make sure data and null terminator fit in buffer
if (this->read_pos_ >= PIPSOLAR_READ_BUFFER_LENGTH - 1) {
this->read_pos_ = 0;
this->empty_uart_buffer_();
ESP_LOGW(TAG, "response data too long, discarding.");
done = true;
break;
}
if (this->state_ == STATE_COMMAND) {
this->state_ = STATE_COMMAND_COMPLETE;
this->read_buffer_[this->read_pos_] = byte;
this->read_pos_++;
// end of answer
if (byte == 0x0D) {
this->read_buffer_[this->read_pos_] = 0;
this->empty_uart_buffer_();
if (this->state_ == STATE_POLL) {
this->state_ = STATE_POLL_COMPLETE;
}
if (this->state_ == STATE_COMMAND) {
this->state_ = STATE_COMMAND_COMPLETE;
}
done = true;
break;
}
}
} // available
if (done) {
break;
}
}
}
if (this->state_ == STATE_COMMAND) {
if (millis() - this->command_start_millis_ > esphome::pipsolar::Pipsolar::COMMAND_TIMEOUT) {

View File

@@ -1,4 +1,5 @@
#include "rd03d.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <cmath>
@@ -80,37 +81,47 @@ void RD03DComponent::dump_config() {
}
void RD03DComponent::loop() {
while (this->available()) {
uint8_t byte = this->read();
ESP_LOGVV(TAG, "Received byte: 0x%02X, buffer_pos: %d", byte, this->buffer_pos_);
// Read all available bytes in batches to reduce UART call overhead.
int avail = this->available();
uint8_t buf[64];
while (avail > 0) {
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}
avail -= to_read;
for (size_t i = 0; i < to_read; i++) {
uint8_t byte = buf[i];
ESP_LOGVV(TAG, "Received byte: 0x%02X, buffer_pos: %d", byte, this->buffer_pos_);
// Check if we're looking for frame header
if (this->buffer_pos_ < FRAME_HEADER_SIZE) {
if (byte == FRAME_HEADER[this->buffer_pos_]) {
this->buffer_[this->buffer_pos_++] = byte;
} else if (byte == FRAME_HEADER[0]) {
// Start over if we see a potential new header
this->buffer_[0] = byte;
this->buffer_pos_ = 1;
} else {
// Check if we're looking for frame header
if (this->buffer_pos_ < FRAME_HEADER_SIZE) {
if (byte == FRAME_HEADER[this->buffer_pos_]) {
this->buffer_[this->buffer_pos_++] = byte;
} else if (byte == FRAME_HEADER[0]) {
// Start over if we see a potential new header
this->buffer_[0] = byte;
this->buffer_pos_ = 1;
} else {
this->buffer_pos_ = 0;
}
continue;
}
// Accumulate data bytes
this->buffer_[this->buffer_pos_++] = byte;
// Check if we have a complete frame
if (this->buffer_pos_ == FRAME_SIZE) {
// Validate footer
if (this->buffer_[FRAME_SIZE - 2] == FRAME_FOOTER[0] && this->buffer_[FRAME_SIZE - 1] == FRAME_FOOTER[1]) {
this->process_frame_();
} else {
ESP_LOGW(TAG, "Invalid frame footer: 0x%02X 0x%02X (expected 0x55 0xCC)", this->buffer_[FRAME_SIZE - 2],
this->buffer_[FRAME_SIZE - 1]);
}
this->buffer_pos_ = 0;
}
continue;
}
// Accumulate data bytes
this->buffer_[this->buffer_pos_++] = byte;
// Check if we have a complete frame
if (this->buffer_pos_ == FRAME_SIZE) {
// Validate footer
if (this->buffer_[FRAME_SIZE - 2] == FRAME_FOOTER[0] && this->buffer_[FRAME_SIZE - 1] == FRAME_FOOTER[1]) {
this->process_frame_();
} else {
ESP_LOGW(TAG, "Invalid frame footer: 0x%02X 0x%02X (expected 0x55 0xCC)", this->buffer_[FRAME_SIZE - 2],
this->buffer_[FRAME_SIZE - 1]);
}
this->buffer_pos_ = 0;
}
}
}

View File

@@ -136,14 +136,21 @@ void RFBridgeComponent::loop() {
this->last_bridge_byte_ = now;
}
while (this->available()) {
uint8_t byte;
this->read_byte(&byte);
if (this->parse_bridge_byte_(byte)) {
ESP_LOGVV(TAG, "Parsed: 0x%02X", byte);
this->last_bridge_byte_ = now;
} else {
this->rx_buffer_.clear();
int avail = this->available();
while (avail > 0) {
uint8_t buf[64];
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}
avail -= to_read;
for (size_t i = 0; i < to_read; i++) {
if (this->parse_bridge_byte_(buf[i])) {
ESP_LOGVV(TAG, "Parsed: 0x%02X", buf[i]);
this->last_bridge_byte_ = now;
} else {
this->rx_buffer_.clear();
}
}
}
}

View File

@@ -106,12 +106,19 @@ void MR24HPC1Component::update_() {
// main loop
void MR24HPC1Component::loop() {
uint8_t byte;
// Read all available bytes in batches to reduce UART call overhead.
int avail = this->available();
uint8_t buf[64];
while (avail > 0) {
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}
avail -= to_read;
// Is there data on the serial port
while (this->available()) {
this->read_byte(&byte);
this->r24_split_data_frame_(byte); // split data frame
for (size_t i = 0; i < to_read; i++) {
this->r24_split_data_frame_(buf[i]); // split data frame
}
}
if ((this->s_output_info_switch_flag_ == OUTPUT_SWTICH_OFF) &&

View File

@@ -30,14 +30,21 @@ void MR60BHA2Component::dump_config() {
// main loop
void MR60BHA2Component::loop() {
uint8_t byte;
// Read all available bytes in batches to reduce UART call overhead.
int avail = this->available();
uint8_t buf[64];
while (avail > 0) {
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}
avail -= to_read;
// Is there data on the serial port
while (this->available()) {
this->read_byte(&byte);
this->rx_message_.push_back(byte);
if (!this->validate_message_()) {
this->rx_message_.clear();
for (size_t i = 0; i < to_read; i++) {
this->rx_message_.push_back(buf[i]);
if (!this->validate_message_()) {
this->rx_message_.clear();
}
}
}
}

View File

@@ -49,12 +49,19 @@ void MR60FDA2Component::setup() {
// main loop
void MR60FDA2Component::loop() {
uint8_t byte;
// Read all available bytes in batches to reduce UART call overhead.
int avail = this->available();
uint8_t buf[64];
while (avail > 0) {
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}
avail -= to_read;
// Is there data on the serial port
while (this->available()) {
this->read_byte(&byte);
this->split_frame_(byte); // split data frame
for (size_t i = 0; i < to_read; i++) {
this->split_frame_(buf[i]); // split data frame
}
}
}

View File

@@ -31,10 +31,19 @@ void Tuya::setup() {
}
void Tuya::loop() {
while (this->available()) {
uint8_t c;
this->read_byte(&c);
this->handle_char_(c);
// Read all available bytes in batches to reduce UART call overhead.
int avail = this->available();
uint8_t buf[64];
while (avail > 0) {
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}
avail -= to_read;
for (size_t i = 0; i < to_read; i++) {
this->handle_char_(buf[i]);
}
}
process_command_queue_();
}

View File

@@ -83,7 +83,7 @@ struct Timer {
}
// Remove before 2026.8.0
ESPDEPRECATED("Use to_str() instead. Removed in 2026.8.0", "2026.2.0")
std::string to_string() const {
std::string to_string() const { // NOLINT
char buffer[TO_STR_BUFFER_SIZE];
return this->to_str(buffer);
}

View File

@@ -1,8 +1,11 @@
from pathlib import Path
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.core import CORE, coroutine_with_priority
from esphome.coroutine import CoroPriority
from esphome.helpers import copy_file_if_changed
CODEOWNERS = ["@esphome/core"]
DEPENDENCIES = ["network"]
@@ -49,5 +52,15 @@ async def to_code(config):
CORE.add_platformio_option(
"lib_ignore", ["ESPAsyncTCP", "AsyncTCP", "AsyncTCP_RP2040W"]
)
# ESPAsyncWebServer uses Hash library for sha1() on RP2040
cg.add_library("Hash", None)
# Fix Hash.h include conflict: Crypto-no-arduino (used by dsmr)
# provides a Hash.h that shadows the framework's Hash library.
# Prepend the framework Hash path so it's found first.
copy_file_if_changed(
Path(__file__).parent / "fix_rp2040_hash.py.script",
CORE.relative_build_path("fix_rp2040_hash.py"),
)
cg.add_platformio_option("extra_scripts", ["pre:fix_rp2040_hash.py"])
# https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json
cg.add_library("ESP32Async/ESPAsyncWebServer", "3.9.6")

View File

@@ -0,0 +1,11 @@
# ESPAsyncWebServer includes <Hash.h> expecting the Arduino-Pico framework's Hash
# library (which provides sha1() functions). However, the Crypto-no-arduino library
# (used by dsmr) also provides a Hash.h that can shadow the framework version when
# PlatformIO's chain+ LDF mode auto-discovers it as a dependency.
# Prepend the framework Hash path to CXXFLAGS so it is found first.
import os
Import("env")
framework_dir = env.PioPlatform().get_package_dir("framework-arduinopico")
hash_src = os.path.join(framework_dir, "libraries", "Hash", "src")
env.Prepend(CXXFLAGS=["-I" + hash_src])

View File

@@ -4,6 +4,7 @@
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/preferences.h"
#include "esphome/core/progmem.h"
#include "esphome/core/string_ref.h"
#include <concepts>
#include <functional>
@@ -56,6 +57,16 @@ template<typename T, typename... X> class TemplatableValue {
this->static_str_ = str;
}
#ifdef USE_ESP8266
// On ESP8266, __FlashStringHelper* is a distinct type from const char*.
// ESPHOME_F(s) expands to F(s) which returns __FlashStringHelper* pointing to PROGMEM.
// Store as FLASH_STRING — value()/is_empty()/ref_or_copy_to() use _P functions
// to access the PROGMEM pointer safely.
TemplatableValue(const __FlashStringHelper *str) requires std::same_as<T, std::string> : type_(FLASH_STRING) {
this->static_str_ = reinterpret_cast<const char *>(str);
}
#endif
template<typename F> TemplatableValue(F value) requires(!std::invocable<F, X...>) : type_(VALUE) {
if constexpr (USE_HEAP_STORAGE) {
this->value_ = new T(std::move(value));
@@ -89,7 +100,7 @@ template<typename T, typename... X> class TemplatableValue {
this->f_ = new std::function<T(X...)>(*other.f_);
} else if (this->type_ == STATELESS_LAMBDA) {
this->stateless_f_ = other.stateless_f_;
} else if (this->type_ == STATIC_STRING) {
} else if (this->type_ == STATIC_STRING || this->type_ == FLASH_STRING) {
this->static_str_ = other.static_str_;
}
}
@@ -108,7 +119,7 @@ template<typename T, typename... X> class TemplatableValue {
other.f_ = nullptr;
} else if (this->type_ == STATELESS_LAMBDA) {
this->stateless_f_ = other.stateless_f_;
} else if (this->type_ == STATIC_STRING) {
} else if (this->type_ == STATIC_STRING || this->type_ == FLASH_STRING) {
this->static_str_ = other.static_str_;
}
other.type_ = NONE;
@@ -141,7 +152,7 @@ template<typename T, typename... X> class TemplatableValue {
} else if (this->type_ == LAMBDA) {
delete this->f_;
}
// STATELESS_LAMBDA/STATIC_STRING/NONE: no cleanup needed (pointers, not heap-allocated)
// STATELESS_LAMBDA/STATIC_STRING/FLASH_STRING/NONE: no cleanup needed (pointers, not heap-allocated)
}
bool has_value() const { return this->type_ != NONE; }
@@ -165,6 +176,17 @@ template<typename T, typename... X> class TemplatableValue {
return std::string(this->static_str_);
}
__builtin_unreachable();
#ifdef USE_ESP8266
case FLASH_STRING:
// PROGMEM pointer — must use _P functions to access on ESP8266
if constexpr (std::same_as<T, std::string>) {
size_t len = strlen_P(this->static_str_);
std::string result(len, '\0');
memcpy_P(result.data(), this->static_str_, len);
return result;
}
__builtin_unreachable();
#endif
case NONE:
default:
return T{};
@@ -186,9 +208,12 @@ template<typename T, typename... X> class TemplatableValue {
}
/// Check if this holds a static string (const char* stored without allocation)
/// The pointer is always directly readable (RAM or flash-mapped).
/// Returns false for FLASH_STRING (PROGMEM on ESP8266, requires _P functions).
bool is_static_string() const { return this->type_ == STATIC_STRING; }
/// Get the static string pointer (only valid if is_static_string() returns true)
/// The pointer is always directly readable — FLASH_STRING uses a separate type.
const char *get_static_string() const { return this->static_str_; }
/// Check if the string value is empty without allocating (for std::string specialization).
@@ -200,6 +225,12 @@ template<typename T, typename... X> class TemplatableValue {
return true;
case STATIC_STRING:
return this->static_str_ == nullptr || this->static_str_[0] == '\0';
#ifdef USE_ESP8266
case FLASH_STRING:
// PROGMEM pointer — must use progmem_read_byte on ESP8266
return this->static_str_ == nullptr ||
progmem_read_byte(reinterpret_cast<const uint8_t *>(this->static_str_)) == '\0';
#endif
case VALUE:
return this->value_->empty();
default: // LAMBDA/STATELESS_LAMBDA - must call value()
@@ -209,8 +240,9 @@ template<typename T, typename... X> class TemplatableValue {
/// Get a StringRef to the string value without heap allocation when possible.
/// For STATIC_STRING/VALUE, returns reference to existing data (no allocation).
/// For FLASH_STRING (ESP8266 PROGMEM), copies to provided buffer via _P functions.
/// For LAMBDA/STATELESS_LAMBDA, calls value(), copies to provided buffer, returns ref to buffer.
/// @param lambda_buf Buffer used only for lambda case (must remain valid while StringRef is used).
/// @param lambda_buf Buffer used only for copy cases (must remain valid while StringRef is used).
/// @param lambda_buf_size Size of the buffer.
/// @return StringRef pointing to the string data.
StringRef ref_or_copy_to(char *lambda_buf, size_t lambda_buf_size) const requires std::same_as<T, std::string> {
@@ -221,6 +253,19 @@ template<typename T, typename... X> class TemplatableValue {
if (this->static_str_ == nullptr)
return StringRef();
return StringRef(this->static_str_, strlen(this->static_str_));
#ifdef USE_ESP8266
case FLASH_STRING:
if (this->static_str_ == nullptr)
return StringRef();
{
// PROGMEM pointer — copy to buffer via _P functions
size_t len = strlen_P(this->static_str_);
size_t copy_len = std::min(len, lambda_buf_size - 1);
memcpy_P(lambda_buf, this->static_str_, copy_len);
lambda_buf[copy_len] = '\0';
return StringRef(lambda_buf, copy_len);
}
#endif
case VALUE:
return StringRef(this->value_->data(), this->value_->size());
default: { // LAMBDA/STATELESS_LAMBDA - must call value() and copy
@@ -239,6 +284,7 @@ template<typename T, typename... X> class TemplatableValue {
LAMBDA,
STATELESS_LAMBDA,
STATIC_STRING, // For const char* when T is std::string - avoids heap allocation
FLASH_STRING, // PROGMEM pointer on ESP8266; never set on other platforms
} type_;
// For std::string, use heap pointer to minimize union size (4 bytes vs 12+).
// For other types, store value inline as before.
@@ -247,7 +293,7 @@ template<typename T, typename... X> class TemplatableValue {
ValueStorage value_; // T for inline storage, T* for heap storage
std::function<T(X...)> *f_;
T (*stateless_f_)(X...);
const char *static_str_; // For STATIC_STRING type
const char *static_str_; // For STATIC_STRING and FLASH_STRING types
};
};

View File

@@ -152,7 +152,10 @@ void Component::set_retry(const std::string &name, uint32_t initial_wait_time, u
void Component::set_retry(const char *name, uint32_t initial_wait_time, uint8_t max_attempts,
std::function<RetryResult(uint8_t)> &&f, float backoff_increase_factor) { // NOLINT
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
App.scheduler.set_retry(this, name, initial_wait_time, max_attempts, std::move(f), backoff_increase_factor);
#pragma GCC diagnostic pop
}
bool Component::cancel_retry(const std::string &name) { // NOLINT
@@ -163,7 +166,10 @@ bool Component::cancel_retry(const std::string &name) { // NOLINT
}
bool Component::cancel_retry(const char *name) { // NOLINT
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
return App.scheduler.cancel_retry(this, name);
#pragma GCC diagnostic pop
}
void Component::set_timeout(const std::string &name, uint32_t timeout, std::function<void()> &&f) { // NOLINT
@@ -203,10 +209,18 @@ bool Component::cancel_interval(uint32_t id) { return App.scheduler.cancel_inter
void Component::set_retry(uint32_t id, uint32_t initial_wait_time, uint8_t max_attempts,
std::function<RetryResult(uint8_t)> &&f, float backoff_increase_factor) { // NOLINT
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
App.scheduler.set_retry(this, id, initial_wait_time, max_attempts, std::move(f), backoff_increase_factor);
#pragma GCC diagnostic pop
}
bool Component::cancel_retry(uint32_t id) { return App.scheduler.cancel_retry(this, id); }
bool Component::cancel_retry(uint32_t id) {
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
return App.scheduler.cancel_retry(this, id);
#pragma GCC diagnostic pop
}
void Component::call_loop() { this->loop(); }
void Component::call_setup() { this->setup(); }
@@ -371,7 +385,10 @@ void Component::set_interval(uint32_t interval, std::function<void()> &&f) { //
}
void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function<RetryResult(uint8_t)> &&f,
float backoff_increase_factor) { // NOLINT
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
App.scheduler.set_retry(this, "", initial_wait_time, max_attempts, std::move(f), backoff_increase_factor);
#pragma GCC diagnostic pop
}
bool Component::is_failed() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; }
bool Component::is_ready() const {

View File

@@ -68,6 +68,7 @@ extern const uint8_t STATUS_LED_OK;
extern const uint8_t STATUS_LED_WARNING;
extern const uint8_t STATUS_LED_ERROR;
// Remove before 2026.8.0
enum class RetryResult { DONE, RETRY };
extern const uint16_t WARN_IF_BLOCKING_OVER_MS;
@@ -347,68 +348,40 @@ class Component {
bool cancel_interval(const char *name); // NOLINT
bool cancel_interval(uint32_t id); // NOLINT
/** Set an retry function with a unique name. Empty name means no cancelling possible.
*
* This will call the retry function f on the next scheduler loop. f should return RetryResult::DONE if
* it is successful and no repeat is required. Otherwise, returning RetryResult::RETRY will call f
* again in the future.
*
* The first retry of f happens after `initial_wait_time` milliseconds. The delay between retries is
* increased by multiplying by `backoff_increase_factor` each time. If no backoff_increase_factor is
* supplied (default = 1.0), the wait time will stay constant.
*
* The retry function f needs to accept a single argument: the number of attempts remaining. On the
* final retry of f, this value will be 0.
*
* This retry function can also be cancelled by name via cancel_retry().
*
* IMPORTANT: Do not rely on this having correct timing. This is only called from
* loop() and therefore can be significantly delayed.
*
* REMARK: It is an error to supply a negative or zero `backoff_increase_factor`, and 1.0 will be used instead.
*
* REMARK: The interval between retries is stored into a `uint32_t`, so this doesn't behave correctly
* if `initial_wait_time * (backoff_increase_factor ** (max_attempts - 2))` overflows.
*
* @param name The identifier for this retry function.
* @param initial_wait_time The time in ms before f is called again
* @param max_attempts The maximum number of executions
* @param f The function (or lambda) that should be called
* @param backoff_increase_factor time between retries is multiplied by this factor on every retry after the first
* @see cancel_retry()
*/
// Remove before 2026.7.0
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0")
/// @deprecated set_retry is deprecated. Use set_timeout or set_interval instead. Removed in 2026.8.0.
// Remove before 2026.8.0
ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.",
"2026.2.0")
void set_retry(const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts, // NOLINT
std::function<RetryResult(uint8_t)> &&f, float backoff_increase_factor = 1.0f); // NOLINT
// Remove before 2026.8.0
ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.",
"2026.2.0")
void set_retry(const char *name, uint32_t initial_wait_time, uint8_t max_attempts, // NOLINT
std::function<RetryResult(uint8_t)> &&f, float backoff_increase_factor = 1.0f); // NOLINT
/** Set a retry function with a numeric ID (zero heap allocation).
*
* @param id The numeric identifier for this retry function
* @param initial_wait_time The wait time after the first execution
* @param max_attempts The max number of attempts
* @param f The function to call
* @param backoff_increase_factor The factor to increase the retry interval by
*/
// Remove before 2026.8.0
ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.",
"2026.2.0")
void set_retry(uint32_t id, uint32_t initial_wait_time, uint8_t max_attempts, // NOLINT
std::function<RetryResult(uint8_t)> &&f, float backoff_increase_factor = 1.0f); // NOLINT
// Remove before 2026.8.0
ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.",
"2026.2.0")
void set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std::function<RetryResult(uint8_t)> &&f, // NOLINT
float backoff_increase_factor = 1.0f); // NOLINT
/** Cancel a retry function.
*
* @param name The identifier for this retry function.
* @return Whether a retry function was deleted.
*/
// Remove before 2026.7.0
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0")
// Remove before 2026.8.0
ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0")
bool cancel_retry(const std::string &name); // NOLINT
bool cancel_retry(const char *name); // NOLINT
bool cancel_retry(uint32_t id); // NOLINT
// Remove before 2026.8.0
ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0")
bool cancel_retry(const char *name); // NOLINT
// Remove before 2026.8.0
ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0")
bool cancel_retry(uint32_t id); // NOLINT
/** Set a timeout function with a unique name.
*

View File

@@ -252,6 +252,11 @@ bool HOT Scheduler::cancel_interval(Component *component, uint32_t id) {
return this->cancel_item_(component, NameType::NUMERIC_ID, nullptr, id, SchedulerItem::INTERVAL);
}
// Suppress deprecation warnings for RetryResult usage in the still-present (but deprecated) retry implementation.
// Remove before 2026.8.0 along with all retry code.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
struct RetryArgs {
// Ordered to minimize padding on 32-bit systems
std::function<RetryResult(uint8_t)> func;
@@ -364,6 +369,8 @@ bool HOT Scheduler::cancel_retry(Component *component, uint32_t id) {
return this->cancel_retry_(component, NameType::NUMERIC_ID, nullptr, id);
}
#pragma GCC diagnostic pop // End suppression of deprecated RetryResult warnings
optional<uint32_t> HOT Scheduler::next_schedule_in(uint32_t now) {
// IMPORTANT: This method should only be called from the main thread (loop task).
// It performs cleanup and accesses items_[0] without holding a lock, which is only

View File

@@ -72,18 +72,30 @@ class Scheduler {
bool cancel_interval(Component *component, const char *name);
bool cancel_interval(Component *component, uint32_t id);
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0")
// Remove before 2026.8.0
ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.",
"2026.2.0")
void set_retry(Component *component, const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts,
std::function<RetryResult(uint8_t)> func, float backoff_increase_factor = 1.0f);
// Remove before 2026.8.0
ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.",
"2026.2.0")
void set_retry(Component *component, const char *name, uint32_t initial_wait_time, uint8_t max_attempts,
std::function<RetryResult(uint8_t)> func, float backoff_increase_factor = 1.0f);
/// Set a retry with a numeric ID (zero heap allocation)
// Remove before 2026.8.0
ESPDEPRECATED("set_retry is deprecated and will be removed in 2026.8.0. Use set_timeout or set_interval instead.",
"2026.2.0")
void set_retry(Component *component, uint32_t id, uint32_t initial_wait_time, uint8_t max_attempts,
std::function<RetryResult(uint8_t)> func, float backoff_increase_factor = 1.0f);
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0")
// Remove before 2026.8.0
ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0")
bool cancel_retry(Component *component, const std::string &name);
// Remove before 2026.8.0
ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0")
bool cancel_retry(Component *component, const char *name);
// Remove before 2026.8.0
ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0")
bool cancel_retry(Component *component, uint32_t id);
// Calculate when the next scheduled item should run
@@ -231,11 +243,14 @@ class Scheduler {
uint32_t hash_or_id, uint32_t delay, std::function<void()> func, bool is_retry = false,
bool skip_cancel = false);
// Common implementation for retry
// Common implementation for retry - Remove before 2026.8.0
// name_type determines storage type: STATIC_STRING uses static_name, others use hash_or_id
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
void set_retry_common_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id,
uint32_t initial_wait_time, uint8_t max_attempts, std::function<RetryResult(uint8_t)> func,
float backoff_increase_factor);
#pragma GCC diagnostic pop
// Common implementation for cancel_retry
bool cancel_retry_(Component *component, NameType name_type, const char *static_name, uint32_t hash_or_id);

View File

@@ -247,6 +247,23 @@ class LogStringLiteral(Literal):
return f"LOG_STR({cpp_string_escape(self.string)})"
class FlashStringLiteral(Literal):
"""A string literal wrapped in ESPHOME_F() for PROGMEM storage on ESP8266.
On ESP8266, ESPHOME_F(s) expands to F(s) which stores the string in flash (PROGMEM).
On other platforms, ESPHOME_F(s) expands to plain s (no-op).
"""
__slots__ = ("string",)
def __init__(self, string: str) -> None:
super().__init__()
self.string = string
def __str__(self) -> str:
return f"ESPHOME_F({cpp_string_escape(self.string)})"
class IntLiteral(Literal):
__slots__ = ("i",)
@@ -761,6 +778,15 @@ async def templatable(
if is_template(value):
return await process_lambda(value, args, return_type=output_type)
if to_exp is None:
# Automatically wrap static strings in ESPHOME_F() for PROGMEM storage on ESP8266.
# On other platforms ESPHOME_F() is a no-op returning const char*.
# Lazy import to avoid circular dependency (cpp_generator <-> cpp_types).
# Identity check (is) avoids brittle string comparison.
if isinstance(value, str) and output_type is not None:
from esphome.cpp_types import std_string
if output_type is std_string:
return FlashStringLiteral(value)
return value
if isinstance(to_exp, dict):
return to_exp[value]

View File

@@ -2881,9 +2881,82 @@ static const char *const TAG = "api.service";
cases = list(RECEIVE_CASES.items())
cases.sort()
serv = file.service[0]
# Build a mapping of message input types to their authentication requirements
message_auth_map: dict[str, bool] = {}
message_conn_map: dict[str, bool] = {}
for m in serv.method:
inp = m.input_type[1:]
needs_conn = get_opt(m, pb.needs_setup_connection, True)
needs_auth = get_opt(m, pb.needs_authentication, True)
# Store authentication requirements for message types
message_auth_map[inp] = needs_auth
message_conn_map[inp] = needs_conn
# Categorize messages by their authentication requirements
no_conn_ids: set[int] = set()
conn_only_ids: set[int] = set()
for id_, (_, _, case_msg_name) in cases:
if case_msg_name in message_auth_map:
needs_auth = message_auth_map[case_msg_name]
needs_conn = message_conn_map[case_msg_name]
if not needs_conn:
no_conn_ids.add(id_)
elif not needs_auth:
conn_only_ids.add(id_)
# Helper to generate case statements with ifdefs
def generate_cases(ids: set[int], comment: str) -> str:
result = ""
for id_ in sorted(ids):
_, ifdef, msg_name = RECEIVE_CASES[id_]
if ifdef:
result += f"#ifdef {ifdef}\n"
result += f" case {msg_name}::MESSAGE_TYPE: {comment}\n"
if ifdef:
result += "#endif\n"
return result
# Generate read_message with auth check before dispatch
hpp += " protected:\n"
hpp += " void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;\n"
out = f"void {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {{\n"
# Auth check block before dispatch switch
out += " // Check authentication/connection requirements\n"
if no_conn_ids or conn_only_ids:
out += " switch (msg_type) {\n"
if no_conn_ids:
out += generate_cases(no_conn_ids, "// No setup required")
out += " break;\n"
if conn_only_ids:
out += generate_cases(conn_only_ids, "// Connection setup only")
out += " if (!this->check_connection_setup_()) {\n"
out += " return;\n"
out += " }\n"
out += " break;\n"
out += " default:\n"
out += " if (!this->check_authenticated_()) {\n"
out += " return;\n"
out += " }\n"
out += " break;\n"
out += " }\n"
else:
out += " if (!this->check_authenticated_()) {\n"
out += " return;\n"
out += " }\n"
# Dispatch switch
out += " switch (msg_type) {\n"
for i, (case, ifdef, message_name) in cases:
if ifdef is not None:
@@ -2902,89 +2975,6 @@ static const char *const TAG = "api.service";
cpp += out
hpp += "};\n"
serv = file.service[0]
class_name = "APIServerConnection"
hpp += "\n"
hpp += f"class {class_name} : public {class_name}Base {{\n"
hpp_protected = ""
cpp += "\n"
# Build a mapping of message input types to their authentication requirements
message_auth_map: dict[str, bool] = {}
message_conn_map: dict[str, bool] = {}
for m in serv.method:
inp = m.input_type[1:]
needs_conn = get_opt(m, pb.needs_setup_connection, True)
needs_auth = get_opt(m, pb.needs_authentication, True)
# Store authentication requirements for message types
message_auth_map[inp] = needs_auth
message_conn_map[inp] = needs_conn
# Generate optimized read_message with authentication checking
# Categorize messages by their authentication requirements
no_conn_ids: set[int] = set()
conn_only_ids: set[int] = set()
for id_, (_, _, case_msg_name) in cases:
if case_msg_name in message_auth_map:
needs_auth = message_auth_map[case_msg_name]
needs_conn = message_conn_map[case_msg_name]
if not needs_conn:
no_conn_ids.add(id_)
elif not needs_auth:
conn_only_ids.add(id_)
# Generate override if we have messages that skip checks
if no_conn_ids or conn_only_ids:
# Helper to generate case statements with ifdefs
def generate_cases(ids: set[int], comment: str) -> str:
result = ""
for id_ in sorted(ids):
_, ifdef, msg_name = RECEIVE_CASES[id_]
if ifdef:
result += f"#ifdef {ifdef}\n"
result += f" case {msg_name}::MESSAGE_TYPE: {comment}\n"
if ifdef:
result += "#endif\n"
return result
hpp_protected += " void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;\n"
cpp += f"\nvoid {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {{\n"
cpp += " // Check authentication/connection requirements for messages\n"
cpp += " switch (msg_type) {\n"
# Messages that don't need any checks
if no_conn_ids:
cpp += generate_cases(no_conn_ids, "// No setup required")
cpp += " break; // Skip all checks for these messages\n"
# Messages that only need connection setup
if conn_only_ids:
cpp += generate_cases(conn_only_ids, "// Connection setup only")
cpp += " if (!this->check_connection_setup_()) {\n"
cpp += " return; // Connection not setup\n"
cpp += " }\n"
cpp += " break;\n"
cpp += " default:\n"
cpp += " // All other messages require authentication (which includes connection check)\n"
cpp += " if (!this->check_authenticated_()) {\n"
cpp += " return; // Authentication failed\n"
cpp += " }\n"
cpp += " break;\n"
cpp += " }\n\n"
cpp += " // Call base implementation to process the message\n"
cpp += f" {class_name}Base::read_message(msg_size, msg_type, msg_data);\n"
cpp += "}\n"
hpp += " protected:\n"
hpp += hpp_protected
hpp += "};\n"
hpp += """\
} // namespace esphome::api

View File

@@ -756,6 +756,53 @@ def lint_no_sprintf(fname, match):
)
@lint_re_check(
# Match std::to_string() or unqualified to_string() calls
# The esphome namespace has "using std::to_string;" so unqualified calls resolve to std::to_string
# Use negative lookbehind for unqualified calls to avoid matching:
# - Function definitions: "const char *to_string(" or "std::string to_string("
# - Method definitions: "Class::to_string("
# - Method calls: ".to_string(" or "->to_string("
# - Other identifiers: "_to_string("
# Also explicitly match std::to_string since : is in the lookbehind
r"(?:(?<![*&.\w>:])to_string|std\s*::\s*to_string)\s*\(" + CPP_RE_EOL,
include=cpp_include,
exclude=[
# Vendored library
"esphome/components/http_request/httplib.h",
# Deprecated helpers that return std::string
"esphome/core/helpers.cpp",
# The using declaration itself
"esphome/core/helpers.h",
# Test fixtures - not production embedded code
"tests/integration/fixtures/*",
],
)
def lint_no_std_to_string(fname, match):
return (
f"{highlight('std::to_string()')} (including unqualified {highlight('to_string()')}) "
f"allocates heap memory. On long-running embedded devices, repeated heap allocations "
f"fragment memory over time.\n"
f"Please use {highlight('snprintf()')} with a stack buffer instead.\n"
f"\n"
f"Buffer sizes and format specifiers (sizes include sign and null terminator):\n"
f" uint8_t: 4 chars - %u (or PRIu8)\n"
f" int8_t: 5 chars - %d (or PRId8)\n"
f" uint16_t: 6 chars - %u (or PRIu16)\n"
f" int16_t: 7 chars - %d (or PRId16)\n"
f" uint32_t: 11 chars - %" + "PRIu32\n"
" int32_t: 12 chars - %" + "PRId32\n"
" uint64_t: 21 chars - %" + "PRIu64\n"
" int64_t: 21 chars - %" + "PRId64\n"
f" float/double: 24 chars - %.8g (15 digits + sign + decimal + e+XXX)\n"
f" 317 chars - %f (for DBL_MAX: 309 int digits + decimal + 6 frac + sign)\n"
f"\n"
f"For sensor values, use value_accuracy_to_buf() from helpers.h.\n"
f'Example: char buf[11]; snprintf(buf, sizeof(buf), "%" PRIu32, value);\n'
f"(If strictly necessary, add `{highlight('// NOLINT')}` to the end of the line)"
)
@lint_re_check(
# Match scanf family functions: scanf, sscanf, fscanf, vscanf, vsscanf, vfscanf
# Also match std:: prefixed versions

View File

@@ -248,6 +248,12 @@ class TestLiterals:
(cg.FloatLiteral(4.2), "4.2f"),
(cg.FloatLiteral(1.23456789), "1.23456789f"),
(cg.FloatLiteral(math.nan), "NAN"),
(cg.FlashStringLiteral("hello"), 'ESPHOME_F("hello")'),
(cg.FlashStringLiteral(""), 'ESPHOME_F("")'),
(
cg.FlashStringLiteral('quote"here'),
'ESPHOME_F("quote\\042here")',
),
),
)
def test_str__simple(self, target: cg.Literal, expected: str):
@@ -624,3 +630,75 @@ class TestProcessLambda:
# Test invalid tuple format (single element)
with pytest.raises(AssertionError):
await cg.process_lambda(lambda_obj, [(int,)])
@pytest.mark.asyncio
async def test_templatable__string_with_std_string_returns_flash_literal() -> None:
"""Static string with std::string output_type returns FlashStringLiteral."""
result = await cg.templatable("hello", [], ct.std_string)
assert isinstance(result, cg.FlashStringLiteral)
assert str(result) == 'ESPHOME_F("hello")'
@pytest.mark.asyncio
async def test_templatable__empty_string_with_std_string() -> None:
"""Empty static string with std::string output_type returns FlashStringLiteral."""
result = await cg.templatable("", [], ct.std_string)
assert isinstance(result, cg.FlashStringLiteral)
assert str(result) == 'ESPHOME_F("")'
@pytest.mark.asyncio
async def test_templatable__string_with_none_output_type() -> None:
"""Static string with output_type=None returns raw string (no wrapping)."""
result = await cg.templatable("hello", [], None)
assert isinstance(result, str)
assert result == "hello"
@pytest.mark.asyncio
async def test_templatable__int_with_std_string() -> None:
"""Non-string value with std::string output_type returns raw value."""
result = await cg.templatable(42, [], ct.std_string)
assert result == 42
@pytest.mark.asyncio
async def test_templatable__string_with_non_string_output_type() -> None:
"""Static string with non-std::string output_type returns raw string."""
result = await cg.templatable("hello", [], ct.bool_)
assert isinstance(result, str)
assert result == "hello"
@pytest.mark.asyncio
async def test_templatable__with_to_exp_callable() -> None:
"""When to_exp is provided, it is applied to non-template values."""
result = await cg.templatable(42, [], None, to_exp=lambda x: x * 2)
assert result == 84
@pytest.mark.asyncio
async def test_templatable__with_to_exp_dict() -> None:
"""When to_exp is a dict, value is looked up."""
mapping: dict[str, int] = {"on": 1, "off": 0}
result = await cg.templatable("on", [], None, to_exp=mapping)
assert result == 1
@pytest.mark.asyncio
async def test_templatable__lambda_with_std_string() -> None:
"""Lambda value returns LambdaExpression, not FlashStringLiteral."""
from esphome.core import Lambda
lambda_obj = Lambda('return "hello";')
result = await cg.templatable(lambda_obj, [], ct.std_string)
assert isinstance(result, cg.LambdaExpression)