1
0
mirror of https://github.com/esphome/esphome.git synced 2025-10-17 17:23:45 +01: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/ethernet_info/* @gtjadsonsantos
esphome/components/event/* @nohat
esphome/components/event_emitter/* @Rapsssito
esphome/components/exposure_notifications/* @OttoWinter
esphome/components/ezo/* @ssieb
esphome/components/ezo_pmp/* @carlos-sarmiento

View File

@@ -61,6 +61,7 @@ CONF_CUSTOM_SERVICES = "custom_services"
CONF_HOMEASSISTANT_SERVICES = "homeassistant_services"
CONF_HOMEASSISTANT_STATES = "homeassistant_states"
CONF_LISTEN_BACKLOG = "listen_backlog"
CONF_MAX_SEND_QUEUE = "max_send_queue"
def validate_encryption_key(value):
@@ -183,6 +184,19 @@ CONFIG_SCHEMA = cv.All(
host=8, # Abundant resources
ln882x=8, # Moderate RAM
): 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),
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]))
if CONF_MAX_CONNECTIONS in config:
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
if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]:

View File

@@ -116,8 +116,7 @@ void APIConnection::start() {
APIError err = this->helper_->init();
if (err != APIError::OK) {
on_fatal_error();
this->log_warning_(LOG_STR("Helper init failed"), err);
this->fatal_error_with_log_(LOG_STR("Helper init failed"), err);
return;
}
this->client_info_.peername = helper_->getpeername();
@@ -147,8 +146,7 @@ void APIConnection::loop() {
APIError err = this->helper_->loop();
if (err != APIError::OK) {
on_fatal_error();
this->log_socket_operation_failed_(err);
this->fatal_error_with_log_(LOG_STR("Socket operation failed"), err);
return;
}
@@ -163,17 +161,13 @@ void APIConnection::loop() {
// No more data available
break;
} else if (err != APIError::OK) {
on_fatal_error();
this->log_warning_(LOG_STR("Reading failed"), err);
this->fatal_error_with_log_(LOG_STR("Reading failed"), err);
return;
} else {
this->last_traffic_ = now;
// read a packet
if (buffer.data_len > 0) {
this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]);
} else {
this->read_message(0, buffer.type, nullptr);
}
this->read_message(buffer.data_len, buffer.type,
buffer.data_len > 0 ? &buffer.container[buffer.data_offset] : nullptr);
if (this->flags_.remove)
return;
}
@@ -1580,8 +1574,7 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) {
delay(0);
APIError err = this->helper_->loop();
if (err != APIError::OK) {
on_fatal_error();
this->log_socket_operation_failed_(err);
this->fatal_error_with_log_(LOG_STR("Socket operation failed"), err);
return false;
}
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)
return false;
if (err != APIError::OK) {
on_fatal_error();
this->log_warning_(LOG_STR("Packet write failed"), err);
this->fatal_error_with_log_(LOG_STR("Packet write failed"), err);
return false;
}
// 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},
std::span<const PacketInfo>(packet_info, packet_count));
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
on_fatal_error();
this->log_warning_(LOG_STR("Batch write failed"), err);
this->fatal_error_with_log_(LOG_STR("Batch write failed"), err);
}
#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);
}
void APIConnection::log_socket_operation_failed_(APIError err) {
this->log_warning_(LOG_STR("Socket operation failed"), err);
}
} // namespace esphome::api
#endif

View File

@@ -732,8 +732,11 @@ class APIConnection final : public APIServerConnection {
// Helper function to log API errors with errno
void log_warning_(const LogString *message, APIError err);
// Specific helper for duplicated error message
void log_socket_operation_failed_(APIError err);
// Helper to handle fatal errors with logging
inline void fatal_error_with_log_(const LogString *message, APIError err) {
this->on_fatal_error();
this->log_warning_(message, err);
}
};
} // 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
APIError APIFrameHelper::loop() {
if (!this->tx_buf_.empty()) {
if (this->tx_buf_count_ > 0) {
APIError err = try_send_tx_buf_();
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
return err;
@@ -103,9 +103,20 @@ APIError APIFrameHelper::handle_socket_write_error_() {
// Helper method to buffer data from IOVs
void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len,
uint16_t offset) {
SendBuffer buffer;
buffer.size = total_write_len - offset;
buffer.data = std::make_unique<uint8_t[]>(buffer.size);
// Check if queue is full
if (this->tx_buf_count_ >= API_MAX_SEND_QUEUE) {
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 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)
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;
std::memcpy(buffer.data.get() + write_pos, src, len);
std::memcpy(buffer->data.get() + write_pos, src, len);
write_pos += len;
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
@@ -141,7 +155,7 @@ APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_
#endif
// 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_();
// If real error occurred (not just WOULD_BLOCK), return it
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
// 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);
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
// 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_() {
// 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 (!tx_buf_empty) {
while (this->tx_buf_count_ > 0) {
// 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
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) {
return this->handle_socket_write_error_();
} else if (sent == 0) {
// Nothing sent but not an error
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
// 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
} else {
// Buffer completely sent, remove it from the queue
this->tx_buf_.pop_front();
// Update empty status for the loop condition
tx_buf_empty = this->tx_buf_.empty();
this->tx_buf_[this->tx_buf_head_].reset();
this->tx_buf_head_ = (this->tx_buf_head_ + 1) % API_MAX_SEND_QUEUE;
this->tx_buf_count_--;
// Continue loop to try sending the next buffer
}
}

View File

@@ -1,7 +1,8 @@
#pragma once
#include <array>
#include <cstdint>
#include <deque>
#include <limits>
#include <memory>
#include <span>
#include <utility>
#include <vector>
@@ -79,7 +80,7 @@ class APIFrameHelper {
virtual APIError init() = 0;
virtual APIError loop();
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(); }
int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); }
APIError close() {
@@ -161,7 +162,7 @@ class APIFrameHelper {
};
// 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<uint8_t> rx_buf_;
@@ -174,7 +175,10 @@ class APIFrameHelper {
State state_{State::INITIALIZE};
uint8_t frame_header_padding_{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
APIError init_common_();

View File

@@ -11,7 +11,7 @@ void CopyLock::setup() {
traits.set_assumed_state(source_->traits.get_assumed_state());
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());
this->publish_state(source_->state);

View File

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

View File

@@ -77,7 +77,7 @@ void BLECharacteristic::notify() {
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 (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)
return;
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)
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,
param->read.conn_id);
if (this->on_read_callback_) {
(*this->on_read_callback_)(param->read.conn_id);
}
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) {
this->EventEmitter<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, uint16_t>::emit_(
BLECharacteristicEvt::VectorEvt::ON_WRITE, this->value_, param->write.conn_id);
if (this->on_write_callback_) {
(*this->on_write_callback_)(this->value_, param->write.conn_id);
}
}
break;
@@ -293,8 +295,9 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt
break;
this->write_event_ = false;
if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC) {
this->EventEmitter<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, uint16_t>::emit_(
BLECharacteristicEvt::VectorEvt::ON_WRITE, this->value_, param->exec_write.conn_id);
if (this->on_write_callback_) {
(*this->on_write_callback_)(this->value_, param->exec_write.conn_id);
}
}
esp_err_t err =
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 "esphome/components/esp32_ble/ble_uuid.h"
#include "esphome/components/event_emitter/event_emitter.h"
#include "esphome/components/bytebuffer/bytebuffer.h"
#include <vector>
#include <span>
#include <functional>
#include <memory>
#ifdef USE_ESP32
@@ -22,22 +24,10 @@ namespace esp32_ble_server {
using namespace esp32_ble;
using namespace bytebuffer;
using namespace event_emitter;
class BLEService;
namespace BLECharacteristicEvt {
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> {
class BLECharacteristic {
public:
BLECharacteristic(ESPBTUUID uuid, uint32_t properties);
~BLECharacteristic();
@@ -76,6 +66,15 @@ class BLECharacteristic : public EventEmitter<BLECharacteristicEvt::VectorEvt, s
bool is_created();
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:
bool write_event_{false};
BLEService *service_{};
@@ -98,6 +97,9 @@ class BLECharacteristic : public EventEmitter<BLECharacteristicEvt::VectorEvt, s
void remove_client_from_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;
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;
this->value_.attr_len = param->write.len;
memcpy(this->value_.attr_value, param->write.value, param->write.len);
this->emit_(BLEDescriptorEvt::VectorEvt::ON_WRITE,
std::vector<uint8_t>(param->write.value, param->write.value + param->write.len),
param->write.conn_id);
if (this->on_write_callback_) {
(*this->on_write_callback_)(std::span<const uint8_t>(param->write.value, param->write.len),
param->write.conn_id);
}
break;
}
default:

View File

@@ -1,30 +1,26 @@
#pragma once
#include "esphome/components/esp32_ble/ble_uuid.h"
#include "esphome/components/event_emitter/event_emitter.h"
#include "esphome/components/bytebuffer/bytebuffer.h"
#ifdef USE_ESP32
#include <esp_gatt_defs.h>
#include <esp_gatts_api.h>
#include <span>
#include <functional>
#include <memory>
namespace esphome {
namespace esp32_ble_server {
using namespace esp32_ble;
using namespace bytebuffer;
using namespace event_emitter;
class BLECharacteristic;
namespace BLEDescriptorEvt {
enum VectorEvt {
ON_WRITE,
};
} // namespace BLEDescriptorEvt
class BLEDescriptor : public EventEmitter<BLEDescriptorEvt::VectorEvt, std::vector<uint8_t>, uint16_t> {
// Base class for BLE descriptors
class BLEDescriptor {
public:
BLEDescriptor(ESPBTUUID uuid, uint16_t max_len = 100, bool read = true, bool write = true);
virtual ~BLEDescriptor();
@@ -39,6 +35,12 @@ class BLEDescriptor : public EventEmitter<BLEDescriptorEvt::VectorEvt, std::vect
bool is_created() { return this->state_ == CREATED; }
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:
BLECharacteristic *characteristic_{nullptr};
ESPBTUUID uuid_;
@@ -46,6 +48,8 @@ class BLEDescriptor : public EventEmitter<BLEDescriptorEvt::VectorEvt, std::vect
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_{};
enum State : uint8_t {

View File

@@ -147,20 +147,28 @@ BLEService *BLEServer::get_service(ESPBTUUID uuid, uint8_t inst_id) {
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,
esp_ble_gatts_cb_param_t *param) {
switch (event) {
case ESP_GATTS_CONNECT_EVT: {
ESP_LOGD(TAG, "BLE Client connected");
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;
}
case ESP_GATTS_DISCONNECT_EVT: {
ESP_LOGD(TAG, "BLE Client disconnected");
this->remove_client_(param->disconnect.conn_id);
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;
}
case ESP_GATTS_REG_EVT: {

View File

@@ -12,6 +12,7 @@
#include <memory>
#include <vector>
#include <unordered_map>
#include <functional>
#ifdef USE_ESP32
@@ -23,18 +24,7 @@ namespace esp32_ble_server {
using namespace esp32_ble;
using namespace bytebuffer;
namespace BLEServerEvt {
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> {
class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEventHandler, public Parented<ESP32BLE> {
public:
void setup() override;
void loop() override;
@@ -65,7 +55,25 @@ class BLEServer : public Component,
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:
enum class CallbackType : uint8_t {
ON_CONNECT,
ON_DISCONNECT,
};
struct CallbackEntry {
CallbackType type;
std::function<void(uint16_t)> callback;
};
struct ServiceEntry {
ESPBTUUID uuid;
uint8_t inst_id;
@@ -77,6 +85,9 @@ class BLEServer : public Component,
int8_t find_client_index_(uint16_t conn_id) const;
void add_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_{};
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) {
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
new Trigger<std::vector<uint8_t>, uint16_t>();
characteristic->EventEmitter<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, uint16_t>::on(
BLECharacteristicEvt::VectorEvt::ON_WRITE,
[on_write_trigger](const std::vector<uint8_t> &data, uint16_t id) { on_write_trigger->trigger(data, id); });
characteristic->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
// Convert span to vector for trigger
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
});
return on_write_trigger;
}
#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> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
new Trigger<std::vector<uint8_t>, uint16_t>();
descriptor->on(
BLEDescriptorEvt::VectorEvt::ON_WRITE,
[on_write_trigger](const std::vector<uint8_t> &data, uint16_t id) { on_write_trigger->trigger(data, id); });
descriptor->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
// Convert span to vector for trigger
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
});
return on_write_trigger;
}
#endif
@@ -35,8 +37,7 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_descriptor_on_write
#ifdef USE_ESP32_BLE_SERVER_ON_CONNECT
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)
server->on(BLEServerEvt::EmptyEvt::ON_CONNECT,
[on_connect_trigger](uint16_t conn_id) { on_connect_trigger->trigger(conn_id); });
server->on_connect([on_connect_trigger](uint16_t conn_id) { on_connect_trigger->trigger(conn_id); });
return on_connect_trigger;
}
#endif
@@ -44,38 +45,22 @@ Trigger<uint16_t> *BLETriggers::create_server_on_connect_trigger(BLEServer *serv
#ifdef USE_ESP32_BLE_SERVER_ON_DISCONNECT
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)
server->on(BLEServerEvt::EmptyEvt::ON_DISCONNECT,
[on_disconnect_trigger](uint16_t conn_id) { on_disconnect_trigger->trigger(conn_id); });
server->on_disconnect([on_disconnect_trigger](uint16_t conn_id) { on_disconnect_trigger->trigger(conn_id); });
return on_disconnect_trigger;
}
#endif
#ifdef USE_ESP32_BLE_SERVER_SET_VALUE_ACTION
void BLECharacteristicSetValueActionManager::set_listener(BLECharacteristic *characteristic,
EventEmitterListenerID listener_id,
const std::function<void()> &pre_notify_listener) {
// Find and remove existing listener for this characteristic
auto *existing = this->find_listener_(characteristic);
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
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
this->listeners_.push_back({characteristic, listener_id, pre_notify_listener_id});
this->listeners_.push_back({characteristic, pre_notify_listener});
}
BLECharacteristicSetValueActionManager::ListenerEntry *BLECharacteristicSetValueActionManager::find_listener_(

View File

@@ -4,7 +4,6 @@
#include "ble_characteristic.h"
#include "ble_descriptor.h"
#include "esphome/components/event_emitter/event_emitter.h"
#include "esphome/core/automation.h"
#include <vector>
@@ -18,10 +17,6 @@ namespace esp32_ble_server {
namespace esp32_ble_server_automations {
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 {
public:
@@ -41,38 +36,29 @@ class BLETriggers {
};
#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 BLECharacteristicSetValueActionManager
: public EventEmitter<BLECharacteristicSetValueActionEvt, BLECharacteristic *> {
class BLECharacteristicSetValueActionManager {
public:
// Singleton pattern
static BLECharacteristicSetValueActionManager *get_instance() {
static BLECharacteristicSetValueActionManager instance;
return &instance;
}
void set_listener(BLECharacteristic *characteristic, EventEmitterListenerID listener_id,
const std::function<void()> &pre_notify_listener);
EventEmitterListenerID get_listener(BLECharacteristic *characteristic) {
void set_listener(BLECharacteristic *characteristic, const std::function<void()> &pre_notify_listener);
bool has_listener(BLECharacteristic *characteristic) { return this->find_listener_(characteristic) != nullptr; }
void emit_pre_notify(BLECharacteristic *characteristic) {
for (const auto &entry : this->listeners_) {
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:
struct ListenerEntry {
BLECharacteristic *characteristic;
EventEmitterListenerID listener_id;
EventEmitterListenerID pre_notify_listener_id;
std::function<void()> pre_notify_listener;
};
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 play(Ts... x) override {
// 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;
// Set initial value
this->parent_->set_value(this->buffer_.value(x...));
// Set the listener for read events
this->listener_id_ = this->parent_->EventEmitter<BLECharacteristicEvt::EmptyEvt, uint16_t>::on(
BLECharacteristicEvt::EmptyEvt::ON_READ, [this, x...](uint16_t id) {
// Set the value of the characteristic every time it is read
this->parent_->set_value(this->buffer_.value(x...));
});
this->parent_->on_read([this, x...](uint16_t id) {
// Set the value of the characteristic every time it is read
this->parent_->set_value(this->buffer_.value(x...));
});
// Set the listener in the global manager so only one BLECharacteristicSetValueAction is set for each characteristic
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:
BLECharacteristic *parent_;
EventEmitterListenerID listener_id_;
};
#endif // USE_ESP32_BLE_SERVER_SET_VALUE_ACTION

View File

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

View File

@@ -41,17 +41,20 @@ static const char *const TAG = "ethernet";
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) \
if ((err) != ESP_OK) { \
ESP_LOGE(TAG, message ": (%d) %s", err, esp_err_to_name(err)); \
this->mark_failed(); \
this->log_error_and_mark_failed_(err, message); \
return; \
}
#define ESPHL_ERROR_CHECK_RET(err, message, ret) \
if ((err) != ESP_OK) { \
ESP_LOGE(TAG, message ": (%d) %s", err, esp_err_to_name(err)); \
this->mark_failed(); \
this->log_error_and_mark_failed_(err, message); \
return ret; \
}

View File

@@ -106,6 +106,7 @@ class EthernetComponent : public Component {
void start_connect_();
void finish_connect_();
void dump_connect_params_();
void log_error_and_mark_failed_(esp_err_t err, const char *message);
#ifdef USE_ETHERNET_KSZ8081
/// @brief Set `RMII Reference Clock Select` bit for KSZ8081.
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 "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#define ARDUINOJSON_ENABLE_STD_STRING 1 // NOLINT

View File

@@ -5,7 +5,7 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/preferences.h"
#include <set>
#include <initializer_list>
namespace esphome {
namespace lock {
@@ -44,16 +44,22 @@ class LockTraits {
bool get_assumed_state() const { return this->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); }
std::set<LockState> get_supported_states() const { return supported_states_; }
void set_supported_states(std::set<LockState> states) { supported_states_ = std::move(states); }
void add_supported_state(LockState state) { supported_states_.insert(state); }
bool supports_state(LockState state) const { return supported_states_mask_ & (1 << state); }
void set_supported_states(std::initializer_list<LockState> states) {
supported_states_mask_ = 0;
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:
bool supports_open_{false};
bool requires_code_{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.

View File

@@ -95,6 +95,7 @@ DEFAULT = "DEFAULT"
CONF_INITIAL_LEVEL = "initial_level"
CONF_LOGGER_ID = "logger_id"
CONF_RUNTIME_TAG_LEVELS = "runtime_tag_levels"
CONF_TASK_LOG_BUFFER_SIZE = "task_log_buffer_size"
UART_SELECTION_ESP32 = {
@@ -249,6 +250,7 @@ CONFIG_SCHEMA = cv.All(
}
),
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.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LoggerMessageTrigger),
@@ -291,8 +293,12 @@ async def to_code(config):
)
cg.add(log.pre_setup())
for tag, log_level in config[CONF_LOGS].items():
cg.add(log.set_log_level(tag, LOG_LEVELS[log_level]))
# 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_define("USE_LOGGER")
this_severity = LOG_LEVEL_SEVERITY.index(level)
@@ -443,6 +449,7 @@ async def logger_set_level_to_code(config, action_id, template_arg, args):
level = LOG_LEVELS[config[CONF_LEVEL]]
logger = await cg.get_variable(config[CONF_LOGGER_ID])
if tag := config.get(CONF_TAG):
cg.add_define("USE_LOGGER_RUNTIME_TAG_LEVELS")
text = str(cg.statement(logger.set_log_level(tag, level)))
else:
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
inline uint8_t Logger::level_for(const char *tag) {
#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS
auto it = this->log_levels_.find(tag);
if (it != this->log_levels_.end())
return it->second;
#endif
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_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)
UARTSelection Logger::get_uart() const { return this->uart_; }
@@ -271,9 +275,11 @@ void Logger::dump_config() {
}
#endif
#ifdef USE_LOGGER_RUNTIME_TAG_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) {

View File

@@ -36,6 +36,13 @@ struct device;
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)
static constexpr char LOG_LEVEL_COLOR_DIGIT[] = {
'\0', // NONE
@@ -133,8 +140,10 @@ class Logger : public Component {
/// Set the default log level for this logger.
void set_log_level(uint8_t level);
#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS
/// 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_; }
// ========== INTERNAL METHODS ==========
@@ -242,7 +251,9 @@ class Logger : public Component {
#endif
// 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)> level_callback_{};
#ifdef USE_ESPHOME_TASK_LOG_BUFFER

View File

@@ -17,6 +17,11 @@ from esphome.coroutine import CoroPriority
CODEOWNERS = ["@esphome/core"]
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")
MDNSComponent = mdns_ns.class_("MDNSComponent", cg.Component)
MDNSTXTRecord = mdns_ns.struct("MDNSTXTRecord")
@@ -91,12 +96,20 @@ async def to_code(config):
cg.add_define("USE_MDNS")
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
# Calculate compile-time service count
service_count = sum(
1 for key in COMPONENTS_WITH_MDNS_SERVICES if key in CORE.config
) + len(config[CONF_SERVICES])
if config[CONF_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]:
txt = [
cg.StructInitializer(

View File

@@ -74,32 +74,12 @@ MDNS_STATIC_CONST_CHAR(NETWORK_THREAD, "thread");
void MDNSComponent::compile_records_() {
this->hostname_ = App.get_name();
// Calculate exact capacity needed for services vector
size_t services_count = 0;
#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);
// IMPORTANT: The #ifdef blocks below must match COMPONENTS_WITH_MDNS_SERVICES
// in mdns/__init__.py. If you add a new service here, update both locations.
#ifdef USE_API
if (api::global_api_server != nullptr) {
this->services_.emplace_back();
auto &service = this->services_.back();
auto &service = this->services_.emplace_next();
service.service_type = MDNS_STR(SERVICE_ESPHOMELIB);
service.proto = MDNS_STR(SERVICE_TCP);
service.port = api::global_api_server->get_port();
@@ -178,30 +158,23 @@ void MDNSComponent::compile_records_() {
#endif // USE_API
#ifdef USE_PROMETHEUS
this->services_.emplace_back();
auto &prom_service = this->services_.back();
auto &prom_service = this->services_.emplace_next();
prom_service.service_type = MDNS_STR(SERVICE_PROMETHEUS);
prom_service.proto = MDNS_STR(SERVICE_TCP);
prom_service.port = USE_WEBSERVER_PORT;
#endif
#ifdef USE_WEBSERVER
this->services_.emplace_back();
auto &web_service = this->services_.back();
auto &web_service = this->services_.emplace_next();
web_service.service_type = MDNS_STR(SERVICE_HTTP);
web_service.proto = MDNS_STR(SERVICE_TCP);
web_service.port = USE_WEBSERVER_PORT;
#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)
// 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->services_.emplace_back();
auto &fallback_service = this->services_.back();
auto &fallback_service = this->services_.emplace_next();
fallback_service.service_type = "_http";
fallback_service.proto = "_tcp";
fallback_service.port = USE_WEBSERVER_PORT;
@@ -214,7 +187,7 @@ void MDNSComponent::dump_config() {
"mDNS:\n"
" Hostname: %s",
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:");
for (const auto &service : this->services_) {
ESP_LOGV(TAG, " - %s, %s, %d", service.service_type.c_str(), service.proto.c_str(),
@@ -227,8 +200,6 @@ void MDNSComponent::dump_config() {
#endif
}
std::vector<MDNSService> MDNSComponent::get_services() { return this->services_; }
} // namespace mdns
} // namespace esphome
#endif

View File

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

View File

@@ -11,6 +11,7 @@ namespace mpr121 {
static const char *const TAG = "mpr121";
void MPR121Component::setup() {
this->disable_loop();
// soft reset device
this->write_byte(MPR121_SOFTRESET, 0x63);
this->set_timeout(100, [this]() {
@@ -51,7 +52,7 @@ void MPR121Component::setup() {
this->write_byte(MPR121_ECR, 0x80 | (this->max_touch_channel_ + 1));
this->flush_gpio_();
this->setup_complete_ = true;
this->enable_loop();
});
}
@@ -80,9 +81,6 @@ void MPR121Component::dump_config() {
}
}
void MPR121Component::loop() {
if (!this->setup_complete_)
return;
uint16_t val = 0;
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);
protected:
bool setup_complete_{false};
std::vector<MPR121Channel *> channels_{};
uint8_t debounce_{0};
uint8_t touch_threshold_{};

View File

@@ -7,6 +7,17 @@ namespace 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::number_increment(bool cycle) {
@@ -42,7 +53,7 @@ void NumberCall::perform() {
const auto &traits = parent->traits;
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;
}
@@ -51,28 +62,28 @@ void NumberCall::perform() {
float max_value = traits.get_max_value();
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_)) {
ESP_LOGW(TAG, "'%s' - No value set for NumberCall", name);
this->log_perform_warning_(LOG_STR("No value"));
return;
}
target_value = this->value_.value();
} else if (this->operation_ == NUMBER_OP_TO_MIN) {
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 {
target_value = min_value;
}
} else if (this->operation_ == NUMBER_OP_TO_MAX) {
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 {
target_value = max_value;
}
} 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()) {
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;
}
auto step = traits.get_step();
@@ -85,9 +96,9 @@ void NumberCall::perform() {
}
}
} 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()) {
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;
}
auto step = traits.get_step();
@@ -102,15 +113,15 @@ void NumberCall::perform() {
}
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;
}
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;
}
ESP_LOGD(TAG, " New number value: %f", target_value);
ESP_LOGD(TAG, " New value: %f", target_value);
this->parent_->control(target_value);
}

View File

@@ -1,6 +1,7 @@
#pragma once
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "number_traits.h"
namespace esphome {
@@ -33,6 +34,10 @@ class NumberCall {
NumberCall &with_cycle(bool cycle);
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_;
NumberOperation operation_{NUMBER_OP_NONE};
optional<float> value_;

View File

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

View File

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

View File

@@ -45,16 +45,16 @@ void SPS30Component::setup() {
}
ESP_LOGV(TAG, " Serial number: %s", this->serial_number_);
bool result;
if (this->fan_interval_.has_value()) {
// override default value
this->result_ =
this->write_command(SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS, this->fan_interval_.value());
result = this->write_command(SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS, this->fan_interval_.value());
} 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]() {
if (this->result_) {
this->set_timeout(20, [this, result]() {
if (result) {
uint16_t secs[2];
if (this->read_data(secs, 2)) {
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();
protected:
bool result_{false};
bool setup_complete_{false};
uint16_t raw_firmware_version_;
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());
this->consecutive_send_failures_ = 0; // Reset failure count on successful send
} 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_++;
if (this->consecutive_send_failures_ >= MAX_CONSECUTIVE_SEND_FAILURES) {
// Too many failures, connection is likely dead
@@ -381,11 +385,14 @@ void WebServer::handle_js_request(AsyncWebServerRequest *request) {
#endif
// 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) {
root["id"] = id;
static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, JsonDetail start_config) {
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) {
root["name"] = obj->get_name();
root["icon"] = obj->get_icon();
root["icon"] = obj->get_icon_ref();
root["entity_category"] = obj->get_entity_category();
bool is_disabled = obj->is_disabled_by_default();
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>
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) {
set_json_id(root, obj, id, start_config);
set_json_id(root, obj, prefix, start_config);
root["value"] = value;
}
template<typename T>
static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const std::string &id,
const std::string &state, const T &value, JsonDetail start_config) {
set_json_value(root, obj, id, value, start_config);
static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const char *prefix, const std::string &state,
const T &value, JsonDetail start_config) {
set_json_value(root, obj, prefix, value, start_config);
root["state"] = state;
}
@@ -442,20 +451,20 @@ std::string WebServer::sensor_json(sensor::Sensor *obj, float value, JsonDetail
json::JsonBuilder builder;
JsonObject root = builder.root();
const auto uom_ref = obj->get_unit_of_measurement_ref();
// Build JSON directly inline
std::string state;
if (std::isnan(value)) {
state = "NA";
} else {
state = value_accuracy_to_string(value, obj->get_accuracy_decimals());
if (!obj->get_unit_of_measurement().empty())
state += " " + obj->get_unit_of_measurement();
state = value_accuracy_with_uom_to_string(value, obj->get_accuracy_decimals(), uom_ref);
}
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) {
this->add_sorting_info_(root, obj);
if (!obj->get_unit_of_measurement().empty())
root["uom"] = obj->get_unit_of_measurement();
if (!uom_ref.empty())
root["uom"] = uom_ref;
}
return builder.serialize();
@@ -494,7 +503,7 @@ std::string WebServer::text_sensor_json(text_sensor::TextSensor *obj, const std:
json::JsonBuilder builder;
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) {
this->add_sorting_info_(root, obj);
}
@@ -567,7 +576,7 @@ std::string WebServer::switch_json(switch_::Switch *obj, bool value, JsonDetail
json::JsonBuilder builder;
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) {
root["assumed_state"] = obj->assumed_state();
this->add_sorting_info_(root, obj);
@@ -607,7 +616,7 @@ std::string WebServer::button_json(button::Button *obj, JsonDetail start_config)
json::JsonBuilder builder;
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) {
this->add_sorting_info_(root, obj);
}
@@ -647,8 +656,7 @@ std::string WebServer::binary_sensor_json(binary_sensor::BinarySensor *obj, bool
json::JsonBuilder builder;
JsonObject root = builder.root();
set_json_icon_state_value(root, obj, "binary_sensor-" + obj->get_object_id(), value ? "ON" : "OFF", value,
start_config);
set_json_icon_state_value(root, obj, "binary_sensor", value ? "ON" : "OFF", value, start_config);
if (start_config == DETAIL_ALL) {
this->add_sorting_info_(root, obj);
}
@@ -717,8 +725,7 @@ std::string WebServer::fan_json(fan::Fan *obj, JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
set_json_icon_state_value(root, obj, "fan-" + obj->get_object_id(), obj->state ? "ON" : "OFF", obj->state,
start_config);
set_json_icon_state_value(root, obj, "fan", obj->state ? "ON" : "OFF", obj->state, start_config);
const auto traits = obj->get_traits();
if (traits.supports_speed()) {
root["speed_level"] = obj->speed;
@@ -793,7 +800,7 @@ std::string WebServer::light_json(light::LightState *obj, JsonDetail start_confi
json::JsonBuilder builder;
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";
light::LightJSONSchema::dump_json(*obj, root);
@@ -881,8 +888,8 @@ std::string WebServer::cover_json(cover::Cover *obj, JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
set_json_icon_state_value(root, obj, "cover-" + obj->get_object_id(), obj->is_fully_closed() ? "CLOSED" : "OPEN",
obj->position, start_config);
set_json_icon_state_value(root, obj, "cover", obj->is_fully_closed() ? "CLOSED" : "OPEN", obj->position,
start_config);
root["current_operation"] = cover::cover_operation_to_str(obj->current_operation);
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;
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) {
root["min_value"] =
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()));
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();
if (!obj->traits.get_unit_of_measurement().empty())
root["uom"] = obj->traits.get_unit_of_measurement();
if (!uom_ref.empty())
root["uom"] = uom_ref;
this->add_sorting_info_(root, obj);
}
if (std::isnan(value)) {
@@ -956,10 +965,8 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail
root["state"] = "NA";
} else {
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()));
if (!obj->traits.get_unit_of_measurement().empty())
state += " " + obj->traits.get_unit_of_measurement();
root["state"] = state;
root["state"] =
value_accuracy_with_uom_to_string(value, step_to_accuracy_decimals(obj->traits.get_step()), uom_ref);
}
return builder.serialize();
@@ -1013,7 +1020,7 @@ std::string WebServer::date_json(datetime::DateEntity *obj, JsonDetail start_con
json::JsonBuilder builder;
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);
root["value"] = value;
root["state"] = value;
@@ -1071,7 +1078,7 @@ std::string WebServer::time_json(datetime::TimeEntity *obj, JsonDetail start_con
json::JsonBuilder builder;
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);
root["value"] = value;
root["state"] = value;
@@ -1129,7 +1136,7 @@ std::string WebServer::datetime_json(datetime::DateTimeEntity *obj, JsonDetail s
json::JsonBuilder builder;
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 =
str_sprintf("%d-%02d-%02d %02d:%02d:%02d", obj->year, obj->month, obj->day, obj->hour, obj->minute, obj->second);
root["value"] = value;
@@ -1184,7 +1191,7 @@ std::string WebServer::text_json(text::Text *obj, const std::string &value, Json
json::JsonBuilder builder;
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["max_length"] = obj->traits.get_max_length();
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;
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) {
JsonArray opt = root["option"].to<JsonArray>();
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
json::JsonBuilder builder;
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();
int8_t target_accuracy = traits.get_target_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;
JsonObject root = builder.root();
set_json_icon_state_value(root, obj, "lock-" + obj->get_object_id(), lock::lock_state_to_string(value), value,
start_config);
set_json_icon_state_value(root, obj, "lock", lock::lock_state_to_string(value), value, start_config);
if (start_config == DETAIL_ALL) {
this->add_sorting_info_(root, obj);
}
@@ -1546,8 +1552,8 @@ std::string WebServer::valve_json(valve::Valve *obj, JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
set_json_icon_state_value(root, obj, "valve-" + obj->get_object_id(), obj->is_fully_closed() ? "CLOSED" : "OPEN",
obj->position, start_config);
set_json_icon_state_value(root, obj, "valve", obj->is_fully_closed() ? "CLOSED" : "OPEN", obj->position,
start_config);
root["current_operation"] = valve::valve_operation_to_str(obj->current_operation);
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();
char buf[16];
set_json_icon_state_value(root, obj, "alarm-control-panel-" + obj->get_object_id(),
PSTR_LOCAL(alarm_control_panel_state_to_string(value)), value, start_config);
set_json_icon_state_value(root, obj, "alarm-control-panel", PSTR_LOCAL(alarm_control_panel_state_to_string(value)),
value, start_config);
if (start_config == DETAIL_ALL) {
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;
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()) {
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()) {
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);
}
@@ -1748,7 +1754,7 @@ std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_c
json::JsonBuilder builder;
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["state"] = update_state_to_string(obj->state);
if (start_config == DETAIL_ALL) {

View File

@@ -25,6 +25,10 @@
#include "esphome/components/web_server/list_entities.h"
#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 web_server_idf {
@@ -46,6 +50,42 @@ DefaultHeaders 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() {
if (this->server_) {
httpd_stop(this->server_);
@@ -164,8 +204,8 @@ esp_err_t AsyncWebServer::request_handler_(AsyncWebServerRequest *request) const
AsyncWebServerRequest::~AsyncWebServerRequest() {
delete this->rsp_;
for (const auto &pair : this->params_) {
delete pair.second; // NOLINT(cppcoreguidelines-owning-memory)
for (auto *param : this->params_) {
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) {
httpd_resp_set_status(*this, code == 200 ? HTTPD_200
: code == 404 ? HTTPD_404
: code == 409 ? HTTPD_409
: to_string(code).c_str());
// Set status code - use constants for common codes to avoid string allocation
const char *status = nullptr;
switch (code) {
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) {
httpd_resp_set_type(*this, content_type);
@@ -265,11 +317,14 @@ void AsyncWebServerRequest::requestAuthentication(const char *realm) const {
#endif
AsyncWebParameter *AsyncWebServerRequest::getParam(const std::string &name) {
auto find = this->params_.find(name);
if (find != this->params_.end()) {
return find->second;
// Check cache first - only successful lookups are cached
for (auto *param : this->params_) {
if (param->name() == name) {
return param;
}
}
// Look up value from query strings
optional<std::string> val = query_key_value(this->post_query_, name);
if (!val.has_value()) {
auto url_query = request_get_url_query(*this);
@@ -278,11 +333,14 @@ AsyncWebParameter *AsyncWebServerRequest::getParam(const std::string &name) {
}
}
AsyncWebParameter *param = nullptr;
if (val.has_value()) {
param = new AsyncWebParameter(val.value()); // NOLINT(cppcoreguidelines-owning-memory)
// Don't cache misses to avoid wasting memory when handlers check for
// optional parameters that don't exist in the request
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;
}
@@ -384,6 +442,9 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest *
this->hd_ = req->handle;
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
// this should always go through since the tcp send buffer is empty on connect
std::string message = ws->get_config_json();
@@ -459,15 +520,45 @@ void AsyncEventSourceResponse::process_buffer_() {
return;
}
int bytes_sent = httpd_socket_send(this->hd_, this->fd_.load(), event_buffer_.c_str() + event_bytes_sent_,
event_buffer_.size() - event_bytes_sent_, 0);
if (bytes_sent == HTTPD_SOCK_ERR_TIMEOUT || bytes_sent == HTTPD_SOCK_ERR_FAIL) {
// Socket error - just return, the connection will be closed by httpd
// and our destroy callback will be called
size_t remaining = event_buffer_.size() - event_bytes_sent_;
int bytes_sent =
httpd_socket_send(this->hd_, this->fd_.load(), event_buffer_.c_str() + event_bytes_sent_, remaining, 0);
if (bytes_sent == HTTPD_SOCK_ERR_TIMEOUT) {
// 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;
}
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;
// 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()) {
event_buffer_.resize(0);
event_bytes_sent_ = 0;

View File

@@ -30,10 +30,12 @@ using String = std::string;
class AsyncWebParameter {
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_; }
protected:
std::string name_;
std::string value_;
};
@@ -174,7 +176,11 @@ class AsyncWebServerRequest {
protected:
httpd_req_t *req_;
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_;
AsyncWebServerRequest(httpd_req_t *req) : req_(req) {}
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::string event_buffer_{""};
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;

View File

@@ -1,7 +1,6 @@
#include "wifi_component.h"
#ifdef USE_WIFI
#include <cinttypes>
#include <map>
#ifdef USE_ESP32
#if (ESP_IDF_VERSION_MAJOR >= 5 && ESP_IDF_VERSION_MINOR >= 1)
@@ -42,6 +41,25 @@ namespace 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; }
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, " Username: " LOG_SECRET("'%s'"), eap_config.username.c_str());
ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), eap_config.password.c_str());
#ifdef USE_ESP32
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
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
#if defined(USE_ESP32) && defined(USE_WIFI_WPA2_EAP) && 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));
#endif
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);

View File

@@ -33,12 +33,22 @@ static const char *const TAG = "component";
// Using namespace-scope static to avoid guard variables (saves 16 bytes total)
// This is safe because ESPHome is single-threaded during initialization
namespace {
struct ComponentErrorMessage {
const Component *component;
const char *message;
};
struct ComponentPriorityOverride {
const Component *component;
float priority;
};
// Error messages for failed components
// 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
// 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 setup_priority {
@@ -134,9 +144,9 @@ void Component::call_dump_config() {
// Look up error message from global vector
const char *error_msg = nullptr;
if (component_error_messages) {
for (const auto &pair : *component_error_messages) {
if (pair.first == this) {
error_msg = pair.second;
for (const auto &entry : *component_error_messages) {
if (entry.component == this) {
error_msg = entry.message;
break;
}
}
@@ -306,17 +316,17 @@ void Component::status_set_error(const char *message) {
if (message != nullptr) {
// Lazy allocate the error messages vector if needed
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
for (auto &pair : *component_error_messages) {
if (pair.first == this) {
pair.second = message;
for (auto &entry : *component_error_messages) {
if (entry.component == this) {
entry.message = message;
return;
}
}
// Add new error message
component_error_messages->emplace_back(this, message);
component_error_messages->emplace_back(ComponentErrorMessage{this, message});
}
}
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
if (setup_priority_overrides) {
// Linear search is fine for small n (typically < 5 overrides)
for (const auto &pair : *setup_priority_overrides) {
if (pair.first == this) {
return pair.second;
for (const auto &entry : *setup_priority_overrides) {
if (entry.component == this) {
return entry.priority;
}
}
}
@@ -367,21 +377,21 @@ float Component::get_actual_setup_priority() const {
void Component::set_setup_priority(float priority) {
// Lazy allocate the vector if needed
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)
setup_priority_overrides->reserve(10);
}
// Check if this component already has an override
for (auto &pair : *setup_priority_overrides) {
if (pair.first == this) {
pair.second = priority;
for (auto &entry : *setup_priority_overrides) {
if (entry.component == this) {
entry.priority = priority;
return;
}
}
// Add new override
setup_priority_overrides->emplace_back(this, priority);
setup_priority_overrides->emplace_back(ComponentPriorityOverride{this, priority});
}
bool Component::has_overridden_loop() const {

View File

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

View File

@@ -3,6 +3,7 @@
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/string_ref.h"
#include <strings.h>
#include <algorithm>
@@ -348,17 +349,34 @@ ParseOnOffState parse_on_off(const char *str, const char *on, const char *off) {
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) {
auto multiplier = powf(10.0f, accuracy_decimals);
value = roundf(value * multiplier) / multiplier;
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.
snprintf(tmp, sizeof(tmp), "%.*f", accuracy_decimals, value);
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) {
// use printf %g to find number of digits based on temperature step
char buf[32];
@@ -613,8 +631,6 @@ bool mac_address_is_valid(const uint8_t *mac) {
if (mac[i] != 0) {
is_all_zeros = false;
}
}
for (uint8_t i = 0; i < 6; i++) {
if (mac[i] != 0xFF) {
is_all_ones = false;
}

View File

@@ -45,6 +45,9 @@
namespace esphome {
// Forward declaration to avoid circular dependency with string_ref.h
class StringRef;
/// @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_; }
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.
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.
int8_t step_to_accuracy_decimals(float step);

View File

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