mirror of
https://github.com/esphome/esphome.git
synced 2026-02-10 01:32:06 +00:00
Compare commits
84 Commits
api-flash-
...
integratio
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86c1803538 | ||
|
|
eb8bb260e5 | ||
|
|
ef079f9113 | ||
|
|
647f39504a | ||
|
|
b6c98f0586 | ||
|
|
77b46ba90f | ||
|
|
8cc6915558 | ||
|
|
10e71255a0 | ||
|
|
30521f7de6 | ||
|
|
c97ae656e4 | ||
|
|
f09e72766e | ||
|
|
a320e87a17 | ||
|
|
45932fabea | ||
|
|
f62ea6bdc2 | ||
|
|
675625cf25 | ||
|
|
6baeaf5b7b | ||
|
|
ca8617cf10 | ||
|
|
a0bc6a9922 | ||
|
|
d5295a894b | ||
|
|
fb6b96ff58 | ||
|
|
e07144ef74 | ||
|
|
1c4cf1a3e8 | ||
|
|
a198df34ee | ||
|
|
15904ab583 | ||
|
|
9c9e8ac388 | ||
|
|
04f4636d36 | ||
|
|
3cbadfe42a | ||
|
|
277a11f0ea | ||
|
|
08cca414e7 | ||
|
|
4db5835b6f | ||
|
|
527003e16b | ||
|
|
e9a0d06880 | ||
|
|
b2879f7f99 | ||
|
|
44e9346e9c | ||
|
|
6670c2b6c4 | ||
|
|
6013b473ca | ||
|
|
cc1f83ac35 | ||
|
|
1f1405364d | ||
|
|
5d3ae8cbec | ||
|
|
59a2f6f538 | ||
|
|
a9c37cae26 | ||
|
|
c8a93f31e9 | ||
|
|
f79448a09a | ||
|
|
5e096826c3 | ||
|
|
457d68256d | ||
|
|
a9029fb67a | ||
|
|
cd891d4b16 | ||
|
|
2784059a64 | ||
|
|
4827f53156 | ||
|
|
8dff0ee449 | ||
|
|
a7f04a6cf9 | ||
|
|
53bde863f5 | ||
|
|
dfb0c8670d | ||
|
|
7490efedd7 | ||
|
|
a0f736b7aa | ||
|
|
21f270677b | ||
|
|
d6e692e302 | ||
|
|
991ce396a9 | ||
|
|
68dfb844bd | ||
|
|
9742880bf7 | ||
|
|
13f9726534 | ||
|
|
dd07e25a8f | ||
|
|
a875a2fb9b | ||
|
|
836bfc625d | ||
|
|
2a17592d57 | ||
|
|
04697ac223 | ||
|
|
3f3cf83aab | ||
|
|
39013388dd | ||
|
|
cfbeea9983 | ||
|
|
8f6e1abbce | ||
|
|
c77d70c093 | ||
|
|
25762c62f8 | ||
|
|
441ec35d9f | ||
|
|
33c831dbb8 | ||
|
|
38aeb9be37 | ||
|
|
6b7c52799d | ||
|
|
f19bb2cd0a | ||
|
|
26c98a1e25 | ||
|
|
b544cf2ffe | ||
|
|
6d1281301f | ||
|
|
901192cca1 | ||
|
|
67e7ba4812 | ||
|
|
572376091e | ||
|
|
e7c9808b87 |
@@ -11,7 +11,6 @@
|
||||
from esphome.cpp_generator import ( # noqa: F401
|
||||
ArrayInitializer,
|
||||
Expression,
|
||||
FlashStringLiteral,
|
||||
LineComment,
|
||||
LogStringLiteral,
|
||||
MockObj,
|
||||
|
||||
@@ -524,31 +524,24 @@ 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, cg.std_string)
|
||||
templ = await cg.templatable(config[CONF_ACTION], args, None)
|
||||
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)
|
||||
if isinstance(templ, str):
|
||||
templ = cg.FlashStringLiteral(templ)
|
||||
cg.add(var.add_data(cg.FlashStringLiteral(key), templ))
|
||||
cg.add(var.add_data(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)
|
||||
if isinstance(templ, str):
|
||||
templ = cg.FlashStringLiteral(templ)
|
||||
cg.add(var.add_data_template(cg.FlashStringLiteral(key), templ))
|
||||
cg.add(var.add_data_template(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(cg.FlashStringLiteral(key), templ))
|
||||
cg.add(var.add_variable(key, templ))
|
||||
|
||||
if on_error := config.get(CONF_ON_ERROR):
|
||||
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES")
|
||||
@@ -616,31 +609,24 @@ 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, cg.std_string)
|
||||
templ = await cg.templatable(config[CONF_EVENT], args, None)
|
||||
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)
|
||||
if isinstance(templ, str):
|
||||
templ = cg.FlashStringLiteral(templ)
|
||||
cg.add(var.add_data(cg.FlashStringLiteral(key), templ))
|
||||
cg.add(var.add_data(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)
|
||||
if isinstance(templ, str):
|
||||
templ = cg.FlashStringLiteral(templ)
|
||||
cg.add(var.add_data_template(cg.FlashStringLiteral(key), templ))
|
||||
cg.add(var.add_data_template(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(cg.FlashStringLiteral(key), templ))
|
||||
cg.add(var.add_variable(key, templ))
|
||||
|
||||
return var
|
||||
|
||||
@@ -663,11 +649,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(cg.FlashStringLiteral("esphome.tag_scanned")))
|
||||
cg.add(var.set_service("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(cg.FlashStringLiteral("tag_id"), templ))
|
||||
cg.add(var.add_data("tag_id", templ))
|
||||
return var
|
||||
|
||||
|
||||
|
||||
@@ -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 APIServerConnectionBase {
|
||||
class APIConnection final : public APIServerConnection {
|
||||
public:
|
||||
friend class APIServer;
|
||||
friend class ListEntitiesIterator;
|
||||
|
||||
@@ -21,23 +21,6 @@ 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;
|
||||
@@ -640,4 +623,28 @@ 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
|
||||
|
||||
@@ -228,4 +228,9 @@ 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
|
||||
|
||||
@@ -25,9 +25,7 @@ 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)); // NOLINT
|
||||
}
|
||||
template<typename T> static std::string value_to_string(T &&val) { return to_string(std::forward<T>(val)); }
|
||||
|
||||
// Overloads for string types - needed because std::to_string doesn't support them
|
||||
static std::string value_to_string(char *val) {
|
||||
@@ -128,20 +126,6 @@ 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;
|
||||
@@ -233,32 +217,7 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
||||
Ts... x) {
|
||||
dest.init(source.size());
|
||||
|
||||
#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.
|
||||
// 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()) {
|
||||
@@ -272,15 +231,14 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
||||
kv.key = StringRef(it.key);
|
||||
|
||||
if (it.value.is_static_string()) {
|
||||
// Static string — pointer directly readable, zero allocation
|
||||
// Static string from YAML - zero allocation
|
||||
kv.value = StringRef(it.value.get_static_string());
|
||||
} else {
|
||||
// Lambda — evaluate and store result
|
||||
// Lambda evaluation - store result, reference it
|
||||
value_storage.push_back(it.value.value(x...));
|
||||
kv.value = StringRef(value_storage.back());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
APIServer *parent_;
|
||||
|
||||
@@ -28,15 +28,28 @@ void DlmsMeterComponent::dump_config() {
|
||||
|
||||
void DlmsMeterComponent::loop() {
|
||||
// Read while data is available, netznoe uses two frames so allow 2x max frame length
|
||||
while (this->available()) {
|
||||
if (this->receive_buffer_.size() >= MBUS_MAX_FRAME_LENGTH * 2) {
|
||||
int avail = this->available();
|
||||
if (avail > 0) {
|
||||
size_t remaining = MBUS_MAX_FRAME_LENGTH * 2 - this->receive_buffer_.size();
|
||||
if (remaining == 0) {
|
||||
ESP_LOGW(TAG, "Receive buffer full, dropping remaining bytes");
|
||||
break;
|
||||
} else {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
// Cap reads to remaining buffer capacity.
|
||||
if (static_cast<size_t>(avail) > remaining) {
|
||||
avail = remaining;
|
||||
}
|
||||
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;
|
||||
this->receive_buffer_.insert(this->receive_buffer_.end(), buf, buf + to_read);
|
||||
this->last_read_ = millis();
|
||||
}
|
||||
}
|
||||
uint8_t c;
|
||||
this->read_byte(&c);
|
||||
this->receive_buffer_.push_back(c);
|
||||
this->last_read_ = millis();
|
||||
}
|
||||
|
||||
if (!this->receive_buffer_.empty() && millis() - this->last_read_ > this->read_timeout_) {
|
||||
|
||||
@@ -40,9 +40,7 @@ bool Dsmr::ready_to_request_data_() {
|
||||
this->start_requesting_data_();
|
||||
}
|
||||
if (!this->requesting_data_) {
|
||||
while (this->available()) {
|
||||
this->read();
|
||||
}
|
||||
this->drain_rx_buffer_();
|
||||
}
|
||||
}
|
||||
return this->requesting_data_;
|
||||
@@ -115,13 +113,21 @@ void Dsmr::stop_requesting_data_() {
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Stop reading data from P1 port");
|
||||
}
|
||||
while (this->available()) {
|
||||
this->read();
|
||||
}
|
||||
this->drain_rx_buffer_();
|
||||
this->requesting_data_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
void Dsmr::drain_rx_buffer_() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Dsmr::reset_telegram_() {
|
||||
this->header_found_ = false;
|
||||
this->footer_found_ = false;
|
||||
@@ -133,120 +139,144 @@ void Dsmr::reset_telegram_() {
|
||||
|
||||
void Dsmr::receive_telegram_() {
|
||||
while (this->available_within_timeout_()) {
|
||||
const char c = this->read();
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
uint8_t buf[64];
|
||||
int avail = this->available();
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
if (!this->read_array(buf, to_read))
|
||||
return;
|
||||
avail -= to_read;
|
||||
|
||||
// Find a new telegram header, i.e. forward slash.
|
||||
if (c == '/') {
|
||||
ESP_LOGV(TAG, "Header of telegram found");
|
||||
this->reset_telegram_();
|
||||
this->header_found_ = true;
|
||||
}
|
||||
if (!this->header_found_)
|
||||
continue;
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
const char c = static_cast<char>(buf[i]);
|
||||
|
||||
// Check for buffer overflow.
|
||||
if (this->bytes_read_ >= this->max_telegram_len_) {
|
||||
this->reset_telegram_();
|
||||
ESP_LOGE(TAG, "Error: telegram larger than buffer (%d bytes)", this->max_telegram_len_);
|
||||
return;
|
||||
}
|
||||
// Find a new telegram header, i.e. forward slash.
|
||||
if (c == '/') {
|
||||
ESP_LOGV(TAG, "Header of telegram found");
|
||||
this->reset_telegram_();
|
||||
this->header_found_ = true;
|
||||
}
|
||||
if (!this->header_found_)
|
||||
continue;
|
||||
|
||||
// Some v2.2 or v3 meters will send a new value which starts with '('
|
||||
// in a new line, while the value belongs to the previous ObisId. For
|
||||
// proper parsing, remove these new line characters.
|
||||
if (c == '(') {
|
||||
while (true) {
|
||||
auto previous_char = this->telegram_[this->bytes_read_ - 1];
|
||||
if (previous_char == '\n' || previous_char == '\r') {
|
||||
this->bytes_read_--;
|
||||
} else {
|
||||
break;
|
||||
// Check for buffer overflow.
|
||||
if (this->bytes_read_ >= this->max_telegram_len_) {
|
||||
this->reset_telegram_();
|
||||
ESP_LOGE(TAG, "Error: telegram larger than buffer (%d bytes)", this->max_telegram_len_);
|
||||
return;
|
||||
}
|
||||
|
||||
// Some v2.2 or v3 meters will send a new value which starts with '('
|
||||
// in a new line, while the value belongs to the previous ObisId. For
|
||||
// proper parsing, remove these new line characters.
|
||||
if (c == '(') {
|
||||
while (true) {
|
||||
auto previous_char = this->telegram_[this->bytes_read_ - 1];
|
||||
if (previous_char == '\n' || previous_char == '\r') {
|
||||
this->bytes_read_--;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store the byte in the buffer.
|
||||
this->telegram_[this->bytes_read_] = c;
|
||||
this->bytes_read_++;
|
||||
|
||||
// Check for a footer, i.e. exclamation mark, followed by a hex checksum.
|
||||
if (c == '!') {
|
||||
ESP_LOGV(TAG, "Footer of telegram found");
|
||||
this->footer_found_ = true;
|
||||
continue;
|
||||
}
|
||||
// Check for the end of the hex checksum, i.e. a newline.
|
||||
if (this->footer_found_ && c == '\n') {
|
||||
// Parse the telegram and publish sensor values.
|
||||
this->parse_telegram();
|
||||
this->reset_telegram_();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store the byte in the buffer.
|
||||
this->telegram_[this->bytes_read_] = c;
|
||||
this->bytes_read_++;
|
||||
|
||||
// Check for a footer, i.e. exclamation mark, followed by a hex checksum.
|
||||
if (c == '!') {
|
||||
ESP_LOGV(TAG, "Footer of telegram found");
|
||||
this->footer_found_ = true;
|
||||
continue;
|
||||
}
|
||||
// Check for the end of the hex checksum, i.e. a newline.
|
||||
if (this->footer_found_ && c == '\n') {
|
||||
// Parse the telegram and publish sensor values.
|
||||
this->parse_telegram();
|
||||
this->reset_telegram_();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Dsmr::receive_encrypted_telegram_() {
|
||||
while (this->available_within_timeout_()) {
|
||||
const char c = this->read();
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
uint8_t buf[64];
|
||||
int avail = this->available();
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
if (!this->read_array(buf, to_read))
|
||||
return;
|
||||
avail -= to_read;
|
||||
|
||||
// Find a new telegram start byte.
|
||||
if (!this->header_found_) {
|
||||
if ((uint8_t) c != 0xDB) {
|
||||
continue;
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
const char c = static_cast<char>(buf[i]);
|
||||
|
||||
// Find a new telegram start byte.
|
||||
if (!this->header_found_) {
|
||||
if ((uint8_t) c != 0xDB) {
|
||||
continue;
|
||||
}
|
||||
ESP_LOGV(TAG, "Start byte 0xDB of encrypted telegram found");
|
||||
this->reset_telegram_();
|
||||
this->header_found_ = true;
|
||||
}
|
||||
|
||||
// Check for buffer overflow.
|
||||
if (this->crypt_bytes_read_ >= this->max_telegram_len_) {
|
||||
this->reset_telegram_();
|
||||
ESP_LOGE(TAG, "Error: encrypted telegram larger than buffer (%d bytes)", this->max_telegram_len_);
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the byte in the buffer.
|
||||
this->crypt_telegram_[this->crypt_bytes_read_] = c;
|
||||
this->crypt_bytes_read_++;
|
||||
|
||||
// Read the length of the incoming encrypted telegram.
|
||||
if (this->crypt_telegram_len_ == 0 && this->crypt_bytes_read_ > 20) {
|
||||
// Complete header + data bytes
|
||||
this->crypt_telegram_len_ = 13 + (this->crypt_telegram_[11] << 8 | this->crypt_telegram_[12]);
|
||||
ESP_LOGV(TAG, "Encrypted telegram length: %d bytes", this->crypt_telegram_len_);
|
||||
}
|
||||
|
||||
// Check for the end of the encrypted telegram.
|
||||
if (this->crypt_telegram_len_ == 0 || this->crypt_bytes_read_ != this->crypt_telegram_len_) {
|
||||
continue;
|
||||
}
|
||||
ESP_LOGV(TAG, "End of encrypted telegram found");
|
||||
|
||||
// Decrypt the encrypted telegram.
|
||||
GCM<AES128> *gcmaes128{new GCM<AES128>()};
|
||||
gcmaes128->setKey(this->decryption_key_.data(), gcmaes128->keySize());
|
||||
// the iv is 8 bytes of the system title + 4 bytes frame counter
|
||||
// system title is at byte 2 and frame counter at byte 15
|
||||
for (int i = 10; i < 14; i++)
|
||||
this->crypt_telegram_[i] = this->crypt_telegram_[i + 4];
|
||||
constexpr uint16_t iv_size{12};
|
||||
gcmaes128->setIV(&this->crypt_telegram_[2], iv_size);
|
||||
gcmaes128->decrypt(reinterpret_cast<uint8_t *>(this->telegram_),
|
||||
// the ciphertext start at byte 18
|
||||
&this->crypt_telegram_[18],
|
||||
// cipher size
|
||||
this->crypt_bytes_read_ - 17);
|
||||
delete gcmaes128; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
|
||||
this->bytes_read_ = strnlen(this->telegram_, this->max_telegram_len_);
|
||||
ESP_LOGV(TAG, "Decrypted telegram size: %d bytes", this->bytes_read_);
|
||||
ESP_LOGVV(TAG, "Decrypted telegram: %s", this->telegram_);
|
||||
|
||||
// Parse the decrypted telegram and publish sensor values.
|
||||
this->parse_telegram();
|
||||
this->reset_telegram_();
|
||||
return;
|
||||
}
|
||||
ESP_LOGV(TAG, "Start byte 0xDB of encrypted telegram found");
|
||||
this->reset_telegram_();
|
||||
this->header_found_ = true;
|
||||
}
|
||||
|
||||
// Check for buffer overflow.
|
||||
if (this->crypt_bytes_read_ >= this->max_telegram_len_) {
|
||||
this->reset_telegram_();
|
||||
ESP_LOGE(TAG, "Error: encrypted telegram larger than buffer (%d bytes)", this->max_telegram_len_);
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the byte in the buffer.
|
||||
this->crypt_telegram_[this->crypt_bytes_read_] = c;
|
||||
this->crypt_bytes_read_++;
|
||||
|
||||
// Read the length of the incoming encrypted telegram.
|
||||
if (this->crypt_telegram_len_ == 0 && this->crypt_bytes_read_ > 20) {
|
||||
// Complete header + data bytes
|
||||
this->crypt_telegram_len_ = 13 + (this->crypt_telegram_[11] << 8 | this->crypt_telegram_[12]);
|
||||
ESP_LOGV(TAG, "Encrypted telegram length: %d bytes", this->crypt_telegram_len_);
|
||||
}
|
||||
|
||||
// Check for the end of the encrypted telegram.
|
||||
if (this->crypt_telegram_len_ == 0 || this->crypt_bytes_read_ != this->crypt_telegram_len_) {
|
||||
continue;
|
||||
}
|
||||
ESP_LOGV(TAG, "End of encrypted telegram found");
|
||||
|
||||
// Decrypt the encrypted telegram.
|
||||
GCM<AES128> *gcmaes128{new GCM<AES128>()};
|
||||
gcmaes128->setKey(this->decryption_key_.data(), gcmaes128->keySize());
|
||||
// the iv is 8 bytes of the system title + 4 bytes frame counter
|
||||
// system title is at byte 2 and frame counter at byte 15
|
||||
for (int i = 10; i < 14; i++)
|
||||
this->crypt_telegram_[i] = this->crypt_telegram_[i + 4];
|
||||
constexpr uint16_t iv_size{12};
|
||||
gcmaes128->setIV(&this->crypt_telegram_[2], iv_size);
|
||||
gcmaes128->decrypt(reinterpret_cast<uint8_t *>(this->telegram_),
|
||||
// the ciphertext start at byte 18
|
||||
&this->crypt_telegram_[18],
|
||||
// cipher size
|
||||
this->crypt_bytes_read_ - 17);
|
||||
delete gcmaes128; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
|
||||
this->bytes_read_ = strnlen(this->telegram_, this->max_telegram_len_);
|
||||
ESP_LOGV(TAG, "Decrypted telegram size: %d bytes", this->bytes_read_);
|
||||
ESP_LOGVV(TAG, "Decrypted telegram: %s", this->telegram_);
|
||||
|
||||
// Parse the decrypted telegram and publish sensor values.
|
||||
this->parse_telegram();
|
||||
this->reset_telegram_();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@ class Dsmr : public Component, public uart::UARTDevice {
|
||||
void receive_telegram_();
|
||||
void receive_encrypted_telegram_();
|
||||
void reset_telegram_();
|
||||
void drain_rx_buffer_();
|
||||
|
||||
/// Wait for UART data to become available within the read timeout.
|
||||
///
|
||||
|
||||
@@ -1435,10 +1435,6 @@ 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")
|
||||
|
||||
@@ -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; // NOLINT
|
||||
std::string to_string() const;
|
||||
const char *to_str(std::span<char, UUID_STR_LEN> output) const;
|
||||
|
||||
protected:
|
||||
|
||||
@@ -134,23 +134,25 @@ 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();
|
||||
}
|
||||
// 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;
|
||||
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;
|
||||
}
|
||||
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:
|
||||
|
||||
@@ -335,9 +335,10 @@ void LD2420Component::revert_config_action() {
|
||||
|
||||
void LD2420Component::loop() {
|
||||
// If there is a active send command do not process it here, the send command call will handle it.
|
||||
while (!this->cmd_active_ && this->available()) {
|
||||
this->readline_(this->read(), this->buffer_data_, MAX_LINE_LENGTH);
|
||||
if (this->cmd_active_) {
|
||||
return;
|
||||
}
|
||||
this->read_batch_(this->buffer_data_);
|
||||
}
|
||||
|
||||
void LD2420Component::update_radar_data(uint16_t const *gate_energy, uint8_t sample_number) {
|
||||
@@ -539,6 +540,23 @@ void LD2420Component::handle_simple_mode_(const uint8_t *inbuf, int len) {
|
||||
}
|
||||
}
|
||||
|
||||
void LD2420Component::read_batch_(std::span<uint8_t, MAX_LINE_LENGTH> buffer) {
|
||||
// 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], buffer.data(), buffer.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) {
|
||||
this->cmd_reply_.command = buffer[CMD_FRAME_COMMAND];
|
||||
this->cmd_reply_.length = buffer[CMD_FRAME_DATA_LENGTH];
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include <span>
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
#endif
|
||||
@@ -165,6 +166,7 @@ class LD2420Component : public Component, public uart::UARTDevice {
|
||||
void handle_energy_mode_(uint8_t *buffer, int len);
|
||||
void handle_ack_data_(uint8_t *buffer, int len);
|
||||
void readline_(int rx_data, uint8_t *buffer, int len);
|
||||
void read_batch_(std::span<uint8_t, MAX_LINE_LENGTH> buffer);
|
||||
void set_calibration_(bool state) { this->calibration_ = state; };
|
||||
bool get_calibration_() { return this->calibration_; };
|
||||
|
||||
|
||||
@@ -228,50 +228,39 @@ void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address
|
||||
return;
|
||||
}
|
||||
|
||||
static constexpr size_t ADDR_SIZE = 1;
|
||||
static constexpr size_t FC_SIZE = 1;
|
||||
static constexpr size_t START_ADDR_SIZE = 2;
|
||||
static constexpr size_t NUM_ENTITIES_SIZE = 2;
|
||||
static constexpr size_t BYTE_COUNT_SIZE = 1;
|
||||
static constexpr size_t MAX_PAYLOAD_SIZE = std::numeric_limits<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;
|
||||
std::vector<uint8_t> data;
|
||||
data.push_back(address);
|
||||
data.push_back(function_code);
|
||||
if (this->role == ModbusRole::CLIENT) {
|
||||
data[pos++] = start_address >> 8;
|
||||
data[pos++] = start_address >> 0;
|
||||
data.push_back(start_address >> 8);
|
||||
data.push_back(start_address >> 0);
|
||||
if (function_code != ModbusFunctionCode::WRITE_SINGLE_COIL &&
|
||||
function_code != ModbusFunctionCode::WRITE_SINGLE_REGISTER) {
|
||||
data[pos++] = number_of_entities >> 8;
|
||||
data[pos++] = number_of_entities >> 0;
|
||||
data.push_back(number_of_entities >> 8);
|
||||
data.push_back(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[pos++] = payload_len; // Byte count is required for write
|
||||
data.push_back(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[pos++] = payload[i];
|
||||
data.push_back(payload[i]);
|
||||
}
|
||||
}
|
||||
|
||||
auto crc = crc16(data, pos);
|
||||
data[pos++] = crc >> 0;
|
||||
data[pos++] = crc >> 8;
|
||||
auto crc = crc16(data.data(), data.size());
|
||||
data.push_back(crc >> 0);
|
||||
data.push_back(crc >> 8);
|
||||
|
||||
if (this->flow_control_pin_ != nullptr)
|
||||
this->flow_control_pin_->digital_write(true);
|
||||
|
||||
this->write_array(data, pos);
|
||||
this->write_array(data);
|
||||
this->flush();
|
||||
|
||||
if (this->flow_control_pin_ != nullptr)
|
||||
@@ -281,7 +270,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, pos));
|
||||
ESP_LOGV(TAG, "Modbus write: %s", format_hex_pretty_to(hex_buf, data.data(), data.size()));
|
||||
}
|
||||
|
||||
// Helper function for lambdas
|
||||
|
||||
@@ -56,17 +56,23 @@ void PylontechComponent::setup() {
|
||||
void PylontechComponent::update() { this->write_str("pwr\n"); }
|
||||
|
||||
void PylontechComponent::loop() {
|
||||
if (this->available() > 0) {
|
||||
int avail = this->available();
|
||||
if (avail > 0) {
|
||||
// pylontech sends a lot of data very suddenly
|
||||
// we need to quickly put it all into our own buffer, otherwise the uart's buffer will overflow
|
||||
uint8_t data;
|
||||
int recv = 0;
|
||||
while (this->available() > 0) {
|
||||
if (this->read_byte(&data)) {
|
||||
buffer_[buffer_index_write_] += (char) data;
|
||||
recv++;
|
||||
if (buffer_[buffer_index_write_].back() == static_cast<char>(ASCII_LF) ||
|
||||
buffer_[buffer_index_write_].length() >= MAX_DATA_LENGTH_BYTES) {
|
||||
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;
|
||||
recv += to_read;
|
||||
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
buffer_[buffer_index_write_] += (char) buf[i];
|
||||
if (buf[i] == ASCII_LF || buffer_[buffer_index_write_].length() >= MAX_DATA_LENGTH_BYTES) {
|
||||
// complete line received
|
||||
buffer_index_write_ = (buffer_index_write_ + 1) % NUM_BUFFERS;
|
||||
}
|
||||
|
||||
@@ -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 { // NOLINT
|
||||
std::string to_string() const {
|
||||
char buffer[TO_STR_BUFFER_SIZE];
|
||||
return this->to_str(buffer);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
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"]
|
||||
@@ -52,15 +49,5 @@ 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")
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
# 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])
|
||||
@@ -4,7 +4,6 @@
|
||||
#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>
|
||||
@@ -57,16 +56,6 @@ 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));
|
||||
@@ -100,7 +89,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 || this->type_ == FLASH_STRING) {
|
||||
} else if (this->type_ == STATIC_STRING) {
|
||||
this->static_str_ = other.static_str_;
|
||||
}
|
||||
}
|
||||
@@ -119,7 +108,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 || this->type_ == FLASH_STRING) {
|
||||
} else if (this->type_ == STATIC_STRING) {
|
||||
this->static_str_ = other.static_str_;
|
||||
}
|
||||
other.type_ = NONE;
|
||||
@@ -152,7 +141,7 @@ template<typename T, typename... X> class TemplatableValue {
|
||||
} else if (this->type_ == LAMBDA) {
|
||||
delete this->f_;
|
||||
}
|
||||
// STATELESS_LAMBDA/STATIC_STRING/FLASH_STRING/NONE: no cleanup needed (pointers, not heap-allocated)
|
||||
// STATELESS_LAMBDA/STATIC_STRING/NONE: no cleanup needed (pointers, not heap-allocated)
|
||||
}
|
||||
|
||||
bool has_value() const { return this->type_ != NONE; }
|
||||
@@ -176,17 +165,6 @@ 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{};
|
||||
@@ -208,12 +186,9 @@ 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).
|
||||
@@ -225,12 +200,6 @@ 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()
|
||||
@@ -240,9 +209,8 @@ 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 copy cases (must remain valid while StringRef is used).
|
||||
/// @param lambda_buf Buffer used only for lambda case (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> {
|
||||
@@ -253,19 +221,6 @@ 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
|
||||
@@ -284,7 +239,6 @@ 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.
|
||||
@@ -293,7 +247,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 and FLASH_STRING types
|
||||
const char *static_str_; // For STATIC_STRING type
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -152,10 +152,7 @@ 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
|
||||
@@ -166,10 +163,7 @@ 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
|
||||
@@ -209,18 +203,10 @@ 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) {
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
return App.scheduler.cancel_retry(this, id);
|
||||
#pragma GCC diagnostic pop
|
||||
}
|
||||
bool Component::cancel_retry(uint32_t id) { return App.scheduler.cancel_retry(this, id); }
|
||||
|
||||
void Component::call_loop() { this->loop(); }
|
||||
void Component::call_setup() { this->setup(); }
|
||||
@@ -385,10 +371,7 @@ 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 {
|
||||
|
||||
@@ -68,7 +68,6 @@ 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;
|
||||
@@ -348,40 +347,68 @@ class Component {
|
||||
bool cancel_interval(const char *name); // NOLINT
|
||||
bool cancel_interval(uint32_t id); // NOLINT
|
||||
|
||||
/// @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")
|
||||
/** 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")
|
||||
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
|
||||
|
||||
// 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")
|
||||
/** 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
|
||||
*/
|
||||
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
|
||||
|
||||
// Remove before 2026.8.0
|
||||
ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0")
|
||||
/** 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")
|
||||
bool cancel_retry(const std::string &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(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
|
||||
bool cancel_retry(const char *name); // NOLINT
|
||||
bool cancel_retry(uint32_t id); // NOLINT
|
||||
|
||||
/** Set a timeout function with a unique name.
|
||||
*
|
||||
|
||||
@@ -252,11 +252,6 @@ 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;
|
||||
@@ -369,8 +364,6 @@ 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
|
||||
|
||||
@@ -72,30 +72,18 @@ class Scheduler {
|
||||
bool cancel_interval(Component *component, const char *name);
|
||||
bool cancel_interval(Component *component, uint32_t id);
|
||||
|
||||
// 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")
|
||||
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.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);
|
||||
// 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")
|
||||
/// Set a retry with a numeric ID (zero heap allocation)
|
||||
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);
|
||||
|
||||
// Remove before 2026.8.0
|
||||
ESPDEPRECATED("cancel_retry is deprecated and will be removed in 2026.8.0.", "2026.2.0")
|
||||
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.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
|
||||
@@ -243,14 +231,11 @@ 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 - Remove before 2026.8.0
|
||||
// Common implementation for retry
|
||||
// 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);
|
||||
|
||||
|
||||
@@ -247,23 +247,6 @@ 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",)
|
||||
|
||||
@@ -778,15 +761,6 @@ 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]
|
||||
|
||||
@@ -2881,82 +2881,9 @@ 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:
|
||||
@@ -2975,6 +2902,89 @@ 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
|
||||
|
||||
@@ -756,53 +756,6 @@ 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
|
||||
|
||||
@@ -248,12 +248,6 @@ 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):
|
||||
@@ -630,75 +624,3 @@ 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)
|
||||
|
||||
Reference in New Issue
Block a user