1
0
mirror of https://github.com/esphome/esphome.git synced 2025-10-27 13:13:50 +00:00

Merge remote-tracking branch 'upstream/dev' into ble_connections_slots_are_shared_client_server

This commit is contained in:
J. Nick Koston
2025-10-05 16:12:48 -05:00
46 changed files with 556 additions and 467 deletions

View File

@@ -160,7 +160,6 @@ esphome/components/esp_ldo/* @clydebarrow
esphome/components/espnow/* @jesserockz esphome/components/espnow/* @jesserockz
esphome/components/ethernet_info/* @gtjadsonsantos esphome/components/ethernet_info/* @gtjadsonsantos
esphome/components/event/* @nohat esphome/components/event/* @nohat
esphome/components/event_emitter/* @Rapsssito
esphome/components/exposure_notifications/* @OttoWinter esphome/components/exposure_notifications/* @OttoWinter
esphome/components/ezo/* @ssieb esphome/components/ezo/* @ssieb
esphome/components/ezo_pmp/* @carlos-sarmiento esphome/components/ezo_pmp/* @carlos-sarmiento

View File

@@ -61,6 +61,7 @@ CONF_CUSTOM_SERVICES = "custom_services"
CONF_HOMEASSISTANT_SERVICES = "homeassistant_services" CONF_HOMEASSISTANT_SERVICES = "homeassistant_services"
CONF_HOMEASSISTANT_STATES = "homeassistant_states" CONF_HOMEASSISTANT_STATES = "homeassistant_states"
CONF_LISTEN_BACKLOG = "listen_backlog" CONF_LISTEN_BACKLOG = "listen_backlog"
CONF_MAX_SEND_QUEUE = "max_send_queue"
def validate_encryption_key(value): def validate_encryption_key(value):
@@ -183,6 +184,19 @@ CONFIG_SCHEMA = cv.All(
host=8, # Abundant resources host=8, # Abundant resources
ln882x=8, # Moderate RAM ln882x=8, # Moderate RAM
): cv.int_range(min=1, max=20), ): cv.int_range(min=1, max=20),
# Maximum queued send buffers per connection before dropping connection
# Each buffer uses ~8-12 bytes overhead plus actual message size
# Platform defaults based on available RAM and typical message rates:
cv.SplitDefault(
CONF_MAX_SEND_QUEUE,
esp8266=5, # Limited RAM, need to fail fast
esp32=8, # More RAM, can buffer more
rp2040=5, # Limited RAM
bk72xx=8, # Moderate RAM
rtl87xx=8, # Moderate RAM
host=16, # Abundant resources
ln882x=8, # Moderate RAM
): cv.int_range(min=1, max=64),
} }
).extend(cv.COMPONENT_SCHEMA), ).extend(cv.COMPONENT_SCHEMA),
cv.rename_key(CONF_SERVICES, CONF_ACTIONS), cv.rename_key(CONF_SERVICES, CONF_ACTIONS),
@@ -205,6 +219,7 @@ async def to_code(config):
cg.add(var.set_listen_backlog(config[CONF_LISTEN_BACKLOG])) cg.add(var.set_listen_backlog(config[CONF_LISTEN_BACKLOG]))
if CONF_MAX_CONNECTIONS in config: if CONF_MAX_CONNECTIONS in config:
cg.add(var.set_max_connections(config[CONF_MAX_CONNECTIONS])) cg.add(var.set_max_connections(config[CONF_MAX_CONNECTIONS]))
cg.add_define("API_MAX_SEND_QUEUE", config[CONF_MAX_SEND_QUEUE])
# Set USE_API_SERVICES if any services are enabled # Set USE_API_SERVICES if any services are enabled
if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]: if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]:

View File

@@ -116,8 +116,7 @@ void APIConnection::start() {
APIError err = this->helper_->init(); APIError err = this->helper_->init();
if (err != APIError::OK) { if (err != APIError::OK) {
on_fatal_error(); this->fatal_error_with_log_(LOG_STR("Helper init failed"), err);
this->log_warning_(LOG_STR("Helper init failed"), err);
return; return;
} }
this->client_info_.peername = helper_->getpeername(); this->client_info_.peername = helper_->getpeername();
@@ -147,8 +146,7 @@ void APIConnection::loop() {
APIError err = this->helper_->loop(); APIError err = this->helper_->loop();
if (err != APIError::OK) { if (err != APIError::OK) {
on_fatal_error(); this->fatal_error_with_log_(LOG_STR("Socket operation failed"), err);
this->log_socket_operation_failed_(err);
return; return;
} }
@@ -163,17 +161,13 @@ void APIConnection::loop() {
// No more data available // No more data available
break; break;
} else if (err != APIError::OK) { } else if (err != APIError::OK) {
on_fatal_error(); this->fatal_error_with_log_(LOG_STR("Reading failed"), err);
this->log_warning_(LOG_STR("Reading failed"), err);
return; return;
} else { } else {
this->last_traffic_ = now; this->last_traffic_ = now;
// read a packet // read a packet
if (buffer.data_len > 0) { this->read_message(buffer.data_len, buffer.type,
this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]); buffer.data_len > 0 ? &buffer.container[buffer.data_offset] : nullptr);
} else {
this->read_message(0, buffer.type, nullptr);
}
if (this->flags_.remove) if (this->flags_.remove)
return; return;
} }
@@ -1580,8 +1574,7 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) {
delay(0); delay(0);
APIError err = this->helper_->loop(); APIError err = this->helper_->loop();
if (err != APIError::OK) { if (err != APIError::OK) {
on_fatal_error(); this->fatal_error_with_log_(LOG_STR("Socket operation failed"), err);
this->log_socket_operation_failed_(err);
return false; return false;
} }
if (this->helper_->can_write_without_blocking()) if (this->helper_->can_write_without_blocking())
@@ -1600,8 +1593,7 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
if (err == APIError::WOULD_BLOCK) if (err == APIError::WOULD_BLOCK)
return false; return false;
if (err != APIError::OK) { if (err != APIError::OK) {
on_fatal_error(); this->fatal_error_with_log_(LOG_STR("Packet write failed"), err);
this->log_warning_(LOG_STR("Packet write failed"), err);
return false; return false;
} }
// Do not set last_traffic_ on send // Do not set last_traffic_ on send
@@ -1787,8 +1779,7 @@ void APIConnection::process_batch_() {
APIError err = this->helper_->write_protobuf_packets(ProtoWriteBuffer{&shared_buf}, APIError err = this->helper_->write_protobuf_packets(ProtoWriteBuffer{&shared_buf},
std::span<const PacketInfo>(packet_info, packet_count)); std::span<const PacketInfo>(packet_info, packet_count));
if (err != APIError::OK && err != APIError::WOULD_BLOCK) { if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
on_fatal_error(); this->fatal_error_with_log_(LOG_STR("Batch write failed"), err);
this->log_warning_(LOG_STR("Batch write failed"), err);
} }
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@@ -1871,9 +1862,5 @@ void APIConnection::log_warning_(const LogString *message, APIError err) {
LOG_STR_ARG(message), LOG_STR_ARG(api_error_to_logstr(err)), errno); LOG_STR_ARG(message), LOG_STR_ARG(api_error_to_logstr(err)), errno);
} }
void APIConnection::log_socket_operation_failed_(APIError err) {
this->log_warning_(LOG_STR("Socket operation failed"), err);
}
} // namespace esphome::api } // namespace esphome::api
#endif #endif

View File

@@ -732,8 +732,11 @@ class APIConnection final : public APIServerConnection {
// Helper function to log API errors with errno // Helper function to log API errors with errno
void log_warning_(const LogString *message, APIError err); void log_warning_(const LogString *message, APIError err);
// Specific helper for duplicated error message // Helper to handle fatal errors with logging
void log_socket_operation_failed_(APIError err); inline void fatal_error_with_log_(const LogString *message, APIError err) {
this->on_fatal_error();
this->log_warning_(message, err);
}
}; };
} // namespace esphome::api } // namespace esphome::api

View File

@@ -81,7 +81,7 @@ const LogString *api_error_to_logstr(APIError err) {
// Default implementation for loop - handles sending buffered data // Default implementation for loop - handles sending buffered data
APIError APIFrameHelper::loop() { APIError APIFrameHelper::loop() {
if (!this->tx_buf_.empty()) { if (this->tx_buf_count_ > 0) {
APIError err = try_send_tx_buf_(); APIError err = try_send_tx_buf_();
if (err != APIError::OK && err != APIError::WOULD_BLOCK) { if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
return err; return err;
@@ -103,9 +103,20 @@ APIError APIFrameHelper::handle_socket_write_error_() {
// Helper method to buffer data from IOVs // Helper method to buffer data from IOVs
void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len,
uint16_t offset) { uint16_t offset) {
SendBuffer buffer; // Check if queue is full
buffer.size = total_write_len - offset; if (this->tx_buf_count_ >= API_MAX_SEND_QUEUE) {
buffer.data = std::make_unique<uint8_t[]>(buffer.size); HELPER_LOG("Send queue full (%u buffers), dropping connection", this->tx_buf_count_);
this->state_ = State::FAILED;
return;
}
uint16_t buffer_size = total_write_len - offset;
auto &buffer = this->tx_buf_[this->tx_buf_tail_];
buffer = std::make_unique<SendBuffer>(SendBuffer{
.data = std::make_unique<uint8_t[]>(buffer_size),
.size = buffer_size,
.offset = 0,
});
uint16_t to_skip = offset; uint16_t to_skip = offset;
uint16_t write_pos = 0; uint16_t write_pos = 0;
@@ -118,12 +129,15 @@ void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt,
// Include this segment (partially or fully) // Include this segment (partially or fully)
const uint8_t *src = reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_skip; const uint8_t *src = reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_skip;
uint16_t len = static_cast<uint16_t>(iov[i].iov_len) - to_skip; uint16_t len = static_cast<uint16_t>(iov[i].iov_len) - to_skip;
std::memcpy(buffer.data.get() + write_pos, src, len); std::memcpy(buffer->data.get() + write_pos, src, len);
write_pos += len; write_pos += len;
to_skip = 0; to_skip = 0;
} }
} }
this->tx_buf_.push_back(std::move(buffer));
// Update circular buffer tracking
this->tx_buf_tail_ = (this->tx_buf_tail_ + 1) % API_MAX_SEND_QUEUE;
this->tx_buf_count_++;
} }
// This method writes data to socket or buffers it // This method writes data to socket or buffers it
@@ -141,7 +155,7 @@ APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_
#endif #endif
// Try to send any existing buffered data first if there is any // Try to send any existing buffered data first if there is any
if (!this->tx_buf_.empty()) { if (this->tx_buf_count_ > 0) {
APIError send_result = try_send_tx_buf_(); APIError send_result = try_send_tx_buf_();
// If real error occurred (not just WOULD_BLOCK), return it // If real error occurred (not just WOULD_BLOCK), return it
if (send_result != APIError::OK && send_result != APIError::WOULD_BLOCK) { if (send_result != APIError::OK && send_result != APIError::WOULD_BLOCK) {
@@ -150,7 +164,7 @@ APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_
// If there is still data in the buffer, we can't send, buffer // If there is still data in the buffer, we can't send, buffer
// the new data and return // the new data and return
if (!this->tx_buf_.empty()) { if (this->tx_buf_count_ > 0) {
this->buffer_data_from_iov_(iov, iovcnt, total_write_len, 0); this->buffer_data_from_iov_(iov, iovcnt, total_write_len, 0);
return APIError::OK; // Success, data buffered return APIError::OK; // Success, data buffered
} }
@@ -178,32 +192,31 @@ APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_
} }
// Common implementation for trying to send buffered data // Common implementation for trying to send buffered data
// IMPORTANT: Caller MUST ensure tx_buf_ is not empty before calling this method // IMPORTANT: Caller MUST ensure tx_buf_count_ > 0 before calling this method
APIError APIFrameHelper::try_send_tx_buf_() { APIError APIFrameHelper::try_send_tx_buf_() {
// Try to send from tx_buf - we assume it's not empty as it's the caller's responsibility to check // Try to send from tx_buf - we assume it's not empty as it's the caller's responsibility to check
bool tx_buf_empty = false; while (this->tx_buf_count_ > 0) {
while (!tx_buf_empty) {
// Get the first buffer in the queue // Get the first buffer in the queue
SendBuffer &front_buffer = this->tx_buf_.front(); SendBuffer *front_buffer = this->tx_buf_[this->tx_buf_head_].get();
// Try to send the remaining data in this buffer // Try to send the remaining data in this buffer
ssize_t sent = this->socket_->write(front_buffer.current_data(), front_buffer.remaining()); ssize_t sent = this->socket_->write(front_buffer->current_data(), front_buffer->remaining());
if (sent == -1) { if (sent == -1) {
return this->handle_socket_write_error_(); return this->handle_socket_write_error_();
} else if (sent == 0) { } else if (sent == 0) {
// Nothing sent but not an error // Nothing sent but not an error
return APIError::WOULD_BLOCK; return APIError::WOULD_BLOCK;
} else if (static_cast<uint16_t>(sent) < front_buffer.remaining()) { } else if (static_cast<uint16_t>(sent) < front_buffer->remaining()) {
// Partially sent, update offset // Partially sent, update offset
// Cast to ensure no overflow issues with uint16_t // Cast to ensure no overflow issues with uint16_t
front_buffer.offset += static_cast<uint16_t>(sent); front_buffer->offset += static_cast<uint16_t>(sent);
return APIError::WOULD_BLOCK; // Stop processing more buffers if we couldn't send a complete buffer return APIError::WOULD_BLOCK; // Stop processing more buffers if we couldn't send a complete buffer
} else { } else {
// Buffer completely sent, remove it from the queue // Buffer completely sent, remove it from the queue
this->tx_buf_.pop_front(); this->tx_buf_[this->tx_buf_head_].reset();
// Update empty status for the loop condition this->tx_buf_head_ = (this->tx_buf_head_ + 1) % API_MAX_SEND_QUEUE;
tx_buf_empty = this->tx_buf_.empty(); this->tx_buf_count_--;
// Continue loop to try sending the next buffer // Continue loop to try sending the next buffer
} }
} }

View File

@@ -1,7 +1,8 @@
#pragma once #pragma once
#include <array>
#include <cstdint> #include <cstdint>
#include <deque>
#include <limits> #include <limits>
#include <memory>
#include <span> #include <span>
#include <utility> #include <utility>
#include <vector> #include <vector>
@@ -79,7 +80,7 @@ class APIFrameHelper {
virtual APIError init() = 0; virtual APIError init() = 0;
virtual APIError loop(); virtual APIError loop();
virtual APIError read_packet(ReadPacketBuffer *buffer) = 0; virtual APIError read_packet(ReadPacketBuffer *buffer) = 0;
bool can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); } bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 0; }
std::string getpeername() { return socket_->getpeername(); } std::string getpeername() { return socket_->getpeername(); }
int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); } int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); }
APIError close() { APIError close() {
@@ -161,7 +162,7 @@ class APIFrameHelper {
}; };
// Containers (size varies, but typically 12+ bytes on 32-bit) // Containers (size varies, but typically 12+ bytes on 32-bit)
std::deque<SendBuffer> tx_buf_; std::array<std::unique_ptr<SendBuffer>, API_MAX_SEND_QUEUE> tx_buf_;
std::vector<struct iovec> reusable_iovs_; std::vector<struct iovec> reusable_iovs_;
std::vector<uint8_t> rx_buf_; std::vector<uint8_t> rx_buf_;
@@ -174,7 +175,10 @@ class APIFrameHelper {
State state_{State::INITIALIZE}; State state_{State::INITIALIZE};
uint8_t frame_header_padding_{0}; uint8_t frame_header_padding_{0};
uint8_t frame_footer_size_{0}; uint8_t frame_footer_size_{0};
// 5 bytes total, 3 bytes padding uint8_t tx_buf_head_{0};
uint8_t tx_buf_tail_{0};
uint8_t tx_buf_count_{0};
// 8 bytes total, 0 bytes padding
// Common initialization for both plaintext and noise protocols // Common initialization for both plaintext and noise protocols
APIError init_common_(); APIError init_common_();

View File

@@ -11,7 +11,7 @@ void CopyLock::setup() {
traits.set_assumed_state(source_->traits.get_assumed_state()); traits.set_assumed_state(source_->traits.get_assumed_state());
traits.set_requires_code(source_->traits.get_requires_code()); traits.set_requires_code(source_->traits.get_requires_code());
traits.set_supported_states(source_->traits.get_supported_states()); traits.set_supported_states_mask(source_->traits.get_supported_states_mask());
traits.set_supports_open(source_->traits.get_supports_open()); traits.set_supports_open(source_->traits.get_supports_open());
this->publish_state(source_->state); this->publish_state(source_->state);

View File

@@ -26,7 +26,7 @@ from esphome.const import (
from esphome.core import CORE from esphome.core import CORE
from esphome.schema_extractors import SCHEMA_EXTRACT from esphome.schema_extractors import SCHEMA_EXTRACT
AUTO_LOAD = ["esp32_ble", "bytebuffer", "event_emitter"] AUTO_LOAD = ["esp32_ble", "bytebuffer"]
CODEOWNERS = ["@jesserockz", "@clydebarrow", "@Rapsssito"] CODEOWNERS = ["@jesserockz", "@clydebarrow", "@Rapsssito"]
DEPENDENCIES = ["esp32"] DEPENDENCIES = ["esp32"]
DOMAIN = "esp32_ble_server" DOMAIN = "esp32_ble_server"

View File

@@ -77,7 +77,7 @@ void BLECharacteristic::notify() {
void BLECharacteristic::add_descriptor(BLEDescriptor *descriptor) { void BLECharacteristic::add_descriptor(BLEDescriptor *descriptor) {
// If the descriptor is the CCCD descriptor, listen to its write event to know if the client wants to be notified // If the descriptor is the CCCD descriptor, listen to its write event to know if the client wants to be notified
if (descriptor->get_uuid() == ESPBTUUID::from_uint16(ESP_GATT_UUID_CHAR_CLIENT_CONFIG)) { if (descriptor->get_uuid() == ESPBTUUID::from_uint16(ESP_GATT_UUID_CHAR_CLIENT_CONFIG)) {
descriptor->on(BLEDescriptorEvt::VectorEvt::ON_WRITE, [this](const std::vector<uint8_t> &value, uint16_t conn_id) { descriptor->on_write([this](std::span<const uint8_t> value, uint16_t conn_id) {
if (value.size() != 2) if (value.size() != 2)
return; return;
uint16_t cccd = encode_uint16(value[1], value[0]); uint16_t cccd = encode_uint16(value[1], value[0]);
@@ -212,8 +212,9 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt
if (!param->read.need_rsp) if (!param->read.need_rsp)
break; // For some reason you can request a read but not want a response break; // For some reason you can request a read but not want a response
this->EventEmitter<BLECharacteristicEvt::EmptyEvt, uint16_t>::emit_(BLECharacteristicEvt::EmptyEvt::ON_READ, if (this->on_read_callback_) {
param->read.conn_id); (*this->on_read_callback_)(param->read.conn_id);
}
uint16_t max_offset = 22; uint16_t max_offset = 22;
@@ -281,8 +282,9 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt
} }
if (!param->write.is_prep) { if (!param->write.is_prep) {
this->EventEmitter<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, uint16_t>::emit_( if (this->on_write_callback_) {
BLECharacteristicEvt::VectorEvt::ON_WRITE, this->value_, param->write.conn_id); (*this->on_write_callback_)(this->value_, param->write.conn_id);
}
} }
break; break;
@@ -293,8 +295,9 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt
break; break;
this->write_event_ = false; this->write_event_ = false;
if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC) { if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC) {
this->EventEmitter<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, uint16_t>::emit_( if (this->on_write_callback_) {
BLECharacteristicEvt::VectorEvt::ON_WRITE, this->value_, param->exec_write.conn_id); (*this->on_write_callback_)(this->value_, param->exec_write.conn_id);
}
} }
esp_err_t err = esp_err_t err =
esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, nullptr); esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, nullptr);

View File

@@ -2,10 +2,12 @@
#include "ble_descriptor.h" #include "ble_descriptor.h"
#include "esphome/components/esp32_ble/ble_uuid.h" #include "esphome/components/esp32_ble/ble_uuid.h"
#include "esphome/components/event_emitter/event_emitter.h"
#include "esphome/components/bytebuffer/bytebuffer.h" #include "esphome/components/bytebuffer/bytebuffer.h"
#include <vector> #include <vector>
#include <span>
#include <functional>
#include <memory>
#ifdef USE_ESP32 #ifdef USE_ESP32
@@ -22,22 +24,10 @@ namespace esp32_ble_server {
using namespace esp32_ble; using namespace esp32_ble;
using namespace bytebuffer; using namespace bytebuffer;
using namespace event_emitter;
class BLEService; class BLEService;
namespace BLECharacteristicEvt { class BLECharacteristic {
enum VectorEvt {
ON_WRITE,
};
enum EmptyEvt {
ON_READ,
};
} // namespace BLECharacteristicEvt
class BLECharacteristic : public EventEmitter<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, uint16_t>,
public EventEmitter<BLECharacteristicEvt::EmptyEvt, uint16_t> {
public: public:
BLECharacteristic(ESPBTUUID uuid, uint32_t properties); BLECharacteristic(ESPBTUUID uuid, uint32_t properties);
~BLECharacteristic(); ~BLECharacteristic();
@@ -76,6 +66,15 @@ class BLECharacteristic : public EventEmitter<BLECharacteristicEvt::VectorEvt, s
bool is_created(); bool is_created();
bool is_failed(); bool is_failed();
// Direct callback registration - only allocates when callback is set
void on_write(std::function<void(std::span<const uint8_t>, uint16_t)> &&callback) {
this->on_write_callback_ =
std::make_unique<std::function<void(std::span<const uint8_t>, uint16_t)>>(std::move(callback));
}
void on_read(std::function<void(uint16_t)> &&callback) {
this->on_read_callback_ = std::make_unique<std::function<void(uint16_t)>>(std::move(callback));
}
protected: protected:
bool write_event_{false}; bool write_event_{false};
BLEService *service_{}; BLEService *service_{};
@@ -98,6 +97,9 @@ class BLECharacteristic : public EventEmitter<BLECharacteristicEvt::VectorEvt, s
void remove_client_from_notify_list_(uint16_t conn_id); void remove_client_from_notify_list_(uint16_t conn_id);
ClientNotificationEntry *find_client_in_notify_list_(uint16_t conn_id); ClientNotificationEntry *find_client_in_notify_list_(uint16_t conn_id);
std::unique_ptr<std::function<void(std::span<const uint8_t>, uint16_t)>> on_write_callback_;
std::unique_ptr<std::function<void(uint16_t)>> on_read_callback_;
esp_gatt_perm_t permissions_ = ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE; esp_gatt_perm_t permissions_ = ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE;
enum State : uint8_t { enum State : uint8_t {

View File

@@ -74,9 +74,10 @@ void BLEDescriptor::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_
break; break;
this->value_.attr_len = param->write.len; this->value_.attr_len = param->write.len;
memcpy(this->value_.attr_value, param->write.value, param->write.len); memcpy(this->value_.attr_value, param->write.value, param->write.len);
this->emit_(BLEDescriptorEvt::VectorEvt::ON_WRITE, if (this->on_write_callback_) {
std::vector<uint8_t>(param->write.value, param->write.value + param->write.len), (*this->on_write_callback_)(std::span<const uint8_t>(param->write.value, param->write.len),
param->write.conn_id); param->write.conn_id);
}
break; break;
} }
default: default:

View File

@@ -1,30 +1,26 @@
#pragma once #pragma once
#include "esphome/components/esp32_ble/ble_uuid.h" #include "esphome/components/esp32_ble/ble_uuid.h"
#include "esphome/components/event_emitter/event_emitter.h"
#include "esphome/components/bytebuffer/bytebuffer.h" #include "esphome/components/bytebuffer/bytebuffer.h"
#ifdef USE_ESP32 #ifdef USE_ESP32
#include <esp_gatt_defs.h> #include <esp_gatt_defs.h>
#include <esp_gatts_api.h> #include <esp_gatts_api.h>
#include <span>
#include <functional>
#include <memory>
namespace esphome { namespace esphome {
namespace esp32_ble_server { namespace esp32_ble_server {
using namespace esp32_ble; using namespace esp32_ble;
using namespace bytebuffer; using namespace bytebuffer;
using namespace event_emitter;
class BLECharacteristic; class BLECharacteristic;
namespace BLEDescriptorEvt { // Base class for BLE descriptors
enum VectorEvt { class BLEDescriptor {
ON_WRITE,
};
} // namespace BLEDescriptorEvt
class BLEDescriptor : public EventEmitter<BLEDescriptorEvt::VectorEvt, std::vector<uint8_t>, uint16_t> {
public: public:
BLEDescriptor(ESPBTUUID uuid, uint16_t max_len = 100, bool read = true, bool write = true); BLEDescriptor(ESPBTUUID uuid, uint16_t max_len = 100, bool read = true, bool write = true);
virtual ~BLEDescriptor(); virtual ~BLEDescriptor();
@@ -39,6 +35,12 @@ class BLEDescriptor : public EventEmitter<BLEDescriptorEvt::VectorEvt, std::vect
bool is_created() { return this->state_ == CREATED; } bool is_created() { return this->state_ == CREATED; }
bool is_failed() { return this->state_ == FAILED; } bool is_failed() { return this->state_ == FAILED; }
// Direct callback registration - only allocates when callback is set
void on_write(std::function<void(std::span<const uint8_t>, uint16_t)> &&callback) {
this->on_write_callback_ =
std::make_unique<std::function<void(std::span<const uint8_t>, uint16_t)>>(std::move(callback));
}
protected: protected:
BLECharacteristic *characteristic_{nullptr}; BLECharacteristic *characteristic_{nullptr};
ESPBTUUID uuid_; ESPBTUUID uuid_;
@@ -46,6 +48,8 @@ class BLEDescriptor : public EventEmitter<BLEDescriptorEvt::VectorEvt, std::vect
esp_attr_value_t value_{}; esp_attr_value_t value_{};
std::unique_ptr<std::function<void(std::span<const uint8_t>, uint16_t)>> on_write_callback_;
esp_gatt_perm_t permissions_{}; esp_gatt_perm_t permissions_{};
enum State : uint8_t { enum State : uint8_t {

View File

@@ -147,20 +147,28 @@ BLEService *BLEServer::get_service(ESPBTUUID uuid, uint8_t inst_id) {
return nullptr; return nullptr;
} }
void BLEServer::dispatch_callbacks_(CallbackType type, uint16_t conn_id) {
for (auto &entry : this->callbacks_) {
if (entry.type == type) {
entry.callback(conn_id);
}
}
}
void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
esp_ble_gatts_cb_param_t *param) { esp_ble_gatts_cb_param_t *param) {
switch (event) { switch (event) {
case ESP_GATTS_CONNECT_EVT: { case ESP_GATTS_CONNECT_EVT: {
ESP_LOGD(TAG, "BLE Client connected"); ESP_LOGD(TAG, "BLE Client connected");
this->add_client_(param->connect.conn_id); this->add_client_(param->connect.conn_id);
this->emit_(BLEServerEvt::EmptyEvt::ON_CONNECT, param->connect.conn_id); this->dispatch_callbacks_(CallbackType::ON_CONNECT, param->connect.conn_id);
break; break;
} }
case ESP_GATTS_DISCONNECT_EVT: { case ESP_GATTS_DISCONNECT_EVT: {
ESP_LOGD(TAG, "BLE Client disconnected"); ESP_LOGD(TAG, "BLE Client disconnected");
this->remove_client_(param->disconnect.conn_id); this->remove_client_(param->disconnect.conn_id);
this->parent_->advertising_start(); this->parent_->advertising_start();
this->emit_(BLEServerEvt::EmptyEvt::ON_DISCONNECT, param->disconnect.conn_id); this->dispatch_callbacks_(CallbackType::ON_DISCONNECT, param->disconnect.conn_id);
break; break;
} }
case ESP_GATTS_REG_EVT: { case ESP_GATTS_REG_EVT: {

View File

@@ -12,6 +12,7 @@
#include <memory> #include <memory>
#include <vector> #include <vector>
#include <unordered_map> #include <unordered_map>
#include <functional>
#ifdef USE_ESP32 #ifdef USE_ESP32
@@ -23,18 +24,7 @@ namespace esp32_ble_server {
using namespace esp32_ble; using namespace esp32_ble;
using namespace bytebuffer; using namespace bytebuffer;
namespace BLEServerEvt { class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEventHandler, public Parented<ESP32BLE> {
enum EmptyEvt {
ON_CONNECT,
ON_DISCONNECT,
};
} // namespace BLEServerEvt
class BLEServer : public Component,
public GATTsEventHandler,
public BLEStatusEventHandler,
public Parented<ESP32BLE>,
public EventEmitter<BLEServerEvt::EmptyEvt, uint16_t> {
public: public:
void setup() override; void setup() override;
void loop() override; void loop() override;
@@ -65,7 +55,25 @@ class BLEServer : public Component,
void ble_before_disabled_event_handler() override; void ble_before_disabled_event_handler() override;
// Direct callback registration - supports multiple callbacks
void on_connect(std::function<void(uint16_t)> &&callback) {
this->callbacks_.push_back({CallbackType::ON_CONNECT, std::move(callback)});
}
void on_disconnect(std::function<void(uint16_t)> &&callback) {
this->callbacks_.push_back({CallbackType::ON_DISCONNECT, std::move(callback)});
}
protected: protected:
enum class CallbackType : uint8_t {
ON_CONNECT,
ON_DISCONNECT,
};
struct CallbackEntry {
CallbackType type;
std::function<void(uint16_t)> callback;
};
struct ServiceEntry { struct ServiceEntry {
ESPBTUUID uuid; ESPBTUUID uuid;
uint8_t inst_id; uint8_t inst_id;
@@ -77,6 +85,9 @@ class BLEServer : public Component,
int8_t find_client_index_(uint16_t conn_id) const; int8_t find_client_index_(uint16_t conn_id) const;
void add_client_(uint16_t conn_id); void add_client_(uint16_t conn_id);
void remove_client_(uint16_t conn_id); void remove_client_(uint16_t conn_id);
void dispatch_callbacks_(CallbackType type, uint16_t conn_id);
std::vector<CallbackEntry> callbacks_;
std::vector<uint8_t> manufacturer_data_{}; std::vector<uint8_t> manufacturer_data_{};
esp_gatt_if_t gatts_if_{0}; esp_gatt_if_t gatts_if_{0};

View File

@@ -14,9 +14,10 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_characteristic_on_w
BLECharacteristic *characteristic) { BLECharacteristic *characteristic) {
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory) Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
new Trigger<std::vector<uint8_t>, uint16_t>(); new Trigger<std::vector<uint8_t>, uint16_t>();
characteristic->EventEmitter<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, uint16_t>::on( characteristic->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
BLECharacteristicEvt::VectorEvt::ON_WRITE, // Convert span to vector for trigger
[on_write_trigger](const std::vector<uint8_t> &data, uint16_t id) { on_write_trigger->trigger(data, id); }); on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
});
return on_write_trigger; return on_write_trigger;
} }
#endif #endif
@@ -25,9 +26,10 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_characteristic_on_w
Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_descriptor_on_write_trigger(BLEDescriptor *descriptor) { Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_descriptor_on_write_trigger(BLEDescriptor *descriptor) {
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory) Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
new Trigger<std::vector<uint8_t>, uint16_t>(); new Trigger<std::vector<uint8_t>, uint16_t>();
descriptor->on( descriptor->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
BLEDescriptorEvt::VectorEvt::ON_WRITE, // Convert span to vector for trigger
[on_write_trigger](const std::vector<uint8_t> &data, uint16_t id) { on_write_trigger->trigger(data, id); }); on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
});
return on_write_trigger; return on_write_trigger;
} }
#endif #endif
@@ -35,8 +37,7 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_descriptor_on_write
#ifdef USE_ESP32_BLE_SERVER_ON_CONNECT #ifdef USE_ESP32_BLE_SERVER_ON_CONNECT
Trigger<uint16_t> *BLETriggers::create_server_on_connect_trigger(BLEServer *server) { Trigger<uint16_t> *BLETriggers::create_server_on_connect_trigger(BLEServer *server) {
Trigger<uint16_t> *on_connect_trigger = new Trigger<uint16_t>(); // NOLINT(cppcoreguidelines-owning-memory) Trigger<uint16_t> *on_connect_trigger = new Trigger<uint16_t>(); // NOLINT(cppcoreguidelines-owning-memory)
server->on(BLEServerEvt::EmptyEvt::ON_CONNECT, server->on_connect([on_connect_trigger](uint16_t conn_id) { on_connect_trigger->trigger(conn_id); });
[on_connect_trigger](uint16_t conn_id) { on_connect_trigger->trigger(conn_id); });
return on_connect_trigger; return on_connect_trigger;
} }
#endif #endif
@@ -44,38 +45,22 @@ Trigger<uint16_t> *BLETriggers::create_server_on_connect_trigger(BLEServer *serv
#ifdef USE_ESP32_BLE_SERVER_ON_DISCONNECT #ifdef USE_ESP32_BLE_SERVER_ON_DISCONNECT
Trigger<uint16_t> *BLETriggers::create_server_on_disconnect_trigger(BLEServer *server) { Trigger<uint16_t> *BLETriggers::create_server_on_disconnect_trigger(BLEServer *server) {
Trigger<uint16_t> *on_disconnect_trigger = new Trigger<uint16_t>(); // NOLINT(cppcoreguidelines-owning-memory) Trigger<uint16_t> *on_disconnect_trigger = new Trigger<uint16_t>(); // NOLINT(cppcoreguidelines-owning-memory)
server->on(BLEServerEvt::EmptyEvt::ON_DISCONNECT, server->on_disconnect([on_disconnect_trigger](uint16_t conn_id) { on_disconnect_trigger->trigger(conn_id); });
[on_disconnect_trigger](uint16_t conn_id) { on_disconnect_trigger->trigger(conn_id); });
return on_disconnect_trigger; return on_disconnect_trigger;
} }
#endif #endif
#ifdef USE_ESP32_BLE_SERVER_SET_VALUE_ACTION #ifdef USE_ESP32_BLE_SERVER_SET_VALUE_ACTION
void BLECharacteristicSetValueActionManager::set_listener(BLECharacteristic *characteristic, void BLECharacteristicSetValueActionManager::set_listener(BLECharacteristic *characteristic,
EventEmitterListenerID listener_id,
const std::function<void()> &pre_notify_listener) { const std::function<void()> &pre_notify_listener) {
// Find and remove existing listener for this characteristic // Find and remove existing listener for this characteristic
auto *existing = this->find_listener_(characteristic); auto *existing = this->find_listener_(characteristic);
if (existing != nullptr) { if (existing != nullptr) {
// Remove the previous listener
characteristic->EventEmitter<BLECharacteristicEvt::EmptyEvt, uint16_t>::off(BLECharacteristicEvt::EmptyEvt::ON_READ,
existing->listener_id);
// Remove the pre-notify listener
this->off(BLECharacteristicSetValueActionEvt::PRE_NOTIFY, existing->pre_notify_listener_id);
// Remove from vector // Remove from vector
this->remove_listener_(characteristic); this->remove_listener_(characteristic);
} }
// Create a new listener for the pre-notify event
EventEmitterListenerID pre_notify_listener_id =
this->on(BLECharacteristicSetValueActionEvt::PRE_NOTIFY,
[pre_notify_listener, characteristic](const BLECharacteristic *evt_characteristic) {
// Only call the pre-notify listener if the characteristic is the one we are interested in
if (characteristic == evt_characteristic) {
pre_notify_listener();
}
});
// Save the entry to the vector // Save the entry to the vector
this->listeners_.push_back({characteristic, listener_id, pre_notify_listener_id}); this->listeners_.push_back({characteristic, pre_notify_listener});
} }
BLECharacteristicSetValueActionManager::ListenerEntry *BLECharacteristicSetValueActionManager::find_listener_( BLECharacteristicSetValueActionManager::ListenerEntry *BLECharacteristicSetValueActionManager::find_listener_(

View File

@@ -4,7 +4,6 @@
#include "ble_characteristic.h" #include "ble_characteristic.h"
#include "ble_descriptor.h" #include "ble_descriptor.h"
#include "esphome/components/event_emitter/event_emitter.h"
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
#include <vector> #include <vector>
@@ -18,10 +17,6 @@ namespace esp32_ble_server {
namespace esp32_ble_server_automations { namespace esp32_ble_server_automations {
using namespace esp32_ble; using namespace esp32_ble;
using namespace event_emitter;
// Invalid listener ID constant - 0 is used as sentinel value in EventEmitter
static constexpr EventEmitterListenerID INVALID_LISTENER_ID = 0;
class BLETriggers { class BLETriggers {
public: public:
@@ -41,38 +36,29 @@ class BLETriggers {
}; };
#ifdef USE_ESP32_BLE_SERVER_SET_VALUE_ACTION #ifdef USE_ESP32_BLE_SERVER_SET_VALUE_ACTION
enum BLECharacteristicSetValueActionEvt {
PRE_NOTIFY,
};
// Class to make sure only one BLECharacteristicSetValueAction is active at a time for each characteristic // Class to make sure only one BLECharacteristicSetValueAction is active at a time for each characteristic
class BLECharacteristicSetValueActionManager class BLECharacteristicSetValueActionManager {
: public EventEmitter<BLECharacteristicSetValueActionEvt, BLECharacteristic *> {
public: public:
// Singleton pattern // Singleton pattern
static BLECharacteristicSetValueActionManager *get_instance() { static BLECharacteristicSetValueActionManager *get_instance() {
static BLECharacteristicSetValueActionManager instance; static BLECharacteristicSetValueActionManager instance;
return &instance; return &instance;
} }
void set_listener(BLECharacteristic *characteristic, EventEmitterListenerID listener_id, void set_listener(BLECharacteristic *characteristic, const std::function<void()> &pre_notify_listener);
const std::function<void()> &pre_notify_listener); bool has_listener(BLECharacteristic *characteristic) { return this->find_listener_(characteristic) != nullptr; }
EventEmitterListenerID get_listener(BLECharacteristic *characteristic) { void emit_pre_notify(BLECharacteristic *characteristic) {
for (const auto &entry : this->listeners_) { for (const auto &entry : this->listeners_) {
if (entry.characteristic == characteristic) { if (entry.characteristic == characteristic) {
return entry.listener_id; entry.pre_notify_listener();
break;
} }
} }
return INVALID_LISTENER_ID;
}
void emit_pre_notify(BLECharacteristic *characteristic) {
this->emit_(BLECharacteristicSetValueActionEvt::PRE_NOTIFY, characteristic);
} }
private: private:
struct ListenerEntry { struct ListenerEntry {
BLECharacteristic *characteristic; BLECharacteristic *characteristic;
EventEmitterListenerID listener_id; std::function<void()> pre_notify_listener;
EventEmitterListenerID pre_notify_listener_id;
}; };
std::vector<ListenerEntry> listeners_; std::vector<ListenerEntry> listeners_;
@@ -87,24 +73,22 @@ template<typename... Ts> class BLECharacteristicSetValueAction : public Action<T
void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); } void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); }
void play(Ts... x) override { void play(Ts... x) override {
// If the listener is already set, do nothing // If the listener is already set, do nothing
if (BLECharacteristicSetValueActionManager::get_instance()->get_listener(this->parent_) == this->listener_id_) if (BLECharacteristicSetValueActionManager::get_instance()->has_listener(this->parent_))
return; return;
// Set initial value // Set initial value
this->parent_->set_value(this->buffer_.value(x...)); this->parent_->set_value(this->buffer_.value(x...));
// Set the listener for read events // Set the listener for read events
this->listener_id_ = this->parent_->EventEmitter<BLECharacteristicEvt::EmptyEvt, uint16_t>::on( this->parent_->on_read([this, x...](uint16_t id) {
BLECharacteristicEvt::EmptyEvt::ON_READ, [this, x...](uint16_t id) {
// Set the value of the characteristic every time it is read // Set the value of the characteristic every time it is read
this->parent_->set_value(this->buffer_.value(x...)); this->parent_->set_value(this->buffer_.value(x...));
}); });
// Set the listener in the global manager so only one BLECharacteristicSetValueAction is set for each characteristic // Set the listener in the global manager so only one BLECharacteristicSetValueAction is set for each characteristic
BLECharacteristicSetValueActionManager::get_instance()->set_listener( BLECharacteristicSetValueActionManager::get_instance()->set_listener(
this->parent_, this->listener_id_, [this, x...]() { this->parent_->set_value(this->buffer_.value(x...)); }); this->parent_, [this, x...]() { this->parent_->set_value(this->buffer_.value(x...)); });
} }
protected: protected:
BLECharacteristic *parent_; BLECharacteristic *parent_;
EventEmitterListenerID listener_id_;
}; };
#endif // USE_ESP32_BLE_SERVER_SET_VALUE_ACTION #endif // USE_ESP32_BLE_SERVER_SET_VALUE_ACTION

View File

@@ -38,8 +38,7 @@ void ESP32ImprovComponent::setup() {
}); });
} }
#endif #endif
global_ble_server->on(BLEServerEvt::EmptyEvt::ON_DISCONNECT, global_ble_server->on_disconnect([this](uint16_t conn_id) { this->set_error_(improv::ERROR_NONE); });
[this](uint16_t conn_id) { this->set_error_(improv::ERROR_NONE); });
// Start with loop disabled - will be enabled by start() when needed // Start with loop disabled - will be enabled by start() when needed
this->disable_loop(); this->disable_loop();
@@ -57,8 +56,7 @@ void ESP32ImprovComponent::setup_characteristics() {
this->error_->add_descriptor(error_descriptor); this->error_->add_descriptor(error_descriptor);
this->rpc_ = this->service_->create_characteristic(improv::RPC_COMMAND_UUID, BLECharacteristic::PROPERTY_WRITE); this->rpc_ = this->service_->create_characteristic(improv::RPC_COMMAND_UUID, BLECharacteristic::PROPERTY_WRITE);
this->rpc_->EventEmitter<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, uint16_t>::on( this->rpc_->on_write([this](std::span<const uint8_t> data, uint16_t id) {
BLECharacteristicEvt::VectorEvt::ON_WRITE, [this](const std::vector<uint8_t> &data, uint16_t id) {
if (!data.empty()) { if (!data.empty()) {
this->incoming_data_.insert(this->incoming_data_.end(), data.begin(), data.end()); this->incoming_data_.insert(this->incoming_data_.end(), data.begin(), data.end());
} }

View File

@@ -41,17 +41,20 @@ static const char *const TAG = "ethernet";
EthernetComponent *global_eth_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) EthernetComponent *global_eth_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void EthernetComponent::log_error_and_mark_failed_(esp_err_t err, const char *message) {
ESP_LOGE(TAG, "%s: (%d) %s", message, err, esp_err_to_name(err));
this->mark_failed();
}
#define ESPHL_ERROR_CHECK(err, message) \ #define ESPHL_ERROR_CHECK(err, message) \
if ((err) != ESP_OK) { \ if ((err) != ESP_OK) { \
ESP_LOGE(TAG, message ": (%d) %s", err, esp_err_to_name(err)); \ this->log_error_and_mark_failed_(err, message); \
this->mark_failed(); \
return; \ return; \
} }
#define ESPHL_ERROR_CHECK_RET(err, message, ret) \ #define ESPHL_ERROR_CHECK_RET(err, message, ret) \
if ((err) != ESP_OK) { \ if ((err) != ESP_OK) { \
ESP_LOGE(TAG, message ": (%d) %s", err, esp_err_to_name(err)); \ this->log_error_and_mark_failed_(err, message); \
this->mark_failed(); \
return ret; \ return ret; \
} }

View File

@@ -106,6 +106,7 @@ class EthernetComponent : public Component {
void start_connect_(); void start_connect_();
void finish_connect_(); void finish_connect_();
void dump_connect_params_(); void dump_connect_params_();
void log_error_and_mark_failed_(esp_err_t err, const char *message);
#ifdef USE_ETHERNET_KSZ8081 #ifdef USE_ETHERNET_KSZ8081
/// @brief Set `RMII Reference Clock Select` bit for KSZ8081. /// @brief Set `RMII Reference Clock Select` bit for KSZ8081.
void ksz8081_set_clock_reference_(esp_eth_mac_t *mac); void ksz8081_set_clock_reference_(esp_eth_mac_t *mac);

View File

@@ -1,5 +0,0 @@
CODEOWNERS = ["@Rapsssito"]
# Allows event_emitter to be configured in yaml, to allow use of the C++ api.
CONFIG_SCHEMA = {}

View File

@@ -1,117 +0,0 @@
#pragma once
#include <vector>
#include <functional>
#include <limits>
#include "esphome/core/log.h"
namespace esphome {
namespace event_emitter {
using EventEmitterListenerID = uint32_t;
static constexpr EventEmitterListenerID INVALID_LISTENER_ID = 0;
// EventEmitter class that can emit events with a specific name (it is highly recommended to use an enum class for this)
// and a list of arguments. Supports multiple listeners for each event.
template<typename EvtType, typename... Args> class EventEmitter {
public:
EventEmitterListenerID on(EvtType event, std::function<void(Args...)> listener) {
EventEmitterListenerID listener_id = this->get_next_id_();
// Find or create event entry
EventEntry *entry = this->find_or_create_event_(event);
entry->listeners.push_back({listener_id, listener});
return listener_id;
}
void off(EvtType event, EventEmitterListenerID id) {
EventEntry *entry = this->find_event_(event);
if (entry == nullptr)
return;
// Remove listener with given id
for (auto it = entry->listeners.begin(); it != entry->listeners.end(); ++it) {
if (it->id == id) {
// Swap with last and pop for efficient removal
*it = entry->listeners.back();
entry->listeners.pop_back();
// Remove event entry if no more listeners
if (entry->listeners.empty()) {
this->remove_event_(event);
}
return;
}
}
}
protected:
void emit_(EvtType event, Args... args) {
EventEntry *entry = this->find_event_(event);
if (entry == nullptr)
return;
// Call all listeners for this event
for (const auto &listener : entry->listeners) {
listener.callback(args...);
}
}
private:
struct Listener {
EventEmitterListenerID id;
std::function<void(Args...)> callback;
};
struct EventEntry {
EvtType event;
std::vector<Listener> listeners;
};
EventEmitterListenerID get_next_id_() {
// Simple incrementing ID, wrapping around at max
EventEmitterListenerID next_id = (this->current_id_ + 1);
if (next_id == INVALID_LISTENER_ID) {
next_id = 1;
}
this->current_id_ = next_id;
return this->current_id_;
}
EventEntry *find_event_(EvtType event) {
for (auto &entry : this->events_) {
if (entry.event == event) {
return &entry;
}
}
return nullptr;
}
EventEntry *find_or_create_event_(EvtType event) {
EventEntry *entry = this->find_event_(event);
if (entry != nullptr)
return entry;
// Create new event entry
this->events_.push_back({event, {}});
return &this->events_.back();
}
void remove_event_(EvtType event) {
for (auto it = this->events_.begin(); it != this->events_.end(); ++it) {
if (it->event == event) {
// Swap with last and pop
*it = this->events_.back();
this->events_.pop_back();
return;
}
}
}
std::vector<EventEntry> events_;
EventEmitterListenerID current_id_ = 0;
};
} // namespace event_emitter
} // namespace esphome

View File

@@ -2,6 +2,7 @@
#include <vector> #include <vector>
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#define ARDUINOJSON_ENABLE_STD_STRING 1 // NOLINT #define ARDUINOJSON_ENABLE_STD_STRING 1 // NOLINT

View File

@@ -5,7 +5,7 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/preferences.h" #include "esphome/core/preferences.h"
#include <set> #include <initializer_list>
namespace esphome { namespace esphome {
namespace lock { namespace lock {
@@ -44,16 +44,22 @@ class LockTraits {
bool get_assumed_state() const { return this->assumed_state_; } bool get_assumed_state() const { return this->assumed_state_; }
void set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } void set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; }
bool supports_state(LockState state) const { return supported_states_.count(state); } bool supports_state(LockState state) const { return supported_states_mask_ & (1 << state); }
std::set<LockState> get_supported_states() const { return supported_states_; } void set_supported_states(std::initializer_list<LockState> states) {
void set_supported_states(std::set<LockState> states) { supported_states_ = std::move(states); } supported_states_mask_ = 0;
void add_supported_state(LockState state) { supported_states_.insert(state); } for (auto state : states) {
supported_states_mask_ |= (1 << state);
}
}
uint8_t get_supported_states_mask() const { return supported_states_mask_; }
void set_supported_states_mask(uint8_t mask) { supported_states_mask_ = mask; }
void add_supported_state(LockState state) { supported_states_mask_ |= (1 << state); }
protected: protected:
bool supports_open_{false}; bool supports_open_{false};
bool requires_code_{false}; bool requires_code_{false};
bool assumed_state_{false}; bool assumed_state_{false};
std::set<LockState> supported_states_ = {LOCK_STATE_NONE, LOCK_STATE_LOCKED, LOCK_STATE_UNLOCKED}; uint8_t supported_states_mask_{(1 << LOCK_STATE_NONE) | (1 << LOCK_STATE_LOCKED) | (1 << LOCK_STATE_UNLOCKED)};
}; };
/** This class is used to encode all control actions on a lock device. /** This class is used to encode all control actions on a lock device.

View File

@@ -95,6 +95,7 @@ DEFAULT = "DEFAULT"
CONF_INITIAL_LEVEL = "initial_level" CONF_INITIAL_LEVEL = "initial_level"
CONF_LOGGER_ID = "logger_id" CONF_LOGGER_ID = "logger_id"
CONF_RUNTIME_TAG_LEVELS = "runtime_tag_levels"
CONF_TASK_LOG_BUFFER_SIZE = "task_log_buffer_size" CONF_TASK_LOG_BUFFER_SIZE = "task_log_buffer_size"
UART_SELECTION_ESP32 = { UART_SELECTION_ESP32 = {
@@ -249,6 +250,7 @@ CONFIG_SCHEMA = cv.All(
} }
), ),
cv.Optional(CONF_INITIAL_LEVEL): is_log_level, cv.Optional(CONF_INITIAL_LEVEL): is_log_level,
cv.Optional(CONF_RUNTIME_TAG_LEVELS, default=False): cv.boolean,
cv.Optional(CONF_ON_MESSAGE): automation.validate_automation( cv.Optional(CONF_ON_MESSAGE): automation.validate_automation(
{ {
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LoggerMessageTrigger), cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LoggerMessageTrigger),
@@ -291,7 +293,11 @@ async def to_code(config):
) )
cg.add(log.pre_setup()) cg.add(log.pre_setup())
for tag, log_level in config[CONF_LOGS].items(): # Enable runtime tag levels if logs are configured or explicitly enabled
logs_config = config[CONF_LOGS]
if logs_config or config[CONF_RUNTIME_TAG_LEVELS]:
cg.add_define("USE_LOGGER_RUNTIME_TAG_LEVELS")
for tag, log_level in logs_config.items():
cg.add(log.set_log_level(tag, LOG_LEVELS[log_level])) cg.add(log.set_log_level(tag, LOG_LEVELS[log_level]))
cg.add_define("USE_LOGGER") cg.add_define("USE_LOGGER")
@@ -443,6 +449,7 @@ async def logger_set_level_to_code(config, action_id, template_arg, args):
level = LOG_LEVELS[config[CONF_LEVEL]] level = LOG_LEVELS[config[CONF_LEVEL]]
logger = await cg.get_variable(config[CONF_LOGGER_ID]) logger = await cg.get_variable(config[CONF_LOGGER_ID])
if tag := config.get(CONF_TAG): if tag := config.get(CONF_TAG):
cg.add_define("USE_LOGGER_RUNTIME_TAG_LEVELS")
text = str(cg.statement(logger.set_log_level(tag, level))) text = str(cg.statement(logger.set_log_level(tag, level)))
else: else:
text = str(cg.statement(logger.set_log_level(level))) text = str(cg.statement(logger.set_log_level(level)))

View File

@@ -148,9 +148,11 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas
#endif // USE_STORE_LOG_STR_IN_FLASH #endif // USE_STORE_LOG_STR_IN_FLASH
inline uint8_t Logger::level_for(const char *tag) { inline uint8_t Logger::level_for(const char *tag) {
#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS
auto it = this->log_levels_.find(tag); auto it = this->log_levels_.find(tag);
if (it != this->log_levels_.end()) if (it != this->log_levels_.end())
return it->second; return it->second;
#endif
return this->current_level_; return this->current_level_;
} }
@@ -220,7 +222,9 @@ void Logger::process_messages_() {
} }
void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; } void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; }
void Logger::set_log_level(const std::string &tag, uint8_t log_level) { this->log_levels_[tag] = log_level; } #ifdef USE_LOGGER_RUNTIME_TAG_LEVELS
void Logger::set_log_level(const char *tag, uint8_t log_level) { this->log_levels_[tag] = log_level; }
#endif
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
UARTSelection Logger::get_uart() const { return this->uart_; } UARTSelection Logger::get_uart() const { return this->uart_; }
@@ -271,9 +275,11 @@ void Logger::dump_config() {
} }
#endif #endif
#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS
for (auto &it : this->log_levels_) { for (auto &it : this->log_levels_) {
ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first.c_str(), LOG_STR_ARG(LOG_LEVELS[it.second])); ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first, LOG_STR_ARG(LOG_LEVELS[it.second]));
} }
#endif
} }
void Logger::set_log_level(uint8_t level) { void Logger::set_log_level(uint8_t level) {

View File

@@ -36,6 +36,13 @@ struct device;
namespace esphome::logger { namespace esphome::logger {
#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS
// Comparison function for const char* keys in log_levels_ map
struct CStrCompare {
bool operator()(const char *a, const char *b) const { return strcmp(a, b) < 0; }
};
#endif
// ANSI color code last digit (30-38 range, store only last digit to save RAM) // ANSI color code last digit (30-38 range, store only last digit to save RAM)
static constexpr char LOG_LEVEL_COLOR_DIGIT[] = { static constexpr char LOG_LEVEL_COLOR_DIGIT[] = {
'\0', // NONE '\0', // NONE
@@ -133,8 +140,10 @@ class Logger : public Component {
/// Set the default log level for this logger. /// Set the default log level for this logger.
void set_log_level(uint8_t level); void set_log_level(uint8_t level);
#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS
/// Set the log level of the specified tag. /// Set the log level of the specified tag.
void set_log_level(const std::string &tag, uint8_t log_level); void set_log_level(const char *tag, uint8_t log_level);
#endif
uint8_t get_log_level() { return this->current_level_; } uint8_t get_log_level() { return this->current_level_; }
// ========== INTERNAL METHODS ========== // ========== INTERNAL METHODS ==========
@@ -242,7 +251,9 @@ class Logger : public Component {
#endif #endif
// Large objects (internally aligned) // Large objects (internally aligned)
std::map<std::string, uint8_t> log_levels_{}; #ifdef USE_LOGGER_RUNTIME_TAG_LEVELS
std::map<const char *, uint8_t, CStrCompare> log_levels_{};
#endif
CallbackManager<void(uint8_t, const char *, const char *, size_t)> log_callback_{}; CallbackManager<void(uint8_t, const char *, const char *, size_t)> log_callback_{};
CallbackManager<void(uint8_t)> level_callback_{}; CallbackManager<void(uint8_t)> level_callback_{};
#ifdef USE_ESPHOME_TASK_LOG_BUFFER #ifdef USE_ESPHOME_TASK_LOG_BUFFER

View File

@@ -17,6 +17,11 @@ from esphome.coroutine import CoroPriority
CODEOWNERS = ["@esphome/core"] CODEOWNERS = ["@esphome/core"]
DEPENDENCIES = ["network"] DEPENDENCIES = ["network"]
# Components that create mDNS services at runtime
# IMPORTANT: If you add a new component here, you must also update the corresponding
# #ifdef blocks in mdns_component.cpp compile_records_() method
COMPONENTS_WITH_MDNS_SERVICES = ("api", "prometheus", "web_server")
mdns_ns = cg.esphome_ns.namespace("mdns") mdns_ns = cg.esphome_ns.namespace("mdns")
MDNSComponent = mdns_ns.class_("MDNSComponent", cg.Component) MDNSComponent = mdns_ns.class_("MDNSComponent", cg.Component)
MDNSTXTRecord = mdns_ns.struct("MDNSTXTRecord") MDNSTXTRecord = mdns_ns.struct("MDNSTXTRecord")
@@ -91,12 +96,20 @@ async def to_code(config):
cg.add_define("USE_MDNS") cg.add_define("USE_MDNS")
var = cg.new_Pvariable(config[CONF_ID]) # Calculate compile-time service count
await cg.register_component(var, config) service_count = sum(
1 for key in COMPONENTS_WITH_MDNS_SERVICES if key in CORE.config
) + len(config[CONF_SERVICES])
if config[CONF_SERVICES]: if config[CONF_SERVICES]:
cg.add_define("USE_MDNS_EXTRA_SERVICES") cg.add_define("USE_MDNS_EXTRA_SERVICES")
# Ensure at least 1 service (fallback service)
cg.add_define("MDNS_SERVICE_COUNT", max(1, service_count))
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
for service in config[CONF_SERVICES]: for service in config[CONF_SERVICES]:
txt = [ txt = [
cg.StructInitializer( cg.StructInitializer(

View File

@@ -74,32 +74,12 @@ MDNS_STATIC_CONST_CHAR(NETWORK_THREAD, "thread");
void MDNSComponent::compile_records_() { void MDNSComponent::compile_records_() {
this->hostname_ = App.get_name(); this->hostname_ = App.get_name();
// Calculate exact capacity needed for services vector // IMPORTANT: The #ifdef blocks below must match COMPONENTS_WITH_MDNS_SERVICES
size_t services_count = 0; // in mdns/__init__.py. If you add a new service here, update both locations.
#ifdef USE_API
if (api::global_api_server != nullptr) {
services_count++;
}
#endif
#ifdef USE_PROMETHEUS
services_count++;
#endif
#ifdef USE_WEBSERVER
services_count++;
#endif
#ifdef USE_MDNS_EXTRA_SERVICES
services_count += this->services_extra_.size();
#endif
// Reserve for fallback service if needed
if (services_count == 0) {
services_count = 1;
}
this->services_.reserve(services_count);
#ifdef USE_API #ifdef USE_API
if (api::global_api_server != nullptr) { if (api::global_api_server != nullptr) {
this->services_.emplace_back(); auto &service = this->services_.emplace_next();
auto &service = this->services_.back();
service.service_type = MDNS_STR(SERVICE_ESPHOMELIB); service.service_type = MDNS_STR(SERVICE_ESPHOMELIB);
service.proto = MDNS_STR(SERVICE_TCP); service.proto = MDNS_STR(SERVICE_TCP);
service.port = api::global_api_server->get_port(); service.port = api::global_api_server->get_port();
@@ -178,30 +158,23 @@ void MDNSComponent::compile_records_() {
#endif // USE_API #endif // USE_API
#ifdef USE_PROMETHEUS #ifdef USE_PROMETHEUS
this->services_.emplace_back(); auto &prom_service = this->services_.emplace_next();
auto &prom_service = this->services_.back();
prom_service.service_type = MDNS_STR(SERVICE_PROMETHEUS); prom_service.service_type = MDNS_STR(SERVICE_PROMETHEUS);
prom_service.proto = MDNS_STR(SERVICE_TCP); prom_service.proto = MDNS_STR(SERVICE_TCP);
prom_service.port = USE_WEBSERVER_PORT; prom_service.port = USE_WEBSERVER_PORT;
#endif #endif
#ifdef USE_WEBSERVER #ifdef USE_WEBSERVER
this->services_.emplace_back(); auto &web_service = this->services_.emplace_next();
auto &web_service = this->services_.back();
web_service.service_type = MDNS_STR(SERVICE_HTTP); web_service.service_type = MDNS_STR(SERVICE_HTTP);
web_service.proto = MDNS_STR(SERVICE_TCP); web_service.proto = MDNS_STR(SERVICE_TCP);
web_service.port = USE_WEBSERVER_PORT; web_service.port = USE_WEBSERVER_PORT;
#endif #endif
#ifdef USE_MDNS_EXTRA_SERVICES
this->services_.insert(this->services_.end(), this->services_extra_.begin(), this->services_extra_.end());
#endif
#if !defined(USE_API) && !defined(USE_PROMETHEUS) && !defined(USE_WEBSERVER) && !defined(USE_MDNS_EXTRA_SERVICES) #if !defined(USE_API) && !defined(USE_PROMETHEUS) && !defined(USE_WEBSERVER) && !defined(USE_MDNS_EXTRA_SERVICES)
// Publish "http" service if not using native API or any other services // Publish "http" service if not using native API or any other services
// This is just to have *some* mDNS service so that .local resolution works // This is just to have *some* mDNS service so that .local resolution works
this->services_.emplace_back(); auto &fallback_service = this->services_.emplace_next();
auto &fallback_service = this->services_.back();
fallback_service.service_type = "_http"; fallback_service.service_type = "_http";
fallback_service.proto = "_tcp"; fallback_service.proto = "_tcp";
fallback_service.port = USE_WEBSERVER_PORT; fallback_service.port = USE_WEBSERVER_PORT;
@@ -214,7 +187,7 @@ void MDNSComponent::dump_config() {
"mDNS:\n" "mDNS:\n"
" Hostname: %s", " Hostname: %s",
this->hostname_.c_str()); this->hostname_.c_str());
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
ESP_LOGV(TAG, " Services:"); ESP_LOGV(TAG, " Services:");
for (const auto &service : this->services_) { for (const auto &service : this->services_) {
ESP_LOGV(TAG, " - %s, %s, %d", service.service_type.c_str(), service.proto.c_str(), ESP_LOGV(TAG, " - %s, %s, %d", service.service_type.c_str(), service.proto.c_str(),
@@ -227,8 +200,6 @@ void MDNSComponent::dump_config() {
#endif #endif
} }
std::vector<MDNSService> MDNSComponent::get_services() { return this->services_; }
} // namespace mdns } // namespace mdns
} // namespace esphome } // namespace esphome
#endif #endif

View File

@@ -2,13 +2,16 @@
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#ifdef USE_MDNS #ifdef USE_MDNS
#include <string> #include <string>
#include <vector>
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/helpers.h"
namespace esphome { namespace esphome {
namespace mdns { namespace mdns {
// Service count is calculated at compile time by Python codegen
// MDNS_SERVICE_COUNT will always be defined
struct MDNSTXTRecord { struct MDNSTXTRecord {
std::string key; std::string key;
TemplatableValue<std::string> value; TemplatableValue<std::string> value;
@@ -36,18 +39,15 @@ class MDNSComponent : public Component {
float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; } float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; }
#ifdef USE_MDNS_EXTRA_SERVICES #ifdef USE_MDNS_EXTRA_SERVICES
void add_extra_service(MDNSService service) { services_extra_.push_back(std::move(service)); } void add_extra_service(MDNSService service) { this->services_.emplace_next() = std::move(service); }
#endif #endif
std::vector<MDNSService> get_services(); const StaticVector<MDNSService, MDNS_SERVICE_COUNT> &get_services() const { return this->services_; }
void on_shutdown() override; void on_shutdown() override;
protected: protected:
#ifdef USE_MDNS_EXTRA_SERVICES StaticVector<MDNSService, MDNS_SERVICE_COUNT> services_{};
std::vector<MDNSService> services_extra_{};
#endif
std::vector<MDNSService> services_{};
std::string hostname_; std::string hostname_;
void compile_records_(); void compile_records_();
}; };

View File

@@ -11,6 +11,7 @@ namespace mpr121 {
static const char *const TAG = "mpr121"; static const char *const TAG = "mpr121";
void MPR121Component::setup() { void MPR121Component::setup() {
this->disable_loop();
// soft reset device // soft reset device
this->write_byte(MPR121_SOFTRESET, 0x63); this->write_byte(MPR121_SOFTRESET, 0x63);
this->set_timeout(100, [this]() { this->set_timeout(100, [this]() {
@@ -51,7 +52,7 @@ void MPR121Component::setup() {
this->write_byte(MPR121_ECR, 0x80 | (this->max_touch_channel_ + 1)); this->write_byte(MPR121_ECR, 0x80 | (this->max_touch_channel_ + 1));
this->flush_gpio_(); this->flush_gpio_();
this->setup_complete_ = true; this->enable_loop();
}); });
} }
@@ -80,9 +81,6 @@ void MPR121Component::dump_config() {
} }
} }
void MPR121Component::loop() { void MPR121Component::loop() {
if (!this->setup_complete_)
return;
uint16_t val = 0; uint16_t val = 0;
this->read_byte_16(MPR121_TOUCHSTATUS_L, &val); this->read_byte_16(MPR121_TOUCHSTATUS_L, &val);

View File

@@ -80,7 +80,6 @@ class MPR121Component : public Component, public i2c::I2CDevice {
void pin_mode(uint8_t ionum, gpio::Flags flags); void pin_mode(uint8_t ionum, gpio::Flags flags);
protected: protected:
bool setup_complete_{false};
std::vector<MPR121Channel *> channels_{}; std::vector<MPR121Channel *> channels_{};
uint8_t debounce_{0}; uint8_t debounce_{0};
uint8_t touch_threshold_{}; uint8_t touch_threshold_{};

View File

@@ -7,6 +7,17 @@ namespace number {
static const char *const TAG = "number"; static const char *const TAG = "number";
// Helper functions to reduce code size for logging
void NumberCall::log_perform_warning_(const LogString *message) {
ESP_LOGW(TAG, "'%s': %s", this->parent_->get_name().c_str(), LOG_STR_ARG(message));
}
void NumberCall::log_perform_warning_value_range_(const LogString *comparison, const LogString *limit_type, float val,
float limit) {
ESP_LOGW(TAG, "'%s': %f %s %s %f", this->parent_->get_name().c_str(), val, LOG_STR_ARG(comparison),
LOG_STR_ARG(limit_type), limit);
}
NumberCall &NumberCall::set_value(float value) { return this->with_operation(NUMBER_OP_SET).with_value(value); } NumberCall &NumberCall::set_value(float value) { return this->with_operation(NUMBER_OP_SET).with_value(value); }
NumberCall &NumberCall::number_increment(bool cycle) { NumberCall &NumberCall::number_increment(bool cycle) {
@@ -42,7 +53,7 @@ void NumberCall::perform() {
const auto &traits = parent->traits; const auto &traits = parent->traits;
if (this->operation_ == NUMBER_OP_NONE) { if (this->operation_ == NUMBER_OP_NONE) {
ESP_LOGW(TAG, "'%s' - NumberCall performed without selecting an operation", name); this->log_perform_warning_(LOG_STR("No operation"));
return; return;
} }
@@ -51,28 +62,28 @@ void NumberCall::perform() {
float max_value = traits.get_max_value(); float max_value = traits.get_max_value();
if (this->operation_ == NUMBER_OP_SET) { if (this->operation_ == NUMBER_OP_SET) {
ESP_LOGD(TAG, "'%s' - Setting number value", name); ESP_LOGD(TAG, "'%s': Setting value", name);
if (!this->value_.has_value() || std::isnan(*this->value_)) { if (!this->value_.has_value() || std::isnan(*this->value_)) {
ESP_LOGW(TAG, "'%s' - No value set for NumberCall", name); this->log_perform_warning_(LOG_STR("No value"));
return; return;
} }
target_value = this->value_.value(); target_value = this->value_.value();
} else if (this->operation_ == NUMBER_OP_TO_MIN) { } else if (this->operation_ == NUMBER_OP_TO_MIN) {
if (std::isnan(min_value)) { if (std::isnan(min_value)) {
ESP_LOGW(TAG, "'%s' - Can't set to min value through NumberCall: no min_value defined", name); this->log_perform_warning_(LOG_STR("min undefined"));
} else { } else {
target_value = min_value; target_value = min_value;
} }
} else if (this->operation_ == NUMBER_OP_TO_MAX) { } else if (this->operation_ == NUMBER_OP_TO_MAX) {
if (std::isnan(max_value)) { if (std::isnan(max_value)) {
ESP_LOGW(TAG, "'%s' - Can't set to max value through NumberCall: no max_value defined", name); this->log_perform_warning_(LOG_STR("max undefined"));
} else { } else {
target_value = max_value; target_value = max_value;
} }
} else if (this->operation_ == NUMBER_OP_INCREMENT) { } else if (this->operation_ == NUMBER_OP_INCREMENT) {
ESP_LOGD(TAG, "'%s' - Increment number, with%s cycling", name, this->cycle_ ? "" : "out"); ESP_LOGD(TAG, "'%s': Increment with%s cycling", name, this->cycle_ ? "" : "out");
if (!parent->has_state()) { if (!parent->has_state()) {
ESP_LOGW(TAG, "'%s' - Can't increment number through NumberCall: no active state to modify", name); this->log_perform_warning_(LOG_STR("Can't increment, no state"));
return; return;
} }
auto step = traits.get_step(); auto step = traits.get_step();
@@ -85,9 +96,9 @@ void NumberCall::perform() {
} }
} }
} else if (this->operation_ == NUMBER_OP_DECREMENT) { } else if (this->operation_ == NUMBER_OP_DECREMENT) {
ESP_LOGD(TAG, "'%s' - Decrement number, with%s cycling", name, this->cycle_ ? "" : "out"); ESP_LOGD(TAG, "'%s': Decrement with%s cycling", name, this->cycle_ ? "" : "out");
if (!parent->has_state()) { if (!parent->has_state()) {
ESP_LOGW(TAG, "'%s' - Can't decrement number through NumberCall: no active state to modify", name); this->log_perform_warning_(LOG_STR("Can't decrement, no state"));
return; return;
} }
auto step = traits.get_step(); auto step = traits.get_step();
@@ -102,15 +113,15 @@ void NumberCall::perform() {
} }
if (target_value < min_value) { if (target_value < min_value) {
ESP_LOGW(TAG, "'%s' - Value %f must not be less than minimum %f", name, target_value, min_value); this->log_perform_warning_value_range_(LOG_STR("<"), LOG_STR("min"), target_value, min_value);
return; return;
} }
if (target_value > max_value) { if (target_value > max_value) {
ESP_LOGW(TAG, "'%s' - Value %f must not be greater than maximum %f", name, target_value, max_value); this->log_perform_warning_value_range_(LOG_STR(">"), LOG_STR("max"), target_value, max_value);
return; return;
} }
ESP_LOGD(TAG, " New number value: %f", target_value); ESP_LOGD(TAG, " New value: %f", target_value);
this->parent_->control(target_value); this->parent_->control(target_value);
} }

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "number_traits.h" #include "number_traits.h"
namespace esphome { namespace esphome {
@@ -33,6 +34,10 @@ class NumberCall {
NumberCall &with_cycle(bool cycle); NumberCall &with_cycle(bool cycle);
protected: protected:
void log_perform_warning_(const LogString *message);
void log_perform_warning_value_range_(const LogString *comparison, const LogString *limit_type, float val,
float limit);
Number *const parent_; Number *const parent_;
NumberOperation operation_{NUMBER_OP_NONE}; NumberOperation operation_{NUMBER_OP_NONE};
optional<float> value_; optional<float> value_;

View File

@@ -143,11 +143,10 @@ void OpenThreadSrpComponent::setup() {
return; return;
} }
// Copy the mdns services to our local instance so that the c_str pointers remain valid for the lifetime of this // Get mdns services and copy their data (strings are copied with strdup below)
// component const auto &mdns_services = this->mdns_->get_services();
this->mdns_services_ = this->mdns_->get_services(); ESP_LOGD(TAG, "Setting up SRP services. count = %d\n", mdns_services.size());
ESP_LOGD(TAG, "Setting up SRP services. count = %d\n", this->mdns_services_.size()); for (const auto &service : mdns_services) {
for (const auto &service : this->mdns_services_) {
otSrpClientBuffersServiceEntry *entry = otSrpClientBuffersAllocateService(instance); otSrpClientBuffersServiceEntry *entry = otSrpClientBuffersAllocateService(instance);
if (!entry) { if (!entry) {
ESP_LOGW(TAG, "Failed to allocate service entry"); ESP_LOGW(TAG, "Failed to allocate service entry");

View File

@@ -57,7 +57,6 @@ class OpenThreadSrpComponent : public Component {
protected: protected:
esphome::mdns::MDNSComponent *mdns_{nullptr}; esphome::mdns::MDNSComponent *mdns_{nullptr};
std::vector<esphome::mdns::MDNSService> mdns_services_;
std::vector<std::unique_ptr<uint8_t[]>> memory_pool_; std::vector<std::unique_ptr<uint8_t[]>> memory_pool_;
void *pool_alloc_(size_t size); void *pool_alloc_(size_t size);
}; };

View File

@@ -45,16 +45,16 @@ void SPS30Component::setup() {
} }
ESP_LOGV(TAG, " Serial number: %s", this->serial_number_); ESP_LOGV(TAG, " Serial number: %s", this->serial_number_);
bool result;
if (this->fan_interval_.has_value()) { if (this->fan_interval_.has_value()) {
// override default value // override default value
this->result_ = result = this->write_command(SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS, this->fan_interval_.value());
this->write_command(SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS, this->fan_interval_.value());
} else { } else {
this->result_ = this->write_command(SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS); result = this->write_command(SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS);
} }
this->set_timeout(20, [this]() { this->set_timeout(20, [this, result]() {
if (this->result_) { if (result) {
uint16_t secs[2]; uint16_t secs[2];
if (this->read_data(secs, 2)) { if (this->read_data(secs, 2)) {
this->fan_interval_ = secs[0] << 16 | secs[1]; this->fan_interval_ = secs[0] << 16 | secs[1];

View File

@@ -30,7 +30,6 @@ class SPS30Component : public PollingComponent, public sensirion_common::Sensiri
bool start_fan_cleaning(); bool start_fan_cleaning();
protected: protected:
bool result_{false};
bool setup_complete_{false}; bool setup_complete_{false};
uint16_t raw_firmware_version_; uint16_t raw_firmware_version_;
char serial_number_[17] = {0}; /// Terminating NULL character char serial_number_[17] = {0}; /// Terminating NULL character

View File

@@ -127,6 +127,10 @@ void DeferredUpdateEventSource::process_deferred_queue_() {
deferred_queue_.erase(deferred_queue_.begin()); deferred_queue_.erase(deferred_queue_.begin());
this->consecutive_send_failures_ = 0; // Reset failure count on successful send this->consecutive_send_failures_ = 0; // Reset failure count on successful send
} else { } else {
// NOTE: Similar logic exists in web_server_idf/web_server_idf.cpp in AsyncEventSourceResponse::process_buffer_()
// The implementations differ due to platform-specific APIs (DISCARDED vs HTTPD_SOCK_ERR_TIMEOUT, close() vs
// fd_.store(0)), but the failure counting and timeout logic should be kept in sync. If you change this logic,
// also update the ESP-IDF implementation.
this->consecutive_send_failures_++; this->consecutive_send_failures_++;
if (this->consecutive_send_failures_ >= MAX_CONSECUTIVE_SEND_FAILURES) { if (this->consecutive_send_failures_ >= MAX_CONSECUTIVE_SEND_FAILURES) {
// Too many failures, connection is likely dead // Too many failures, connection is likely dead
@@ -381,11 +385,14 @@ void WebServer::handle_js_request(AsyncWebServerRequest *request) {
#endif #endif
// Helper functions to reduce code size by avoiding macro expansion // Helper functions to reduce code size by avoiding macro expansion
static void set_json_id(JsonObject &root, EntityBase *obj, const std::string &id, JsonDetail start_config) { static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, JsonDetail start_config) {
root["id"] = id; char id_buf[160]; // object_id can be up to 128 chars + prefix + dash + null
const auto &object_id = obj->get_object_id();
snprintf(id_buf, sizeof(id_buf), "%s-%s", prefix, object_id.c_str());
root["id"] = id_buf;
if (start_config == DETAIL_ALL) { if (start_config == DETAIL_ALL) {
root["name"] = obj->get_name(); root["name"] = obj->get_name();
root["icon"] = obj->get_icon(); root["icon"] = obj->get_icon_ref();
root["entity_category"] = obj->get_entity_category(); root["entity_category"] = obj->get_entity_category();
bool is_disabled = obj->is_disabled_by_default(); bool is_disabled = obj->is_disabled_by_default();
if (is_disabled) if (is_disabled)
@@ -393,17 +400,19 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const std::string &id
} }
} }
// Keep as separate function even though only used once: reduces code size by ~48 bytes
// by allowing compiler to share code between template instantiations (bool, float, etc.)
template<typename T> template<typename T>
static void set_json_value(JsonObject &root, EntityBase *obj, const std::string &id, const T &value, static void set_json_value(JsonObject &root, EntityBase *obj, const char *prefix, const T &value,
JsonDetail start_config) { JsonDetail start_config) {
set_json_id(root, obj, id, start_config); set_json_id(root, obj, prefix, start_config);
root["value"] = value; root["value"] = value;
} }
template<typename T> template<typename T>
static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const std::string &id, static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const char *prefix, const std::string &state,
const std::string &state, const T &value, JsonDetail start_config) { const T &value, JsonDetail start_config) {
set_json_value(root, obj, id, value, start_config); set_json_value(root, obj, prefix, value, start_config);
root["state"] = state; root["state"] = state;
} }
@@ -442,20 +451,20 @@ std::string WebServer::sensor_json(sensor::Sensor *obj, float value, JsonDetail
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
const auto uom_ref = obj->get_unit_of_measurement_ref();
// Build JSON directly inline // Build JSON directly inline
std::string state; std::string state;
if (std::isnan(value)) { if (std::isnan(value)) {
state = "NA"; state = "NA";
} else { } else {
state = value_accuracy_to_string(value, obj->get_accuracy_decimals()); state = value_accuracy_with_uom_to_string(value, obj->get_accuracy_decimals(), uom_ref);
if (!obj->get_unit_of_measurement().empty())
state += " " + obj->get_unit_of_measurement();
} }
set_json_icon_state_value(root, obj, "sensor-" + obj->get_object_id(), state, value, start_config); set_json_icon_state_value(root, obj, "sensor", state, value, start_config);
if (start_config == DETAIL_ALL) { if (start_config == DETAIL_ALL) {
this->add_sorting_info_(root, obj); this->add_sorting_info_(root, obj);
if (!obj->get_unit_of_measurement().empty()) if (!uom_ref.empty())
root["uom"] = obj->get_unit_of_measurement(); root["uom"] = uom_ref;
} }
return builder.serialize(); return builder.serialize();
@@ -494,7 +503,7 @@ std::string WebServer::text_sensor_json(text_sensor::TextSensor *obj, const std:
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
set_json_icon_state_value(root, obj, "text_sensor-" + obj->get_object_id(), value, value, start_config); set_json_icon_state_value(root, obj, "text_sensor", value, value, start_config);
if (start_config == DETAIL_ALL) { if (start_config == DETAIL_ALL) {
this->add_sorting_info_(root, obj); this->add_sorting_info_(root, obj);
} }
@@ -567,7 +576,7 @@ std::string WebServer::switch_json(switch_::Switch *obj, bool value, JsonDetail
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
set_json_icon_state_value(root, obj, "switch-" + obj->get_object_id(), value ? "ON" : "OFF", value, start_config); set_json_icon_state_value(root, obj, "switch", value ? "ON" : "OFF", value, start_config);
if (start_config == DETAIL_ALL) { if (start_config == DETAIL_ALL) {
root["assumed_state"] = obj->assumed_state(); root["assumed_state"] = obj->assumed_state();
this->add_sorting_info_(root, obj); this->add_sorting_info_(root, obj);
@@ -607,7 +616,7 @@ std::string WebServer::button_json(button::Button *obj, JsonDetail start_config)
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
set_json_id(root, obj, "button-" + obj->get_object_id(), start_config); set_json_id(root, obj, "button", start_config);
if (start_config == DETAIL_ALL) { if (start_config == DETAIL_ALL) {
this->add_sorting_info_(root, obj); this->add_sorting_info_(root, obj);
} }
@@ -647,8 +656,7 @@ std::string WebServer::binary_sensor_json(binary_sensor::BinarySensor *obj, bool
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
set_json_icon_state_value(root, obj, "binary_sensor-" + obj->get_object_id(), value ? "ON" : "OFF", value, set_json_icon_state_value(root, obj, "binary_sensor", value ? "ON" : "OFF", value, start_config);
start_config);
if (start_config == DETAIL_ALL) { if (start_config == DETAIL_ALL) {
this->add_sorting_info_(root, obj); this->add_sorting_info_(root, obj);
} }
@@ -717,8 +725,7 @@ std::string WebServer::fan_json(fan::Fan *obj, JsonDetail start_config) {
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
set_json_icon_state_value(root, obj, "fan-" + obj->get_object_id(), obj->state ? "ON" : "OFF", obj->state, set_json_icon_state_value(root, obj, "fan", obj->state ? "ON" : "OFF", obj->state, start_config);
start_config);
const auto traits = obj->get_traits(); const auto traits = obj->get_traits();
if (traits.supports_speed()) { if (traits.supports_speed()) {
root["speed_level"] = obj->speed; root["speed_level"] = obj->speed;
@@ -793,7 +800,7 @@ std::string WebServer::light_json(light::LightState *obj, JsonDetail start_confi
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
set_json_id(root, obj, "light-" + obj->get_object_id(), start_config); set_json_id(root, obj, "light", start_config);
root["state"] = obj->remote_values.is_on() ? "ON" : "OFF"; root["state"] = obj->remote_values.is_on() ? "ON" : "OFF";
light::LightJSONSchema::dump_json(*obj, root); light::LightJSONSchema::dump_json(*obj, root);
@@ -881,8 +888,8 @@ std::string WebServer::cover_json(cover::Cover *obj, JsonDetail start_config) {
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
set_json_icon_state_value(root, obj, "cover-" + obj->get_object_id(), obj->is_fully_closed() ? "CLOSED" : "OPEN", set_json_icon_state_value(root, obj, "cover", obj->is_fully_closed() ? "CLOSED" : "OPEN", obj->position,
obj->position, start_config); start_config);
root["current_operation"] = cover::cover_operation_to_str(obj->current_operation); root["current_operation"] = cover::cover_operation_to_str(obj->current_operation);
if (obj->get_traits().get_supports_position()) if (obj->get_traits().get_supports_position())
@@ -939,7 +946,9 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
set_json_id(root, obj, "number-" + obj->get_object_id(), start_config); const auto uom_ref = obj->traits.get_unit_of_measurement_ref();
set_json_id(root, obj, "number", start_config);
if (start_config == DETAIL_ALL) { if (start_config == DETAIL_ALL) {
root["min_value"] = root["min_value"] =
value_accuracy_to_string(obj->traits.get_min_value(), step_to_accuracy_decimals(obj->traits.get_step())); value_accuracy_to_string(obj->traits.get_min_value(), step_to_accuracy_decimals(obj->traits.get_step()));
@@ -947,8 +956,8 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail
value_accuracy_to_string(obj->traits.get_max_value(), step_to_accuracy_decimals(obj->traits.get_step())); value_accuracy_to_string(obj->traits.get_max_value(), step_to_accuracy_decimals(obj->traits.get_step()));
root["step"] = value_accuracy_to_string(obj->traits.get_step(), step_to_accuracy_decimals(obj->traits.get_step())); root["step"] = value_accuracy_to_string(obj->traits.get_step(), step_to_accuracy_decimals(obj->traits.get_step()));
root["mode"] = (int) obj->traits.get_mode(); root["mode"] = (int) obj->traits.get_mode();
if (!obj->traits.get_unit_of_measurement().empty()) if (!uom_ref.empty())
root["uom"] = obj->traits.get_unit_of_measurement(); root["uom"] = uom_ref;
this->add_sorting_info_(root, obj); this->add_sorting_info_(root, obj);
} }
if (std::isnan(value)) { if (std::isnan(value)) {
@@ -956,10 +965,8 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail
root["state"] = "NA"; root["state"] = "NA";
} else { } else {
root["value"] = value_accuracy_to_string(value, step_to_accuracy_decimals(obj->traits.get_step())); root["value"] = value_accuracy_to_string(value, step_to_accuracy_decimals(obj->traits.get_step()));
std::string state = value_accuracy_to_string(value, step_to_accuracy_decimals(obj->traits.get_step())); root["state"] =
if (!obj->traits.get_unit_of_measurement().empty()) value_accuracy_with_uom_to_string(value, step_to_accuracy_decimals(obj->traits.get_step()), uom_ref);
state += " " + obj->traits.get_unit_of_measurement();
root["state"] = state;
} }
return builder.serialize(); return builder.serialize();
@@ -1013,7 +1020,7 @@ std::string WebServer::date_json(datetime::DateEntity *obj, JsonDetail start_con
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
set_json_id(root, obj, "date-" + obj->get_object_id(), start_config); set_json_id(root, obj, "date", start_config);
std::string value = str_sprintf("%d-%02d-%02d", obj->year, obj->month, obj->day); std::string value = str_sprintf("%d-%02d-%02d", obj->year, obj->month, obj->day);
root["value"] = value; root["value"] = value;
root["state"] = value; root["state"] = value;
@@ -1071,7 +1078,7 @@ std::string WebServer::time_json(datetime::TimeEntity *obj, JsonDetail start_con
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
set_json_id(root, obj, "time-" + obj->get_object_id(), start_config); set_json_id(root, obj, "time", start_config);
std::string value = str_sprintf("%02d:%02d:%02d", obj->hour, obj->minute, obj->second); std::string value = str_sprintf("%02d:%02d:%02d", obj->hour, obj->minute, obj->second);
root["value"] = value; root["value"] = value;
root["state"] = value; root["state"] = value;
@@ -1129,7 +1136,7 @@ std::string WebServer::datetime_json(datetime::DateTimeEntity *obj, JsonDetail s
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
set_json_id(root, obj, "datetime-" + obj->get_object_id(), start_config); set_json_id(root, obj, "datetime", start_config);
std::string value = std::string value =
str_sprintf("%d-%02d-%02d %02d:%02d:%02d", obj->year, obj->month, obj->day, obj->hour, obj->minute, obj->second); str_sprintf("%d-%02d-%02d %02d:%02d:%02d", obj->year, obj->month, obj->day, obj->hour, obj->minute, obj->second);
root["value"] = value; root["value"] = value;
@@ -1184,7 +1191,7 @@ std::string WebServer::text_json(text::Text *obj, const std::string &value, Json
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
set_json_id(root, obj, "text-" + obj->get_object_id(), start_config); set_json_id(root, obj, "text", start_config);
root["min_length"] = obj->traits.get_min_length(); root["min_length"] = obj->traits.get_min_length();
root["max_length"] = obj->traits.get_max_length(); root["max_length"] = obj->traits.get_max_length();
root["pattern"] = obj->traits.get_pattern(); root["pattern"] = obj->traits.get_pattern();
@@ -1245,7 +1252,7 @@ std::string WebServer::select_json(select::Select *obj, const std::string &value
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
set_json_icon_state_value(root, obj, "select-" + obj->get_object_id(), value, value, start_config); set_json_icon_state_value(root, obj, "select", value, value, start_config);
if (start_config == DETAIL_ALL) { if (start_config == DETAIL_ALL) {
JsonArray opt = root["option"].to<JsonArray>(); JsonArray opt = root["option"].to<JsonArray>();
for (auto &option : obj->traits.get_options()) { for (auto &option : obj->traits.get_options()) {
@@ -1314,7 +1321,7 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
set_json_id(root, obj, "climate-" + obj->get_object_id(), start_config); set_json_id(root, obj, "climate", start_config);
const auto traits = obj->get_traits(); const auto traits = obj->get_traits();
int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals(); int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals();
int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals(); int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals();
@@ -1467,8 +1474,7 @@ std::string WebServer::lock_json(lock::Lock *obj, lock::LockState value, JsonDet
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
set_json_icon_state_value(root, obj, "lock-" + obj->get_object_id(), lock::lock_state_to_string(value), value, set_json_icon_state_value(root, obj, "lock", lock::lock_state_to_string(value), value, start_config);
start_config);
if (start_config == DETAIL_ALL) { if (start_config == DETAIL_ALL) {
this->add_sorting_info_(root, obj); this->add_sorting_info_(root, obj);
} }
@@ -1546,8 +1552,8 @@ std::string WebServer::valve_json(valve::Valve *obj, JsonDetail start_config) {
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
set_json_icon_state_value(root, obj, "valve-" + obj->get_object_id(), obj->is_fully_closed() ? "CLOSED" : "OPEN", set_json_icon_state_value(root, obj, "valve", obj->is_fully_closed() ? "CLOSED" : "OPEN", obj->position,
obj->position, start_config); start_config);
root["current_operation"] = valve::valve_operation_to_str(obj->current_operation); root["current_operation"] = valve::valve_operation_to_str(obj->current_operation);
if (obj->get_traits().get_supports_position()) if (obj->get_traits().get_supports_position())
@@ -1630,8 +1636,8 @@ std::string WebServer::alarm_control_panel_json(alarm_control_panel::AlarmContro
JsonObject root = builder.root(); JsonObject root = builder.root();
char buf[16]; char buf[16];
set_json_icon_state_value(root, obj, "alarm-control-panel-" + obj->get_object_id(), set_json_icon_state_value(root, obj, "alarm-control-panel", PSTR_LOCAL(alarm_control_panel_state_to_string(value)),
PSTR_LOCAL(alarm_control_panel_state_to_string(value)), value, start_config); value, start_config);
if (start_config == DETAIL_ALL) { if (start_config == DETAIL_ALL) {
this->add_sorting_info_(root, obj); this->add_sorting_info_(root, obj);
} }
@@ -1676,7 +1682,7 @@ std::string WebServer::event_json(event::Event *obj, const std::string &event_ty
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
set_json_id(root, obj, "event-" + obj->get_object_id(), start_config); set_json_id(root, obj, "event", start_config);
if (!event_type.empty()) { if (!event_type.empty()) {
root["event_type"] = event_type; root["event_type"] = event_type;
} }
@@ -1685,7 +1691,7 @@ std::string WebServer::event_json(event::Event *obj, const std::string &event_ty
for (auto const &event_type : obj->get_event_types()) { for (auto const &event_type : obj->get_event_types()) {
event_types.add(event_type); event_types.add(event_type);
} }
root["device_class"] = obj->get_device_class(); root["device_class"] = obj->get_device_class_ref();
this->add_sorting_info_(root, obj); this->add_sorting_info_(root, obj);
} }
@@ -1748,7 +1754,7 @@ std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_c
json::JsonBuilder builder; json::JsonBuilder builder;
JsonObject root = builder.root(); JsonObject root = builder.root();
set_json_id(root, obj, "update-" + obj->get_object_id(), start_config); set_json_id(root, obj, "update", start_config);
root["value"] = obj->update_info.latest_version; root["value"] = obj->update_info.latest_version;
root["state"] = update_state_to_string(obj->state); root["state"] = update_state_to_string(obj->state);
if (start_config == DETAIL_ALL) { if (start_config == DETAIL_ALL) {

View File

@@ -25,6 +25,10 @@
#include "esphome/components/web_server/list_entities.h" #include "esphome/components/web_server/list_entities.h"
#endif // USE_WEBSERVER #endif // USE_WEBSERVER
// Include socket headers after Arduino headers to avoid IPADDR_NONE/INADDR_NONE macro conflicts
#include <cerrno>
#include <sys/socket.h>
namespace esphome { namespace esphome {
namespace web_server_idf { namespace web_server_idf {
@@ -46,6 +50,42 @@ DefaultHeaders default_headers_instance;
DefaultHeaders &DefaultHeaders::Instance() { return default_headers_instance; } DefaultHeaders &DefaultHeaders::Instance() { return default_headers_instance; }
namespace {
// Non-blocking send function to prevent watchdog timeouts when TCP buffers are full
/**
* Sends data on a socket in non-blocking mode.
*
* @param hd HTTP server handle (unused).
* @param sockfd Socket file descriptor.
* @param buf Buffer to send.
* @param buf_len Length of buffer.
* @param flags Flags for send().
* @return
* - Number of bytes sent on success.
* - HTTPD_SOCK_ERR_INVALID if buf is nullptr.
* - HTTPD_SOCK_ERR_TIMEOUT if the send buffer is full (EAGAIN/EWOULDBLOCK).
* - HTTPD_SOCK_ERR_FAIL for other errors.
*/
int nonblocking_send(httpd_handle_t hd, int sockfd, const char *buf, size_t buf_len, int flags) {
if (buf == nullptr) {
return HTTPD_SOCK_ERR_INVALID;
}
// Use MSG_DONTWAIT to prevent blocking when TCP send buffer is full
int ret = send(sockfd, buf, buf_len, flags | MSG_DONTWAIT);
if (ret < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// Buffer full - retry later
return HTTPD_SOCK_ERR_TIMEOUT;
}
// Real error
ESP_LOGD(TAG, "send error: errno %d", errno);
return HTTPD_SOCK_ERR_FAIL;
}
return ret;
}
} // namespace
void AsyncWebServer::end() { void AsyncWebServer::end() {
if (this->server_) { if (this->server_) {
httpd_stop(this->server_); httpd_stop(this->server_);
@@ -164,8 +204,8 @@ esp_err_t AsyncWebServer::request_handler_(AsyncWebServerRequest *request) const
AsyncWebServerRequest::~AsyncWebServerRequest() { AsyncWebServerRequest::~AsyncWebServerRequest() {
delete this->rsp_; delete this->rsp_;
for (const auto &pair : this->params_) { for (auto *param : this->params_) {
delete pair.second; // NOLINT(cppcoreguidelines-owning-memory) delete param; // NOLINT(cppcoreguidelines-owning-memory)
} }
} }
@@ -205,10 +245,22 @@ void AsyncWebServerRequest::redirect(const std::string &url) {
} }
void AsyncWebServerRequest::init_response_(AsyncWebServerResponse *rsp, int code, const char *content_type) { void AsyncWebServerRequest::init_response_(AsyncWebServerResponse *rsp, int code, const char *content_type) {
httpd_resp_set_status(*this, code == 200 ? HTTPD_200 // Set status code - use constants for common codes to avoid string allocation
: code == 404 ? HTTPD_404 const char *status = nullptr;
: code == 409 ? HTTPD_409 switch (code) {
: to_string(code).c_str()); case 200:
status = HTTPD_200;
break;
case 404:
status = HTTPD_404;
break;
case 409:
status = HTTPD_409;
break;
default:
break;
}
httpd_resp_set_status(*this, status == nullptr ? to_string(code).c_str() : status);
if (content_type && *content_type) { if (content_type && *content_type) {
httpd_resp_set_type(*this, content_type); httpd_resp_set_type(*this, content_type);
@@ -265,11 +317,14 @@ void AsyncWebServerRequest::requestAuthentication(const char *realm) const {
#endif #endif
AsyncWebParameter *AsyncWebServerRequest::getParam(const std::string &name) { AsyncWebParameter *AsyncWebServerRequest::getParam(const std::string &name) {
auto find = this->params_.find(name); // Check cache first - only successful lookups are cached
if (find != this->params_.end()) { for (auto *param : this->params_) {
return find->second; if (param->name() == name) {
return param;
}
} }
// Look up value from query strings
optional<std::string> val = query_key_value(this->post_query_, name); optional<std::string> val = query_key_value(this->post_query_, name);
if (!val.has_value()) { if (!val.has_value()) {
auto url_query = request_get_url_query(*this); auto url_query = request_get_url_query(*this);
@@ -278,11 +333,14 @@ AsyncWebParameter *AsyncWebServerRequest::getParam(const std::string &name) {
} }
} }
AsyncWebParameter *param = nullptr; // Don't cache misses to avoid wasting memory when handlers check for
if (val.has_value()) { // optional parameters that don't exist in the request
param = new AsyncWebParameter(val.value()); // NOLINT(cppcoreguidelines-owning-memory) if (!val.has_value()) {
return nullptr;
} }
this->params_.insert({name, param});
auto *param = new AsyncWebParameter(name, val.value()); // NOLINT(cppcoreguidelines-owning-memory)
this->params_.push_back(param);
return param; return param;
} }
@@ -384,6 +442,9 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest *
this->hd_ = req->handle; this->hd_ = req->handle;
this->fd_.store(httpd_req_to_sockfd(req)); this->fd_.store(httpd_req_to_sockfd(req));
// Use non-blocking send to prevent watchdog timeouts when TCP buffers are full
httpd_sess_set_send_override(this->hd_, this->fd_.load(), nonblocking_send);
// Configure reconnect timeout and send config // Configure reconnect timeout and send config
// this should always go through since the tcp send buffer is empty on connect // this should always go through since the tcp send buffer is empty on connect
std::string message = ws->get_config_json(); std::string message = ws->get_config_json();
@@ -459,15 +520,45 @@ void AsyncEventSourceResponse::process_buffer_() {
return; return;
} }
int bytes_sent = httpd_socket_send(this->hd_, this->fd_.load(), event_buffer_.c_str() + event_bytes_sent_, size_t remaining = event_buffer_.size() - event_bytes_sent_;
event_buffer_.size() - event_bytes_sent_, 0); int bytes_sent =
if (bytes_sent == HTTPD_SOCK_ERR_TIMEOUT || bytes_sent == HTTPD_SOCK_ERR_FAIL) { httpd_socket_send(this->hd_, this->fd_.load(), event_buffer_.c_str() + event_bytes_sent_, remaining, 0);
// Socket error - just return, the connection will be closed by httpd if (bytes_sent == HTTPD_SOCK_ERR_TIMEOUT) {
// and our destroy callback will be called // EAGAIN/EWOULDBLOCK - socket buffer full, try again later
// NOTE: Similar logic exists in web_server/web_server.cpp in DeferredUpdateEventSource::process_deferred_queue_()
// The implementations differ due to platform-specific APIs (HTTPD_SOCK_ERR_TIMEOUT vs DISCARDED, fd_.store(0) vs
// close()), but the failure counting and timeout logic should be kept in sync. If you change this logic, also
// update the Arduino implementation.
this->consecutive_send_failures_++;
if (this->consecutive_send_failures_ >= MAX_CONSECUTIVE_SEND_FAILURES) {
// Too many failures, connection is likely dead
ESP_LOGW(TAG, "Closing stuck EventSource connection after %" PRIu16 " failed sends",
this->consecutive_send_failures_);
this->fd_.store(0); // Mark for cleanup
this->deferred_queue_.clear();
}
return; return;
} }
if (bytes_sent == HTTPD_SOCK_ERR_FAIL) {
// Real socket error - connection will be closed by httpd and destroy callback will be called
return;
}
if (bytes_sent <= 0) {
// Unexpected error or zero bytes sent
ESP_LOGW(TAG, "Unexpected send result: %d", bytes_sent);
return;
}
// Successful send - reset failure counter
this->consecutive_send_failures_ = 0;
event_bytes_sent_ += bytes_sent; event_bytes_sent_ += bytes_sent;
// Log partial sends for debugging
if (event_bytes_sent_ < event_buffer_.size()) {
ESP_LOGV(TAG, "Partial send: %d/%zu bytes (total: %zu/%zu)", bytes_sent, remaining, event_bytes_sent_,
event_buffer_.size());
}
if (event_bytes_sent_ == event_buffer_.size()) { if (event_bytes_sent_ == event_buffer_.size()) {
event_buffer_.resize(0); event_buffer_.resize(0);
event_bytes_sent_ = 0; event_bytes_sent_ = 0;

View File

@@ -30,10 +30,12 @@ using String = std::string;
class AsyncWebParameter { class AsyncWebParameter {
public: public:
AsyncWebParameter(std::string value) : value_(std::move(value)) {} AsyncWebParameter(std::string name, std::string value) : name_(std::move(name)), value_(std::move(value)) {}
const std::string &name() const { return this->name_; }
const std::string &value() const { return this->value_; } const std::string &value() const { return this->value_; }
protected: protected:
std::string name_;
std::string value_; std::string value_;
}; };
@@ -174,7 +176,11 @@ class AsyncWebServerRequest {
protected: protected:
httpd_req_t *req_; httpd_req_t *req_;
AsyncWebServerResponse *rsp_{}; AsyncWebServerResponse *rsp_{};
std::map<std::string, AsyncWebParameter *> params_; // Use vector instead of map/unordered_map: most requests have 0-3 params, so linear search
// is faster than tree/hash overhead. AsyncWebParameter stores both name and value to avoid
// duplicate storage. Only successful lookups are cached to prevent cache pollution when
// handlers check for optional parameters that don't exist.
std::vector<AsyncWebParameter *> params_;
std::string post_query_; std::string post_query_;
AsyncWebServerRequest(httpd_req_t *req) : req_(req) {} AsyncWebServerRequest(httpd_req_t *req) : req_(req) {}
AsyncWebServerRequest(httpd_req_t *req, std::string post_query) : req_(req), post_query_(std::move(post_query)) {} AsyncWebServerRequest(httpd_req_t *req, std::string post_query) : req_(req), post_query_(std::move(post_query)) {}
@@ -283,6 +289,8 @@ class AsyncEventSourceResponse {
std::unique_ptr<esphome::web_server::ListEntitiesIterator> entities_iterator_; std::unique_ptr<esphome::web_server::ListEntitiesIterator> entities_iterator_;
std::string event_buffer_{""}; std::string event_buffer_{""};
size_t event_bytes_sent_; size_t event_bytes_sent_;
uint16_t consecutive_send_failures_{0};
static constexpr uint16_t MAX_CONSECUTIVE_SEND_FAILURES = 2500; // ~20 seconds at 125Hz loop rate
}; };
using AsyncEventSourceClient = AsyncEventSourceResponse; using AsyncEventSourceClient = AsyncEventSourceResponse;

View File

@@ -1,7 +1,6 @@
#include "wifi_component.h" #include "wifi_component.h"
#ifdef USE_WIFI #ifdef USE_WIFI
#include <cinttypes> #include <cinttypes>
#include <map>
#ifdef USE_ESP32 #ifdef USE_ESP32
#if (ESP_IDF_VERSION_MAJOR >= 5 && ESP_IDF_VERSION_MINOR >= 1) #if (ESP_IDF_VERSION_MAJOR >= 5 && ESP_IDF_VERSION_MINOR >= 1)
@@ -42,6 +41,25 @@ namespace wifi {
static const char *const TAG = "wifi"; static const char *const TAG = "wifi";
#if defined(USE_ESP32) && defined(USE_WIFI_WPA2_EAP) && ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
static const char *eap_phase2_to_str(esp_eap_ttls_phase2_types type) {
switch (type) {
case ESP_EAP_TTLS_PHASE2_PAP:
return "pap";
case ESP_EAP_TTLS_PHASE2_CHAP:
return "chap";
case ESP_EAP_TTLS_PHASE2_MSCHAP:
return "mschap";
case ESP_EAP_TTLS_PHASE2_MSCHAPV2:
return "mschapv2";
case ESP_EAP_TTLS_PHASE2_EAP:
return "eap";
default:
return "unknown";
}
}
#endif
float WiFiComponent::get_setup_priority() const { return setup_priority::WIFI; } float WiFiComponent::get_setup_priority() const { return setup_priority::WIFI; }
void WiFiComponent::setup() { void WiFiComponent::setup() {
@@ -344,15 +362,8 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) {
ESP_LOGV(TAG, " Identity: " LOG_SECRET("'%s'"), eap_config.identity.c_str()); ESP_LOGV(TAG, " Identity: " LOG_SECRET("'%s'"), eap_config.identity.c_str());
ESP_LOGV(TAG, " Username: " LOG_SECRET("'%s'"), eap_config.username.c_str()); ESP_LOGV(TAG, " Username: " LOG_SECRET("'%s'"), eap_config.username.c_str());
ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), eap_config.password.c_str()); ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), eap_config.password.c_str());
#ifdef USE_ESP32 #if defined(USE_ESP32) && defined(USE_WIFI_WPA2_EAP) && ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE ESP_LOGV(TAG, " TTLS Phase 2: " LOG_SECRET("'%s'"), eap_phase2_to_str(eap_config.ttls_phase_2));
std::map<esp_eap_ttls_phase2_types, std::string> phase2types = {{ESP_EAP_TTLS_PHASE2_PAP, "pap"},
{ESP_EAP_TTLS_PHASE2_CHAP, "chap"},
{ESP_EAP_TTLS_PHASE2_MSCHAP, "mschap"},
{ESP_EAP_TTLS_PHASE2_MSCHAPV2, "mschapv2"},
{ESP_EAP_TTLS_PHASE2_EAP, "eap"}};
ESP_LOGV(TAG, " TTLS Phase 2: " LOG_SECRET("'%s'"), phase2types[eap_config.ttls_phase_2].c_str());
#endif
#endif #endif
bool ca_cert_present = eap_config.ca_cert != nullptr && strlen(eap_config.ca_cert); bool ca_cert_present = eap_config.ca_cert != nullptr && strlen(eap_config.ca_cert);
bool client_cert_present = eap_config.client_cert != nullptr && strlen(eap_config.client_cert); bool client_cert_present = eap_config.client_cert != nullptr && strlen(eap_config.client_cert);

View File

@@ -33,12 +33,22 @@ static const char *const TAG = "component";
// Using namespace-scope static to avoid guard variables (saves 16 bytes total) // Using namespace-scope static to avoid guard variables (saves 16 bytes total)
// This is safe because ESPHome is single-threaded during initialization // This is safe because ESPHome is single-threaded during initialization
namespace { namespace {
struct ComponentErrorMessage {
const Component *component;
const char *message;
};
struct ComponentPriorityOverride {
const Component *component;
float priority;
};
// Error messages for failed components // Error messages for failed components
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
std::unique_ptr<std::vector<std::pair<const Component *, const char *>>> component_error_messages; std::unique_ptr<std::vector<ComponentErrorMessage>> component_error_messages;
// Setup priority overrides - freed after setup completes // Setup priority overrides - freed after setup completes
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
std::unique_ptr<std::vector<std::pair<const Component *, float>>> setup_priority_overrides; std::unique_ptr<std::vector<ComponentPriorityOverride>> setup_priority_overrides;
} // namespace } // namespace
namespace setup_priority { namespace setup_priority {
@@ -134,9 +144,9 @@ void Component::call_dump_config() {
// Look up error message from global vector // Look up error message from global vector
const char *error_msg = nullptr; const char *error_msg = nullptr;
if (component_error_messages) { if (component_error_messages) {
for (const auto &pair : *component_error_messages) { for (const auto &entry : *component_error_messages) {
if (pair.first == this) { if (entry.component == this) {
error_msg = pair.second; error_msg = entry.message;
break; break;
} }
} }
@@ -306,17 +316,17 @@ void Component::status_set_error(const char *message) {
if (message != nullptr) { if (message != nullptr) {
// Lazy allocate the error messages vector if needed // Lazy allocate the error messages vector if needed
if (!component_error_messages) { if (!component_error_messages) {
component_error_messages = std::make_unique<std::vector<std::pair<const Component *, const char *>>>(); component_error_messages = std::make_unique<std::vector<ComponentErrorMessage>>();
} }
// Check if this component already has an error message // Check if this component already has an error message
for (auto &pair : *component_error_messages) { for (auto &entry : *component_error_messages) {
if (pair.first == this) { if (entry.component == this) {
pair.second = message; entry.message = message;
return; return;
} }
} }
// Add new error message // Add new error message
component_error_messages->emplace_back(this, message); component_error_messages->emplace_back(ComponentErrorMessage{this, message});
} }
} }
void Component::status_clear_warning() { void Component::status_clear_warning() {
@@ -356,9 +366,9 @@ float Component::get_actual_setup_priority() const {
// Check if there's an override in the global vector // Check if there's an override in the global vector
if (setup_priority_overrides) { if (setup_priority_overrides) {
// Linear search is fine for small n (typically < 5 overrides) // Linear search is fine for small n (typically < 5 overrides)
for (const auto &pair : *setup_priority_overrides) { for (const auto &entry : *setup_priority_overrides) {
if (pair.first == this) { if (entry.component == this) {
return pair.second; return entry.priority;
} }
} }
} }
@@ -367,21 +377,21 @@ float Component::get_actual_setup_priority() const {
void Component::set_setup_priority(float priority) { void Component::set_setup_priority(float priority) {
// Lazy allocate the vector if needed // Lazy allocate the vector if needed
if (!setup_priority_overrides) { if (!setup_priority_overrides) {
setup_priority_overrides = std::make_unique<std::vector<std::pair<const Component *, float>>>(); setup_priority_overrides = std::make_unique<std::vector<ComponentPriorityOverride>>();
// Reserve some space to avoid reallocations (most configs have < 10 overrides) // Reserve some space to avoid reallocations (most configs have < 10 overrides)
setup_priority_overrides->reserve(10); setup_priority_overrides->reserve(10);
} }
// Check if this component already has an override // Check if this component already has an override
for (auto &pair : *setup_priority_overrides) { for (auto &entry : *setup_priority_overrides) {
if (pair.first == this) { if (entry.component == this) {
pair.second = priority; entry.priority = priority;
return; return;
} }
} }
// Add new override // Add new override
setup_priority_overrides->emplace_back(this, priority); setup_priority_overrides->emplace_back(ComponentPriorityOverride{this, priority});
} }
bool Component::has_overridden_loop() const { bool Component::has_overridden_loop() const {

View File

@@ -48,6 +48,7 @@
#define USE_LIGHT #define USE_LIGHT
#define USE_LOCK #define USE_LOCK
#define USE_LOGGER #define USE_LOGGER
#define USE_LOGGER_RUNTIME_TAG_LEVELS
#define USE_LVGL #define USE_LVGL
#define USE_LVGL_ANIMIMG #define USE_LVGL_ANIMIMG
#define USE_LVGL_ARC #define USE_LVGL_ARC
@@ -82,6 +83,7 @@
#define USE_LVGL_TILEVIEW #define USE_LVGL_TILEVIEW
#define USE_LVGL_TOUCHSCREEN #define USE_LVGL_TOUCHSCREEN
#define USE_MDNS #define USE_MDNS
#define MDNS_SERVICE_COUNT 3
#define USE_MEDIA_PLAYER #define USE_MEDIA_PLAYER
#define USE_NEXTION_TFT_UPLOAD #define USE_NEXTION_TFT_UPLOAD
#define USE_NUMBER #define USE_NUMBER
@@ -115,6 +117,7 @@
#define USE_API_NOISE #define USE_API_NOISE
#define USE_API_PLAINTEXT #define USE_API_PLAINTEXT
#define USE_API_SERVICES #define USE_API_SERVICES
#define API_MAX_SEND_QUEUE 8
#define USE_MD5 #define USE_MD5
#define USE_SHA256 #define USE_SHA256
#define USE_MQTT #define USE_MQTT

View File

@@ -3,6 +3,7 @@
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/string_ref.h"
#include <strings.h> #include <strings.h>
#include <algorithm> #include <algorithm>
@@ -348,17 +349,34 @@ ParseOnOffState parse_on_off(const char *str, const char *on, const char *off) {
return PARSE_NONE; return PARSE_NONE;
} }
std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) { static inline void normalize_accuracy_decimals(float &value, int8_t &accuracy_decimals) {
if (accuracy_decimals < 0) { if (accuracy_decimals < 0) {
auto multiplier = powf(10.0f, accuracy_decimals); auto multiplier = powf(10.0f, accuracy_decimals);
value = roundf(value * multiplier) / multiplier; value = roundf(value * multiplier) / multiplier;
accuracy_decimals = 0; accuracy_decimals = 0;
} }
}
std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) {
normalize_accuracy_decimals(value, accuracy_decimals);
char tmp[32]; // should be enough, but we should maybe improve this at some point. char tmp[32]; // should be enough, but we should maybe improve this at some point.
snprintf(tmp, sizeof(tmp), "%.*f", accuracy_decimals, value); snprintf(tmp, sizeof(tmp), "%.*f", accuracy_decimals, value);
return std::string(tmp); return std::string(tmp);
} }
std::string value_accuracy_with_uom_to_string(float value, int8_t accuracy_decimals, StringRef unit_of_measurement) {
normalize_accuracy_decimals(value, accuracy_decimals);
// Buffer sized for float (up to ~15 chars) + space + typical UOM (usually <20 chars like "μS/cm")
// snprintf truncates safely if exceeded, though ESPHome UOMs are typically short
char tmp[64];
if (unit_of_measurement.empty()) {
snprintf(tmp, sizeof(tmp), "%.*f", accuracy_decimals, value);
} else {
snprintf(tmp, sizeof(tmp), "%.*f %s", accuracy_decimals, value, unit_of_measurement.c_str());
}
return std::string(tmp);
}
int8_t step_to_accuracy_decimals(float step) { int8_t step_to_accuracy_decimals(float step) {
// use printf %g to find number of digits based on temperature step // use printf %g to find number of digits based on temperature step
char buf[32]; char buf[32];
@@ -613,8 +631,6 @@ bool mac_address_is_valid(const uint8_t *mac) {
if (mac[i] != 0) { if (mac[i] != 0) {
is_all_zeros = false; is_all_zeros = false;
} }
}
for (uint8_t i = 0; i < 6; i++) {
if (mac[i] != 0xFF) { if (mac[i] != 0xFF) {
is_all_ones = false; is_all_ones = false;
} }

View File

@@ -45,6 +45,9 @@
namespace esphome { namespace esphome {
// Forward declaration to avoid circular dependency with string_ref.h
class StringRef;
/// @name STL backports /// @name STL backports
///@{ ///@{
@@ -127,6 +130,16 @@ template<typename T, size_t N> class StaticVector {
} }
} }
// Return reference to next element and increment count (with bounds checking)
T &emplace_next() {
if (count_ >= N) {
// Should never happen with proper size calculation
// Return reference to last element to avoid crash
return data_[N - 1];
}
return data_[count_++];
}
size_t size() const { return count_; } size_t size() const { return count_; }
bool empty() const { return count_ == 0; } bool empty() const { return count_ == 0; }
@@ -600,6 +613,8 @@ ParseOnOffState parse_on_off(const char *str, const char *on = nullptr, const ch
/// Create a string from a value and an accuracy in decimals. /// Create a string from a value and an accuracy in decimals.
std::string value_accuracy_to_string(float value, int8_t accuracy_decimals); std::string value_accuracy_to_string(float value, int8_t accuracy_decimals);
/// Create a string from a value, an accuracy in decimals, and a unit of measurement.
std::string value_accuracy_with_uom_to_string(float value, int8_t accuracy_decimals, StringRef unit_of_measurement);
/// Derive accuracy in decimals from an increment step. /// Derive accuracy in decimals from an increment step.
int8_t step_to_accuracy_decimals(float step); int8_t step_to_accuracy_decimals(float step);

View File

@@ -6,11 +6,16 @@ esphome:
format: "Warning: Logger level is %d" format: "Warning: Logger level is %d"
args: [id(logger_id).get_log_level()] args: [id(logger_id).get_log_level()]
- logger.set_level: WARN - logger.set_level: WARN
- logger.set_level:
level: ERROR
tag: mqtt.client
logger: logger:
id: logger_id id: logger_id
level: DEBUG level: DEBUG
initial_level: INFO initial_level: INFO
logs:
mqtt.component: WARN
select: select:
- platform: logger - platform: logger