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:
@@ -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
|
||||
|
@@ -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]:
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -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_();
|
||||
|
@@ -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);
|
||||
|
@@ -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"
|
||||
|
@@ -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);
|
||||
|
@@ -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 {
|
||||
|
@@ -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:
|
||||
|
@@ -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 {
|
||||
|
@@ -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: {
|
||||
|
@@ -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};
|
||||
|
@@ -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_(
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -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; \
|
||||
}
|
||||
|
||||
|
@@ -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);
|
||||
|
@@ -1,5 +0,0 @@
|
||||
CODEOWNERS = ["@Rapsssito"]
|
||||
|
||||
# Allows event_emitter to be configured in yaml, to allow use of the C++ api.
|
||||
|
||||
CONFIG_SCHEMA = {}
|
@@ -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
|
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#define ARDUINOJSON_ENABLE_STD_STRING 1 // NOLINT
|
||||
|
@@ -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.
|
||||
|
@@ -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)))
|
||||
|
@@ -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) {
|
||||
|
@@ -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
|
||||
|
@@ -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(
|
||||
|
@@ -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
|
||||
|
@@ -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_();
|
||||
};
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -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_{};
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
|
@@ -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_;
|
||||
|
@@ -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");
|
||||
|
@@ -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);
|
||||
};
|
||||
|
@@ -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];
|
||||
|
@@ -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
|
||||
|
@@ -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) {
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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);
|
||||
|
@@ -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 {
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user