1
0
mirror of https://github.com/esphome/esphome.git synced 2026-02-10 17:51:53 +00:00

Compare commits

..

1 Commits

Author SHA1 Message Date
J. Nick Koston
39bde3eb73 Revert early returns in process_active_iterator_()
The if/else pattern produces smaller code than early returns.
2026-02-09 20:21:02 -06:00
129 changed files with 1333 additions and 5114 deletions

View File

@@ -1 +1 @@
0f2b9a65dce7c59289d3aeb40936360a62a7be937b585147b45bb1509eaafb36
37ec8d5a343c8d0a485fd2118cbdabcbccd7b9bca197e4a392be75087974dced

View File

@@ -5,7 +5,6 @@ from __future__ import annotations
from collections import defaultdict
from collections.abc import Callable
import heapq
import json
from operator import itemgetter
import sys
from typing import TYPE_CHECKING
@@ -541,28 +540,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
return "\n".join(lines)
def to_json(self) -> str:
"""Export analysis results as JSON."""
data = {
"components": {
name: {
"text": mem.text_size,
"rodata": mem.rodata_size,
"data": mem.data_size,
"bss": mem.bss_size,
"flash_total": mem.flash_total,
"ram_total": mem.ram_total,
"symbol_count": mem.symbol_count,
}
for name, mem in self.components.items()
},
"totals": {
"flash": sum(c.flash_total for c in self.components.values()),
"ram": sum(c.ram_total for c in self.components.values()),
},
}
return json.dumps(data, indent=2)
def dump_uncategorized_symbols(self, output_file: str | None = None) -> None:
"""Dump uncategorized symbols for analysis."""
# Sort by size descending

View File

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

View File

@@ -524,24 +524,24 @@ async def homeassistant_service_to_code(
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
serv = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, serv, False)
templ = await cg.templatable(config[CONF_ACTION], args, cg.std_string)
templ = await cg.templatable(config[CONF_ACTION], args, None)
cg.add(var.set_service(templ))
# Initialize FixedVectors with exact sizes from config
cg.add(var.init_data(len(config[CONF_DATA])))
for key, value in config[CONF_DATA].items():
templ = await cg.templatable(value, args, cg.std_string)
cg.add(var.add_data(cg.FlashStringLiteral(key), templ))
templ = await cg.templatable(value, args, None)
cg.add(var.add_data(key, templ))
cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE])))
for key, value in config[CONF_DATA_TEMPLATE].items():
templ = await cg.templatable(value, args, cg.std_string)
cg.add(var.add_data_template(cg.FlashStringLiteral(key), templ))
templ = await cg.templatable(value, args, None)
cg.add(var.add_data_template(key, templ))
cg.add(var.init_variables(len(config[CONF_VARIABLES])))
for key, value in config[CONF_VARIABLES].items():
templ = await cg.templatable(value, args, cg.std_string)
cg.add(var.add_variable(cg.FlashStringLiteral(key), templ))
templ = await cg.templatable(value, args, None)
cg.add(var.add_variable(key, templ))
if on_error := config.get(CONF_ON_ERROR):
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES")
@@ -609,24 +609,24 @@ async def homeassistant_event_to_code(config, action_id, template_arg, args):
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
serv = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, serv, True)
templ = await cg.templatable(config[CONF_EVENT], args, cg.std_string)
templ = await cg.templatable(config[CONF_EVENT], args, None)
cg.add(var.set_service(templ))
# Initialize FixedVectors with exact sizes from config
cg.add(var.init_data(len(config[CONF_DATA])))
for key, value in config[CONF_DATA].items():
templ = await cg.templatable(value, args, cg.std_string)
cg.add(var.add_data(cg.FlashStringLiteral(key), templ))
templ = await cg.templatable(value, args, None)
cg.add(var.add_data(key, templ))
cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE])))
for key, value in config[CONF_DATA_TEMPLATE].items():
templ = await cg.templatable(value, args, cg.std_string)
cg.add(var.add_data_template(cg.FlashStringLiteral(key), templ))
templ = await cg.templatable(value, args, None)
cg.add(var.add_data_template(key, templ))
cg.add(var.init_variables(len(config[CONF_VARIABLES])))
for key, value in config[CONF_VARIABLES].items():
templ = await cg.templatable(value, args, cg.std_string)
cg.add(var.add_variable(cg.FlashStringLiteral(key), templ))
templ = await cg.templatable(value, args, None)
cg.add(var.add_variable(key, templ))
return var
@@ -649,11 +649,11 @@ async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, arg
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
serv = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, serv, True)
cg.add(var.set_service(cg.FlashStringLiteral("esphome.tag_scanned")))
cg.add(var.set_service("esphome.tag_scanned"))
# Initialize FixedVector with exact size (1 data field)
cg.add(var.init_data(1))
templ = await cg.templatable(config[CONF_TAG], args, cg.std_string)
cg.add(var.add_data(cg.FlashStringLiteral("tag_id"), templ))
cg.add(var.add_data("tag_id", templ))
return var

View File

@@ -1155,11 +1155,9 @@ enum WaterHeaterCommandHasField {
WATER_HEATER_COMMAND_HAS_NONE = 0;
WATER_HEATER_COMMAND_HAS_MODE = 1;
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE = 2;
WATER_HEATER_COMMAND_HAS_STATE = 4 [deprecated=true];
WATER_HEATER_COMMAND_HAS_STATE = 4;
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8;
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH = 16;
WATER_HEATER_COMMAND_HAS_ON_STATE = 32;
WATER_HEATER_COMMAND_HAS_AWAY_STATE = 64;
}
message WaterHeaterCommandRequest {

View File

@@ -133,8 +133,8 @@ void APIConnection::start() {
return;
}
// Initialize client name with peername (IP address) until Hello message provides actual name
char peername[socket::SOCKADDR_STR_LEN];
this->helper_->set_client_name(this->helper_->get_peername_to(peername), strlen(peername));
const char *peername = this->helper_->get_client_peername();
this->helper_->set_client_name(peername, strlen(peername));
}
APIConnection::~APIConnection() {
@@ -179,8 +179,8 @@ void APIConnection::begin_iterator_(ActiveIterator type) {
void APIConnection::loop() {
if (this->flags_.next_close) {
// requested a disconnect - don't close socket here, let APIServer::loop() do it
// so getpeername() still works for the disconnect trigger
// requested a disconnect
this->helper_->close();
this->flags_.remove = true;
return;
}
@@ -309,8 +309,7 @@ bool APIConnection::send_disconnect_response_() {
return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE);
}
void APIConnection::on_disconnect_response() {
// Don't close socket here, let APIServer::loop() do it
// so getpeername() still works for the disconnect trigger
this->helper_->close();
this->flags_.remove = true;
}
@@ -1360,12 +1359,8 @@ void APIConnection::on_water_heater_command_request(const WaterHeaterCommandRequ
call.set_target_temperature_low(msg.target_temperature_low);
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH)
call.set_target_temperature_high(msg.target_temperature_high);
if ((msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_AWAY_STATE) ||
(msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_STATE)) {
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_STATE) {
call.set_away((msg.state & water_heater::WATER_HEATER_STATE_AWAY) != 0);
}
if ((msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_ON_STATE) ||
(msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_STATE)) {
call.set_on((msg.state & water_heater::WATER_HEATER_STATE_ON) != 0);
}
call.perform();
@@ -1486,11 +1481,8 @@ void APIConnection::complete_authentication_() {
this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED);
this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("connected"));
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
{
char peername[socket::SOCKADDR_STR_LEN];
this->parent_->get_client_connected_trigger()->trigger(std::string(this->helper_->get_client_name()),
std::string(this->helper_->get_peername_to(peername)));
}
this->parent_->get_client_connected_trigger()->trigger(std::string(this->helper_->get_client_name()),
std::string(this->helper_->get_client_peername()));
#endif
#ifdef USE_HOMEASSISTANT_TIME
if (homeassistant::global_homeassistant_time != nullptr) {
@@ -1509,9 +1501,8 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) {
this->helper_->set_client_name(msg.client_info.c_str(), msg.client_info.size());
this->client_api_version_major_ = msg.api_version_major;
this->client_api_version_minor_ = msg.api_version_minor;
char peername[socket::SOCKADDR_STR_LEN];
ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->helper_->get_client_name(),
this->helper_->get_peername_to(peername), this->client_api_version_major_, this->client_api_version_minor_);
this->helper_->get_client_peername(), this->client_api_version_major_, this->client_api_version_minor_);
HelloResponse resp;
resp.api_version_major = 1;
@@ -1859,8 +1850,7 @@ void APIConnection::on_no_setup_connection() {
this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("no connection setup"));
}
void APIConnection::on_fatal_error() {
// Don't close socket here - keep it open so getpeername() works for logging
// Socket will be closed when client is removed from the list in APIServer::loop()
this->helper_->close();
this->flags_.remove = true;
}
@@ -1921,6 +1911,10 @@ bool APIConnection::schedule_batch_() {
}
void APIConnection::process_batch_() {
// Ensure MessageInfo remains trivially destructible for our placement new approach
static_assert(std::is_trivially_destructible<MessageInfo>::value,
"MessageInfo must remain trivially destructible with this placement-new approach");
if (this->deferred_batch_.empty()) {
this->flags_.batch_scheduled = false;
return;
@@ -1945,10 +1939,6 @@ void APIConnection::process_batch_() {
for (size_t i = 0; i < num_items; i++) {
total_estimated_size += this->deferred_batch_[i].estimated_size;
}
// Clamp to MAX_BATCH_PACKET_SIZE — we won't send more than that per batch
if (total_estimated_size > MAX_BATCH_PACKET_SIZE) {
total_estimated_size = MAX_BATCH_PACKET_SIZE;
}
this->prepare_first_message_buffer(shared_buf, header_padding, total_estimated_size);
@@ -1972,20 +1962,7 @@ void APIConnection::process_batch_() {
return;
}
// Multi-message path — heavy stack frame isolated in separate noinline function
this->process_batch_multi_(shared_buf, num_items, header_padding, footer_size);
}
// Separated from process_batch_() so the single-message fast path gets a minimal
// stack frame without the MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo) array.
void APIConnection::process_batch_multi_(std::vector<uint8_t> &shared_buf, size_t num_items, uint8_t header_padding,
uint8_t footer_size) {
// Ensure MessageInfo remains trivially destructible for our placement new approach
static_assert(std::is_trivially_destructible<MessageInfo>::value,
"MessageInfo must remain trivially destructible with this placement-new approach");
const size_t messages_to_process = std::min(num_items, MAX_MESSAGES_PER_BATCH);
const uint8_t frame_overhead = header_padding + footer_size;
size_t messages_to_process = std::min(num_items, MAX_MESSAGES_PER_BATCH);
// Stack-allocated array for message info
alignas(MessageInfo) char message_info_storage[MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo)];
@@ -2012,7 +1989,7 @@ void APIConnection::process_batch_multi_(std::vector<uint8_t> &shared_buf, size_
// Message was encoded successfully
// payload_size is header_padding + actual payload size + footer_size
uint16_t proto_payload_size = payload_size - frame_overhead;
uint16_t proto_payload_size = payload_size - header_padding - footer_size;
// Use placement new to construct MessageInfo in pre-allocated stack array
// This avoids default-constructing all MAX_MESSAGES_PER_BATCH elements
// Explicit destruction is not needed because MessageInfo is trivially destructible,
@@ -2028,38 +2005,42 @@ void APIConnection::process_batch_multi_(std::vector<uint8_t> &shared_buf, size_
current_offset = shared_buf.size() + footer_size;
}
if (items_processed > 0) {
// Add footer space for the last message (for Noise protocol MAC)
if (footer_size > 0) {
shared_buf.resize(shared_buf.size() + footer_size);
}
// Send all collected messages
APIError err = this->helper_->write_protobuf_messages(ProtoWriteBuffer{&shared_buf},
std::span<const MessageInfo>(message_info, items_processed));
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
this->fatal_error_with_log_(LOG_STR("Batch write failed"), err);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
// Log messages after send attempt for VV debugging
// It's safe to use the buffer for logging at this point regardless of send result
for (size_t i = 0; i < items_processed; i++) {
const auto &item = this->deferred_batch_[i];
this->log_batch_item_(item);
}
#endif
// Partial batch — remove processed items and reschedule
if (items_processed < this->deferred_batch_.size()) {
this->deferred_batch_.remove_front(items_processed);
this->schedule_batch_();
return;
}
if (items_processed == 0) {
this->deferred_batch_.clear();
return;
}
// All items processed (or none could be processed)
this->clear_batch_();
// Add footer space for the last message (for Noise protocol MAC)
if (footer_size > 0) {
shared_buf.resize(shared_buf.size() + footer_size);
}
// Send all collected messages
APIError err = this->helper_->write_protobuf_messages(ProtoWriteBuffer{&shared_buf},
std::span<const MessageInfo>(message_info, items_processed));
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
this->fatal_error_with_log_(LOG_STR("Batch write failed"), err);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
// Log messages after send attempt for VV debugging
// It's safe to use the buffer for logging at this point regardless of send result
for (size_t i = 0; i < items_processed; i++) {
const auto &item = this->deferred_batch_[i];
this->log_batch_item_(item);
}
#endif
// Handle remaining items more efficiently
if (items_processed < this->deferred_batch_.size()) {
// Remove processed items from the beginning
this->deferred_batch_.remove_front(items_processed);
// Reschedule for remaining items
this->schedule_batch_();
} else {
// All items processed
this->clear_batch_();
}
}
// Dispatch message encoding based on message_type
@@ -2226,14 +2207,12 @@ void APIConnection::process_state_subscriptions_() {
#endif // USE_API_HOMEASSISTANT_STATES
void APIConnection::log_client_(int level, const LogString *message) {
char peername[socket::SOCKADDR_STR_LEN];
esp_log_printf_(level, TAG, __LINE__, ESPHOME_LOG_FORMAT("%s (%s): %s"), this->helper_->get_client_name(),
this->helper_->get_peername_to(peername), LOG_STR_ARG(message));
this->helper_->get_client_peername(), LOG_STR_ARG(message));
}
void APIConnection::log_warning_(const LogString *message, APIError err) {
char peername[socket::SOCKADDR_STR_LEN];
ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->helper_->get_client_name(), this->helper_->get_peername_to(peername),
ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->helper_->get_client_name(), this->helper_->get_client_peername(),
LOG_STR_ARG(message), LOG_STR_ARG(api_error_to_logstr(err)), errno);
}

View File

@@ -280,10 +280,8 @@ class APIConnection final : public APIServerConnectionBase {
bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override;
const char *get_name() const { return this->helper_->get_client_name(); }
/// Get peer name (IP address) into caller-provided buffer, returns buf for convenience
const char *get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const {
return this->helper_->get_peername_to(buf);
}
/// Get peer name (IP address) - cached at connection init time
const char *get_peername() const { return this->helper_->get_client_peername(); }
protected:
// Helper function to handle authentication completion
@@ -548,8 +546,8 @@ class APIConnection final : public APIServerConnectionBase {
batch_start_time = 0;
}
// Remove processed items from the front — noinline to keep memmove out of warm callers
void remove_front(size_t count) __attribute__((noinline)) { items.erase(items.begin(), items.begin() + count); }
// Remove processed items from the front
void remove_front(size_t count) { items.erase(items.begin(), items.begin() + count); }
bool empty() const { return items.empty(); }
size_t size() const { return items.size(); }
@@ -621,8 +619,6 @@ class APIConnection final : public APIServerConnectionBase {
bool schedule_batch_();
void process_batch_();
void process_batch_multi_(std::vector<uint8_t> &shared_buf, size_t num_items, uint8_t header_padding,
uint8_t footer_size) __attribute__((noinline));
void clear_batch_() {
this->deferred_batch_.clear();
this->flags_.batch_scheduled = false;

View File

@@ -16,12 +16,7 @@ static const char *const TAG = "api.frame_helper";
static constexpr size_t API_MAX_LOG_BYTES = 168;
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
#define HELPER_LOG(msg, ...) \
do { \
char peername_buf[socket::SOCKADDR_STR_LEN]; \
this->get_peername_to(peername_buf); \
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, peername_buf, ##__VA_ARGS__); \
} while (0)
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
#else
#define HELPER_LOG(msg, ...) ((void) 0)
#endif
@@ -245,20 +240,13 @@ APIError APIFrameHelper::try_send_tx_buf_() {
return APIError::OK; // All buffers sent successfully
}
const char *APIFrameHelper::get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const {
if (this->socket_) {
this->socket_->getpeername_to(buf);
} else {
buf[0] = '\0';
}
return buf.data();
}
APIError APIFrameHelper::init_common_() {
if (state_ != State::INITIALIZE || this->socket_ == nullptr) {
HELPER_LOG("Bad state for init %d", (int) state_);
return APIError::BAD_STATE;
}
// Cache peername now while socket is valid - needed for error logging after socket failure
this->socket_->getpeername_to(this->client_peername_);
int err = this->socket_->setblocking(false);
if (err != 0) {
state_ = State::FAILED;

View File

@@ -90,9 +90,8 @@ class APIFrameHelper {
// Get client name (null-terminated)
const char *get_client_name() const { return this->client_name_; }
// Get client peername/IP into caller-provided buffer (fetches on-demand from socket)
// Returns pointer to buf for convenience in printf-style calls
const char *get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const;
// Get client peername/IP (null-terminated, cached at init time for availability after socket failure)
const char *get_client_peername() const { return this->client_peername_; }
// Set client name from buffer with length (truncates if needed)
void set_client_name(const char *name, size_t len) {
size_t copy_len = std::min(len, sizeof(this->client_name_) - 1);
@@ -106,8 +105,6 @@ class APIFrameHelper {
bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 0; }
int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); }
APIError close() {
if (state_ == State::CLOSED)
return APIError::OK; // Already closed
state_ = State::CLOSED;
int err = this->socket_->close();
if (err == -1)
@@ -234,6 +231,8 @@ class APIFrameHelper {
// Client name buffer - stores name from Hello message or initial peername
char client_name_[CLIENT_INFO_NAME_MAX_LEN]{};
// Cached peername/IP address - captured at init time for availability after socket failure
char client_peername_[socket::SOCKADDR_STR_LEN]{};
// Group smaller types together
uint16_t rx_buf_len_ = 0;

View File

@@ -29,12 +29,7 @@ static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit")
static constexpr size_t API_MAX_LOG_BYTES = 168;
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
#define HELPER_LOG(msg, ...) \
do { \
char peername_buf[socket::SOCKADDR_STR_LEN]; \
this->get_peername_to(peername_buf); \
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, peername_buf, ##__VA_ARGS__); \
} while (0)
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
#else
#define HELPER_LOG(msg, ...) ((void) 0)
#endif

View File

@@ -21,12 +21,7 @@ static const char *const TAG = "api.plaintext";
static constexpr size_t API_MAX_LOG_BYTES = 168;
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
#define HELPER_LOG(msg, ...) \
do { \
char peername_buf[socket::SOCKADDR_STR_LEN]; \
this->get_peername_to(peername_buf); \
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, peername_buf, ##__VA_ARGS__); \
} while (0)
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
#else
#define HELPER_LOG(msg, ...) ((void) 0)
#endif

View File

@@ -147,8 +147,6 @@ enum WaterHeaterCommandHasField : uint32_t {
WATER_HEATER_COMMAND_HAS_STATE = 4,
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8,
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH = 16,
WATER_HEATER_COMMAND_HAS_ON_STATE = 32,
WATER_HEATER_COMMAND_HAS_AWAY_STATE = 64,
};
#ifdef USE_NUMBER
enum NumberMode : uint32_t {

View File

@@ -385,10 +385,6 @@ const char *proto_enum_to_string<enums::WaterHeaterCommandHasField>(enums::Water
return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW";
case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH:
return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH";
case enums::WATER_HEATER_COMMAND_HAS_ON_STATE:
return "WATER_HEATER_COMMAND_HAS_ON_STATE";
case enums::WATER_HEATER_COMMAND_HAS_AWAY_STATE:
return "WATER_HEATER_COMMAND_HAS_AWAY_STATE";
default:
return "UNKNOWN";
}

View File

@@ -117,7 +117,37 @@ void APIServer::setup() {
void APIServer::loop() {
// Accept new clients only if the socket exists and has incoming connections
if (this->socket_ && this->socket_->ready()) {
this->accept_new_connections_();
while (true) {
struct sockaddr_storage source_addr;
socklen_t addr_len = sizeof(source_addr);
auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len);
if (!sock)
break;
char peername[socket::SOCKADDR_STR_LEN];
sock->getpeername_to(peername);
// Check if we're at the connection limit
if (this->clients_.size() >= this->max_connections_) {
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, peername);
// Immediately close - socket destructor will handle cleanup
sock.reset();
continue;
}
ESP_LOGD(TAG, "Accept %s", peername);
auto *conn = new APIConnection(std::move(sock), this);
this->clients_.emplace_back(conn);
conn->start();
// First client connected - clear warning and update timestamp
if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) {
this->status_clear_warning();
this->last_connected_ = App.get_loop_component_start_time();
}
}
}
if (this->clients_.empty()) {
@@ -148,84 +178,42 @@ void APIServer::loop() {
while (client_index < this->clients_.size()) {
auto &client = this->clients_[client_index];
if (client->flags_.remove) {
// Rare case: handle disconnection (don't increment - swapped element needs processing)
this->remove_client_(client_index);
} else {
if (!client->flags_.remove) {
// Common case: process active client
client->loop();
client_index++;
}
}
}
void APIServer::remove_client_(size_t client_index) {
auto &client = this->clients_[client_index];
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
this->unregister_active_action_calls_for_connection(client.get());
#endif
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
// Save client info before closing socket and removal for the trigger
char peername_buf[socket::SOCKADDR_STR_LEN];
std::string client_name(client->get_name());
std::string client_peername(client->get_peername_to(peername_buf));
#endif
// Close socket now (was deferred from on_fatal_error to allow getpeername)
client->helper_->close();
// Swap with the last element and pop (avoids expensive vector shifts)
if (client_index < this->clients_.size() - 1) {
std::swap(this->clients_[client_index], this->clients_.back());
}
this->clients_.pop_back();
// Last client disconnected - set warning and start tracking for reboot timeout
if (this->clients_.empty() && this->reboot_timeout_ != 0) {
this->status_set_warning();
this->last_connected_ = App.get_loop_component_start_time();
}
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
// Fire trigger after client is removed so api.connected reflects the true state
this->client_disconnected_trigger_.trigger(client_name, client_peername);
#endif
}
void APIServer::accept_new_connections_() {
while (true) {
struct sockaddr_storage source_addr;
socklen_t addr_len = sizeof(source_addr);
auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len);
if (!sock)
break;
char peername[socket::SOCKADDR_STR_LEN];
sock->getpeername_to(peername);
// Check if we're at the connection limit
if (this->clients_.size() >= this->max_connections_) {
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, peername);
// Immediately close - socket destructor will handle cleanup
sock.reset();
continue;
}
ESP_LOGD(TAG, "Accept %s", peername);
// Rare case: handle disconnection
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
this->unregister_active_action_calls_for_connection(client.get());
#endif
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
auto *conn = new APIConnection(std::move(sock), this);
this->clients_.emplace_back(conn);
conn->start();
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
// Save client info before removal for the trigger
std::string client_name(client->get_name());
std::string client_peername(client->get_peername());
#endif
// First client connected - clear warning and update timestamp
if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) {
this->status_clear_warning();
// Swap with the last element and pop (avoids expensive vector shifts)
if (client_index < this->clients_.size() - 1) {
std::swap(this->clients_[client_index], this->clients_.back());
}
this->clients_.pop_back();
// Last client disconnected - set warning and start tracking for reboot timeout
if (this->clients_.empty() && this->reboot_timeout_ != 0) {
this->status_set_warning();
this->last_connected_ = App.get_loop_component_start_time();
}
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
// Fire trigger after client is removed so api.connected reflects the true state
this->client_disconnected_trigger_.trigger(client_name, client_peername);
#endif
// Don't increment client_index since we need to process the swapped element
}
}

View File

@@ -234,11 +234,6 @@ class APIServer : public Component,
#endif
protected:
// Accept incoming socket connections. Only called when socket has pending connections.
void __attribute__((noinline)) accept_new_connections_();
// Remove a disconnected client by index. Swaps with last element and pops.
void __attribute__((noinline)) remove_client_(size_t client_index);
#ifdef USE_API_NOISE
bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg,
const psk_t &active_psk, bool make_active);

View File

@@ -128,20 +128,6 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
this->add_kv_(this->variables_, key, std::forward<V>(value));
}
#ifdef USE_ESP8266
// On ESP8266, ESPHOME_F() returns __FlashStringHelper* (PROGMEM pointer).
// Store as const char* — populate_service_map copies from PROGMEM at play() time.
template<typename V> void add_data(const __FlashStringHelper *key, V &&value) {
this->add_kv_(this->data_, reinterpret_cast<const char *>(key), std::forward<V>(value));
}
template<typename V> void add_data_template(const __FlashStringHelper *key, V &&value) {
this->add_kv_(this->data_template_, reinterpret_cast<const char *>(key), std::forward<V>(value));
}
template<typename V> void add_variable(const __FlashStringHelper *key, V &&value) {
this->add_kv_(this->variables_, reinterpret_cast<const char *>(key), std::forward<V>(value));
}
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
template<typename T> void set_response_template(T response_template) {
this->response_template_ = response_template;
@@ -233,31 +219,7 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
Ts... x) {
dest.init(source.size());
#ifdef USE_ESP8266
// On ESP8266, keys may be in PROGMEM (from ESPHOME_F in codegen) and
// FLASH_STRING values need copying via _P functions.
// Allocate storage for all keys + all values (2 entries per source item).
// strlen_P/memcpy_P handle both RAM and PROGMEM pointers safely.
value_storage.init(source.size() * 2);
for (auto &it : source) {
auto &kv = dest.emplace_back();
// Key: copy from possible PROGMEM
{
size_t key_len = strlen_P(it.key);
value_storage.push_back(std::string(key_len, '\0'));
memcpy_P(value_storage.back().data(), it.key, key_len);
kv.key = StringRef(value_storage.back());
}
// Value: value() handles FLASH_STRING via _P functions internally
value_storage.push_back(it.value.value(x...));
kv.value = StringRef(value_storage.back());
}
#else
// On non-ESP8266, strings are directly readable from flash-mapped memory.
// Count non-static strings to allocate exact storage needed.
// Count non-static strings to allocate exact storage needed
size_t lambda_count = 0;
for (const auto &it : source) {
if (!it.value.is_static_string()) {
@@ -271,15 +233,14 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
kv.key = StringRef(it.key);
if (it.value.is_static_string()) {
// Static string — pointer directly readable, zero allocation
// Static string from YAML - zero allocation
kv.value = StringRef(it.value.get_static_string());
} else {
// Lambda evaluate and store result
// Lambda evaluation - store result, reference it
value_storage.push_back(it.value.value(x...));
kv.value = StringRef(value_storage.back());
}
}
#endif
}
APIServer *parent_;

View File

@@ -264,9 +264,9 @@ template<typename... Ts> class APIRespondAction : public Action<Ts...> {
// Build and send JSON response
json::JsonBuilder builder;
this->json_builder_(x..., builder.root());
auto json_buf = builder.serialize();
std::string json_str = builder.serialize();
this->parent_->send_action_response(call_id, success, StringRef(error_message),
reinterpret_cast<const uint8_t *>(json_buf.data()), json_buf.size());
reinterpret_cast<const uint8_t *>(json_str.data()), json_str.size());
return;
}
#endif

View File

@@ -3,7 +3,6 @@
#include "bedjet_hub.h"
#include "bedjet_child.h"
#include "bedjet_const.h"
#include "esphome/components/esp32_ble/ble_uuid.h"
#include "esphome/core/application.h"
#include <cinttypes>

View File

@@ -159,10 +159,6 @@ BK72XX_BOARD_PINS = {
"A0": 23,
},
"cbu": {
"SPI0_CS": 15,
"SPI0_MISO": 17,
"SPI0_MOSI": 16,
"SPI0_SCK": 14,
"WIRE1_SCL": 20,
"WIRE1_SDA": 21,
"WIRE2_SCL": 0,
@@ -231,10 +227,6 @@ BK72XX_BOARD_PINS = {
"A0": 23,
},
"generic-bk7231t-qfn32-tuya": {
"SPI0_CS": 15,
"SPI0_MISO": 17,
"SPI0_MOSI": 16,
"SPI0_SCK": 14,
"WIRE1_SCL": 20,
"WIRE1_SDA": 21,
"WIRE2_SCL": 0,
@@ -303,10 +295,6 @@ BK72XX_BOARD_PINS = {
"A0": 23,
},
"generic-bk7231n-qfn32-tuya": {
"SPI0_CS": 15,
"SPI0_MISO": 17,
"SPI0_MOSI": 16,
"SPI0_SCK": 14,
"WIRE1_SCL": 20,
"WIRE1_SDA": 21,
"WIRE2_SCL": 0,
@@ -497,7 +485,8 @@ BK72XX_BOARD_PINS = {
},
"cb3s": {
"WIRE1_SCL": 20,
"WIRE1_SDA": 21,
"WIRE1_SDA_0": 21,
"WIRE1_SDA_1": 21,
"SERIAL1_RX": 10,
"SERIAL1_TX": 11,
"SERIAL2_TX": 0,
@@ -658,10 +647,6 @@ BK72XX_BOARD_PINS = {
"A0": 23,
},
"generic-bk7252": {
"SPI0_CS": 15,
"SPI0_MISO": 17,
"SPI0_MOSI": 16,
"SPI0_SCK": 14,
"WIRE1_SCL": 20,
"WIRE1_SDA": 21,
"WIRE2_SCL": 0,
@@ -1111,10 +1096,6 @@ BK72XX_BOARD_PINS = {
"A0": 23,
},
"cb3se": {
"SPI0_CS": 15,
"SPI0_MISO": 17,
"SPI0_MOSI": 16,
"SPI0_SCK": 14,
"WIRE2_SCL": 0,
"WIRE2_SDA": 1,
"SERIAL1_RX": 10,

View File

@@ -6,9 +6,8 @@
*/
#include "bmp3xx_base.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include "esphome/core/hal.h"
#include <cinttypes>
namespace esphome {
@@ -27,18 +26,46 @@ static const LogString *chip_type_to_str(uint8_t chip_type) {
}
}
// Oversampling strings indexed by Oversampling enum (0-5): NONE, X2, X4, X8, X16, X32
PROGMEM_STRING_TABLE(OversamplingStrings, "None", "2x", "4x", "8x", "16x", "32x", "");
static const LogString *oversampling_to_str(Oversampling oversampling) {
return OversamplingStrings::get_log_str(static_cast<uint8_t>(oversampling), OversamplingStrings::LAST_INDEX);
switch (oversampling) {
case Oversampling::OVERSAMPLING_NONE:
return LOG_STR("None");
case Oversampling::OVERSAMPLING_X2:
return LOG_STR("2x");
case Oversampling::OVERSAMPLING_X4:
return LOG_STR("4x");
case Oversampling::OVERSAMPLING_X8:
return LOG_STR("8x");
case Oversampling::OVERSAMPLING_X16:
return LOG_STR("16x");
case Oversampling::OVERSAMPLING_X32:
return LOG_STR("32x");
default:
return LOG_STR("");
}
}
// IIR filter strings indexed by IIRFilter enum (0-7): OFF, 2, 4, 8, 16, 32, 64, 128
PROGMEM_STRING_TABLE(IIRFilterStrings, "OFF", "2x", "4x", "8x", "16x", "32x", "64x", "128x", "");
static const LogString *iir_filter_to_str(IIRFilter filter) {
return IIRFilterStrings::get_log_str(static_cast<uint8_t>(filter), IIRFilterStrings::LAST_INDEX);
switch (filter) {
case IIRFilter::IIR_FILTER_OFF:
return LOG_STR("OFF");
case IIRFilter::IIR_FILTER_2:
return LOG_STR("2x");
case IIRFilter::IIR_FILTER_4:
return LOG_STR("4x");
case IIRFilter::IIR_FILTER_8:
return LOG_STR("8x");
case IIRFilter::IIR_FILTER_16:
return LOG_STR("16x");
case IIRFilter::IIR_FILTER_32:
return LOG_STR("32x");
case IIRFilter::IIR_FILTER_64:
return LOG_STR("64x");
case IIRFilter::IIR_FILTER_128:
return LOG_STR("128x");
default:
return LOG_STR("");
}
}
void BMP3XXComponent::setup() {

View File

@@ -11,26 +11,57 @@
*/
#include "bmp581_base.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include "esphome/core/hal.h"
namespace esphome::bmp581_base {
static const char *const TAG = "bmp581";
// Oversampling strings indexed by Oversampling enum (0-7): NONE, X2, X4, X8, X16, X32, X64, X128
PROGMEM_STRING_TABLE(OversamplingStrings, "None", "2x", "4x", "8x", "16x", "32x", "64x", "128x", "");
static const LogString *oversampling_to_str(Oversampling oversampling) {
return OversamplingStrings::get_log_str(static_cast<uint8_t>(oversampling), OversamplingStrings::LAST_INDEX);
switch (oversampling) {
case Oversampling::OVERSAMPLING_NONE:
return LOG_STR("None");
case Oversampling::OVERSAMPLING_X2:
return LOG_STR("2x");
case Oversampling::OVERSAMPLING_X4:
return LOG_STR("4x");
case Oversampling::OVERSAMPLING_X8:
return LOG_STR("8x");
case Oversampling::OVERSAMPLING_X16:
return LOG_STR("16x");
case Oversampling::OVERSAMPLING_X32:
return LOG_STR("32x");
case Oversampling::OVERSAMPLING_X64:
return LOG_STR("64x");
case Oversampling::OVERSAMPLING_X128:
return LOG_STR("128x");
default:
return LOG_STR("");
}
}
// IIR filter strings indexed by IIRFilter enum (0-7): OFF, 2, 4, 8, 16, 32, 64, 128
PROGMEM_STRING_TABLE(IIRFilterStrings, "OFF", "2x", "4x", "8x", "16x", "32x", "64x", "128x", "");
static const LogString *iir_filter_to_str(IIRFilter filter) {
return IIRFilterStrings::get_log_str(static_cast<uint8_t>(filter), IIRFilterStrings::LAST_INDEX);
switch (filter) {
case IIRFilter::IIR_FILTER_OFF:
return LOG_STR("OFF");
case IIRFilter::IIR_FILTER_2:
return LOG_STR("2x");
case IIRFilter::IIR_FILTER_4:
return LOG_STR("4x");
case IIRFilter::IIR_FILTER_8:
return LOG_STR("8x");
case IIRFilter::IIR_FILTER_16:
return LOG_STR("16x");
case IIRFilter::IIR_FILTER_32:
return LOG_STR("32x");
case IIRFilter::IIR_FILTER_64:
return LOG_STR("64x");
case IIRFilter::IIR_FILTER_128:
return LOG_STR("128x");
default:
return LOG_STR("");
}
}
void BMP581Component::dump_config() {

View File

@@ -1,7 +1,6 @@
#include "debug_component.h"
#ifdef USE_ESP8266
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include <Esp.h>
extern "C" {
@@ -20,31 +19,27 @@ namespace debug {
static const char *const TAG = "debug";
// PROGMEM string table for reset reasons, indexed by reason code (0-6), with "Unknown" as fallback
// clang-format off
PROGMEM_STRING_TABLE(ResetReasonStrings,
"Power On", // 0 = REASON_DEFAULT_RST
"Hardware Watchdog", // 1 = REASON_WDT_RST
"Exception", // 2 = REASON_EXCEPTION_RST
"Software Watchdog", // 3 = REASON_SOFT_WDT_RST
"Software/System restart", // 4 = REASON_SOFT_RESTART
"Deep-Sleep Wake", // 5 = REASON_DEEP_SLEEP_AWAKE
"External System", // 6 = REASON_EXT_SYS_RST
"Unknown" // 7 = fallback
);
// clang-format on
static_assert(REASON_DEFAULT_RST == 0, "Reset reason enum values must match table indices");
static_assert(REASON_EXT_SYS_RST == 6, "Reset reason enum values must match table indices");
// PROGMEM string table for flash chip modes, indexed by mode code (0-3), with "UNKNOWN" as fallback
PROGMEM_STRING_TABLE(FlashModeStrings, "QIO", "QOUT", "DIO", "DOUT", "UNKNOWN");
static_assert(FM_QIO == 0, "Flash mode enum values must match table indices");
static_assert(FM_DOUT == 3, "Flash mode enum values must match table indices");
// Get reset reason string from reason code (no heap allocation)
// Returns LogString* pointing to flash (PROGMEM) on ESP8266
static const LogString *get_reset_reason_str(uint32_t reason) {
return ResetReasonStrings::get_log_str(reason, ResetReasonStrings::LAST_INDEX);
switch (reason) {
case REASON_DEFAULT_RST:
return LOG_STR("Power On");
case REASON_WDT_RST:
return LOG_STR("Hardware Watchdog");
case REASON_EXCEPTION_RST:
return LOG_STR("Exception");
case REASON_SOFT_WDT_RST:
return LOG_STR("Software Watchdog");
case REASON_SOFT_RESTART:
return LOG_STR("Software/System restart");
case REASON_DEEP_SLEEP_AWAKE:
return LOG_STR("Deep-Sleep Wake");
case REASON_EXT_SYS_RST:
return LOG_STR("External System");
default:
return LOG_STR("Unknown");
}
}
// Size for core version hex buffer
@@ -97,8 +92,23 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
constexpr size_t size = DEVICE_INFO_BUFFER_SIZE;
char *buf = buffer.data();
// NOLINTNEXTLINE(readability-static-accessed-through-instance)
const LogString *flash_mode = FlashModeStrings::get_log_str(ESP.getFlashChipMode(), FlashModeStrings::LAST_INDEX);
const LogString *flash_mode;
switch (ESP.getFlashChipMode()) { // NOLINT(readability-static-accessed-through-instance)
case FM_QIO:
flash_mode = LOG_STR("QIO");
break;
case FM_QOUT:
flash_mode = LOG_STR("QOUT");
break;
case FM_DIO:
flash_mode = LOG_STR("DIO");
break;
case FM_DOUT:
flash_mode = LOG_STR("DOUT");
break;
default:
flash_mode = LOG_STR("UNKNOWN");
}
uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT(readability-static-accessed-through-instance)
uint32_t flash_speed = ESP.getFlashChipSpeed() / 1000000; // NOLINT(readability-static-accessed-through-instance)
ESP_LOGD(TAG, "Flash Chip: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed,

View File

@@ -63,13 +63,11 @@ def validate_auto_clear(value):
return cv.boolean(value)
def basic_display_schema(default_update_interval: str = "1s") -> cv.Schema:
"""Create a basic display schema with configurable default update interval."""
return cv.Schema(
{
cv.Exclusive(CONF_LAMBDA, CONF_LAMBDA): cv.lambda_,
}
).extend(cv.polling_component_schema(default_update_interval))
BASIC_DISPLAY_SCHEMA = cv.Schema(
{
cv.Exclusive(CONF_LAMBDA, CONF_LAMBDA): cv.lambda_,
}
).extend(cv.polling_component_schema("1s"))
def _validate_test_card(config):
@@ -83,41 +81,34 @@ def _validate_test_card(config):
return config
def full_display_schema(default_update_interval: str = "1s") -> cv.Schema:
"""Create a full display schema with configurable default update interval."""
schema = basic_display_schema(default_update_interval).extend(
{
cv.Optional(CONF_ROTATION): validate_rotation,
cv.Exclusive(CONF_PAGES, CONF_LAMBDA): cv.All(
cv.ensure_list(
{
cv.GenerateID(): cv.declare_id(DisplayPage),
cv.Required(CONF_LAMBDA): cv.lambda_,
}
),
cv.Length(min=1),
),
cv.Optional(CONF_ON_PAGE_CHANGE): automation.validate_automation(
FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend(
{
cv.Optional(CONF_ROTATION): validate_rotation,
cv.Exclusive(CONF_PAGES, CONF_LAMBDA): cv.All(
cv.ensure_list(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DisplayOnPageChangeTrigger
),
cv.Optional(CONF_FROM): cv.use_id(DisplayPage),
cv.Optional(CONF_TO): cv.use_id(DisplayPage),
cv.GenerateID(): cv.declare_id(DisplayPage),
cv.Required(CONF_LAMBDA): cv.lambda_,
}
),
cv.Optional(
CONF_AUTO_CLEAR_ENABLED, default=CONF_UNSPECIFIED
): validate_auto_clear,
cv.Optional(CONF_SHOW_TEST_CARD): cv.boolean,
}
)
schema.add_extra(_validate_test_card)
return schema
BASIC_DISPLAY_SCHEMA = basic_display_schema("1s")
FULL_DISPLAY_SCHEMA = full_display_schema("1s")
cv.Length(min=1),
),
cv.Optional(CONF_ON_PAGE_CHANGE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DisplayOnPageChangeTrigger
),
cv.Optional(CONF_FROM): cv.use_id(DisplayPage),
cv.Optional(CONF_TO): cv.use_id(DisplayPage),
}
),
cv.Optional(
CONF_AUTO_CLEAR_ENABLED, default=CONF_UNSPECIFIED
): validate_auto_clear,
cv.Optional(CONF_SHOW_TEST_CARD): cv.boolean,
}
)
FULL_DISPLAY_SCHEMA.add_extra(_validate_test_card)
async def setup_display_core_(var, config):

View File

@@ -135,7 +135,6 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = (
"esp_driver_dac", # DAC driver - only needed by esp32_dac component
"esp_driver_i2s", # I2S driver - only needed by i2s_audio component
"esp_driver_mcpwm", # MCPWM driver - ESPHome doesn't use motor control PWM
"esp_driver_pcnt", # PCNT driver - only needed by pulse_counter, hlw8012 components
"esp_driver_rmt", # RMT driver - only needed by remote_transmitter/receiver, neopixelbus
"esp_driver_touch_sens", # Touch sensor driver - only needed by esp32_touch
"esp_driver_twai", # TWAI/CAN driver - only needed by esp32_can component

View File

@@ -85,6 +85,7 @@ void ESP32InternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpi
break;
}
gpio_set_intr_type(this->get_pin_num(), idf_type);
gpio_intr_enable(this->get_pin_num());
if (!isr_service_installed) {
auto res = gpio_install_isr_service(ESP_INTR_FLAG_LEVEL3);
if (res != ESP_OK) {
@@ -94,7 +95,6 @@ void ESP32InternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpi
isr_service_installed = true;
}
gpio_isr_handler_add(this->get_pin_num(), func, arg);
gpio_intr_enable(this->get_pin_num());
}
size_t ESP32InternalGPIOPin::dump_summary(char *buffer, size_t len) const {

View File

@@ -19,7 +19,16 @@ static constexpr size_t KEY_BUFFER_SIZE = 12;
struct NVSData {
uint32_t key;
SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.)
std::unique_ptr<uint8_t[]> data;
size_t len;
void set_data(const uint8_t *src, size_t size) {
if (!this->data || this->len != size) {
this->data = std::make_unique<uint8_t[]>(size);
this->len = size;
}
memcpy(this->data.get(), src, size);
}
};
static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@@ -32,14 +41,14 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend {
// try find in pending saves and update that
for (auto &obj : s_pending_save) {
if (obj.key == this->key) {
obj.data.set(data, len);
obj.set_data(data, len);
return true;
}
}
NVSData save{};
save.key = this->key;
save.data.set(data, len);
s_pending_save.push_back(std::move(save));
save.set_data(data, len);
s_pending_save.emplace_back(std::move(save));
ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len);
return true;
}
@@ -47,11 +56,11 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend {
// try find in pending saves and load from that
for (auto &obj : s_pending_save) {
if (obj.key == this->key) {
if (obj.data.size() != len) {
if (obj.len != len) {
// size mismatch
return false;
}
memcpy(data, obj.data.data(), len);
memcpy(data, obj.data.get(), len);
return true;
}
}
@@ -127,10 +136,10 @@ class ESP32Preferences : public ESPPreferences {
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str);
if (this->is_changed_(this->nvs_handle, save, key_str)) {
esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.data(), save.data.size());
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size());
esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.get(), save.len);
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.len);
if (err != 0) {
ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.data.size(), esp_err_to_name(err));
ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.len, esp_err_to_name(err));
failed++;
last_err = err;
last_key = save.key;
@@ -138,7 +147,7 @@ class ESP32Preferences : public ESPPreferences {
}
written++;
} else {
ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.data.size());
ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.len);
cached++;
}
s_pending_save.erase(s_pending_save.begin() + i);
@@ -169,7 +178,7 @@ class ESP32Preferences : public ESPPreferences {
return true;
}
// Check size first before allocating memory
if (actual_len != to_save.data.size()) {
if (actual_len != to_save.len) {
return true;
}
// Most preferences are small, use stack buffer with heap fallback for large ones
@@ -179,7 +188,7 @@ class ESP32Preferences : public ESPPreferences {
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err));
return true;
}
return memcmp(to_save.data.data(), stored_data.get(), to_save.data.size()) != 0;
return memcmp(to_save.data.get(), stored_data.get(), to_save.len) != 0;
}
bool reset() override {

View File

@@ -98,10 +98,6 @@ void ESP32BLE::advertising_set_service_data(const std::vector<uint8_t> &data) {
}
void ESP32BLE::advertising_set_manufacturer_data(const std::vector<uint8_t> &data) {
this->advertising_set_manufacturer_data(std::span<const uint8_t>(data));
}
void ESP32BLE::advertising_set_manufacturer_data(std::span<const uint8_t> data) {
this->advertising_init_();
this->advertising_->set_manufacturer_data(data);
this->advertising_start();
@@ -373,9 +369,42 @@ bool ESP32BLE::ble_dismantle_() {
}
void ESP32BLE::loop() {
if (this->state_ != BLE_COMPONENT_STATE_ACTIVE) {
this->loop_handle_state_transition_not_active_();
return;
switch (this->state_) {
case BLE_COMPONENT_STATE_OFF:
case BLE_COMPONENT_STATE_DISABLED:
return;
case BLE_COMPONENT_STATE_DISABLE: {
ESP_LOGD(TAG, "Disabling");
#ifdef ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT
for (auto *ble_event_handler : this->ble_status_event_handlers_) {
ble_event_handler->ble_before_disabled_event_handler();
}
#endif
if (!ble_dismantle_()) {
ESP_LOGE(TAG, "Could not be dismantled");
this->mark_failed();
return;
}
this->state_ = BLE_COMPONENT_STATE_DISABLED;
return;
}
case BLE_COMPONENT_STATE_ENABLE: {
ESP_LOGD(TAG, "Enabling");
this->state_ = BLE_COMPONENT_STATE_OFF;
if (!ble_setup_()) {
ESP_LOGE(TAG, "Could not be set up");
this->mark_failed();
return;
}
this->state_ = BLE_COMPONENT_STATE_ACTIVE;
return;
}
case BLE_COMPONENT_STATE_ACTIVE:
break;
}
BLEEvent *ble_event = this->ble_events_.pop();
@@ -491,37 +520,6 @@ void ESP32BLE::loop() {
}
}
void ESP32BLE::loop_handle_state_transition_not_active_() {
// Caller ensures state_ != ACTIVE
if (this->state_ == BLE_COMPONENT_STATE_DISABLE) {
ESP_LOGD(TAG, "Disabling");
#ifdef ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT
for (auto *ble_event_handler : this->ble_status_event_handlers_) {
ble_event_handler->ble_before_disabled_event_handler();
}
#endif
if (!ble_dismantle_()) {
ESP_LOGE(TAG, "Could not be dismantled");
this->mark_failed();
return;
}
this->state_ = BLE_COMPONENT_STATE_DISABLED;
} else if (this->state_ == BLE_COMPONENT_STATE_ENABLE) {
ESP_LOGD(TAG, "Enabling");
this->state_ = BLE_COMPONENT_STATE_OFF;
if (!ble_setup_()) {
ESP_LOGE(TAG, "Could not be set up");
this->mark_failed();
return;
}
this->state_ = BLE_COMPONENT_STATE_ACTIVE;
}
}
// Helper function to load new event data based on type
void load_ble_event(BLEEvent *event, esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
event->load_gap_event(e, p);

View File

@@ -118,7 +118,6 @@ class ESP32BLE : public Component {
void advertising_start();
void advertising_set_service_data(const std::vector<uint8_t> &data);
void advertising_set_manufacturer_data(const std::vector<uint8_t> &data);
void advertising_set_manufacturer_data(std::span<const uint8_t> data);
void advertising_set_appearance(uint16_t appearance) { this->appearance_ = appearance; }
void advertising_set_service_data_and_name(std::span<const uint8_t> data, bool include_name);
void advertising_add_service_uuid(ESPBTUUID uuid);
@@ -156,10 +155,6 @@ class ESP32BLE : public Component {
#endif
static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param);
// Handle non-ACTIVE state transitions (DISABLE, ENABLE, OFF, DISABLED).
// Extracted from loop() to keep the hot event-processing path small.
void __attribute__((noinline)) loop_handle_state_transition_not_active_();
bool ble_setup_();
bool ble_dismantle_();
bool ble_pre_setup_();

View File

@@ -59,10 +59,6 @@ void BLEAdvertising::set_service_data(const std::vector<uint8_t> &data) {
}
void BLEAdvertising::set_manufacturer_data(const std::vector<uint8_t> &data) {
this->set_manufacturer_data(std::span<const uint8_t>(data));
}
void BLEAdvertising::set_manufacturer_data(std::span<const uint8_t> data) {
delete[] this->advertising_data_.p_manufacturer_data;
this->advertising_data_.p_manufacturer_data = nullptr;
this->advertising_data_.manufacturer_len = data.size();

View File

@@ -28,7 +28,6 @@ class BLEAdvertising {
void set_scan_response(bool scan_response) { this->scan_response_ = scan_response; }
void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; }
void set_manufacturer_data(const std::vector<uint8_t> &data);
void set_manufacturer_data(std::span<const uint8_t> data);
void set_appearance(uint16_t appearance) { this->advertising_data_.appearance = appearance; }
void set_service_data(const std::vector<uint8_t> &data);
void set_service_data(std::span<const uint8_t> data);

View File

@@ -1,6 +1,5 @@
#include "esp32_ble_beacon.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#ifdef USE_ESP32

View File

@@ -15,10 +15,7 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_characteristic_on_w
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
new Trigger<std::vector<uint8_t>, uint16_t>();
characteristic->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
// Convert span to vector for trigger - copy is necessary because:
// 1. Trigger stores the data for use in automation actions that execute later
// 2. The span is only valid during this callback (points to temporary BLE stack data)
// 3. User lambdas in automations need persistent data they can access asynchronously
// Convert span to vector for trigger
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
});
return on_write_trigger;
@@ -30,10 +27,7 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_descriptor_on_write
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
new Trigger<std::vector<uint8_t>, uint16_t>();
descriptor->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
// Convert span to vector for trigger - copy is necessary because:
// 1. Trigger stores the data for use in automation actions that execute later
// 2. The span is only valid during this callback (points to temporary BLE stack data)
// 3. User lambdas in automations need persistent data they can access asynchronously
// Convert span to vector for trigger
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
});
return on_write_trigger;

View File

@@ -811,8 +811,8 @@ void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) {
ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed");
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
char hex_buf[format_hex_pretty_size(PHY_REG_SIZE)];
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE));
#endif
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE));
/*
* Bit 7 is `RMII Reference Clock Select`. Default is `0`.
@@ -829,10 +829,8 @@ void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) {
ESPHL_ERROR_CHECK(err, "Write PHY Control 2 failed");
err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2));
ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed");
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s",
format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE));
#endif
}
}
#endif // USE_ETHERNET_KSZ8081

View File

@@ -68,7 +68,7 @@ void FanCall::validate_() {
auto traits = this->parent_.get_traits();
if (this->speed_.has_value()) {
this->speed_ = clamp(*this->speed_, 1, static_cast<int>(traits.supported_speed_count()));
this->speed_ = clamp(*this->speed_, 1, traits.supported_speed_count());
// https://developers.home-assistant.io/docs/core/entity/fan/#preset-modes
// "Manually setting a speed must disable any set preset mode"

View File

@@ -11,7 +11,7 @@ namespace fan {
class FanTraits {
public:
FanTraits() = default;
FanTraits(bool oscillation, bool speed, bool direction, uint8_t speed_count)
FanTraits(bool oscillation, bool speed, bool direction, int speed_count)
: oscillation_(oscillation), speed_(speed), direction_(direction), speed_count_(speed_count) {}
/// Return if this fan supports oscillation.
@@ -23,9 +23,9 @@ class FanTraits {
/// Set whether this fan supports speed levels.
void set_speed(bool speed) { this->speed_ = speed; }
/// Return how many speed levels the fan has
uint8_t supported_speed_count() const { return this->speed_count_; }
int supported_speed_count() const { return this->speed_count_; }
/// Set how many speed levels this fan has.
void set_supported_speed_count(uint8_t speed_count) { this->speed_count_ = speed_count; }
void set_supported_speed_count(int speed_count) { this->speed_count_ = speed_count; }
/// Return if this fan supports changing direction
bool supports_direction() const { return this->direction_; }
/// Set whether this fan supports changing direction
@@ -64,7 +64,7 @@ class FanTraits {
bool oscillation_{false};
bool speed_{false};
bool direction_{false};
uint8_t speed_count_{};
int speed_count_{};
std::vector<const char *> preset_modes_{};
};

View File

@@ -39,7 +39,7 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_DECAY_MODE, default="SLOW"): cv.enum(
DECAY_MODE_OPTIONS, upper=True
),
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1, max=255),
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1),
cv.Optional(CONF_ENABLE_PIN): cv.use_id(output.FloatOutput),
cv.Optional(CONF_PRESET_MODES): validate_preset_modes,
}

View File

@@ -15,7 +15,7 @@ enum DecayMode {
class HBridgeFan : public Component, public fan::Fan {
public:
HBridgeFan(uint8_t speed_count, DecayMode decay_mode) : speed_count_(speed_count), decay_mode_(decay_mode) {}
HBridgeFan(int speed_count, DecayMode decay_mode) : speed_count_(speed_count), decay_mode_(decay_mode) {}
void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; }
void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; }
@@ -33,7 +33,7 @@ class HBridgeFan : public Component, public fan::Fan {
output::FloatOutput *pin_b_;
output::FloatOutput *enable_{nullptr};
output::BinaryOutput *oscillating_{nullptr};
uint8_t speed_count_{};
int speed_count_{};
DecayMode decay_mode_{DECAY_MODE_SLOW};
fan::FanTraits traits_;
std::vector<const char *> preset_modes_{};

View File

@@ -94,7 +94,10 @@ CONFIG_SCHEMA = cv.Schema(
async def to_code(config):
if CORE.is_esp32:
include_builtin_idf_component("esp_driver_pcnt")
# Re-enable ESP-IDF's legacy driver component (excluded by default to save compile time)
# HLW8012 uses pulse_counter's PCNT storage which requires driver/pcnt.h
# TODO: Remove this once pulse_counter migrates to new PCNT API (driver/pulse_cnt.h)
include_builtin_idf_component("driver")
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)

View File

@@ -103,42 +103,6 @@ inline bool is_success(int const status) { return status >= HTTP_STATUS_OK && st
* - ESP-IDF: blocking reads, 0 only returned when all content read
* - Arduino: non-blocking, 0 means "no data yet" or "all content read"
*
* Chunked responses that complete in a reasonable time work correctly on both
* platforms. The limitation below applies only to *streaming* chunked
* responses where data arrives slowly over a long period.
*
* Streaming chunked responses are NOT supported (all platforms):
* The read helpers (http_read_loop_result, http_read_fully) block the main
* event loop until all response data is received. For streaming responses
* where data trickles in slowly (e.g., TTS streaming via ffmpeg proxy),
* this starves the event loop on both ESP-IDF and Arduino. If data arrives
* just often enough to avoid the caller's timeout, the loop runs
* indefinitely. If data stops entirely, ESP-IDF fails with
* -ESP_ERR_HTTP_EAGAIN (transport timeout) while Arduino spins with
* delay(1) until the caller's timeout fires. Supporting streaming requires
* a non-blocking incremental read pattern that yields back to the event
* loop between chunks. Components that need streaming should use
* esp_http_client directly on a separate FreeRTOS task with
* esp_http_client_is_complete_data_received() for completion detection
* (see audio_reader.cpp for an example).
*
* Chunked transfer encoding - platform differences:
* - ESP-IDF HttpContainer:
* HttpContainerIDF overrides is_read_complete() to call
* esp_http_client_is_complete_data_received(), which is the
* authoritative completion check for both chunked and non-chunked
* transfers. When esp_http_client_read() returns 0 for a completed
* chunked response, read() returns 0 and is_read_complete() returns
* true, so callers get COMPLETE from http_read_loop_result().
*
* - Arduino HttpContainer:
* Chunked responses are decoded internally (see
* HttpContainerArduino::read_chunked_()). When the final chunk arrives,
* is_chunked_ is cleared and content_length is set to bytes_read_.
* Completion is then detected via is_read_complete(), and a subsequent
* read() returns 0 to indicate "all content read" (not
* HTTP_ERROR_CONNECTION_CLOSED).
*
* Use the helper functions below instead of checking return values directly:
* - http_read_loop_result(): for manual loops with per-chunk processing
* - http_read_fully(): for simple "read N bytes into buffer" operations
@@ -240,13 +204,9 @@ class HttpContainer : public Parented<HttpRequestComponent> {
size_t get_bytes_read() const { return this->bytes_read_; }
/// Check if all expected content has been read.
/// Base implementation handles non-chunked responses and status-code-based no-body checks.
/// Platform implementations may override for chunked completion detection:
/// - ESP-IDF: overrides to call esp_http_client_is_complete_data_received() for chunked.
/// - Arduino: read_chunked_() clears is_chunked_ and sets content_length on the final
/// chunk, after which the base implementation detects completion.
virtual bool is_read_complete() const {
/// Check if all expected content has been read
/// For chunked responses, returns false (completion detected via read() returning error/EOF)
bool is_read_complete() const {
// Per RFC 9112, these responses have no body:
// - 1xx (Informational), 204 No Content, 205 Reset Content, 304 Not Modified
if ((this->status_code >= 100 && this->status_code < 200) || this->status_code == HTTP_STATUS_NO_CONTENT ||

View File

@@ -218,50 +218,32 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
return container;
}
bool HttpContainerIDF::is_read_complete() const {
// Base class handles no-body status codes and non-chunked content_length completion
if (HttpContainer::is_read_complete()) {
return true;
}
// For chunked responses, use the authoritative ESP-IDF completion check
return this->is_chunked_ && esp_http_client_is_complete_data_received(this->client_);
}
// ESP-IDF HTTP read implementation (blocking mode)
//
// WARNING: Return values differ from BSD sockets! See http_request.h for full documentation.
//
// esp_http_client_read() in blocking mode returns:
// > 0: bytes read
// 0: all chunked data received (is_chunk_complete true) or connection closed
// -ESP_ERR_HTTP_EAGAIN: transport timeout, no data available yet
// 0: connection closed (end of stream)
// < 0: error
//
// We normalize to HttpContainer::read() contract:
// > 0: bytes read
// 0: all content read (for both content_length-based and chunked completion)
// 0: all content read (only returned when content_length is known and fully read)
// < 0: error/connection closed
//
// Note on chunked transfer encoding:
// esp_http_client_fetch_headers() returns 0 for chunked responses (no Content-Length header).
// When esp_http_client_read() returns 0 for a chunked response, is_read_complete() calls
// esp_http_client_is_complete_data_received() to distinguish successful completion from
// connection errors. Callers use http_read_loop_result() which checks is_read_complete()
// to return COMPLETE for successful chunked EOF.
//
// Streaming chunked responses are not supported (see http_request.h for details).
// When data stops arriving, esp_http_client_read() returns -ESP_ERR_HTTP_EAGAIN
// after its internal transport timeout (configured via timeout_ms) expires.
// This is passed through as a negative return value, which callers treat as an error.
// We handle this by skipping the content_length check when content_length is 0,
// allowing esp_http_client_read() to handle chunked decoding internally and signal EOF
// by returning 0.
int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
const uint32_t start = millis();
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
// Check if we've already read all expected content (non-chunked and no-body only).
// Use the base class check here, NOT the override: esp_http_client_is_complete_data_received()
// returns true as soon as all data arrives from the network, but data may still be in
// the client's internal buffer waiting to be consumed by esp_http_client_read().
if (HttpContainer::is_read_complete()) {
// Check if we've already read all expected content (non-chunked only)
// For chunked responses (content_length == 0), esp_http_client_read() handles EOF
if (this->is_read_complete()) {
return 0; // All content read successfully
}
@@ -276,18 +258,15 @@ int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
return read_len_or_error;
}
// esp_http_client_read() returns 0 when:
// - Known content_length: connection closed before all data received (error)
// - Chunked encoding: all chunks received (is_chunk_complete true, genuine EOF)
//
// Return 0 in both cases. Callers use http_read_loop_result() which calls
// is_read_complete() to distinguish these:
// - Chunked complete: is_read_complete() returns true (via
// esp_http_client_is_complete_data_received()), caller gets COMPLETE
// - Non-chunked incomplete: is_read_complete() returns false, caller
// eventually gets TIMEOUT (since no more data arrives)
// esp_http_client_read() returns 0 in two cases:
// 1. Known content_length: connection closed before all data received (error)
// 2. Chunked encoding (content_length == 0): end of stream reached (EOF)
// For case 1, returning HTTP_ERROR_CONNECTION_CLOSED is correct.
// For case 2, 0 indicates that all chunked data has already been delivered
// in previous successful read() calls, so treating this as a closed
// connection does not cause any loss of response data.
if (read_len_or_error == 0) {
return 0;
return HTTP_ERROR_CONNECTION_CLOSED;
}
// Negative value - error, return the actual error code for debugging

View File

@@ -16,7 +16,6 @@ class HttpContainerIDF : public HttpContainer {
HttpContainerIDF(esp_http_client_handle_t client) : client_(client) {}
int read(uint8_t *buf, size_t max_len) override;
void end() override;
bool is_read_complete() const override;
/// @brief Feeds the watchdog timer if the executing task has one attached
void feed_wdt();

View File

@@ -90,14 +90,16 @@ void HttpRequestUpdate::update_task(void *params) {
UPDATE_RETURN;
}
size_t read_index = container->get_bytes_read();
size_t content_length = container->content_length;
container->end();
container.reset(); // Release ownership of the container's shared_ptr
bool valid = false;
{ // Scope to ensure JsonDocument is destroyed before deallocating buffer
valid = json::parse_json(data, read_index, [this_update](JsonObject root) -> bool {
{ // Ensures the response string falls out of scope and deallocates before the task ends
std::string response((char *) data, read_index);
allocator.deallocate(data, container->content_length);
container->end();
container.reset(); // Release ownership of the container's shared_ptr
valid = json::parse_json(response, [this_update](JsonObject root) -> bool {
if (!root[ESPHOME_F("name")].is<const char *>() || !root[ESPHOME_F("version")].is<const char *>() ||
!root[ESPHOME_F("builds")].is<JsonArray>()) {
ESP_LOGE(TAG, "Manifest does not contain required fields");
@@ -135,7 +137,6 @@ void HttpRequestUpdate::update_task(void *params) {
return false;
});
}
allocator.deallocate(data, content_length);
if (!valid) {
ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str());
@@ -156,12 +157,17 @@ void HttpRequestUpdate::update_task(void *params) {
}
}
{ // Ensures the current version string falls out of scope and deallocates before the task ends
std::string current_version;
#ifdef ESPHOME_PROJECT_VERSION
this_update->update_info_.current_version = ESPHOME_PROJECT_VERSION;
current_version = ESPHOME_PROJECT_VERSION;
#else
this_update->update_info_.current_version = ESPHOME_VERSION;
current_version = ESPHOME_VERSION;
#endif
this_update->update_info_.current_version = current_version;
}
bool trigger_update_available = false;
if (this_update->update_info_.latest_version.empty() ||

View File

@@ -119,7 +119,7 @@ void IDFI2CBus::dump_config() {
if (s.second) {
ESP_LOGCONFIG(TAG, "Found device at address 0x%02X", s.first);
} else {
ESP_LOGCONFIG(TAG, "Unknown error at address 0x%02X", s.first);
ESP_LOGE(TAG, "Unknown error at address 0x%02X", s.first);
}
}
}

View File

@@ -267,26 +267,16 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
for (auto &scan : results) {
if (scan.get_is_hidden())
continue;
const char *ssid_cstr = scan.get_ssid().c_str();
// Check if we've already sent this SSID
bool duplicate = false;
for (const auto &seen : networks) {
if (strcmp(seen.c_str(), ssid_cstr) == 0) {
duplicate = true;
break;
}
}
if (duplicate)
const std::string &ssid = scan.get_ssid();
if (std::find(networks.begin(), networks.end(), ssid) != networks.end())
continue;
// Only allocate std::string after confirming it's not a duplicate
std::string ssid(ssid_cstr);
// Send each ssid separately to avoid overflowing the buffer
char rssi_buf[5]; // int8_t: -128 to 127, max 4 chars + null
*int8_to_str(rssi_buf, scan.get_rssi()) = '\0';
std::vector<uint8_t> data =
improv::build_rpc_response(improv::GET_WIFI_NETWORKS, {ssid, rssi_buf, YESNO(scan.get_with_auth())}, false);
this->send_response_(data);
networks.push_back(std::move(ssid));
networks.push_back(ssid);
}
// Send empty response to signify the end of the list.
std::vector<uint8_t> data =

View File

@@ -15,7 +15,7 @@ static const char *const TAG = "json";
static SpiRamAllocator global_json_allocator;
#endif
SerializationBuffer<> build_json(const json_build_t &f) {
std::string build_json(const json_build_t &f) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
JsonBuilder builder;
JsonObject root = builder.root();
@@ -25,13 +25,8 @@ SerializationBuffer<> build_json(const json_build_t &f) {
}
bool parse_json(const std::string &data, const json_parse_t &f) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
return parse_json(reinterpret_cast<const uint8_t *>(data.c_str()), data.size(), f);
}
bool parse_json(const uint8_t *data, size_t len, const json_parse_t &f) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
JsonDocument doc = parse_json(data, len);
JsonDocument doc = parse_json(reinterpret_cast<const uint8_t *>(data.c_str()), data.size());
if (doc.overflowed() || doc.isNull())
return false;
return f(doc.as<JsonObject>());
@@ -66,62 +61,14 @@ JsonDocument parse_json(const uint8_t *data, size_t len) {
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
}
SerializationBuffer<> JsonBuilder::serialize() {
// ===========================================================================================
// CRITICAL: NRVO (Named Return Value Optimization) - DO NOT REFACTOR WITHOUT UNDERSTANDING
// ===========================================================================================
//
// This function is carefully structured to enable NRVO. The compiler constructs `result`
// directly in the caller's stack frame, eliminating the move constructor call entirely.
//
// WITHOUT NRVO: Each return would trigger SerializationBuffer's move constructor, which
// must memcpy up to 768 bytes of stack buffer content. This happens on EVERY JSON
// serialization (sensor updates, web server responses, MQTT publishes, etc.).
//
// WITH NRVO: Zero memcpy, zero move constructor overhead. The buffer lives directly
// where the caller needs it.
//
// Requirements for NRVO to work:
// 1. Single named variable (`result`) returned from ALL paths
// 2. All paths must return the SAME variable (not different variables)
// 3. No std::move() on the return statement
//
// If you must modify this function:
// - Keep a single `result` variable declared at the top
// - All code paths must return `result` (not a different variable)
// - Verify NRVO still works by checking the disassembly for move constructor calls
// - Test: objdump -d -C firmware.elf | grep "SerializationBuffer.*SerializationBuffer"
// Should show only destructor, NOT move constructor
//
// Why we avoid measureJson(): It instantiates DummyWriter templates adding ~1KB flash.
// Instead, try stack buffer first. 768 bytes covers 99.9% of JSON payloads (sensors ~200B,
// lights ~170B, climate ~700B). Only entities with 40+ options exceed this.
//
// ===========================================================================================
constexpr size_t buf_size = SerializationBuffer<>::BUFFER_SIZE;
SerializationBuffer<> result(buf_size - 1); // Max content size (reserve 1 for null)
std::string JsonBuilder::serialize() {
if (doc_.overflowed()) {
ESP_LOGE(TAG, "JSON document overflow");
auto *buf = result.data_writable_();
buf[0] = '{';
buf[1] = '}';
buf[2] = '\0';
result.set_size_(2);
return result;
return "{}";
}
size_t size = serializeJson(doc_, result.data_writable_(), buf_size);
if (size < buf_size) {
// Fits in stack buffer - update size to actual length
result.set_size_(size);
return result;
}
// Needs heap allocation - reallocate and serialize again with exact size
result.reallocate_heap_(size);
serializeJson(doc_, result.data_writable_(), size + 1);
return result;
std::string output;
serializeJson(doc_, output);
return output;
}
} // namespace json

View File

@@ -1,7 +1,5 @@
#pragma once
#include <cstring>
#include <string>
#include <vector>
#include "esphome/core/defines.h"
@@ -16,108 +14,6 @@
namespace esphome {
namespace json {
/// Buffer for JSON serialization that uses stack allocation for small payloads.
/// Template parameter STACK_SIZE specifies the stack buffer size (default 768 bytes).
/// Supports move semantics for efficient return-by-value.
template<size_t STACK_SIZE = 768> class SerializationBuffer {
public:
static constexpr size_t BUFFER_SIZE = STACK_SIZE; ///< Stack buffer size for this instantiation
/// Construct with known size (typically from measureJson)
explicit SerializationBuffer(size_t size) : size_(size) {
if (size + 1 <= STACK_SIZE) {
buffer_ = stack_buffer_;
} else {
heap_buffer_ = new char[size + 1];
buffer_ = heap_buffer_;
}
buffer_[0] = '\0';
}
~SerializationBuffer() { delete[] heap_buffer_; }
// Move constructor - works with same template instantiation
SerializationBuffer(SerializationBuffer &&other) noexcept : heap_buffer_(other.heap_buffer_), size_(other.size_) {
if (other.buffer_ == other.stack_buffer_) {
// Stack buffer - must copy content
std::memcpy(stack_buffer_, other.stack_buffer_, size_ + 1);
buffer_ = stack_buffer_;
} else {
// Heap buffer - steal ownership
buffer_ = heap_buffer_;
other.heap_buffer_ = nullptr;
}
// Leave moved-from object in valid empty state
other.stack_buffer_[0] = '\0';
other.buffer_ = other.stack_buffer_;
other.size_ = 0;
}
// Move assignment
SerializationBuffer &operator=(SerializationBuffer &&other) noexcept {
if (this != &other) {
delete[] heap_buffer_;
heap_buffer_ = other.heap_buffer_;
size_ = other.size_;
if (other.buffer_ == other.stack_buffer_) {
std::memcpy(stack_buffer_, other.stack_buffer_, size_ + 1);
buffer_ = stack_buffer_;
} else {
buffer_ = heap_buffer_;
other.heap_buffer_ = nullptr;
}
// Leave moved-from object in valid empty state
other.stack_buffer_[0] = '\0';
other.buffer_ = other.stack_buffer_;
other.size_ = 0;
}
return *this;
}
// Delete copy operations
SerializationBuffer(const SerializationBuffer &) = delete;
SerializationBuffer &operator=(const SerializationBuffer &) = delete;
/// Get null-terminated C string
const char *c_str() const { return buffer_; }
/// Get data pointer
const char *data() const { return buffer_; }
/// Get string length (excluding null terminator)
size_t size() const { return size_; }
/// Implicit conversion to std::string for backward compatibility
/// WARNING: This allocates a new std::string on the heap. Prefer using
/// c_str() or data()/size() directly when possible to avoid allocation.
operator std::string() const { return std::string(buffer_, size_); } // NOLINT(google-explicit-constructor)
private:
friend class JsonBuilder; ///< Allows JsonBuilder::serialize() to call private methods
/// Get writable buffer (for serialization)
char *data_writable_() { return buffer_; }
/// Set actual size after serialization (must not exceed allocated size)
/// Also ensures null termination for c_str() safety
void set_size_(size_t size) {
size_ = size;
buffer_[size] = '\0';
}
/// Reallocate to heap buffer with new size (for when stack buffer is too small)
/// This invalidates any previous buffer content. Used by JsonBuilder::serialize().
void reallocate_heap_(size_t size) {
delete[] heap_buffer_;
heap_buffer_ = new char[size + 1];
buffer_ = heap_buffer_;
size_ = size;
buffer_[0] = '\0';
}
char stack_buffer_[STACK_SIZE];
char *heap_buffer_{nullptr};
char *buffer_;
size_t size_;
};
#ifdef USE_PSRAM
// Build an allocator for the JSON Library using the RAMAllocator class
// This is only compiled when PSRAM is enabled
@@ -150,13 +46,10 @@ using json_parse_t = std::function<bool(JsonObject)>;
using json_build_t = std::function<void(JsonObject)>;
/// Build a JSON string with the provided json build function.
/// Returns SerializationBuffer for stack-first allocation; implicitly converts to std::string.
SerializationBuffer<> build_json(const json_build_t &f);
std::string build_json(const json_build_t &f);
/// Parse a JSON string and run the provided json parse function if it's valid.
bool parse_json(const std::string &data, const json_parse_t &f);
/// Parse JSON from raw bytes and run the provided json parse function if it's valid.
bool parse_json(const uint8_t *data, size_t len, const json_parse_t &f);
/// Parse a JSON string and return the root JsonDocument (or an unbound object on error)
JsonDocument parse_json(const uint8_t *data, size_t len);
@@ -176,9 +69,7 @@ class JsonBuilder {
return root_;
}
/// Serialize the JSON document to a SerializationBuffer (stack-first allocation)
/// Uses 768-byte stack buffer by default, falls back to heap for larger JSON
SerializationBuffer<> serialize();
std::string serialize();
private:
#ifdef USE_PSRAM

View File

@@ -11,7 +11,7 @@ static const char *const TAG = "kuntze";
static const uint8_t CMD_READ_REG = 0x03;
static const uint16_t REGISTER[] = {4136, 4160, 4680, 6000, 4688, 4728, 5832};
// Maximum bytes to log for Modbus responses (2 registers = 4 bytes, plus byte count = 5 bytes)
// Maximum bytes to log for Modbus responses (2 registers = 4, plus count = 5)
static constexpr size_t KUNTZE_MAX_LOG_BYTES = 8;
void Kuntze::on_modbus_data(const std::vector<uint8_t> &data) {

View File

@@ -1,8 +1,7 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components import uart
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_ON_DATA, CONF_THROTTLE, CONF_TRIGGER_ID
from esphome.const import CONF_ID, CONF_THROTTLE
AUTO_LOAD = ["ld24xx"]
DEPENDENCIES = ["uart"]
@@ -12,8 +11,6 @@ MULTI_CONF = True
ld2450_ns = cg.esphome_ns.namespace("ld2450")
LD2450Component = ld2450_ns.class_("LD2450Component", cg.Component, uart.UARTDevice)
LD2450DataTrigger = ld2450_ns.class_("LD2450DataTrigger", automation.Trigger.template())
CONF_LD2450_ID = "ld2450_id"
CONFIG_SCHEMA = cv.All(
@@ -23,11 +20,6 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_THROTTLE): cv.invalid(
f"{CONF_THROTTLE} has been removed; use per-sensor filters, instead"
),
cv.Optional(CONF_ON_DATA): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LD2450DataTrigger),
}
),
}
)
.extend(uart.UART_DEVICE_SCHEMA)
@@ -53,6 +45,3 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
for conf in config.get(CONF_ON_DATA, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)

View File

@@ -413,10 +413,6 @@ void LD2450Component::restart_and_read_all_info() {
this->set_timeout(1500, [this]() { this->read_all_info(); });
}
void LD2450Component::add_on_data_callback(std::function<void()> &&callback) {
this->data_callback_.add(std::move(callback));
}
// Send command with values to LD2450
void LD2450Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) {
ESP_LOGV(TAG, "Sending COMMAND %02X", command);
@@ -617,8 +613,6 @@ void LD2450Component::handle_periodic_data_() {
this->still_presence_millis_ = App.get_loop_component_start_time();
}
#endif
this->data_callback_.call();
}
bool LD2450Component::handle_ack_data_() {

View File

@@ -141,9 +141,6 @@ class LD2450Component : public Component, public uart::UARTDevice {
int32_t zone2_x1, int32_t zone2_y1, int32_t zone2_x2, int32_t zone2_y2, int32_t zone3_x1,
int32_t zone3_y1, int32_t zone3_x2, int32_t zone3_y2);
/// Add a callback that will be called after each successfully processed periodic data frame.
void add_on_data_callback(std::function<void()> &&callback);
protected:
void send_command_(uint8_t command_str, const uint8_t *command_value, uint8_t command_value_len);
void set_config_mode_(bool enable);
@@ -193,15 +190,6 @@ class LD2450Component : public Component, public uart::UARTDevice {
#ifdef USE_TEXT_SENSOR
std::array<text_sensor::TextSensor *, 3> direction_text_sensors_{};
#endif
LazyCallbackManager<void()> data_callback_;
};
class LD2450DataTrigger : public Trigger<> {
public:
explicit LD2450DataTrigger(LD2450Component *parent) {
parent->add_on_data_callback([this]() { this->trigger(); });
}
};
} // namespace esphome::ld2450

View File

@@ -193,14 +193,14 @@ def _notify_old_style(config):
# The dev and latest branches will be at *least* this version, which is what matters.
# Use GitHub releases directly to avoid PlatformIO moderation delays.
ARDUINO_VERSIONS = {
"dev": (cv.Version(1, 12, 1), "https://github.com/libretiny-eu/libretiny.git"),
"dev": (cv.Version(1, 11, 0), "https://github.com/libretiny-eu/libretiny.git"),
"latest": (
cv.Version(1, 12, 1),
"https://github.com/libretiny-eu/libretiny.git#v1.12.1",
cv.Version(1, 11, 0),
"https://github.com/libretiny-eu/libretiny.git#v1.11.0",
),
"recommended": (
cv.Version(1, 12, 1),
"https://github.com/libretiny-eu/libretiny.git#v1.12.1",
cv.Version(1, 11, 0),
"https://github.com/libretiny-eu/libretiny.git#v1.11.0",
),
}

View File

@@ -18,7 +18,16 @@ static constexpr size_t KEY_BUFFER_SIZE = 12;
struct NVSData {
uint32_t key;
SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.)
std::unique_ptr<uint8_t[]> data;
size_t len;
void set_data(const uint8_t *src, size_t size) {
if (!this->data || this->len != size) {
this->data = std::make_unique<uint8_t[]>(size);
this->len = size;
}
memcpy(this->data.get(), src, size);
}
};
static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@@ -33,14 +42,14 @@ class LibreTinyPreferenceBackend : public ESPPreferenceBackend {
// try find in pending saves and update that
for (auto &obj : s_pending_save) {
if (obj.key == this->key) {
obj.data.set(data, len);
obj.set_data(data, len);
return true;
}
}
NVSData save{};
save.key = this->key;
save.data.set(data, len);
s_pending_save.push_back(std::move(save));
save.set_data(data, len);
s_pending_save.emplace_back(std::move(save));
ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len);
return true;
}
@@ -49,11 +58,11 @@ class LibreTinyPreferenceBackend : public ESPPreferenceBackend {
// try find in pending saves and load from that
for (auto &obj : s_pending_save) {
if (obj.key == this->key) {
if (obj.data.size() != len) {
if (obj.len != len) {
// size mismatch
return false;
}
memcpy(data, obj.data.data(), len);
memcpy(data, obj.data.get(), len);
return true;
}
}
@@ -117,11 +126,11 @@ class LibreTinyPreferences : public ESPPreferences {
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
ESP_LOGVV(TAG, "Checking if FDB data %s has changed", key_str);
if (this->is_changed_(&this->db, save, key_str)) {
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size());
fdb_blob_make(&this->blob, save.data.data(), save.data.size());
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.len);
fdb_blob_make(&this->blob, save.data.get(), save.len);
fdb_err_t err = fdb_kv_set_blob(&this->db, key_str, &this->blob);
if (err != FDB_NO_ERR) {
ESP_LOGV(TAG, "fdb_kv_set_blob('%s', len=%zu) failed: %d", key_str, save.data.size(), err);
ESP_LOGV(TAG, "fdb_kv_set_blob('%s', len=%zu) failed: %d", key_str, save.len, err);
failed++;
last_err = err;
last_key = save.key;
@@ -129,7 +138,7 @@ class LibreTinyPreferences : public ESPPreferences {
}
written++;
} else {
ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.data.size());
ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.len);
cached++;
}
s_pending_save.erase(s_pending_save.begin() + i);
@@ -153,7 +162,7 @@ class LibreTinyPreferences : public ESPPreferences {
}
// Check size first - if different, data has changed
if (kv.value_len != to_save.data.size()) {
if (kv.value_len != to_save.len) {
return true;
}
@@ -167,7 +176,7 @@ class LibreTinyPreferences : public ESPPreferences {
}
// Compare the actual data
return memcmp(to_save.data.data(), stored_data.get(), kv.value_len) != 0;
return memcmp(to_save.data.get(), stored_data.get(), kv.value_len) != 0;
}
bool reset() override {

View File

@@ -154,26 +154,28 @@ LN882X_BOARD_PINS = {
"A7": 21,
},
"wb02a": {
"WIRE0_SCL_0": 1,
"WIRE0_SCL_1": 2,
"WIRE0_SCL_0": 7,
"WIRE0_SCL_1": 5,
"WIRE0_SCL_2": 3,
"WIRE0_SCL_3": 4,
"WIRE0_SCL_4": 5,
"WIRE0_SCL_5": 7,
"WIRE0_SCL_6": 9,
"WIRE0_SCL_7": 10,
"WIRE0_SCL_8": 24,
"WIRE0_SCL_9": 25,
"WIRE0_SDA_0": 1,
"WIRE0_SDA_1": 2,
"WIRE0_SCL_3": 10,
"WIRE0_SCL_4": 2,
"WIRE0_SCL_5": 1,
"WIRE0_SCL_6": 4,
"WIRE0_SCL_7": 5,
"WIRE0_SCL_8": 9,
"WIRE0_SCL_9": 24,
"WIRE0_SCL_10": 25,
"WIRE0_SDA_0": 7,
"WIRE0_SDA_1": 5,
"WIRE0_SDA_2": 3,
"WIRE0_SDA_3": 4,
"WIRE0_SDA_4": 5,
"WIRE0_SDA_5": 7,
"WIRE0_SDA_6": 9,
"WIRE0_SDA_7": 10,
"WIRE0_SDA_8": 24,
"WIRE0_SDA_9": 25,
"WIRE0_SDA_3": 10,
"WIRE0_SDA_4": 2,
"WIRE0_SDA_5": 1,
"WIRE0_SDA_6": 4,
"WIRE0_SDA_7": 5,
"WIRE0_SDA_8": 9,
"WIRE0_SDA_9": 24,
"WIRE0_SDA_10": 25,
"SERIAL0_RX": 3,
"SERIAL0_TX": 2,
"SERIAL1_RX": 24,
@@ -219,32 +221,32 @@ LN882X_BOARD_PINS = {
"A1": 4,
},
"wl2s": {
"WIRE0_SCL_0": 0,
"WIRE0_SCL_1": 1,
"WIRE0_SCL_2": 2,
"WIRE0_SCL_3": 3,
"WIRE0_SCL_4": 5,
"WIRE0_SCL_5": 7,
"WIRE0_SCL_6": 9,
"WIRE0_SCL_7": 10,
"WIRE0_SCL_8": 11,
"WIRE0_SCL_9": 12,
"WIRE0_SCL_10": 19,
"WIRE0_SCL_11": 24,
"WIRE0_SCL_12": 25,
"WIRE0_SDA_0": 0,
"WIRE0_SDA_1": 1,
"WIRE0_SDA_2": 2,
"WIRE0_SDA_3": 3,
"WIRE0_SDA_4": 5,
"WIRE0_SDA_5": 7,
"WIRE0_SDA_6": 9,
"WIRE0_SDA_7": 10,
"WIRE0_SDA_8": 11,
"WIRE0_SDA_9": 12,
"WIRE0_SDA_10": 19,
"WIRE0_SDA_11": 24,
"WIRE0_SDA_12": 25,
"WIRE0_SCL_0": 7,
"WIRE0_SCL_1": 12,
"WIRE0_SCL_2": 3,
"WIRE0_SCL_3": 10,
"WIRE0_SCL_4": 2,
"WIRE0_SCL_5": 0,
"WIRE0_SCL_6": 19,
"WIRE0_SCL_7": 11,
"WIRE0_SCL_8": 9,
"WIRE0_SCL_9": 24,
"WIRE0_SCL_10": 25,
"WIRE0_SCL_11": 5,
"WIRE0_SCL_12": 1,
"WIRE0_SDA_0": 7,
"WIRE0_SDA_1": 12,
"WIRE0_SDA_2": 3,
"WIRE0_SDA_3": 10,
"WIRE0_SDA_4": 2,
"WIRE0_SDA_5": 0,
"WIRE0_SDA_6": 19,
"WIRE0_SDA_7": 11,
"WIRE0_SDA_8": 9,
"WIRE0_SDA_9": 24,
"WIRE0_SDA_10": 25,
"WIRE0_SDA_11": 5,
"WIRE0_SDA_12": 1,
"SERIAL0_RX": 3,
"SERIAL0_TX": 2,
"SERIAL1_RX": 24,
@@ -299,24 +301,24 @@ LN882X_BOARD_PINS = {
"A2": 1,
},
"ln-02": {
"WIRE0_SCL_0": 0,
"WIRE0_SCL_1": 1,
"WIRE0_SCL_2": 2,
"WIRE0_SCL_3": 3,
"WIRE0_SCL_4": 9,
"WIRE0_SCL_5": 11,
"WIRE0_SCL_6": 19,
"WIRE0_SCL_7": 24,
"WIRE0_SCL_8": 25,
"WIRE0_SDA_0": 0,
"WIRE0_SDA_1": 1,
"WIRE0_SDA_2": 2,
"WIRE0_SDA_3": 3,
"WIRE0_SDA_4": 9,
"WIRE0_SDA_5": 11,
"WIRE0_SDA_6": 19,
"WIRE0_SDA_7": 24,
"WIRE0_SDA_8": 25,
"WIRE0_SCL_0": 11,
"WIRE0_SCL_1": 19,
"WIRE0_SCL_2": 3,
"WIRE0_SCL_3": 24,
"WIRE0_SCL_4": 2,
"WIRE0_SCL_5": 25,
"WIRE0_SCL_6": 1,
"WIRE0_SCL_7": 0,
"WIRE0_SCL_8": 9,
"WIRE0_SDA_0": 11,
"WIRE0_SDA_1": 19,
"WIRE0_SDA_2": 3,
"WIRE0_SDA_3": 24,
"WIRE0_SDA_4": 2,
"WIRE0_SDA_5": 25,
"WIRE0_SDA_6": 1,
"WIRE0_SDA_7": 0,
"WIRE0_SDA_8": 9,
"SERIAL0_RX": 3,
"SERIAL0_TX": 2,
"SERIAL1_RX": 24,

View File

@@ -1,51 +0,0 @@
#ifdef USE_ESP8266
#include "logger.h"
#include "esphome/core/log.h"
namespace esphome::logger {
static const char *const TAG = "logger";
void Logger::pre_setup() {
if (this->baud_rate_ > 0) {
switch (this->uart_) {
case UART_SELECTION_UART0:
case UART_SELECTION_UART0_SWAP:
this->hw_serial_ = &Serial;
Serial.begin(this->baud_rate_);
if (this->uart_ == UART_SELECTION_UART0_SWAP) {
Serial.swap();
}
Serial.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE);
break;
case UART_SELECTION_UART1:
this->hw_serial_ = &Serial1;
Serial1.begin(this->baud_rate_);
Serial1.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE);
break;
}
} else {
uart_set_debug(UART_NO);
}
global_logger = this;
ESP_LOGI(TAG, "Log initialized");
}
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
const LogString *Logger::get_uart_selection_() {
switch (this->uart_) {
case UART_SELECTION_UART0:
return LOG_STR("UART0");
case UART_SELECTION_UART1:
return LOG_STR("UART1");
case UART_SELECTION_UART0_SWAP:
default:
return LOG_STR("UART0_SWAP");
}
}
} // namespace esphome::logger
#endif

View File

@@ -1,22 +0,0 @@
#if defined(USE_HOST)
#include "logger.h"
namespace esphome::logger {
void HOT Logger::write_msg_(const char *msg) {
time_t rawtime;
struct tm *timeinfo;
char buffer[80];
time(&rawtime);
timeinfo = localtime(&rawtime);
strftime(buffer, sizeof buffer, "[%H:%M:%S]", timeinfo);
fputs(buffer, stdout);
puts(msg);
}
void Logger::pre_setup() { global_logger = this; }
} // namespace esphome::logger
#endif

View File

@@ -1,70 +0,0 @@
#ifdef USE_LIBRETINY
#include "logger.h"
namespace esphome::logger {
static const char *const TAG = "logger";
void Logger::pre_setup() {
if (this->baud_rate_ > 0) {
switch (this->uart_) {
#if LT_HW_UART0
case UART_SELECTION_UART0:
this->hw_serial_ = &Serial0;
Serial0.begin(this->baud_rate_);
break;
#endif
#if LT_HW_UART1
case UART_SELECTION_UART1:
this->hw_serial_ = &Serial1;
Serial1.begin(this->baud_rate_);
break;
#endif
#if LT_HW_UART2
case UART_SELECTION_UART2:
this->hw_serial_ = &Serial2;
Serial2.begin(this->baud_rate_);
break;
#endif
default:
this->hw_serial_ = &Serial;
Serial.begin(this->baud_rate_);
if (this->uart_ != UART_SELECTION_DEFAULT) {
ESP_LOGW(TAG, " The chosen logger UART port is not available on this board."
"The default port was used instead.");
}
break;
}
// change lt_log() port to match default Serial
if (this->uart_ == UART_SELECTION_DEFAULT) {
this->uart_ = (UARTSelection) (LT_UART_DEFAULT_SERIAL + 1);
lt_log_set_port(LT_UART_DEFAULT_SERIAL);
} else {
lt_log_set_port(this->uart_ - 1);
}
}
global_logger = this;
ESP_LOGI(TAG, "Log initialized");
}
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
const LogString *Logger::get_uart_selection_() {
switch (this->uart_) {
case UART_SELECTION_DEFAULT:
return LOG_STR("DEFAULT");
case UART_SELECTION_UART0:
return LOG_STR("UART0");
case UART_SELECTION_UART1:
return LOG_STR("UART1");
case UART_SELECTION_UART2:
default:
return LOG_STR("UART2");
}
}
} // namespace esphome::logger
#endif // USE_LIBRETINY

View File

@@ -1,48 +0,0 @@
#ifdef USE_RP2040
#include "logger.h"
#include "esphome/core/log.h"
namespace esphome::logger {
static const char *const TAG = "logger";
void Logger::pre_setup() {
if (this->baud_rate_ > 0) {
switch (this->uart_) {
case UART_SELECTION_UART0:
this->hw_serial_ = &Serial1;
Serial1.begin(this->baud_rate_);
break;
case UART_SELECTION_UART1:
this->hw_serial_ = &Serial2;
Serial2.begin(this->baud_rate_);
break;
case UART_SELECTION_USB_CDC:
this->hw_serial_ = &Serial;
Serial.begin(this->baud_rate_);
break;
}
}
global_logger = this;
ESP_LOGI(TAG, "Log initialized");
}
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
const LogString *Logger::get_uart_selection_() {
switch (this->uart_) {
case UART_SELECTION_UART0:
return LOG_STR("UART0");
case UART_SELECTION_UART1:
return LOG_STR("UART1");
#ifdef USE_LOGGER_USB_CDC
case UART_SELECTION_USB_CDC:
return LOG_STR("USB_CDC");
#endif
default:
return LOG_STR("UNKNOWN");
}
}
} // namespace esphome::logger
#endif // USE_RP2040

View File

@@ -1,96 +0,0 @@
#ifdef USE_ZEPHYR
#include "esphome/core/application.h"
#include "esphome/core/log.h"
#include "logger.h"
#include <zephyr/device.h>
#include <zephyr/drivers/uart.h>
#include <zephyr/usb/usb_device.h>
namespace esphome::logger {
static const char *const TAG = "logger";
#ifdef USE_LOGGER_USB_CDC
void Logger::loop() {
if (this->uart_ != UART_SELECTION_USB_CDC || nullptr == this->uart_dev_) {
return;
}
static bool opened = false;
uint32_t dtr = 0;
uart_line_ctrl_get(this->uart_dev_, UART_LINE_CTRL_DTR, &dtr);
/* Poll if the DTR flag was set, optional */
if (opened == dtr) {
return;
}
if (!opened) {
App.schedule_dump_config();
}
opened = !opened;
}
#endif
void Logger::pre_setup() {
if (this->baud_rate_ > 0) {
static const struct device *uart_dev = nullptr;
switch (this->uart_) {
case UART_SELECTION_UART0:
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(uart0));
break;
case UART_SELECTION_UART1:
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(uart1));
break;
#ifdef USE_LOGGER_USB_CDC
case UART_SELECTION_USB_CDC:
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(cdc_acm_uart0));
if (device_is_ready(uart_dev)) {
usb_enable(nullptr);
}
break;
#endif
}
if (!device_is_ready(uart_dev)) {
ESP_LOGE(TAG, "%s is not ready.", LOG_STR_ARG(get_uart_selection_()));
} else {
this->uart_dev_ = uart_dev;
}
}
global_logger = this;
ESP_LOGI(TAG, "Log initialized");
}
void HOT Logger::write_msg_(const char *msg) {
#ifdef CONFIG_PRINTK
printk("%s\n", msg);
#endif
if (nullptr == this->uart_dev_) {
return;
}
while (*msg) {
uart_poll_out(this->uart_dev_, *msg);
++msg;
}
uart_poll_out(this->uart_dev_, '\n');
}
const LogString *Logger::get_uart_selection_() {
switch (this->uart_) {
case UART_SELECTION_UART0:
return LOG_STR("UART0");
case UART_SELECTION_UART1:
return LOG_STR("UART1");
#ifdef USE_LOGGER_USB_CDC
case UART_SELECTION_USB_CDC:
return LOG_STR("USB_CDC");
#endif
default:
return LOG_STR("UNKNOWN");
}
}
} // namespace esphome::logger
#endif

View File

@@ -56,7 +56,7 @@ void MCP23016::pin_mode(uint8_t pin, gpio::Flags flags) {
this->update_reg_(pin, false, iodir);
}
}
float MCP23016::get_setup_priority() const { return setup_priority::IO; }
float MCP23016::get_setup_priority() const { return setup_priority::HARDWARE; }
bool MCP23016::read_reg_(uint8_t reg, uint8_t *value) {
if (this->is_failed())
return false;

View File

@@ -170,8 +170,10 @@ void MQTTClientComponent::send_device_info_() {
void MQTTClientComponent::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) {
(void) tag;
if (level <= this->log_level_ && this->is_connected()) {
this->publish(this->log_message_.topic.c_str(), message, message_len, this->log_message_.qos,
this->log_message_.retain);
this->publish({.topic = this->log_message_.topic,
.payload = std::string(message, message_len),
.qos = this->log_message_.qos,
.retain = this->log_message_.retain});
}
}
#endif
@@ -540,8 +542,8 @@ bool MQTTClientComponent::publish(const char *topic, const char *payload, size_t
}
bool MQTTClientComponent::publish_json(const char *topic, const json::json_build_t &f, uint8_t qos, bool retain) {
auto message = json::build_json(f);
return this->publish(topic, message.c_str(), message.size(), qos, retain);
std::string message = json::build_json(f);
return this->publish(topic, message.c_str(), message.length(), qos, retain);
}
void MQTTClientComponent::enable() {

View File

@@ -300,11 +300,9 @@ const EntityBase *MQTTClimateComponent::get_entity() const { return this->device
bool MQTTClimateComponent::publish_state_() {
auto traits = this->device_->get_traits();
// Reusable stack buffer for topic construction (avoids heap allocation per publish)
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
// mode
bool success = true;
if (!this->publish(this->get_mode_state_topic_to(topic_buf), climate_mode_to_mqtt_str(this->device_->mode)))
if (!this->publish(this->get_mode_state_topic(), climate_mode_to_mqtt_str(this->device_->mode)))
success = false;
int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals();
int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals();
@@ -313,70 +311,68 @@ bool MQTTClimateComponent::publish_state_() {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE) &&
!std::isnan(this->device_->current_temperature)) {
len = value_accuracy_to_buf(payload, this->device_->current_temperature, current_accuracy);
if (!this->publish(this->get_current_temperature_state_topic_to(topic_buf), payload, len))
if (!this->publish(this->get_current_temperature_state_topic(), payload, len))
success = false;
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
len = value_accuracy_to_buf(payload, this->device_->target_temperature_low, target_accuracy);
if (!this->publish(this->get_target_temperature_low_state_topic_to(topic_buf), payload, len))
if (!this->publish(this->get_target_temperature_low_state_topic(), payload, len))
success = false;
len = value_accuracy_to_buf(payload, this->device_->target_temperature_high, target_accuracy);
if (!this->publish(this->get_target_temperature_high_state_topic_to(topic_buf), payload, len))
if (!this->publish(this->get_target_temperature_high_state_topic(), payload, len))
success = false;
} else {
len = value_accuracy_to_buf(payload, this->device_->target_temperature, target_accuracy);
if (!this->publish(this->get_target_temperature_state_topic_to(topic_buf), payload, len))
if (!this->publish(this->get_target_temperature_state_topic(), payload, len))
success = false;
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY) &&
!std::isnan(this->device_->current_humidity)) {
len = value_accuracy_to_buf(payload, this->device_->current_humidity, 0);
if (!this->publish(this->get_current_humidity_state_topic_to(topic_buf), payload, len))
if (!this->publish(this->get_current_humidity_state_topic(), payload, len))
success = false;
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY) &&
!std::isnan(this->device_->target_humidity)) {
len = value_accuracy_to_buf(payload, this->device_->target_humidity, 0);
if (!this->publish(this->get_target_humidity_state_topic_to(topic_buf), payload, len))
if (!this->publish(this->get_target_humidity_state_topic(), payload, len))
success = false;
}
if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) {
if (this->device_->has_custom_preset()) {
if (!this->publish(this->get_preset_state_topic_to(topic_buf), this->device_->get_custom_preset().c_str()))
if (!this->publish(this->get_preset_state_topic(), this->device_->get_custom_preset()))
success = false;
} else if (this->device_->preset.has_value()) {
if (!this->publish(this->get_preset_state_topic_to(topic_buf),
climate_preset_to_mqtt_str(this->device_->preset.value())))
if (!this->publish(this->get_preset_state_topic(), climate_preset_to_mqtt_str(this->device_->preset.value())))
success = false;
} else if (!this->publish(this->get_preset_state_topic_to(topic_buf), "")) {
} else if (!this->publish(this->get_preset_state_topic(), "")) {
success = false;
}
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) {
if (!this->publish(this->get_action_state_topic_to(topic_buf), climate_action_to_mqtt_str(this->device_->action)))
if (!this->publish(this->get_action_state_topic(), climate_action_to_mqtt_str(this->device_->action)))
success = false;
}
if (traits.get_supports_fan_modes()) {
if (this->device_->has_custom_fan_mode()) {
if (!this->publish(this->get_fan_mode_state_topic_to(topic_buf), this->device_->get_custom_fan_mode().c_str()))
if (!this->publish(this->get_fan_mode_state_topic(), this->device_->get_custom_fan_mode()))
success = false;
} else if (this->device_->fan_mode.has_value()) {
if (!this->publish(this->get_fan_mode_state_topic_to(topic_buf),
if (!this->publish(this->get_fan_mode_state_topic(),
climate_fan_mode_to_mqtt_str(this->device_->fan_mode.value())))
success = false;
} else if (!this->publish(this->get_fan_mode_state_topic_to(topic_buf), "")) {
} else if (!this->publish(this->get_fan_mode_state_topic(), "")) {
success = false;
}
}
if (traits.get_supports_swing_modes()) {
if (!this->publish(this->get_swing_mode_state_topic_to(topic_buf),
climate_swing_mode_to_mqtt_str(this->device_->swing_mode)))
if (!this->publish(this->get_swing_mode_state_topic(), climate_swing_mode_to_mqtt_str(this->device_->swing_mode)))
success = false;
}

View File

@@ -59,11 +59,6 @@ void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, b
\
public: \
void set_custom_##name##_##type##_topic(const std::string &topic) { this->custom_##name##_##type##_topic_ = topic; } \
StringRef get_##name##_##type##_topic_to(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf) const { \
if (!this->custom_##name##_##type##_topic_.empty()) \
return StringRef(this->custom_##name##_##type##_topic_.data(), this->custom_##name##_##type##_topic_.size()); \
return this->get_default_topic_for_to_(buf, #name "/" #type, sizeof(#name "/" #type) - 1); \
} \
std::string get_##name##_##type##_topic() const { \
if (this->custom_##name##_##type##_topic_.empty()) \
return this->get_default_topic_for_(#name "/" #type); \

View File

@@ -112,19 +112,19 @@ bool MQTTCoverComponent::send_initial_state() { return this->publish_state(); }
bool MQTTCoverComponent::publish_state() {
auto traits = this->cover_->get_traits();
bool success = true;
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
if (traits.get_supports_position()) {
char pos[VALUE_ACCURACY_MAX_LEN];
size_t len = value_accuracy_to_buf(pos, roundf(this->cover_->position * 100), 0);
if (!this->publish(this->get_position_state_topic_to(topic_buf), pos, len))
if (!this->publish(this->get_position_state_topic(), pos, len))
success = false;
}
if (traits.get_supports_tilt()) {
char pos[VALUE_ACCURACY_MAX_LEN];
size_t len = value_accuracy_to_buf(pos, roundf(this->cover_->tilt * 100), 0);
if (!this->publish(this->get_tilt_state_topic_to(topic_buf), pos, len))
if (!this->publish(this->get_tilt_state_topic(), pos, len))
success = false;
}
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
if (!this->publish(this->get_state_topic_to_(topic_buf),
cover_state_to_mqtt_str(this->cover_->current_operation, this->cover_->position,
traits.get_supports_position())))

View File

@@ -173,20 +173,19 @@ bool MQTTFanComponent::publish_state() {
this->publish(this->get_state_topic_to_(topic_buf), state_s);
bool failed = false;
if (this->state_->get_traits().supports_direction()) {
bool success = this->publish(this->get_direction_state_topic_to(topic_buf),
fan_direction_to_mqtt_str(this->state_->direction));
bool success = this->publish(this->get_direction_state_topic(), fan_direction_to_mqtt_str(this->state_->direction));
failed = failed || !success;
}
if (this->state_->get_traits().supports_oscillation()) {
bool success = this->publish(this->get_oscillation_state_topic_to(topic_buf),
fan_oscillation_to_mqtt_str(this->state_->oscillating));
bool success =
this->publish(this->get_oscillation_state_topic(), fan_oscillation_to_mqtt_str(this->state_->oscillating));
failed = failed || !success;
}
auto traits = this->state_->get_traits();
if (traits.supports_speed()) {
char buf[12];
size_t len = buf_append_printf(buf, sizeof(buf), 0, "%d", this->state_->speed);
bool success = this->publish(this->get_speed_level_state_topic_to(topic_buf), buf, len);
bool success = this->publish(this->get_speed_level_state_topic(), buf, len);
failed = failed || !success;
}
return !failed;

View File

@@ -87,13 +87,13 @@ bool MQTTValveComponent::send_initial_state() { return this->publish_state(); }
bool MQTTValveComponent::publish_state() {
auto traits = this->valve_->get_traits();
bool success = true;
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
if (traits.get_supports_position()) {
char pos[VALUE_ACCURACY_MAX_LEN];
size_t len = value_accuracy_to_buf(pos, roundf(this->valve_->position * 100), 0);
if (!this->publish(this->get_position_state_topic_to(topic_buf), pos, len))
if (!this->publish(this->get_position_state_topic(), pos, len))
success = false;
}
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
if (!this->publish(this->get_state_topic_to_(topic_buf),
valve_state_to_mqtt_str(this->valve_->current_operation, this->valve_->position,
traits.get_supports_position())))

View File

@@ -1,11 +1,6 @@
#include "pulse_counter_sensor.h"
#include "esphome/core/log.h"
#ifdef HAS_PCNT
#include <esp_private/esp_clk.h>
#include <hal/pcnt_ll.h>
#endif
namespace esphome {
namespace pulse_counter {
@@ -61,107 +56,103 @@ pulse_counter_t BasicPulseCounterStorage::read_raw_value() {
#ifdef HAS_PCNT
bool HwPulseCounterStorage::pulse_counter_setup(InternalGPIOPin *pin) {
static pcnt_unit_t next_pcnt_unit = PCNT_UNIT_0;
static pcnt_channel_t next_pcnt_channel = PCNT_CHANNEL_0;
this->pin = pin;
this->pin->setup();
pcnt_unit_config_t unit_config = {
.low_limit = INT16_MIN,
.high_limit = INT16_MAX,
.flags = {.accum_count = true},
};
esp_err_t error = pcnt_new_unit(&unit_config, &this->pcnt_unit);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Creating PCNT unit failed: %s", esp_err_to_name(error));
return false;
this->pcnt_unit = next_pcnt_unit;
this->pcnt_channel = next_pcnt_channel;
next_pcnt_unit = pcnt_unit_t(int(next_pcnt_unit) + 1);
if (int(next_pcnt_unit) >= PCNT_UNIT_0 + PCNT_UNIT_MAX) {
next_pcnt_unit = PCNT_UNIT_0;
next_pcnt_channel = pcnt_channel_t(int(next_pcnt_channel) + 1);
}
pcnt_chan_config_t chan_config = {
.edge_gpio_num = this->pin->get_pin(),
.level_gpio_num = -1,
};
error = pcnt_new_channel(this->pcnt_unit, &chan_config, &this->pcnt_channel);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Creating PCNT channel failed: %s", esp_err_to_name(error));
return false;
}
ESP_LOGCONFIG(TAG,
" PCNT Unit Number: %u\n"
" PCNT Channel Number: %u",
this->pcnt_unit, this->pcnt_channel);
pcnt_channel_edge_action_t rising = PCNT_CHANNEL_EDGE_ACTION_HOLD;
pcnt_channel_edge_action_t falling = PCNT_CHANNEL_EDGE_ACTION_HOLD;
pcnt_count_mode_t rising = PCNT_COUNT_DIS, falling = PCNT_COUNT_DIS;
switch (this->rising_edge_mode) {
case PULSE_COUNTER_DISABLE:
rising = PCNT_CHANNEL_EDGE_ACTION_HOLD;
rising = PCNT_COUNT_DIS;
break;
case PULSE_COUNTER_INCREMENT:
rising = PCNT_CHANNEL_EDGE_ACTION_INCREASE;
rising = PCNT_COUNT_INC;
break;
case PULSE_COUNTER_DECREMENT:
rising = PCNT_CHANNEL_EDGE_ACTION_DECREASE;
rising = PCNT_COUNT_DEC;
break;
}
switch (this->falling_edge_mode) {
case PULSE_COUNTER_DISABLE:
falling = PCNT_CHANNEL_EDGE_ACTION_HOLD;
falling = PCNT_COUNT_DIS;
break;
case PULSE_COUNTER_INCREMENT:
falling = PCNT_CHANNEL_EDGE_ACTION_INCREASE;
falling = PCNT_COUNT_INC;
break;
case PULSE_COUNTER_DECREMENT:
falling = PCNT_CHANNEL_EDGE_ACTION_DECREASE;
falling = PCNT_COUNT_DEC;
break;
}
error = pcnt_channel_set_edge_action(this->pcnt_channel, rising, falling);
pcnt_config_t pcnt_config = {
.pulse_gpio_num = this->pin->get_pin(),
.ctrl_gpio_num = PCNT_PIN_NOT_USED,
.lctrl_mode = PCNT_MODE_KEEP,
.hctrl_mode = PCNT_MODE_KEEP,
.pos_mode = rising,
.neg_mode = falling,
.counter_h_lim = 0,
.counter_l_lim = 0,
.unit = this->pcnt_unit,
.channel = this->pcnt_channel,
};
esp_err_t error = pcnt_unit_config(&pcnt_config);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Setting PCNT edge action failed: %s", esp_err_to_name(error));
ESP_LOGE(TAG, "Configuring Pulse Counter failed: %s", esp_err_to_name(error));
return false;
}
if (this->filter_us != 0) {
uint32_t max_glitch_ns = PCNT_LL_MAX_GLITCH_WIDTH * 1000000u / (uint32_t) esp_clk_apb_freq();
pcnt_glitch_filter_config_t filter_config = {
.max_glitch_ns = std::min(this->filter_us * 1000u, max_glitch_ns),
};
error = pcnt_unit_set_glitch_filter(this->pcnt_unit, &filter_config);
uint16_t filter_val = std::min(static_cast<unsigned int>(this->filter_us * 80u), 1023u);
ESP_LOGCONFIG(TAG, " Filter Value: %" PRIu32 "us (val=%u)", this->filter_us, filter_val);
error = pcnt_set_filter_value(this->pcnt_unit, filter_val);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Setting PCNT glitch filter failed: %s", esp_err_to_name(error));
ESP_LOGE(TAG, "Setting filter value failed: %s", esp_err_to_name(error));
return false;
}
error = pcnt_filter_enable(this->pcnt_unit);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Enabling filter failed: %s", esp_err_to_name(error));
return false;
}
}
error = pcnt_unit_add_watch_point(this->pcnt_unit, INT16_MIN);
error = pcnt_counter_pause(this->pcnt_unit);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Adding PCNT low limit watch point failed: %s", esp_err_to_name(error));
ESP_LOGE(TAG, "Pausing pulse counter failed: %s", esp_err_to_name(error));
return false;
}
error = pcnt_unit_add_watch_point(this->pcnt_unit, INT16_MAX);
error = pcnt_counter_clear(this->pcnt_unit);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Adding PCNT high limit watch point failed: %s", esp_err_to_name(error));
ESP_LOGE(TAG, "Clearing pulse counter failed: %s", esp_err_to_name(error));
return false;
}
error = pcnt_unit_enable(this->pcnt_unit);
error = pcnt_counter_resume(this->pcnt_unit);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Enabling PCNT unit failed: %s", esp_err_to_name(error));
return false;
}
error = pcnt_unit_clear_count(this->pcnt_unit);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Clearing PCNT unit failed: %s", esp_err_to_name(error));
return false;
}
error = pcnt_unit_start(this->pcnt_unit);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Starting PCNT unit failed: %s", esp_err_to_name(error));
ESP_LOGE(TAG, "Resuming pulse counter failed: %s", esp_err_to_name(error));
return false;
}
return true;
}
pulse_counter_t HwPulseCounterStorage::read_raw_value() {
int count;
pcnt_unit_get_count(this->pcnt_unit, &count);
pulse_counter_t ret = count - this->last_value;
this->last_value = count;
pulse_counter_t counter;
pcnt_get_counter_value(this->pcnt_unit, &counter);
pulse_counter_t ret = counter - this->last_value;
this->last_value = counter;
return ret;
}
#endif // HAS_PCNT

View File

@@ -6,13 +6,14 @@
#include <cinttypes>
#if defined(USE_ESP32)
#include <soc/soc_caps.h>
#ifdef SOC_PCNT_SUPPORTED
#include <driver/pulse_cnt.h>
// TODO: Migrate from legacy PCNT API (driver/pcnt.h) to new PCNT API (driver/pulse_cnt.h)
// The legacy PCNT API is deprecated in ESP-IDF 5.x. Migration would allow removing the
// "driver" IDF component dependency. See:
// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/migration-guides/release-5.x/5.0/peripherals.html#id6
#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3)
#include <driver/pcnt.h>
#define HAS_PCNT
#endif // SOC_PCNT_SUPPORTED
#endif // USE_ESP32
#endif // defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3)
namespace esphome {
namespace pulse_counter {
@@ -23,7 +24,11 @@ enum PulseCounterCountMode {
PULSE_COUNTER_DECREMENT,
};
#ifdef HAS_PCNT
using pulse_counter_t = int16_t;
#else // HAS_PCNT
using pulse_counter_t = int32_t;
#endif // HAS_PCNT
struct PulseCounterStorageBase {
virtual bool pulse_counter_setup(InternalGPIOPin *pin) = 0;
@@ -53,8 +58,8 @@ struct HwPulseCounterStorage : public PulseCounterStorageBase {
bool pulse_counter_setup(InternalGPIOPin *pin) override;
pulse_counter_t read_raw_value() override;
pcnt_unit_handle_t pcnt_unit{nullptr};
pcnt_channel_handle_t pcnt_channel{nullptr};
pcnt_unit_t pcnt_unit;
pcnt_channel_t pcnt_channel;
};
#endif // HAS_PCNT

View File

@@ -129,7 +129,10 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
use_pcnt = config.get(CONF_USE_PCNT)
if CORE.is_esp32 and use_pcnt:
include_builtin_idf_component("esp_driver_pcnt")
# Re-enable ESP-IDF's legacy driver component (excluded by default to save compile time)
# Provides driver/pcnt.h header for hardware pulse counter API
# TODO: Remove this once pulse_counter migrates to new PCNT API (driver/pulse_cnt.h)
include_builtin_idf_component("driver")
var = await sensor.new_sensor(config, use_pcnt)
await cg.register_component(var, config)

View File

@@ -1,5 +1,5 @@
import esphome.codegen as cg
from esphome.components import audio, esp32, socket, speaker
from esphome.components import audio, esp32, speaker
import esphome.config_validation as cv
from esphome.const import (
CONF_BITS_PER_SAMPLE,
@@ -34,7 +34,7 @@ def _set_stream_limits(config):
return config
def _validate_audio_compatibility(config):
def _validate_audio_compatability(config):
inherit_property_from(CONF_BITS_PER_SAMPLE, CONF_OUTPUT_SPEAKER)(config)
inherit_property_from(CONF_NUM_CHANNELS, CONF_OUTPUT_SPEAKER)(config)
inherit_property_from(CONF_SAMPLE_RATE, CONF_OUTPUT_SPEAKER)(config)
@@ -73,13 +73,10 @@ CONFIG_SCHEMA = cv.All(
)
FINAL_VALIDATE_SCHEMA = _validate_audio_compatibility
FINAL_VALIDATE_SCHEMA = _validate_audio_compatability
async def to_code(config):
# Enable wake_loop_threadsafe for immediate command processing from other tasks
socket.require_wake_loop_threadsafe()
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await speaker.register_speaker(var, config)
@@ -89,11 +86,12 @@ async def to_code(config):
cg.add(var.set_buffer_duration(config[CONF_BUFFER_DURATION]))
if config.get(CONF_TASK_STACK_IN_PSRAM):
cg.add(var.set_task_stack_in_psram(True))
esp32.add_idf_sdkconfig_option(
"CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True
)
if task_stack_in_psram := config.get(CONF_TASK_STACK_IN_PSRAM):
cg.add(var.set_task_stack_in_psram(task_stack_in_psram))
if task_stack_in_psram and config[CONF_TASK_STACK_IN_PSRAM]:
esp32.add_idf_sdkconfig_option(
"CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True
)
cg.add(var.set_target_bits_per_sample(config[CONF_BITS_PER_SAMPLE]))
cg.add(var.set_target_sample_rate(config[CONF_SAMPLE_RATE]))

View File

@@ -4,8 +4,6 @@
#include "esphome/components/audio/audio_resampler.h"
#include "esphome/core/application.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -19,17 +17,13 @@ static const UBaseType_t RESAMPLER_TASK_PRIORITY = 1;
static const uint32_t TRANSFER_BUFFER_DURATION_MS = 50;
static const uint32_t TASK_DELAY_MS = 20;
static const uint32_t TASK_STACK_SIZE = 3072;
static const uint32_t STATE_TRANSITION_TIMEOUT_MS = 5000;
static const char *const TAG = "resampler_speaker";
enum ResamplingEventGroupBits : uint32_t {
COMMAND_STOP = (1 << 0), // signals stop request
COMMAND_START = (1 << 1), // signals start request
COMMAND_FINISH = (1 << 2), // signals finish request (graceful stop)
TASK_COMMAND_STOP = (1 << 5), // signals the task to stop
COMMAND_STOP = (1 << 0), // stops the resampler task
STATE_STARTING = (1 << 10),
STATE_RUNNING = (1 << 11),
STATE_STOPPING = (1 << 12),
@@ -40,16 +34,9 @@ enum ResamplingEventGroupBits : uint32_t {
ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits
};
void ResamplerSpeaker::dump_config() {
ESP_LOGCONFIG(TAG,
"Resampler Speaker:\n"
" Target Bits Per Sample: %u\n"
" Target Sample Rate: %" PRIu32 " Hz",
this->target_bits_per_sample_, this->target_sample_rate_);
}
void ResamplerSpeaker::setup() {
this->event_group_ = xEventGroupCreate();
if (this->event_group_ == nullptr) {
ESP_LOGE(TAG, "Failed to create event group");
this->mark_failed();
@@ -68,155 +55,81 @@ void ResamplerSpeaker::setup() {
this->audio_output_callback_(new_frames, write_timestamp);
}
});
// Start with loop disabled since no task is running and no commands are pending
this->disable_loop();
}
void ResamplerSpeaker::loop() {
uint32_t event_group_bits = xEventGroupGetBits(this->event_group_);
// Process commands with priority: STOP > FINISH > START
// This ensures stop commands take precedence over conflicting start commands
if (event_group_bits & ResamplingEventGroupBits::COMMAND_STOP) {
if (this->state_ == speaker::STATE_RUNNING || this->state_ == speaker::STATE_STARTING) {
// Clear STOP, START, and FINISH bits - stop takes precedence
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::COMMAND_STOP |
ResamplingEventGroupBits::COMMAND_START |
ResamplingEventGroupBits::COMMAND_FINISH);
this->waiting_for_output_ = false;
this->enter_stopping_state_();
} else if (this->state_ == speaker::STATE_STOPPED) {
// Already stopped, just clear the command bits
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::COMMAND_STOP |
ResamplingEventGroupBits::COMMAND_START |
ResamplingEventGroupBits::COMMAND_FINISH);
}
// Leave bits set if STATE_STOPPING - will be processed once stopped
} else if (event_group_bits & ResamplingEventGroupBits::COMMAND_FINISH) {
if (this->state_ == speaker::STATE_RUNNING) {
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::COMMAND_FINISH);
this->output_speaker_->finish();
} else if (this->state_ == speaker::STATE_STOPPED) {
// Already stopped, just clear the command bit
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::COMMAND_FINISH);
}
// Leave bit set if transitioning states - will be processed once state allows
} else if (event_group_bits & ResamplingEventGroupBits::COMMAND_START) {
if (this->state_ == speaker::STATE_STOPPED) {
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::COMMAND_START);
this->state_ = speaker::STATE_STARTING;
} else if (this->state_ == speaker::STATE_RUNNING) {
// Already running, just clear the command bit
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::COMMAND_START);
}
// Leave bit set if transitioning states - will be processed once state allows
}
// Re-read bits after command processing (enter_stopping_state_ may have set task bits)
event_group_bits = xEventGroupGetBits(this->event_group_);
if (event_group_bits & ResamplingEventGroupBits::STATE_STARTING) {
ESP_LOGD(TAG, "Starting");
ESP_LOGD(TAG, "Starting resampler task");
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::STATE_STARTING);
}
if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_NO_MEM) {
this->status_set_error(LOG_STR("Not enough memory"));
this->status_set_error(LOG_STR("Resampler task failed to allocate the internal buffers"));
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_NO_MEM);
this->enter_stopping_state_();
this->state_ = speaker::STATE_STOPPING;
}
if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_NOT_SUPPORTED) {
this->status_set_error(LOG_STR("Unsupported stream"));
this->status_set_error(LOG_STR("Cannot resample due to an unsupported audio stream"));
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_NOT_SUPPORTED);
this->enter_stopping_state_();
this->state_ = speaker::STATE_STOPPING;
}
if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_FAIL) {
this->status_set_error(LOG_STR("Resampler failure"));
this->status_set_error(LOG_STR("Resampler task failed"));
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_FAIL);
this->enter_stopping_state_();
this->state_ = speaker::STATE_STOPPING;
}
if (event_group_bits & ResamplingEventGroupBits::STATE_RUNNING) {
ESP_LOGV(TAG, "Started");
ESP_LOGD(TAG, "Started resampler task");
this->status_clear_error();
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::STATE_RUNNING);
}
if (event_group_bits & ResamplingEventGroupBits::STATE_STOPPING) {
ESP_LOGV(TAG, "Stopping");
ESP_LOGD(TAG, "Stopping resampler task");
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::STATE_STOPPING);
}
if (event_group_bits & ResamplingEventGroupBits::STATE_STOPPED) {
this->delete_task_();
ESP_LOGD(TAG, "Stopped");
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ALL_BITS);
if (this->delete_task_() == ESP_OK) {
ESP_LOGD(TAG, "Stopped resampler task");
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ALL_BITS);
}
}
switch (this->state_) {
case speaker::STATE_STARTING: {
if (!this->waiting_for_output_) {
esp_err_t err = this->start_();
if (err == ESP_OK) {
this->callback_remainder_ = 0; // reset callback remainder
this->status_clear_error();
this->waiting_for_output_ = true;
this->state_start_ms_ = App.get_loop_component_start_time();
} else {
this->set_start_error_(err);
this->waiting_for_output_ = false;
this->enter_stopping_state_();
}
esp_err_t err = this->start_();
if (err == ESP_OK) {
this->status_clear_error();
this->state_ = speaker::STATE_RUNNING;
} else {
if (this->output_speaker_->is_running()) {
this->state_ = speaker::STATE_RUNNING;
this->waiting_for_output_ = false;
} else if ((App.get_loop_component_start_time() - this->state_start_ms_) > STATE_TRANSITION_TIMEOUT_MS) {
// Timed out waiting for the output speaker to start
this->waiting_for_output_ = false;
this->enter_stopping_state_();
switch (err) {
case ESP_ERR_INVALID_STATE:
this->status_set_error(LOG_STR("Failed to start resampler: resampler task failed to start"));
break;
case ESP_ERR_NO_MEM:
this->status_set_error(LOG_STR("Failed to start resampler: not enough memory for task stack"));
default:
this->status_set_error(LOG_STR("Failed to start resampler"));
break;
}
this->state_ = speaker::STATE_STOPPING;
}
break;
}
case speaker::STATE_RUNNING:
if (this->output_speaker_->is_stopped()) {
this->enter_stopping_state_();
}
break;
case speaker::STATE_STOPPING: {
if ((this->output_speaker_->get_pause_state()) ||
((App.get_loop_component_start_time() - this->state_start_ms_) > STATE_TRANSITION_TIMEOUT_MS)) {
// If output speaker is paused or stopping timeout exceeded, force stop
this->output_speaker_->stop();
this->state_ = speaker::STATE_STOPPING;
}
if (this->output_speaker_->is_stopped() && (this->task_handle_ == nullptr)) {
// Only transition to stopped state once the output speaker and resampler task are fully stopped
this->waiting_for_output_ = false;
this->state_ = speaker::STATE_STOPPED;
}
break;
}
case speaker::STATE_STOPPING:
this->stop_();
this->state_ = speaker::STATE_STOPPED;
break;
case speaker::STATE_STOPPED:
event_group_bits = xEventGroupGetBits(this->event_group_);
if (event_group_bits == 0) {
// No pending events, disable loop to save CPU cycles
this->disable_loop();
}
break;
}
}
void ResamplerSpeaker::set_start_error_(esp_err_t err) {
switch (err) {
case ESP_ERR_INVALID_STATE:
this->status_set_error(LOG_STR("Task failed to start"));
break;
case ESP_ERR_NO_MEM:
this->status_set_error(LOG_STR("Not enough memory"));
break;
default:
this->status_set_error(LOG_STR("Failed to start"));
break;
}
}
@@ -230,33 +143,16 @@ size_t ResamplerSpeaker::play(const uint8_t *data, size_t length, TickType_t tic
if ((this->output_speaker_->is_running()) && (!this->requires_resampling_())) {
bytes_written = this->output_speaker_->play(data, length, ticks_to_wait);
} else {
std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
if (temp_ring_buffer) {
// Only write to the ring buffer if the reference is valid
if (this->ring_buffer_.use_count() == 1) {
std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
bytes_written = temp_ring_buffer->write_without_replacement(data, length, ticks_to_wait);
} else {
// Delay to avoid repeatedly hammering while waiting for the speaker to start
vTaskDelay(ticks_to_wait);
}
}
return bytes_written;
}
void ResamplerSpeaker::send_command_(uint32_t command_bit, bool wake_loop) {
this->enable_loop_soon_any_context();
uint32_t event_bits = xEventGroupGetBits(this->event_group_);
if (!(event_bits & command_bit)) {
xEventGroupSetBits(this->event_group_, command_bit);
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
if (wake_loop) {
App.wake_loop_threadsafe();
}
#endif
}
}
void ResamplerSpeaker::start() { this->send_command_(ResamplingEventGroupBits::COMMAND_START, true); }
void ResamplerSpeaker::start() { this->state_ = speaker::STATE_STARTING; }
esp_err_t ResamplerSpeaker::start_() {
this->target_stream_info_ = audio::AudioStreamInfo(
@@ -289,7 +185,7 @@ esp_err_t ResamplerSpeaker::start_task_() {
}
if (this->task_handle_ == nullptr) {
this->task_handle_ = xTaskCreateStatic(resample_task, "resampler", TASK_STACK_SIZE, (void *) this,
this->task_handle_ = xTaskCreateStatic(resample_task, "sample", TASK_STACK_SIZE, (void *) this,
RESAMPLER_TASK_PRIORITY, this->task_stack_buffer_, &this->task_stack_);
}
@@ -300,47 +196,43 @@ esp_err_t ResamplerSpeaker::start_task_() {
return ESP_OK;
}
void ResamplerSpeaker::stop() { this->send_command_(ResamplingEventGroupBits::COMMAND_STOP); }
void ResamplerSpeaker::stop() { this->state_ = speaker::STATE_STOPPING; }
void ResamplerSpeaker::enter_stopping_state_() {
this->state_ = speaker::STATE_STOPPING;
this->state_start_ms_ = App.get_loop_component_start_time();
void ResamplerSpeaker::stop_() {
if (this->task_handle_ != nullptr) {
xEventGroupSetBits(this->event_group_, ResamplingEventGroupBits::TASK_COMMAND_STOP);
xEventGroupSetBits(this->event_group_, ResamplingEventGroupBits::COMMAND_STOP);
}
this->output_speaker_->stop();
}
void ResamplerSpeaker::delete_task_() {
if (this->task_handle_ != nullptr) {
// Delete the suspended task
vTaskDelete(this->task_handle_);
esp_err_t ResamplerSpeaker::delete_task_() {
if (!this->task_created_) {
this->task_handle_ = nullptr;
}
if (this->task_stack_buffer_ != nullptr) {
// Deallocate the task stack buffer
if (this->task_stack_in_psram_) {
RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_EXTERNAL);
stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE);
} else {
RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_INTERNAL);
stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE);
if (this->task_stack_buffer_ != nullptr) {
if (this->task_stack_in_psram_) {
RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_EXTERNAL);
stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE);
} else {
RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_INTERNAL);
stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE);
}
this->task_stack_buffer_ = nullptr;
}
this->task_stack_buffer_ = nullptr;
return ESP_OK;
}
return ESP_ERR_INVALID_STATE;
}
void ResamplerSpeaker::finish() { this->send_command_(ResamplingEventGroupBits::COMMAND_FINISH); }
void ResamplerSpeaker::finish() { this->output_speaker_->finish(); }
bool ResamplerSpeaker::has_buffered_data() const {
bool has_ring_buffer_data = false;
if (this->requires_resampling_()) {
std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
if (temp_ring_buffer) {
has_ring_buffer_data = (temp_ring_buffer->available() > 0);
}
if (this->requires_resampling_() && (this->ring_buffer_.use_count() > 0)) {
has_ring_buffer_data = (this->ring_buffer_.lock()->available() > 0);
}
return (has_ring_buffer_data || this->output_speaker_->has_buffered_data());
}
@@ -361,8 +253,9 @@ bool ResamplerSpeaker::requires_resampling_() const {
}
void ResamplerSpeaker::resample_task(void *params) {
ResamplerSpeaker *this_resampler = static_cast<ResamplerSpeaker *>(params);
ResamplerSpeaker *this_resampler = (ResamplerSpeaker *) params;
this_resampler->task_created_ = true;
xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_STARTING);
std::unique_ptr<audio::AudioResampler> resampler =
@@ -376,7 +269,7 @@ void ResamplerSpeaker::resample_task(void *params) {
std::shared_ptr<RingBuffer> temp_ring_buffer =
RingBuffer::create(this_resampler->audio_stream_info_.ms_to_bytes(this_resampler->buffer_duration_ms_));
if (!temp_ring_buffer) {
if (temp_ring_buffer.use_count() == 0) {
err = ESP_ERR_NO_MEM;
} else {
this_resampler->ring_buffer_ = temp_ring_buffer;
@@ -398,7 +291,7 @@ void ResamplerSpeaker::resample_task(void *params) {
while (err == ESP_OK) {
uint32_t event_bits = xEventGroupGetBits(this_resampler->event_group_);
if (event_bits & ResamplingEventGroupBits::TASK_COMMAND_STOP) {
if (event_bits & ResamplingEventGroupBits::COMMAND_STOP) {
break;
}
@@ -417,8 +310,8 @@ void ResamplerSpeaker::resample_task(void *params) {
xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_STOPPING);
resampler.reset();
xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_STOPPED);
vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it
this_resampler->task_created_ = false;
vTaskDelete(nullptr);
}
} // namespace resampler

View File

@@ -8,16 +8,14 @@
#include "esphome/core/component.h"
#include <freertos/FreeRTOS.h>
#include <freertos/event_groups.h>
#include <freertos/FreeRTOS.h>
namespace esphome {
namespace resampler {
class ResamplerSpeaker : public Component, public speaker::Speaker {
public:
float get_setup_priority() const override { return esphome::setup_priority::DATA; }
void dump_config() override;
void setup() override;
void loop() override;
@@ -67,18 +65,13 @@ class ResamplerSpeaker : public Component, public speaker::Speaker {
/// ESP_ERR_INVALID_STATE if the task wasn't created
esp_err_t start_task_();
/// @brief Transitions to STATE_STOPPING, records the stopping timestamp, sends the task stop command if the task is
/// running, and stops the output speaker.
void enter_stopping_state_();
/// @brief Stops the output speaker. If the resampling task is running, it sends the stop command.
void stop_();
/// @brief Sets the appropriate status error based on the start failure reason.
void set_start_error_(esp_err_t err);
/// @brief Deletes the resampler task if suspended, deallocates the task stack, and resets the related pointers.
void delete_task_();
/// @brief Sends a command via event group bits, enables the loop, and optionally wakes the main loop.
void send_command_(uint32_t command_bit, bool wake_loop = false);
/// @brief Deallocates the task stack and resets the pointers.
/// @return ESP_OK if successful
/// ESP_ERR_INVALID_STATE if the task hasn't stopped itself
esp_err_t delete_task_();
inline bool requires_resampling_() const;
static void resample_task(void *params);
@@ -90,7 +83,7 @@ class ResamplerSpeaker : public Component, public speaker::Speaker {
speaker::Speaker *output_speaker_{nullptr};
bool task_stack_in_psram_{false};
bool waiting_for_output_{false};
bool task_created_{false};
TaskHandle_t task_handle_{nullptr};
StaticTask_t task_stack_;
@@ -105,7 +98,6 @@ class ResamplerSpeaker : public Component, public speaker::Speaker {
uint32_t target_sample_rate_;
uint32_t buffer_duration_ms_;
uint32_t state_start_ms_{0};
uint64_t callback_remainder_{0};
};

View File

@@ -71,10 +71,6 @@ RTL87XX_BOARDS = {
"name": "WR3L Wi-Fi Module",
"family": FAMILY_RTL8710B,
},
"wbru": {
"name": "WBRU Wi-Fi Module",
"family": FAMILY_RTL8720C,
},
"wr2le": {
"name": "WR2LE Wi-Fi Module",
"family": FAMILY_RTL8710B,
@@ -87,14 +83,6 @@ RTL87XX_BOARDS = {
"name": "T103_V1.0",
"family": FAMILY_RTL8710B,
},
"cr3l": {
"name": "CR3L Wi-Fi Module",
"family": FAMILY_RTL8720C,
},
"generic-rtl8720cm-4mb-1712k": {
"name": "Generic - RTL8720CM (4M/1712k)",
"family": FAMILY_RTL8720C,
},
"generic-rtl8720cf-2mb-896k": {
"name": "Generic - RTL8720CF (2M/896k)",
"family": FAMILY_RTL8720C,
@@ -115,10 +103,6 @@ RTL87XX_BOARDS = {
"name": "WR2L Wi-Fi Module",
"family": FAMILY_RTL8710B,
},
"wbr1": {
"name": "WBR1 Wi-Fi Module",
"family": FAMILY_RTL8720C,
},
"wr1": {
"name": "WR1 Wi-Fi Module",
"family": FAMILY_RTL8710B,
@@ -135,10 +119,10 @@ RTL87XX_BOARD_PINS = {
"SPI1_MISO": 22,
"SPI1_MOSI": 23,
"SPI1_SCK": 18,
"WIRE0_SCL_0": 22,
"WIRE0_SCL_1": 29,
"WIRE0_SDA_0": 19,
"WIRE0_SDA_1": 30,
"WIRE0_SCL_0": 29,
"WIRE0_SCL_1": 22,
"WIRE0_SDA_0": 30,
"WIRE0_SDA_1": 19,
"WIRE1_SCL": 18,
"WIRE1_SDA": 23,
"SERIAL0_CTS": 19,
@@ -246,10 +230,10 @@ RTL87XX_BOARD_PINS = {
"A1": 41,
},
"wbr3": {
"WIRE0_SCL_0": 2,
"WIRE0_SCL_1": 11,
"WIRE0_SCL_2": 15,
"WIRE0_SCL_3": 19,
"WIRE0_SCL_0": 11,
"WIRE0_SCL_1": 2,
"WIRE0_SCL_2": 19,
"WIRE0_SCL_3": 15,
"WIRE0_SDA_0": 3,
"WIRE0_SDA_1": 12,
"WIRE0_SDA_2": 16,
@@ -258,10 +242,10 @@ RTL87XX_BOARD_PINS = {
"SERIAL0_TX_0": 11,
"SERIAL0_TX_1": 14,
"SERIAL1_CTS": 4,
"SERIAL1_RX_0": 0,
"SERIAL1_RX_1": 2,
"SERIAL1_TX_0": 1,
"SERIAL1_TX_1": 3,
"SERIAL1_RX_0": 2,
"SERIAL1_RX_1": 0,
"SERIAL1_TX_0": 3,
"SERIAL1_TX_1": 1,
"SERIAL2_CTS": 19,
"SERIAL2_RX": 15,
"SERIAL2_TX": 16,
@@ -312,12 +296,6 @@ RTL87XX_BOARD_PINS = {
},
"generic-rtl8710bn-2mb-468k": {
"SPI0_CS": 19,
"SPI0_FCS": 6,
"SPI0_FD0": 9,
"SPI0_FD1": 7,
"SPI0_FD2": 8,
"SPI0_FD3": 11,
"SPI0_FSCK": 10,
"SPI0_MISO": 22,
"SPI0_MOSI": 23,
"SPI0_SCK": 18,
@@ -418,10 +396,10 @@ RTL87XX_BOARD_PINS = {
"SPI1_MISO": 22,
"SPI1_MOSI": 23,
"SPI1_SCK": 18,
"WIRE0_SCL_0": 22,
"WIRE0_SCL_1": 29,
"WIRE0_SDA_0": 19,
"WIRE0_SDA_1": 30,
"WIRE0_SCL_0": 29,
"WIRE0_SCL_1": 22,
"WIRE0_SDA_0": 30,
"WIRE0_SDA_1": 19,
"WIRE1_SCL": 18,
"WIRE1_SDA": 23,
"SERIAL0_CTS": 19,
@@ -485,10 +463,10 @@ RTL87XX_BOARD_PINS = {
"SPI1_MISO": 22,
"SPI1_MOSI": 23,
"SPI1_SCK": 18,
"WIRE0_SCL_0": 22,
"WIRE0_SCL_1": 29,
"WIRE0_SDA_0": 19,
"WIRE0_SDA_1": 30,
"WIRE0_SCL_0": 29,
"WIRE0_SCL_1": 22,
"WIRE0_SDA_0": 30,
"WIRE0_SDA_1": 19,
"WIRE1_SCL": 18,
"WIRE1_SDA": 23,
"SERIAL0_CTS": 19,
@@ -736,12 +714,6 @@ RTL87XX_BOARD_PINS = {
},
"generic-rtl8710bn-2mb-788k": {
"SPI0_CS": 19,
"SPI0_FCS": 6,
"SPI0_FD0": 9,
"SPI0_FD1": 7,
"SPI0_FD2": 8,
"SPI0_FD3": 11,
"SPI0_FSCK": 10,
"SPI0_MISO": 22,
"SPI0_MOSI": 23,
"SPI0_SCK": 18,
@@ -835,12 +807,6 @@ RTL87XX_BOARD_PINS = {
},
"generic-rtl8710bx-4mb-980k": {
"SPI0_CS": 19,
"SPI0_FCS": 6,
"SPI0_FD0": 9,
"SPI0_FD1": 7,
"SPI0_FD2": 8,
"SPI0_FD3": 11,
"SPI0_FSCK": 10,
"SPI0_MISO": 22,
"SPI0_MOSI": 23,
"SPI0_SCK": 18,
@@ -991,8 +957,8 @@ RTL87XX_BOARD_PINS = {
"SPI1_MISO": 22,
"SPI1_MOSI": 23,
"SPI1_SCK": 18,
"WIRE0_SCL_0": 22,
"WIRE0_SCL_1": 29,
"WIRE0_SCL_0": 29,
"WIRE0_SCL_1": 22,
"WIRE0_SDA_0": 19,
"WIRE0_SDA_1": 30,
"WIRE1_SCL": 18,
@@ -1122,99 +1088,6 @@ RTL87XX_BOARD_PINS = {
"A0": 19,
"A1": 41,
},
"wbru": {
"SPI0_CS_0": 2,
"SPI0_CS_1": 7,
"SPI0_CS_2": 15,
"SPI0_MISO_0": 10,
"SPI0_MISO_1": 20,
"SPI0_MOSI_0": 4,
"SPI0_MOSI_1": 9,
"SPI0_MOSI_2": 19,
"SPI0_SCK_0": 3,
"SPI0_SCK_1": 8,
"SPI0_SCK_2": 16,
"WIRE0_SCL_0": 2,
"WIRE0_SCL_1": 11,
"WIRE0_SCL_2": 15,
"WIRE0_SCL_3": 19,
"WIRE0_SDA_0": 3,
"WIRE0_SDA_1": 12,
"WIRE0_SDA_2": 16,
"WIRE0_SDA_3": 20,
"SERIAL0_CTS": 10,
"SERIAL0_RTS": 9,
"SERIAL0_RX_0": 12,
"SERIAL0_RX_1": 13,
"SERIAL0_TX_0": 11,
"SERIAL0_TX_1": 14,
"SERIAL1_CTS": 4,
"SERIAL1_RX_0": 0,
"SERIAL1_RX_1": 2,
"SERIAL1_TX": 3,
"SERIAL2_CTS": 19,
"SERIAL2_RTS": 20,
"SERIAL2_RX": 15,
"SERIAL2_TX": 16,
"CS0": 7,
"CTS0": 10,
"CTS1": 4,
"CTS2": 19,
"MOSI0": 19,
"PA00": 0,
"PA0": 0,
"PA02": 2,
"PA2": 2,
"PA03": 3,
"PA3": 3,
"PA04": 4,
"PA4": 4,
"PA07": 7,
"PA7": 7,
"PA08": 8,
"PA8": 8,
"PA09": 9,
"PA9": 9,
"PA10": 10,
"PA11": 11,
"PA12": 12,
"PA13": 13,
"PA14": 14,
"PA15": 15,
"PA16": 16,
"PA17": 17,
"PA18": 18,
"PA19": 19,
"PA20": 20,
"PWM0": 0,
"PWM1": 12,
"PWM5": 17,
"PWM6": 18,
"RTS0": 9,
"RTS2": 20,
"RX2": 15,
"SCK0": 16,
"TX1": 3,
"TX2": 16,
"D0": 8,
"D1": 9,
"D2": 2,
"D3": 3,
"D4": 4,
"D5": 15,
"D6": 16,
"D7": 11,
"D8": 12,
"D9": 17,
"D10": 18,
"D11": 19,
"D12": 14,
"D13": 13,
"D14": 20,
"D15": 0,
"D16": 10,
"D17": 7,
},
"wr2le": {
"MISO0": 22,
"MISO1": 22,
@@ -1243,21 +1116,21 @@ RTL87XX_BOARD_PINS = {
"SPI0_MISO": 20,
"SPI0_MOSI_0": 4,
"SPI0_MOSI_1": 19,
"SPI0_SCK_0": 3,
"SPI0_SCK_1": 16,
"SPI0_SCK_0": 16,
"SPI0_SCK_1": 3,
"WIRE0_SCL_0": 2,
"WIRE0_SCL_1": 15,
"WIRE0_SCL_2": 19,
"WIRE0_SDA_0": 3,
"WIRE0_SDA_0": 20,
"WIRE0_SDA_1": 16,
"WIRE0_SDA_2": 20,
"WIRE0_SDA_2": 3,
"SERIAL0_RX": 13,
"SERIAL0_TX": 14,
"SERIAL1_CTS": 4,
"SERIAL1_RX_0": 0,
"SERIAL1_RX_1": 2,
"SERIAL1_TX_0": 1,
"SERIAL1_TX_1": 3,
"SERIAL1_RX_0": 2,
"SERIAL1_RX_1": 0,
"SERIAL1_TX_0": 3,
"SERIAL1_TX_1": 1,
"SERIAL2_CTS": 19,
"SERIAL2_RTS": 20,
"SERIAL2_RX": 15,
@@ -1378,168 +1251,6 @@ RTL87XX_BOARD_PINS = {
"A0": 19,
"A1": 41,
},
"cr3l": {
"SPI0_CS_0": 2,
"SPI0_CS_1": 15,
"SPI0_MISO": 20,
"SPI0_MOSI_0": 4,
"SPI0_MOSI_1": 19,
"SPI0_SCK_0": 3,
"SPI0_SCK_1": 16,
"WIRE0_SCL_0": 2,
"WIRE0_SCL_1": 15,
"WIRE0_SCL_2": 19,
"WIRE0_SDA_0": 3,
"WIRE0_SDA_1": 16,
"WIRE0_SDA_2": 20,
"SERIAL0_RX": 13,
"SERIAL0_TX": 14,
"SERIAL1_CTS": 4,
"SERIAL1_RX": 2,
"SERIAL1_TX": 3,
"SERIAL2_CTS": 19,
"SERIAL2_RTS": 20,
"SERIAL2_RX": 15,
"SERIAL2_TX": 16,
"CTS1": 4,
"CTS2": 19,
"MISO0": 20,
"PA02": 2,
"PA2": 2,
"PA03": 3,
"PA3": 3,
"PA04": 4,
"PA4": 4,
"PA13": 13,
"PA14": 14,
"PA15": 15,
"PA16": 16,
"PA17": 17,
"PA18": 18,
"PA19": 19,
"PA20": 20,
"PWM0": 20,
"PWM5": 17,
"PWM6": 18,
"RTS2": 20,
"RX0": 13,
"RX1": 2,
"RX2": 15,
"SCL0": 19,
"SDA0": 16,
"TX0": 14,
"TX1": 3,
"TX2": 16,
"D0": 20,
"D1": 2,
"D2": 3,
"D3": 4,
"D4": 15,
"D5": 16,
"D6": 17,
"D7": 18,
"D8": 19,
"D9": 13,
"D10": 14,
},
"generic-rtl8720cm-4mb-1712k": {
"SPI0_CS_0": 2,
"SPI0_CS_1": 7,
"SPI0_CS_2": 15,
"SPI0_MISO_0": 10,
"SPI0_MISO_1": 20,
"SPI0_MOSI_0": 4,
"SPI0_MOSI_1": 9,
"SPI0_MOSI_2": 19,
"SPI0_SCK_0": 3,
"SPI0_SCK_1": 8,
"SPI0_SCK_2": 16,
"WIRE0_SCL_0": 2,
"WIRE0_SCL_1": 11,
"WIRE0_SCL_2": 15,
"WIRE0_SCL_3": 19,
"WIRE0_SDA_0": 3,
"WIRE0_SDA_1": 12,
"WIRE0_SDA_2": 16,
"WIRE0_SDA_3": 20,
"SERIAL0_CTS": 10,
"SERIAL0_RTS": 9,
"SERIAL0_RX_0": 12,
"SERIAL0_RX_1": 13,
"SERIAL0_TX_0": 11,
"SERIAL0_TX_1": 14,
"SERIAL1_CTS": 4,
"SERIAL1_RX_0": 0,
"SERIAL1_RX_1": 2,
"SERIAL1_TX_0": 1,
"SERIAL1_TX_1": 3,
"SERIAL2_CTS": 19,
"SERIAL2_RTS": 20,
"SERIAL2_RX": 15,
"SERIAL2_TX": 16,
"CS0": 15,
"CTS0": 10,
"CTS1": 4,
"CTS2": 19,
"MOSI0": 19,
"PA00": 0,
"PA0": 0,
"PA01": 1,
"PA1": 1,
"PA02": 2,
"PA2": 2,
"PA03": 3,
"PA3": 3,
"PA04": 4,
"PA4": 4,
"PA07": 7,
"PA7": 7,
"PA08": 8,
"PA8": 8,
"PA09": 9,
"PA9": 9,
"PA10": 10,
"PA11": 11,
"PA12": 12,
"PA13": 13,
"PA14": 14,
"PA15": 15,
"PA16": 16,
"PA17": 17,
"PA18": 18,
"PA19": 19,
"PA20": 20,
"PA23": 23,
"PWM0": 20,
"PWM5": 17,
"PWM6": 18,
"PWM7": 23,
"RTS0": 9,
"RTS2": 20,
"RX2": 15,
"SCK0": 16,
"TX2": 16,
"D0": 0,
"D1": 1,
"D2": 2,
"D3": 3,
"D4": 4,
"D5": 7,
"D6": 8,
"D7": 9,
"D8": 10,
"D9": 11,
"D10": 12,
"D11": 13,
"D12": 14,
"D13": 15,
"D14": 16,
"D15": 17,
"D16": 18,
"D17": 19,
"D18": 20,
"D19": 23,
},
"generic-rtl8720cf-2mb-896k": {
"SPI0_CS_0": 2,
"SPI0_CS_1": 7,
@@ -1745,8 +1456,8 @@ RTL87XX_BOARD_PINS = {
"SPI1_MISO": 22,
"SPI1_MOSI": 23,
"SPI1_SCK": 18,
"WIRE0_SCL_0": 22,
"WIRE0_SCL_1": 29,
"WIRE0_SCL_0": 29,
"WIRE0_SCL_1": 22,
"WIRE0_SDA_0": 19,
"WIRE0_SDA_1": 30,
"WIRE1_SCL": 18,
@@ -1874,65 +1585,6 @@ RTL87XX_BOARD_PINS = {
"D4": 12,
"A0": 19,
},
"wbr1": {
"WIRE0_SCL_0": 2,
"WIRE0_SCL_1": 11,
"WIRE0_SCL_2": 15,
"WIRE0_SDA_0": 3,
"WIRE0_SDA_1": 12,
"WIRE0_SDA_2": 16,
"SERIAL0_RX_0": 12,
"SERIAL0_RX_1": 13,
"SERIAL0_TX_0": 11,
"SERIAL0_TX_1": 14,
"SERIAL1_CTS": 4,
"SERIAL1_RX_0": 0,
"SERIAL1_RX_1": 2,
"SERIAL1_TX_0": 1,
"SERIAL1_TX_1": 3,
"SERIAL2_RX": 15,
"SERIAL2_TX": 16,
"CTS1": 4,
"MOSI0": 4,
"PA00": 0,
"PA0": 0,
"PA01": 1,
"PA1": 1,
"PA02": 2,
"PA2": 2,
"PA03": 3,
"PA3": 3,
"PA04": 4,
"PA4": 4,
"PA11": 11,
"PA12": 12,
"PA13": 13,
"PA14": 14,
"PA15": 15,
"PA16": 16,
"PA17": 17,
"PA18": 18,
"PWM5": 17,
"PWM6": 18,
"PWM7": 13,
"RX2": 15,
"SCL0": 15,
"SDA0": 12,
"TX2": 16,
"D0": 14,
"D1": 13,
"D2": 2,
"D3": 3,
"D4": 16,
"D5": 4,
"D6": 11,
"D7": 15,
"D8": 12,
"D9": 17,
"D10": 18,
"D11": 0,
"D12": 1,
},
"wr1": {
"SPI0_CS": 19,
"SPI0_MISO": 22,
@@ -1942,10 +1594,10 @@ RTL87XX_BOARD_PINS = {
"SPI1_MISO": 22,
"SPI1_MOSI": 23,
"SPI1_SCK": 18,
"WIRE0_SCL_0": 22,
"WIRE0_SCL_1": 29,
"WIRE0_SDA_0": 19,
"WIRE0_SDA_1": 30,
"WIRE0_SCL_0": 29,
"WIRE0_SCL_1": 22,
"WIRE0_SDA_0": 30,
"WIRE0_SDA_1": 19,
"WIRE1_SCL": 18,
"WIRE1_SDA": 23,
"SERIAL0_CTS": 19,

View File

@@ -2,7 +2,6 @@
#include <cmath>
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
namespace esphome {
namespace rtttl {
@@ -376,13 +375,22 @@ void Rtttl::loop() {
}
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
// RTTTL state strings indexed by State enum (0-4): STOPPED, INIT, STARTING, RUNNING, STOPPING
PROGMEM_STRING_TABLE(RtttlStateStrings, "STATE_STOPPED", "STATE_INIT", "STATE_STARTING", "STATE_RUNNING",
"STATE_STOPPING", "UNKNOWN");
static const LogString *state_to_string(State state) {
return RtttlStateStrings::get_log_str(static_cast<uint8_t>(state), RtttlStateStrings::LAST_INDEX);
}
switch (state) {
case STATE_STOPPED:
return LOG_STR("STATE_STOPPED");
case STATE_STARTING:
return LOG_STR("STATE_STARTING");
case STATE_RUNNING:
return LOG_STR("STATE_RUNNING");
case STATE_STOPPING:
return LOG_STR("STATE_STOPPING");
case STATE_INIT:
return LOG_STR("STATE_INIT");
default:
return LOG_STR("UNKNOWN");
}
};
#endif
void Rtttl::set_state_(State state) {

View File

@@ -78,21 +78,23 @@ class Select : public EntityBase {
void add_on_state_callback(std::function<void(size_t)> &&callback);
/** Set the value of the select by index, this is an optional virtual method.
*
* This method is called by the SelectCall when the index is already known.
* Default implementation converts to string and calls control().
* Override this to work directly with indices and avoid string conversions.
*
* @param index The index as validated by the SelectCall.
*/
virtual void control(size_t index) { this->control(this->option_at(index)); }
protected:
friend class SelectCall;
size_t active_index_{0};
/** Set the value of the select by index, this is an optional virtual method.
*
* IMPORTANT: At least ONE of the two control() methods must be overridden by derived classes.
* Overriding this index-based version is PREFERRED as it avoids string conversions.
*
* This method is called by the SelectCall when the index is already known.
* Default implementation converts to string and calls control(const std::string&).
*
* @param index The index as validated by the SelectCall.
*/
virtual void control(size_t index) { this->control(this->option_at(index)); }
/** Set the value of the select, this is a virtual method that each select integration can implement.
*
* IMPORTANT: At least ONE of the two control() methods must be overridden by derived classes.

View File

@@ -25,7 +25,7 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_SPEED): cv.invalid(
"Configuring individual speeds is deprecated."
),
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1, max=255),
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1),
cv.Optional(CONF_PRESET_MODES): validate_preset_modes,
}
)

View File

@@ -10,7 +10,7 @@ namespace speed {
class SpeedFan : public Component, public fan::Fan {
public:
SpeedFan(uint8_t speed_count) : speed_count_(speed_count) {}
SpeedFan(int speed_count) : speed_count_(speed_count) {}
void setup() override;
void dump_config() override;
void set_output(output::FloatOutput *output) { this->output_ = output; }
@@ -26,7 +26,7 @@ class SpeedFan : public Component, public fan::Fan {
output::FloatOutput *output_;
output::BinaryOutput *oscillating_{nullptr};
output::BinaryOutput *direction_{nullptr};
uint8_t speed_count_{};
int speed_count_{};
fan::FanTraits traits_;
std::vector<const char *> preset_modes_{};
};

View File

@@ -4,7 +4,6 @@
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include <cinttypes>
#include <utility>
@@ -1545,19 +1544,42 @@ void Sprinkler::log_multiplier_zero_warning_(const LogString *method_name) {
ESP_LOGW(TAG, "%s called but multiplier is set to zero; no action taken", LOG_STR_ARG(method_name));
}
// Request origin strings indexed by SprinklerValveRunRequestOrigin enum (0-2): USER, CYCLE, QUEUE
PROGMEM_STRING_TABLE(SprinklerRequestOriginStrings, "USER", "CYCLE", "QUEUE", "UNKNOWN");
const LogString *Sprinkler::req_as_str_(SprinklerValveRunRequestOrigin origin) {
return SprinklerRequestOriginStrings::get_log_str(static_cast<uint8_t>(origin),
SprinklerRequestOriginStrings::LAST_INDEX);
switch (origin) {
case USER:
return LOG_STR("USER");
case CYCLE:
return LOG_STR("CYCLE");
case QUEUE:
return LOG_STR("QUEUE");
default:
return LOG_STR("UNKNOWN");
}
}
// Sprinkler state strings indexed by SprinklerState enum (0-4): IDLE, STARTING, ACTIVE, STOPPING, BYPASS
PROGMEM_STRING_TABLE(SprinklerStateStrings, "IDLE", "STARTING", "ACTIVE", "STOPPING", "BYPASS", "UNKNOWN");
const LogString *Sprinkler::state_as_str_(SprinklerState state) {
return SprinklerStateStrings::get_log_str(static_cast<uint8_t>(state), SprinklerStateStrings::LAST_INDEX);
switch (state) {
case IDLE:
return LOG_STR("IDLE");
case STARTING:
return LOG_STR("STARTING");
case ACTIVE:
return LOG_STR("ACTIVE");
case STOPPING:
return LOG_STR("STOPPING");
case BYPASS:
return LOG_STR("BYPASS");
default:
return LOG_STR("UNKNOWN");
}
}
void Sprinkler::start_timer_(const SprinklerTimerIndex timer_index) {

View File

@@ -1,7 +1,6 @@
#include "ssd1306_base.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
namespace esphome {
namespace ssd1306_base {
@@ -41,55 +40,6 @@ static const uint8_t SSD1305_COMMAND_SET_AREA_COLOR = 0xD8;
static const uint8_t SH1107_COMMAND_SET_START_LINE = 0xDC;
static const uint8_t SH1107_COMMAND_CHARGE_PUMP = 0xAD;
// Verify first enum value and table sizes match SSD1306_MODEL_COUNT
static_assert(SSD1306_MODEL_128_32 == 0, "SSD1306Model enum must start at 0");
// PROGMEM lookup table indexed by SSD1306Model enum (width, height per model)
struct ModelDimensions {
uint8_t width;
uint8_t height;
};
static const ModelDimensions MODEL_DIMS[] PROGMEM = {
{128, 32}, // SSD1306_MODEL_128_32
{128, 64}, // SSD1306_MODEL_128_64
{96, 16}, // SSD1306_MODEL_96_16
{64, 48}, // SSD1306_MODEL_64_48
{64, 32}, // SSD1306_MODEL_64_32
{72, 40}, // SSD1306_MODEL_72_40
{128, 32}, // SH1106_MODEL_128_32
{128, 64}, // SH1106_MODEL_128_64
{96, 16}, // SH1106_MODEL_96_16
{64, 48}, // SH1106_MODEL_64_48
{64, 128}, // SH1107_MODEL_128_64 (note: width is 64, height is 128)
{128, 128}, // SH1107_MODEL_128_128
{128, 32}, // SSD1305_MODEL_128_32
{128, 64}, // SSD1305_MODEL_128_64
};
// clang-format off
PROGMEM_STRING_TABLE(ModelStrings,
"SSD1306 128x32", // SSD1306_MODEL_128_32
"SSD1306 128x64", // SSD1306_MODEL_128_64
"SSD1306 96x16", // SSD1306_MODEL_96_16
"SSD1306 64x48", // SSD1306_MODEL_64_48
"SSD1306 64x32", // SSD1306_MODEL_64_32
"SSD1306 72x40", // SSD1306_MODEL_72_40
"SH1106 128x32", // SH1106_MODEL_128_32
"SH1106 128x64", // SH1106_MODEL_128_64
"SH1106 96x16", // SH1106_MODEL_96_16
"SH1106 64x48", // SH1106_MODEL_64_48
"SH1107 128x64", // SH1107_MODEL_128_64
"SH1107 128x128", // SH1107_MODEL_128_128
"SSD1305 128x32", // SSD1305_MODEL_128_32
"SSD1305 128x64", // SSD1305_MODEL_128_64
"Unknown" // fallback
);
// clang-format on
static_assert(sizeof(MODEL_DIMS) / sizeof(MODEL_DIMS[0]) == SSD1306_MODEL_COUNT,
"MODEL_DIMS must have one entry per SSD1306Model");
static_assert(ModelStrings::COUNT == SSD1306_MODEL_COUNT + 1,
"ModelStrings must have one entry per SSD1306Model plus fallback");
void SSD1306::setup() {
this->init_internal_(this->get_buffer_length_());
@@ -324,14 +274,54 @@ void SSD1306::turn_off() {
this->is_on_ = false;
}
int SSD1306::get_height_internal() {
if (this->model_ >= SSD1306_MODEL_COUNT)
return 0;
return progmem_read_byte(&MODEL_DIMS[this->model_].height);
switch (this->model_) {
case SH1107_MODEL_128_64:
case SH1107_MODEL_128_128:
return 128;
case SSD1306_MODEL_128_32:
case SSD1306_MODEL_64_32:
case SH1106_MODEL_128_32:
case SSD1305_MODEL_128_32:
return 32;
case SSD1306_MODEL_128_64:
case SH1106_MODEL_128_64:
case SSD1305_MODEL_128_64:
return 64;
case SSD1306_MODEL_96_16:
case SH1106_MODEL_96_16:
return 16;
case SSD1306_MODEL_64_48:
case SH1106_MODEL_64_48:
return 48;
case SSD1306_MODEL_72_40:
return 40;
default:
return 0;
}
}
int SSD1306::get_width_internal() {
if (this->model_ >= SSD1306_MODEL_COUNT)
return 0;
return progmem_read_byte(&MODEL_DIMS[this->model_].width);
switch (this->model_) {
case SSD1306_MODEL_128_32:
case SH1106_MODEL_128_32:
case SSD1306_MODEL_128_64:
case SH1106_MODEL_128_64:
case SSD1305_MODEL_128_32:
case SSD1305_MODEL_128_64:
case SH1107_MODEL_128_128:
return 128;
case SSD1306_MODEL_96_16:
case SH1106_MODEL_96_16:
return 96;
case SSD1306_MODEL_64_48:
case SSD1306_MODEL_64_32:
case SH1106_MODEL_64_48:
case SH1107_MODEL_128_64:
return 64;
case SSD1306_MODEL_72_40:
return 72;
default:
return 0;
}
}
size_t SSD1306::get_buffer_length_() {
return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / 8u;
@@ -371,8 +361,37 @@ void SSD1306::init_reset_() {
this->reset_pin_->digital_write(true);
}
}
const LogString *SSD1306::model_str_() {
return ModelStrings::get_log_str(static_cast<uint8_t>(this->model_), ModelStrings::LAST_INDEX);
const char *SSD1306::model_str_() {
switch (this->model_) {
case SSD1306_MODEL_128_32:
return "SSD1306 128x32";
case SSD1306_MODEL_128_64:
return "SSD1306 128x64";
case SSD1306_MODEL_64_32:
return "SSD1306 64x32";
case SSD1306_MODEL_96_16:
return "SSD1306 96x16";
case SSD1306_MODEL_64_48:
return "SSD1306 64x48";
case SSD1306_MODEL_72_40:
return "SSD1306 72x40";
case SH1106_MODEL_128_32:
return "SH1106 128x32";
case SH1106_MODEL_128_64:
return "SH1106 128x64";
case SH1106_MODEL_96_16:
return "SH1106 96x16";
case SH1106_MODEL_64_48:
return "SH1106 64x48";
case SH1107_MODEL_128_64:
return "SH1107 128x64";
case SSD1305_MODEL_128_32:
return "SSD1305 128x32";
case SSD1305_MODEL_128_64:
return "SSD1305 128x64";
default:
return "Unknown";
}
}
} // namespace ssd1306_base

View File

@@ -22,9 +22,6 @@ enum SSD1306Model {
SH1107_MODEL_128_128,
SSD1305_MODEL_128_32,
SSD1305_MODEL_128_64,
// When adding a new model, add it before SSD1306_MODEL_COUNT and update
// MODEL_DIMS and ModelStrings tables in ssd1306_base.cpp
SSD1306_MODEL_COUNT, // must be last
};
class SSD1306 : public display::DisplayBuffer {
@@ -73,7 +70,7 @@ class SSD1306 : public display::DisplayBuffer {
int get_height_internal() override;
int get_width_internal() override;
size_t get_buffer_length_();
const LogString *model_str_();
const char *model_str_();
SSD1306Model model_{SSD1306_MODEL_128_64};
GPIOPin *reset_pin_{nullptr};

View File

@@ -28,7 +28,7 @@ void I2CSSD1306::dump_config() {
" Offset X: %d\n"
" Offset Y: %d\n"
" Inverted Color: %s",
LOG_STR_ARG(this->model_str_()), YESNO(this->external_vcc_), YESNO(this->flip_x_), YESNO(this->flip_y_),
this->model_str_(), YESNO(this->external_vcc_), YESNO(this->flip_x_), YESNO(this->flip_y_),
this->offset_x_, this->offset_y_, YESNO(this->invert_));
LOG_I2C_DEVICE(this);
LOG_PIN(" Reset Pin: ", this->reset_pin_);

View File

@@ -24,7 +24,7 @@ void SPISSD1306::dump_config() {
" Offset X: %d\n"
" Offset Y: %d\n"
" Inverted Color: %s",
LOG_STR_ARG(this->model_str_()), YESNO(this->external_vcc_), YESNO(this->flip_x_), YESNO(this->flip_y_),
this->model_str_(), YESNO(this->external_vcc_), YESNO(this->flip_x_), YESNO(this->flip_y_),
this->offset_x_, this->offset_y_, YESNO(this->invert_));
LOG_PIN(" CS Pin: ", this->cs_);
LOG_PIN(" DC Pin: ", this->dc_pin_);

View File

@@ -19,7 +19,7 @@ CONFIG_SCHEMA = (
{
cv.Optional(CONF_HAS_DIRECTION, default=False): cv.boolean,
cv.Optional(CONF_HAS_OSCILLATING, default=False): cv.boolean,
cv.Optional(CONF_SPEED_COUNT): cv.int_range(min=1, max=255),
cv.Optional(CONF_SPEED_COUNT): cv.int_range(min=1),
cv.Optional(CONF_PRESET_MODES): validate_preset_modes,
}
)

View File

@@ -12,7 +12,7 @@ class TemplateFan final : public Component, public fan::Fan {
void dump_config() override;
void set_has_direction(bool has_direction) { this->has_direction_ = has_direction; }
void set_has_oscillating(bool has_oscillating) { this->has_oscillating_ = has_oscillating; }
void set_speed_count(uint8_t count) { this->speed_count_ = count; }
void set_speed_count(int count) { this->speed_count_ = count; }
void set_preset_modes(std::initializer_list<const char *> presets) { this->preset_modes_ = presets; }
fan::FanTraits get_traits() override { return this->traits_; }
@@ -21,7 +21,7 @@ class TemplateFan final : public Component, public fan::Fan {
bool has_oscillating_{false};
bool has_direction_{false};
uint8_t speed_count_{0};
int speed_count_{0};
fan::FanTraits traits_;
std::vector<const char *> preset_modes_{};
};

View File

@@ -3,7 +3,6 @@ import esphome.codegen as cg
from esphome.components import water_heater
import esphome.config_validation as cv
from esphome.const import (
CONF_AWAY,
CONF_ID,
CONF_MODE,
CONF_OPTIMISTIC,
@@ -19,7 +18,6 @@ from esphome.types import ConfigType
from .. import template_ns
CONF_CURRENT_TEMPERATURE = "current_temperature"
CONF_IS_ON = "is_on"
TemplateWaterHeater = template_ns.class_(
"TemplateWaterHeater", cg.Component, water_heater.WaterHeater
@@ -53,8 +51,6 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(
water_heater.validate_water_heater_mode
),
cv.Optional(CONF_AWAY): cv.returning_lambda,
cv.Optional(CONF_IS_ON): cv.returning_lambda,
}
)
.extend(cv.COMPONENT_SCHEMA)
@@ -102,22 +98,6 @@ async def to_code(config: ConfigType) -> None:
if CONF_SUPPORTED_MODES in config:
cg.add(var.set_supported_modes(config[CONF_SUPPORTED_MODES]))
if CONF_AWAY in config:
template_ = await cg.process_lambda(
config[CONF_AWAY],
[],
return_type=cg.optional.template(bool),
)
cg.add(var.set_away_lambda(template_))
if CONF_IS_ON in config:
template_ = await cg.process_lambda(
config[CONF_IS_ON],
[],
return_type=cg.optional.template(bool),
)
cg.add(var.set_is_on_lambda(template_))
@automation.register_action(
"water_heater.template.publish",
@@ -130,8 +110,6 @@ async def to_code(config: ConfigType) -> None:
cv.Optional(CONF_MODE): cv.templatable(
water_heater.validate_water_heater_mode
),
cv.Optional(CONF_AWAY): cv.templatable(cv.boolean),
cv.Optional(CONF_IS_ON): cv.templatable(cv.boolean),
}
),
)
@@ -156,12 +134,4 @@ async def water_heater_template_publish_to_code(
template_ = await cg.templatable(mode, args, water_heater.WaterHeaterMode)
cg.add(var.set_mode(template_))
if CONF_AWAY in config:
template_ = await cg.templatable(config[CONF_AWAY], args, bool)
cg.add(var.set_away(template_))
if CONF_IS_ON in config:
template_ = await cg.templatable(config[CONF_IS_ON], args, bool)
cg.add(var.set_is_on(template_))
return var

View File

@@ -11,15 +11,12 @@ class TemplateWaterHeaterPublishAction : public Action<Ts...>, public Parented<T
TEMPLATABLE_VALUE(float, current_temperature)
TEMPLATABLE_VALUE(float, target_temperature)
TEMPLATABLE_VALUE(water_heater::WaterHeaterMode, mode)
TEMPLATABLE_VALUE(bool, away)
TEMPLATABLE_VALUE(bool, is_on)
void play(const Ts &...x) override {
if (this->current_temperature_.has_value()) {
this->parent_->set_current_temperature(this->current_temperature_.value(x...));
}
bool needs_call = this->target_temperature_.has_value() || this->mode_.has_value() || this->away_.has_value() ||
this->is_on_.has_value();
bool needs_call = this->target_temperature_.has_value() || this->mode_.has_value();
if (needs_call) {
auto call = this->parent_->make_call();
if (this->target_temperature_.has_value()) {
@@ -28,12 +25,6 @@ class TemplateWaterHeaterPublishAction : public Action<Ts...>, public Parented<T
if (this->mode_.has_value()) {
call.set_mode(this->mode_.value(x...));
}
if (this->away_.has_value()) {
call.set_away(this->away_.value(x...));
}
if (this->is_on_.has_value()) {
call.set_on(this->is_on_.value(x...));
}
call.perform();
} else {
this->parent_->publish_state();

View File

@@ -17,7 +17,7 @@ void TemplateWaterHeater::setup() {
}
}
if (!this->current_temperature_f_.has_value() && !this->target_temperature_f_.has_value() &&
!this->mode_f_.has_value() && !this->away_f_.has_value() && !this->is_on_f_.has_value())
!this->mode_f_.has_value())
this->disable_loop();
}
@@ -32,12 +32,6 @@ water_heater::WaterHeaterTraits TemplateWaterHeater::traits() {
if (this->target_temperature_f_.has_value()) {
traits.add_feature_flags(water_heater::WATER_HEATER_SUPPORTS_TARGET_TEMPERATURE);
}
if (this->away_f_.has_value()) {
traits.set_supports_away_mode(true);
}
if (this->is_on_f_.has_value()) {
traits.add_feature_flags(water_heater::WATER_HEATER_SUPPORTS_ON_OFF);
}
return traits;
}
@@ -68,22 +62,6 @@ void TemplateWaterHeater::loop() {
}
}
auto away = this->away_f_.call();
if (away.has_value()) {
if (*away != this->is_away()) {
this->set_state_flag_(water_heater::WATER_HEATER_STATE_AWAY, *away);
changed = true;
}
}
auto is_on = this->is_on_f_.call();
if (is_on.has_value()) {
if (*is_on != this->is_on()) {
this->set_state_flag_(water_heater::WATER_HEATER_STATE_ON, *is_on);
changed = true;
}
}
if (changed) {
this->publish_state();
}
@@ -112,17 +90,6 @@ void TemplateWaterHeater::control(const water_heater::WaterHeaterCall &call) {
}
}
if (call.get_away().has_value()) {
if (this->optimistic_) {
this->set_state_flag_(water_heater::WATER_HEATER_STATE_AWAY, *call.get_away());
}
}
if (call.get_on().has_value()) {
if (this->optimistic_) {
this->set_state_flag_(water_heater::WATER_HEATER_STATE_ON, *call.get_on());
}
}
this->set_trigger_.trigger();
if (this->optimistic_) {

View File

@@ -24,8 +24,6 @@ class TemplateWaterHeater : public Component, public water_heater::WaterHeater {
this->target_temperature_f_.set(std::forward<F>(f));
}
template<typename F> void set_mode_lambda(F &&f) { this->mode_f_.set(std::forward<F>(f)); }
template<typename F> void set_away_lambda(F &&f) { this->away_f_.set(std::forward<F>(f)); }
template<typename F> void set_is_on_lambda(F &&f) { this->is_on_f_.set(std::forward<F>(f)); }
void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
void set_restore_mode(TemplateWaterHeaterRestoreMode restore_mode) { this->restore_mode_ = restore_mode; }
@@ -51,8 +49,6 @@ class TemplateWaterHeater : public Component, public water_heater::WaterHeater {
TemplateLambda<float> current_temperature_f_;
TemplateLambda<float> target_temperature_f_;
TemplateLambda<water_heater::WaterHeaterMode> mode_f_;
TemplateLambda<bool> away_f_;
TemplateLambda<bool> is_on_f_;
TemplateWaterHeaterRestoreMode restore_mode_{WATER_HEATER_NO_RESTORE};
water_heater::WaterHeaterModeMask supported_modes_;
bool optimistic_{true};

View File

@@ -1,481 +0,0 @@
#include "esphome/core/defines.h"
#ifdef USE_TIME_TIMEZONE
#include "posix_tz.h"
#include <cctype>
namespace esphome::time {
// Global timezone - set once at startup, rarely changes
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) - intentional mutable state
static ParsedTimezone global_tz_{};
void set_global_tz(const ParsedTimezone &tz) { global_tz_ = tz; }
const ParsedTimezone &get_global_tz() { return global_tz_; }
namespace internal {
// Helper to parse an unsigned integer from string, updating pointer
static uint32_t parse_uint(const char *&p) {
uint32_t value = 0;
while (std::isdigit(static_cast<unsigned char>(*p))) {
value = value * 10 + (*p - '0');
p++;
}
return value;
}
bool is_leap_year(int year) { return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); }
// Get days in year (avoids duplicate is_leap_year calls)
static inline int days_in_year(int year) { return is_leap_year(year) ? 366 : 365; }
// Convert days since epoch to year, updating days to remainder
static int __attribute__((noinline)) days_to_year(int64_t &days) {
int year = 1970;
int diy;
while (days >= (diy = days_in_year(year))) {
days -= diy;
year++;
}
while (days < 0) {
year--;
days += days_in_year(year);
}
return year;
}
// Extract just the year from a UTC epoch
static int epoch_to_year(time_t epoch) {
int64_t days = epoch / 86400;
if (epoch < 0 && epoch % 86400 != 0)
days--;
return days_to_year(days);
}
int days_in_month(int year, int month) {
switch (month) {
case 2:
return is_leap_year(year) ? 29 : 28;
case 4:
case 6:
case 9:
case 11:
return 30;
default:
return 31;
}
}
// Zeller-like algorithm for day of week (0 = Sunday)
int __attribute__((noinline)) day_of_week(int year, int month, int day) {
// Adjust for January/February
if (month < 3) {
month += 12;
year--;
}
int k = year % 100;
int j = year / 100;
int h = (day + (13 * (month + 1)) / 5 + k + k / 4 + j / 4 - 2 * j) % 7;
// Convert from Zeller (0=Sat) to standard (0=Sun)
return ((h + 6) % 7);
}
void __attribute__((noinline)) epoch_to_tm_utc(time_t epoch, struct tm *out_tm) {
// Days since epoch
int64_t days = epoch / 86400;
int32_t remaining_secs = epoch % 86400;
if (remaining_secs < 0) {
days--;
remaining_secs += 86400;
}
out_tm->tm_sec = remaining_secs % 60;
remaining_secs /= 60;
out_tm->tm_min = remaining_secs % 60;
out_tm->tm_hour = remaining_secs / 60;
// Day of week (Jan 1, 1970 was Thursday = 4)
out_tm->tm_wday = static_cast<int>((days + 4) % 7);
if (out_tm->tm_wday < 0)
out_tm->tm_wday += 7;
// Calculate year (updates days to day-of-year)
int year = days_to_year(days);
out_tm->tm_year = year - 1900;
out_tm->tm_yday = static_cast<int>(days);
// Calculate month and day
int month = 1;
int dim;
while (days >= (dim = days_in_month(year, month))) {
days -= dim;
month++;
}
out_tm->tm_mon = month - 1;
out_tm->tm_mday = static_cast<int>(days) + 1;
out_tm->tm_isdst = 0;
}
bool skip_tz_name(const char *&p) {
if (*p == '<') {
// Angle-bracket quoted name: <+07>, <-03>, <AEST>
p++; // skip '<'
while (*p && *p != '>') {
p++;
}
if (*p == '>') {
p++; // skip '>'
return true;
}
return false; // Unterminated
}
// Standard name: 3+ letters
const char *start = p;
while (*p && std::isalpha(static_cast<unsigned char>(*p))) {
p++;
}
return (p - start) >= 3;
}
int32_t __attribute__((noinline)) parse_offset(const char *&p) {
int sign = 1;
if (*p == '-') {
sign = -1;
p++;
} else if (*p == '+') {
p++;
}
int hours = parse_uint(p);
int minutes = 0;
int seconds = 0;
if (*p == ':') {
p++;
minutes = parse_uint(p);
if (*p == ':') {
p++;
seconds = parse_uint(p);
}
}
return sign * (hours * 3600 + minutes * 60 + seconds);
}
// Helper to parse the optional /time suffix (reuses parse_offset logic)
static void parse_transition_time(const char *&p, DSTRule &rule) {
rule.time_seconds = 2 * 3600; // Default 02:00
if (*p == '/') {
p++;
rule.time_seconds = parse_offset(p);
}
}
void __attribute__((noinline)) julian_to_month_day(int julian_day, int &out_month, int &out_day) {
// J format: day 1-365, Feb 29 is NOT counted even in leap years
// So day 60 is always March 1
// Iterate forward through months (no array needed)
int remaining = julian_day;
out_month = 1;
while (out_month <= 12) {
// Days in month for non-leap year (J format ignores leap years)
int dim = days_in_month(2001, out_month); // 2001 is non-leap year
if (remaining <= dim) {
out_day = remaining;
return;
}
remaining -= dim;
out_month++;
}
out_day = remaining;
}
void __attribute__((noinline)) day_of_year_to_month_day(int day_of_year, int year, int &out_month, int &out_day) {
// Plain format: day 0-365, Feb 29 IS counted in leap years
// Day 0 = Jan 1
int remaining = day_of_year;
out_month = 1;
while (out_month <= 12) {
int days_this_month = days_in_month(year, out_month);
if (remaining < days_this_month) {
out_day = remaining + 1;
return;
}
remaining -= days_this_month;
out_month++;
}
// Shouldn't reach here with valid input
out_month = 12;
out_day = 31;
}
bool parse_dst_rule(const char *&p, DSTRule &rule) {
rule = {}; // Zero initialize
if (*p == 'M' || *p == 'm') {
// M format: Mm.w.d (month.week.day)
rule.type = DSTRuleType::MONTH_WEEK_DAY;
p++;
rule.month = parse_uint(p);
if (rule.month < 1 || rule.month > 12)
return false;
if (*p++ != '.')
return false;
rule.week = parse_uint(p);
if (rule.week < 1 || rule.week > 5)
return false;
if (*p++ != '.')
return false;
rule.day_of_week = parse_uint(p);
if (rule.day_of_week > 6)
return false;
} else if (*p == 'J' || *p == 'j') {
// J format: Jn (Julian day 1-365, not counting Feb 29)
rule.type = DSTRuleType::JULIAN_NO_LEAP;
p++;
rule.day = parse_uint(p);
if (rule.day < 1 || rule.day > 365)
return false;
} else if (std::isdigit(static_cast<unsigned char>(*p))) {
// Plain number format: n (day 0-365, counting Feb 29)
rule.type = DSTRuleType::DAY_OF_YEAR;
rule.day = parse_uint(p);
if (rule.day > 365)
return false;
} else {
return false;
}
// Parse optional /time suffix
parse_transition_time(p, rule);
return true;
}
// Calculate days from Jan 1 of given year to given month/day
static int __attribute__((noinline)) days_from_year_start(int year, int month, int day) {
int days = day - 1;
for (int m = 1; m < month; m++) {
days += days_in_month(year, m);
}
return days;
}
// Calculate days from epoch to Jan 1 of given year (for DST transition calculations)
// Only supports years >= 1970. Timezone is either compiled in from YAML or set by
// Home Assistant, so pre-1970 dates are not a concern.
static int64_t __attribute__((noinline)) days_to_year_start(int year) {
int64_t days = 0;
for (int y = 1970; y < year; y++) {
days += days_in_year(y);
}
return days;
}
time_t __attribute__((noinline)) calculate_dst_transition(int year, const DSTRule &rule, int32_t base_offset_seconds) {
int month, day;
switch (rule.type) {
case DSTRuleType::MONTH_WEEK_DAY: {
// Find the nth occurrence of day_of_week in the given month
int first_dow = day_of_week(year, rule.month, 1);
// Days until first occurrence of target day
int days_until_first = (rule.day_of_week - first_dow + 7) % 7;
int first_occurrence = 1 + days_until_first;
if (rule.week == 5) {
// "Last" occurrence - find the last one in the month
int dim = days_in_month(year, rule.month);
day = first_occurrence;
while (day + 7 <= dim) {
day += 7;
}
} else {
// nth occurrence
day = first_occurrence + (rule.week - 1) * 7;
}
month = rule.month;
break;
}
case DSTRuleType::JULIAN_NO_LEAP:
// J format: day 1-365, Feb 29 not counted
julian_to_month_day(rule.day, month, day);
break;
case DSTRuleType::DAY_OF_YEAR:
// Plain format: day 0-365, Feb 29 counted
day_of_year_to_month_day(rule.day, year, month, day);
break;
case DSTRuleType::NONE:
// Should never be called with NONE, but handle it gracefully
month = 1;
day = 1;
break;
}
// Calculate days from epoch to this date
int64_t days = days_to_year_start(year) + days_from_year_start(year, month, day);
// Convert to epoch and add transition time and base offset
return days * 86400 + rule.time_seconds + base_offset_seconds;
}
} // namespace internal
bool __attribute__((noinline)) is_in_dst(time_t utc_epoch, const ParsedTimezone &tz) {
if (!tz.has_dst()) {
return false;
}
int year = internal::epoch_to_year(utc_epoch);
// Calculate DST start and end for this year
// DST start transition happens in standard time
time_t dst_start = internal::calculate_dst_transition(year, tz.dst_start, tz.std_offset_seconds);
// DST end transition happens in daylight time
time_t dst_end = internal::calculate_dst_transition(year, tz.dst_end, tz.dst_offset_seconds);
if (dst_start < dst_end) {
// Northern hemisphere: DST is between start and end
return (utc_epoch >= dst_start && utc_epoch < dst_end);
} else {
// Southern hemisphere: DST is outside the range (wraps around year)
return (utc_epoch >= dst_start || utc_epoch < dst_end);
}
}
bool parse_posix_tz(const char *tz_string, ParsedTimezone &result) {
if (!tz_string || !*tz_string) {
return false;
}
const char *p = tz_string;
// Initialize result (dst_start/dst_end default to type=NONE, so has_dst() returns false)
result.std_offset_seconds = 0;
result.dst_offset_seconds = 0;
result.dst_start = {};
result.dst_end = {};
// Skip standard timezone name
if (!internal::skip_tz_name(p)) {
return false;
}
// Parse standard offset (required)
if (!*p || (!std::isdigit(static_cast<unsigned char>(*p)) && *p != '+' && *p != '-')) {
return false;
}
result.std_offset_seconds = internal::parse_offset(p);
// Check for DST name
if (!*p) {
return true; // No DST
}
// If next char is comma, there's no DST name but there are rules (invalid)
if (*p == ',') {
return false;
}
// Check if there's something that looks like a DST name start
// (letter or angle bracket). If not, treat as trailing garbage and return success.
if (!std::isalpha(static_cast<unsigned char>(*p)) && *p != '<') {
return true; // No DST, trailing characters ignored
}
if (!internal::skip_tz_name(p)) {
return false; // Invalid DST name (started but malformed)
}
// Optional DST offset (default is std - 1 hour)
if (*p && *p != ',' && (std::isdigit(static_cast<unsigned char>(*p)) || *p == '+' || *p == '-')) {
result.dst_offset_seconds = internal::parse_offset(p);
} else {
result.dst_offset_seconds = result.std_offset_seconds - 3600;
}
// Parse DST rules (required when DST name is present)
if (*p != ',') {
// DST name without rules - treat as no DST since we can't determine transitions
return true;
}
p++;
if (!internal::parse_dst_rule(p, result.dst_start)) {
return false;
}
// Second rule is required per POSIX
if (*p != ',') {
return false;
}
p++;
// has_dst() now returns true since dst_start.type was set by parse_dst_rule
return internal::parse_dst_rule(p, result.dst_end);
}
bool epoch_to_local_tm(time_t utc_epoch, const ParsedTimezone &tz, struct tm *out_tm) {
if (!out_tm) {
return false;
}
// Determine DST status once (avoids duplicate is_in_dst calculation)
bool in_dst = is_in_dst(utc_epoch, tz);
int32_t offset = in_dst ? tz.dst_offset_seconds : tz.std_offset_seconds;
// Apply offset (POSIX offset is positive west, so subtract to get local)
time_t local_epoch = utc_epoch - offset;
internal::epoch_to_tm_utc(local_epoch, out_tm);
out_tm->tm_isdst = in_dst ? 1 : 0;
return true;
}
} // namespace esphome::time
#ifndef USE_HOST
// Override libc's localtime functions to use our timezone on embedded platforms.
// This allows user lambdas calling ::localtime() to get correct local time
// without needing the TZ environment variable (which pulls in scanf bloat).
// On host, we use the normal TZ mechanism since there's no memory constraint.
// Thread-safe version
extern "C" struct tm *localtime_r(const time_t *timer, struct tm *result) {
if (timer == nullptr || result == nullptr) {
return nullptr;
}
esphome::time::epoch_to_local_tm(*timer, esphome::time::get_global_tz(), result);
return result;
}
// Non-thread-safe version (uses static buffer, standard libc behavior)
extern "C" struct tm *localtime(const time_t *timer) {
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
static struct tm localtime_buf;
return localtime_r(timer, &localtime_buf);
}
#endif // !USE_HOST
#endif // USE_TIME_TIMEZONE

View File

@@ -1,132 +0,0 @@
#pragma once
#ifdef USE_TIME_TIMEZONE
#include <cstdint>
#include <ctime>
namespace esphome::time {
/// Type of DST transition rule
enum class DSTRuleType : uint8_t {
NONE = 0, ///< No DST rule (used to indicate no DST)
MONTH_WEEK_DAY, ///< M format: Mm.w.d (e.g., M3.2.0 = 2nd Sunday of March)
JULIAN_NO_LEAP, ///< J format: Jn (day 1-365, Feb 29 not counted)
DAY_OF_YEAR, ///< Plain number: n (day 0-365, Feb 29 counted in leap years)
};
/// Rule for DST transition (packed for 32-bit: 12 bytes)
struct DSTRule {
int32_t time_seconds; ///< Seconds after midnight (default 7200 = 2:00 AM)
uint16_t day; ///< Day of year (for JULIAN_NO_LEAP and DAY_OF_YEAR)
DSTRuleType type; ///< Type of rule
uint8_t month; ///< Month 1-12 (for MONTH_WEEK_DAY)
uint8_t week; ///< Week 1-5, 5 = last (for MONTH_WEEK_DAY)
uint8_t day_of_week; ///< Day 0-6, 0 = Sunday (for MONTH_WEEK_DAY)
};
/// Parsed POSIX timezone information (packed for 32-bit: 32 bytes)
struct ParsedTimezone {
int32_t std_offset_seconds; ///< Standard time offset from UTC in seconds (positive = west)
int32_t dst_offset_seconds; ///< DST offset from UTC in seconds
DSTRule dst_start; ///< When DST starts
DSTRule dst_end; ///< When DST ends
/// Check if this timezone has DST rules
bool has_dst() const { return this->dst_start.type != DSTRuleType::NONE; }
};
/// Parse a POSIX TZ string into a ParsedTimezone struct.
/// Supports formats like:
/// - "EST5" (simple offset, no DST)
/// - "EST5EDT,M3.2.0,M11.1.0" (with DST, M-format rules)
/// - "CST6CDT,M3.2.0/2,M11.1.0/2" (with transition times)
/// - "<+07>-7" (angle-bracket notation for special names)
/// - "IST-5:30" (half-hour offsets)
/// - "EST5EDT,J60,J300" (J-format: Julian day without leap day)
/// - "EST5EDT,60,300" (plain day number: day of year with leap day)
/// @param tz_string The POSIX TZ string to parse
/// @param result Output: the parsed timezone data
/// @return true if parsing succeeded, false on error
bool parse_posix_tz(const char *tz_string, ParsedTimezone &result);
/// Convert a UTC epoch to local time using the parsed timezone.
/// This replaces libc's localtime() to avoid scanf dependency.
/// @param utc_epoch Unix timestamp in UTC
/// @param tz The parsed timezone
/// @param[out] out_tm Output tm struct with local time
/// @return true on success
bool epoch_to_local_tm(time_t utc_epoch, const ParsedTimezone &tz, struct tm *out_tm);
/// Set the global timezone used by epoch_to_local_tm() when called without a timezone.
/// This is called by RealTimeClock::apply_timezone_() to enable ESPTime::from_epoch_local()
/// to work without libc's localtime().
void set_global_tz(const ParsedTimezone &tz);
/// Get the global timezone.
const ParsedTimezone &get_global_tz();
/// Check if a given UTC epoch falls within DST for the parsed timezone.
/// @param utc_epoch Unix timestamp in UTC
/// @param tz The parsed timezone
/// @return true if DST is in effect at the given time
bool is_in_dst(time_t utc_epoch, const ParsedTimezone &tz);
// Internal helper functions exposed for testing
namespace internal {
/// Skip a timezone name (letters or <...> quoted format)
/// @param p Pointer to current position, updated on return
/// @return true if a valid name was found
bool skip_tz_name(const char *&p);
/// Parse an offset in format [-]hh[:mm[:ss]]
/// @param p Pointer to current position, updated on return
/// @return Offset in seconds
int32_t parse_offset(const char *&p);
/// Parse a DST rule in format Mm.w.d[/time], Jn[/time], or n[/time]
/// @param p Pointer to current position, updated on return
/// @param rule Output: the parsed rule
/// @return true if parsing succeeded
bool parse_dst_rule(const char *&p, DSTRule &rule);
/// Convert Julian day (J format, 1-365 not counting Feb 29) to month/day
/// @param julian_day Day number 1-365
/// @param[out] month Output: month 1-12
/// @param[out] day Output: day of month
void julian_to_month_day(int julian_day, int &month, int &day);
/// Convert day of year (plain format, 0-365 counting Feb 29) to month/day
/// @param day_of_year Day number 0-365
/// @param year The year (for leap year calculation)
/// @param[out] month Output: month 1-12
/// @param[out] day Output: day of month
void day_of_year_to_month_day(int day_of_year, int year, int &month, int &day);
/// Calculate day of week for any date (0 = Sunday)
/// Uses a simplified algorithm that works for years 1970-2099
int day_of_week(int year, int month, int day);
/// Get the number of days in a month
int days_in_month(int year, int month);
/// Check if a year is a leap year
bool is_leap_year(int year);
/// Convert epoch to year/month/day/hour/min/sec (UTC)
void epoch_to_tm_utc(time_t epoch, struct tm *out_tm);
/// Calculate the epoch timestamp for a DST transition in a given year.
/// @param year The year (e.g., 2026)
/// @param rule The DST rule (month, week, day_of_week, time)
/// @param base_offset_seconds The timezone offset to apply (std or dst depending on context)
/// @return Unix epoch timestamp of the transition
time_t calculate_dst_transition(int year, const DSTRule &rule, int32_t base_offset_seconds);
} // namespace internal
} // namespace esphome::time
#endif // USE_TIME_TIMEZONE

View File

@@ -14,8 +14,8 @@
#include <sys/time.h>
#endif
#include <cerrno>
#include <cinttypes>
#include <cstdlib>
namespace esphome::time {
@@ -23,33 +23,9 @@ static const char *const TAG = "time";
RealTimeClock::RealTimeClock() = default;
ESPTime __attribute__((noinline)) RealTimeClock::now() {
#ifdef USE_TIME_TIMEZONE
time_t epoch = this->timestamp_now();
struct tm local_tm;
if (epoch_to_local_tm(epoch, get_global_tz(), &local_tm)) {
return ESPTime::from_c_tm(&local_tm, epoch);
}
// Fallback to UTC if parsing failed
return ESPTime::from_epoch_utc(epoch);
#else
return ESPTime::from_epoch_local(this->timestamp_now());
#endif
}
void RealTimeClock::dump_config() {
#ifdef USE_TIME_TIMEZONE
const auto &tz = get_global_tz();
// POSIX offset is positive west, negate for conventional UTC+X display
int std_h = -tz.std_offset_seconds / 3600;
int std_m = (std::abs(tz.std_offset_seconds) % 3600) / 60;
if (tz.has_dst()) {
int dst_h = -tz.dst_offset_seconds / 3600;
int dst_m = (std::abs(tz.dst_offset_seconds) % 3600) / 60;
ESP_LOGCONFIG(TAG, "Timezone: UTC%+d:%02d (DST UTC%+d:%02d)", std_h, std_m, dst_h, dst_m);
} else {
ESP_LOGCONFIG(TAG, "Timezone: UTC%+d:%02d", std_h, std_m);
}
ESP_LOGCONFIG(TAG, "Timezone: '%s'", this->timezone_.c_str());
#endif
auto time = this->now();
ESP_LOGCONFIG(TAG, "Current time: %04d-%02d-%02d %02d:%02d:%02d", time.year, time.month, time.day_of_month, time.hour,
@@ -96,6 +72,11 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) {
ret = settimeofday(&timev, nullptr);
}
#ifdef USE_TIME_TIMEZONE
// Move timezone back to local timezone.
this->apply_timezone_();
#endif
if (ret != 0) {
ESP_LOGW(TAG, "setimeofday() failed with code %d", ret);
}
@@ -108,29 +89,9 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) {
}
#ifdef USE_TIME_TIMEZONE
void RealTimeClock::apply_timezone_(const char *tz) {
ParsedTimezone parsed{};
// Handle null or empty input - use UTC
if (tz == nullptr || *tz == '\0') {
set_global_tz(parsed);
return;
}
#ifdef USE_HOST
// On host platform, also set TZ environment variable for libc compatibility
setenv("TZ", tz, 1);
void RealTimeClock::apply_timezone_() {
setenv("TZ", this->timezone_.c_str(), 1);
tzset();
#endif
// Parse the POSIX TZ string using our custom parser
if (!parse_posix_tz(tz, parsed)) {
ESP_LOGW(TAG, "Failed to parse timezone: %s", tz);
// parsed stays as default (UTC) on failure
}
// Set global timezone for all time conversions
set_global_tz(parsed);
}
#endif

View File

@@ -6,9 +6,6 @@
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/core/time.h"
#ifdef USE_TIME_TIMEZONE
#include "posix_tz.h"
#endif
namespace esphome::time {
@@ -23,31 +20,26 @@ class RealTimeClock : public PollingComponent {
explicit RealTimeClock();
#ifdef USE_TIME_TIMEZONE
/// Set the time zone from a POSIX TZ string.
void set_timezone(const char *tz) { this->apply_timezone_(tz); }
/// Set the time zone from a character buffer with known length.
/// The buffer does not need to be null-terminated.
void set_timezone(const char *tz, size_t len) {
if (tz == nullptr) {
this->apply_timezone_(nullptr);
return;
}
// Stack buffer - TZ strings from tzdata are typically short (< 50 chars)
char buf[128];
if (len >= sizeof(buf))
len = sizeof(buf) - 1;
memcpy(buf, tz, len);
buf[len] = '\0';
this->apply_timezone_(buf);
/// Set the time zone.
void set_timezone(const std::string &tz) {
this->timezone_ = tz;
this->apply_timezone_();
}
/// Set the time zone from a std::string.
void set_timezone(const std::string &tz) { this->apply_timezone_(tz.c_str()); }
/// Set the time zone from raw buffer, only if it differs from the current one.
void set_timezone(const char *tz, size_t len) {
if (this->timezone_.length() != len || memcmp(this->timezone_.c_str(), tz, len) != 0) {
this->timezone_.assign(tz, len);
this->apply_timezone_();
}
}
/// Get the time zone currently in use.
std::string get_timezone() { return this->timezone_; }
#endif
/// Get the time in the currently defined timezone.
ESPTime now();
ESPTime now() { return ESPTime::from_epoch_local(this->timestamp_now()); }
/// Get the time without any time zone or DST corrections.
ESPTime utcnow() { return ESPTime::from_epoch_utc(this->timestamp_now()); }
@@ -66,7 +58,8 @@ class RealTimeClock : public PollingComponent {
void synchronize_epoch_(uint32_t epoch);
#ifdef USE_TIME_TIMEZONE
void apply_timezone_(const char *tz);
std::string timezone_{};
void apply_timezone_();
#endif
LazyCallbackManager<void()> time_sync_callback_;

View File

@@ -22,7 +22,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_SPEED_DATAPOINT): cv.uint8_t,
cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t,
cv.Optional(CONF_DIRECTION_DATAPOINT): cv.uint8_t,
cv.Optional(CONF_SPEED_COUNT, default=3): cv.int_range(min=1, max=255),
cv.Optional(CONF_SPEED_COUNT, default=3): cv.int_range(min=1, max=256),
}
)
.extend(cv.COMPONENT_SCHEMA),

View File

@@ -9,7 +9,7 @@ namespace tuya {
class TuyaFan : public Component, public fan::Fan {
public:
TuyaFan(Tuya *parent, uint8_t speed_count) : parent_(parent), speed_count_(speed_count) {}
TuyaFan(Tuya *parent, int speed_count) : parent_(parent), speed_count_(speed_count) {}
void setup() override;
void dump_config() override;
void set_speed_id(uint8_t speed_id) { this->speed_id_ = speed_id; }
@@ -27,7 +27,7 @@ class TuyaFan : public Component, public fan::Fan {
optional<uint8_t> switch_id_{};
optional<uint8_t> oscillation_id_{};
optional<uint8_t> direction_id_{};
uint8_t speed_count_{};
int speed_count_{};
TuyaDatapointType speed_type_{};
TuyaDatapointType oscillation_type_{};
};

View File

@@ -430,14 +430,12 @@ void VoiceAssistant::client_subscription(api::APIConnection *client, bool subscr
}
if (this->api_client_ != nullptr) {
char current_peername[socket::SOCKADDR_STR_LEN];
char new_peername[socket::SOCKADDR_STR_LEN];
ESP_LOGE(TAG,
"Multiple API Clients attempting to connect to Voice Assistant\n"
"Current client: %s (%s)\n"
"New client: %s (%s)",
this->api_client_->get_name(), this->api_client_->get_peername_to(current_peername), client->get_name(),
client->get_peername_to(new_peername));
this->api_client_->get_name(), this->api_client_->get_peername(), client->get_name(),
client->get_peername());
return;
}

View File

@@ -1,4 +1,3 @@
// Trigger CI memory impact (uses updated ESPAsyncWebServer from web_server_base)
#include "web_server.h"
#ifdef USE_WEBSERVER
#include "esphome/components/json/json_util.h"
@@ -214,7 +213,7 @@ void DeferredUpdateEventSource::deq_push_back_with_dedup_(void *source, message_
void DeferredUpdateEventSource::process_deferred_queue_() {
while (!deferred_queue_.empty()) {
DeferredEvent &de = deferred_queue_.front();
auto message = de.message_generator_(web_server_, de.source_);
std::string message = de.message_generator_(web_server_, de.source_);
if (this->send(message.c_str(), "state") != DISCARDED) {
// O(n) but memory efficiency is more important than speed here which is why std::vector was chosen
deferred_queue_.erase(deferred_queue_.begin());
@@ -271,7 +270,7 @@ void DeferredUpdateEventSource::deferrable_send_state(void *source, const char *
// deferred queue still not empty which means downstream event queue full, no point trying to send first
deq_push_back_with_dedup_(source, message_generator);
} else {
auto message = message_generator(web_server_, source);
std::string message = message_generator(web_server_, source);
if (this->send(message.c_str(), "state") == DISCARDED) {
deq_push_back_with_dedup_(source, message_generator);
} else {
@@ -325,7 +324,7 @@ void DeferredUpdateEventSourceList::on_client_connect_(DeferredUpdateEventSource
ws->defer([ws, source]() {
// Configure reconnect timeout and send config
// this should always go through since the AsyncEventSourceClient event queue is empty on connect
auto message = ws->get_config_json();
std::string message = ws->get_config_json();
source->try_send_nodefer(message.c_str(), "ping", millis(), 30000);
#ifdef USE_WEBSERVER_SORTING
@@ -334,10 +333,10 @@ void DeferredUpdateEventSourceList::on_client_connect_(DeferredUpdateEventSource
JsonObject root = builder.root();
root[ESPHOME_F("name")] = group.second.name;
root[ESPHOME_F("sorting_weight")] = group.second.weight;
auto group_msg = builder.serialize();
message = builder.serialize();
// up to 31 groups should be able to be queued initially without defer
source->try_send_nodefer(group_msg.c_str(), "sorting_group");
source->try_send_nodefer(message.c_str(), "sorting_group");
}
#endif
@@ -370,7 +369,7 @@ void WebServer::set_css_include(const char *css_include) { this->css_include_ =
void WebServer::set_js_include(const char *js_include) { this->js_include_ = js_include; }
#endif
json::SerializationBuffer<> WebServer::get_config_json() {
std::string WebServer::get_config_json() {
json::JsonBuilder builder;
JsonObject root = builder.root();
@@ -602,20 +601,20 @@ void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlM
// Note: request->method() is always HTTP_GET here (canHandle ensures this)
if (entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->sensor_json_(obj, obj->state, detail);
std::string data = this->sensor_json_(obj, obj->state, detail);
request->send(200, "application/json", data.c_str());
return;
}
}
request->send(404);
}
json::SerializationBuffer<> WebServer::sensor_state_json_generator(WebServer *web_server, void *source) {
std::string WebServer::sensor_state_json_generator(WebServer *web_server, void *source) {
return web_server->sensor_json_((sensor::Sensor *) (source), ((sensor::Sensor *) (source))->state, DETAIL_STATE);
}
json::SerializationBuffer<> WebServer::sensor_all_json_generator(WebServer *web_server, void *source) {
std::string WebServer::sensor_all_json_generator(WebServer *web_server, void *source) {
return web_server->sensor_json_((sensor::Sensor *) (source), ((sensor::Sensor *) (source))->state, DETAIL_ALL);
}
json::SerializationBuffer<> WebServer::sensor_json_(sensor::Sensor *obj, float value, JsonDetail start_config) {
std::string WebServer::sensor_json_(sensor::Sensor *obj, float value, JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
@@ -649,23 +648,23 @@ void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const
// Note: request->method() is always HTTP_GET here (canHandle ensures this)
if (entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->text_sensor_json_(obj, obj->state, detail);
std::string data = this->text_sensor_json_(obj, obj->state, detail);
request->send(200, "application/json", data.c_str());
return;
}
}
request->send(404);
}
json::SerializationBuffer<> WebServer::text_sensor_state_json_generator(WebServer *web_server, void *source) {
std::string WebServer::text_sensor_state_json_generator(WebServer *web_server, void *source) {
return web_server->text_sensor_json_((text_sensor::TextSensor *) (source),
((text_sensor::TextSensor *) (source))->state, DETAIL_STATE);
}
json::SerializationBuffer<> WebServer::text_sensor_all_json_generator(WebServer *web_server, void *source) {
std::string WebServer::text_sensor_all_json_generator(WebServer *web_server, void *source) {
return web_server->text_sensor_json_((text_sensor::TextSensor *) (source),
((text_sensor::TextSensor *) (source))->state, DETAIL_ALL);
}
json::SerializationBuffer<> WebServer::text_sensor_json_(text_sensor::TextSensor *obj, const std::string &value,
JsonDetail start_config) {
std::string WebServer::text_sensor_json_(text_sensor::TextSensor *obj, const std::string &value,
JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
@@ -710,7 +709,7 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->switch_json_(obj, obj->state, detail);
std::string data = this->switch_json_(obj, obj->state, detail);
request->send(200, "application/json", data.c_str());
return;
}
@@ -735,13 +734,13 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM
}
request->send(404);
}
json::SerializationBuffer<> WebServer::switch_state_json_generator(WebServer *web_server, void *source) {
std::string WebServer::switch_state_json_generator(WebServer *web_server, void *source) {
return web_server->switch_json_((switch_::Switch *) (source), ((switch_::Switch *) (source))->state, DETAIL_STATE);
}
json::SerializationBuffer<> WebServer::switch_all_json_generator(WebServer *web_server, void *source) {
std::string WebServer::switch_all_json_generator(WebServer *web_server, void *source) {
return web_server->switch_json_((switch_::Switch *) (source), ((switch_::Switch *) (source))->state, DETAIL_ALL);
}
json::SerializationBuffer<> WebServer::switch_json_(switch_::Switch *obj, bool value, JsonDetail start_config) {
std::string WebServer::switch_json_(switch_::Switch *obj, bool value, JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
@@ -763,7 +762,7 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM
continue;
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->button_json_(obj, detail);
std::string data = this->button_json_(obj, detail);
request->send(200, "application/json", data.c_str());
} else if (match.method_equals(ESPHOME_F("press"))) {
DEFER_ACTION(obj, obj->press());
@@ -776,10 +775,10 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM
}
request->send(404);
}
json::SerializationBuffer<> WebServer::button_all_json_generator(WebServer *web_server, void *source) {
std::string WebServer::button_all_json_generator(WebServer *web_server, void *source) {
return web_server->button_json_((button::Button *) (source), DETAIL_ALL);
}
json::SerializationBuffer<> WebServer::button_json_(button::Button *obj, JsonDetail start_config) {
std::string WebServer::button_json_(button::Button *obj, JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
@@ -806,23 +805,22 @@ void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, con
// Note: request->method() is always HTTP_GET here (canHandle ensures this)
if (entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->binary_sensor_json_(obj, obj->state, detail);
std::string data = this->binary_sensor_json_(obj, obj->state, detail);
request->send(200, "application/json", data.c_str());
return;
}
}
request->send(404);
}
json::SerializationBuffer<> WebServer::binary_sensor_state_json_generator(WebServer *web_server, void *source) {
std::string WebServer::binary_sensor_state_json_generator(WebServer *web_server, void *source) {
return web_server->binary_sensor_json_((binary_sensor::BinarySensor *) (source),
((binary_sensor::BinarySensor *) (source))->state, DETAIL_STATE);
}
json::SerializationBuffer<> WebServer::binary_sensor_all_json_generator(WebServer *web_server, void *source) {
std::string WebServer::binary_sensor_all_json_generator(WebServer *web_server, void *source) {
return web_server->binary_sensor_json_((binary_sensor::BinarySensor *) (source),
((binary_sensor::BinarySensor *) (source))->state, DETAIL_ALL);
}
json::SerializationBuffer<> WebServer::binary_sensor_json_(binary_sensor::BinarySensor *obj, bool value,
JsonDetail start_config) {
std::string WebServer::binary_sensor_json_(binary_sensor::BinarySensor *obj, bool value, JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
@@ -849,7 +847,7 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->fan_json_(obj, detail);
std::string data = this->fan_json_(obj, detail);
request->send(200, "application/json", data.c_str());
} else if (match.method_equals(ESPHOME_F("toggle"))) {
DEFER_ACTION(obj, obj->toggle().perform());
@@ -890,13 +888,13 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
}
request->send(404);
}
json::SerializationBuffer<> WebServer::fan_state_json_generator(WebServer *web_server, void *source) {
std::string WebServer::fan_state_json_generator(WebServer *web_server, void *source) {
return web_server->fan_json_((fan::Fan *) (source), DETAIL_STATE);
}
json::SerializationBuffer<> WebServer::fan_all_json_generator(WebServer *web_server, void *source) {
std::string WebServer::fan_all_json_generator(WebServer *web_server, void *source) {
return web_server->fan_json_((fan::Fan *) (source), DETAIL_ALL);
}
json::SerializationBuffer<> WebServer::fan_json_(fan::Fan *obj, JsonDetail start_config) {
std::string WebServer::fan_json_(fan::Fan *obj, JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
@@ -930,7 +928,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->light_json_(obj, detail);
std::string data = this->light_json_(obj, detail);
request->send(200, "application/json", data.c_str());
} else if (match.method_equals(ESPHOME_F("toggle"))) {
DEFER_ACTION(obj, obj->toggle().perform());
@@ -969,13 +967,13 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa
}
request->send(404);
}
json::SerializationBuffer<> WebServer::light_state_json_generator(WebServer *web_server, void *source) {
std::string WebServer::light_state_json_generator(WebServer *web_server, void *source) {
return web_server->light_json_((light::LightState *) (source), DETAIL_STATE);
}
json::SerializationBuffer<> WebServer::light_all_json_generator(WebServer *web_server, void *source) {
std::string WebServer::light_all_json_generator(WebServer *web_server, void *source) {
return web_server->light_json_((light::LightState *) (source), DETAIL_ALL);
}
json::SerializationBuffer<> WebServer::light_json_(light::LightState *obj, JsonDetail start_config) {
std::string WebServer::light_json_(light::LightState *obj, JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
@@ -1009,7 +1007,7 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->cover_json_(obj, detail);
std::string data = this->cover_json_(obj, detail);
request->send(200, "application/json", data.c_str());
return;
}
@@ -1057,13 +1055,13 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa
}
request->send(404);
}
json::SerializationBuffer<> WebServer::cover_state_json_generator(WebServer *web_server, void *source) {
std::string WebServer::cover_state_json_generator(WebServer *web_server, void *source) {
return web_server->cover_json_((cover::Cover *) (source), DETAIL_STATE);
}
json::SerializationBuffer<> WebServer::cover_all_json_generator(WebServer *web_server, void *source) {
std::string WebServer::cover_all_json_generator(WebServer *web_server, void *source) {
return web_server->cover_json_((cover::Cover *) (source), DETAIL_ALL);
}
json::SerializationBuffer<> WebServer::cover_json_(cover::Cover *obj, JsonDetail start_config) {
std::string WebServer::cover_json_(cover::Cover *obj, JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
@@ -1098,7 +1096,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->number_json_(obj, obj->state, detail);
std::string data = this->number_json_(obj, obj->state, detail);
request->send(200, "application/json", data.c_str());
return;
}
@@ -1117,13 +1115,13 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM
request->send(404);
}
json::SerializationBuffer<> WebServer::number_state_json_generator(WebServer *web_server, void *source) {
std::string WebServer::number_state_json_generator(WebServer *web_server, void *source) {
return web_server->number_json_((number::Number *) (source), ((number::Number *) (source))->state, DETAIL_STATE);
}
json::SerializationBuffer<> WebServer::number_all_json_generator(WebServer *web_server, void *source) {
std::string WebServer::number_all_json_generator(WebServer *web_server, void *source) {
return web_server->number_json_((number::Number *) (source), ((number::Number *) (source))->state, DETAIL_ALL);
}
json::SerializationBuffer<> WebServer::number_json_(number::Number *obj, float value, JsonDetail start_config) {
std::string WebServer::number_json_(number::Number *obj, float value, JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
@@ -1165,7 +1163,7 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat
continue;
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->date_json_(obj, detail);
std::string data = this->date_json_(obj, detail);
request->send(200, "application/json", data.c_str());
return;
}
@@ -1190,13 +1188,13 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat
request->send(404);
}
json::SerializationBuffer<> WebServer::date_state_json_generator(WebServer *web_server, void *source) {
std::string WebServer::date_state_json_generator(WebServer *web_server, void *source) {
return web_server->date_json_((datetime::DateEntity *) (source), DETAIL_STATE);
}
json::SerializationBuffer<> WebServer::date_all_json_generator(WebServer *web_server, void *source) {
std::string WebServer::date_all_json_generator(WebServer *web_server, void *source) {
return web_server->date_json_((datetime::DateEntity *) (source), DETAIL_ALL);
}
json::SerializationBuffer<> WebServer::date_json_(datetime::DateEntity *obj, JsonDetail start_config) {
std::string WebServer::date_json_(datetime::DateEntity *obj, JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
@@ -1225,7 +1223,7 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat
continue;
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->time_json_(obj, detail);
std::string data = this->time_json_(obj, detail);
request->send(200, "application/json", data.c_str());
return;
}
@@ -1249,13 +1247,13 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat
}
request->send(404);
}
json::SerializationBuffer<> WebServer::time_state_json_generator(WebServer *web_server, void *source) {
std::string WebServer::time_state_json_generator(WebServer *web_server, void *source) {
return web_server->time_json_((datetime::TimeEntity *) (source), DETAIL_STATE);
}
json::SerializationBuffer<> WebServer::time_all_json_generator(WebServer *web_server, void *source) {
std::string WebServer::time_all_json_generator(WebServer *web_server, void *source) {
return web_server->time_json_((datetime::TimeEntity *) (source), DETAIL_ALL);
}
json::SerializationBuffer<> WebServer::time_json_(datetime::TimeEntity *obj, JsonDetail start_config) {
std::string WebServer::time_json_(datetime::TimeEntity *obj, JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
@@ -1284,7 +1282,7 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur
continue;
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->datetime_json_(obj, detail);
std::string data = this->datetime_json_(obj, detail);
request->send(200, "application/json", data.c_str());
return;
}
@@ -1308,13 +1306,13 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur
}
request->send(404);
}
json::SerializationBuffer<> WebServer::datetime_state_json_generator(WebServer *web_server, void *source) {
std::string WebServer::datetime_state_json_generator(WebServer *web_server, void *source) {
return web_server->datetime_json_((datetime::DateTimeEntity *) (source), DETAIL_STATE);
}
json::SerializationBuffer<> WebServer::datetime_all_json_generator(WebServer *web_server, void *source) {
std::string WebServer::datetime_all_json_generator(WebServer *web_server, void *source) {
return web_server->datetime_json_((datetime::DateTimeEntity *) (source), DETAIL_ALL);
}
json::SerializationBuffer<> WebServer::datetime_json_(datetime::DateTimeEntity *obj, JsonDetail start_config) {
std::string WebServer::datetime_json_(datetime::DateTimeEntity *obj, JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
@@ -1345,7 +1343,7 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->text_json_(obj, obj->state, detail);
std::string data = this->text_json_(obj, obj->state, detail);
request->send(200, "application/json", data.c_str());
return;
}
@@ -1364,13 +1362,13 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat
request->send(404);
}
json::SerializationBuffer<> WebServer::text_state_json_generator(WebServer *web_server, void *source) {
std::string WebServer::text_state_json_generator(WebServer *web_server, void *source) {
return web_server->text_json_((text::Text *) (source), ((text::Text *) (source))->state, DETAIL_STATE);
}
json::SerializationBuffer<> WebServer::text_all_json_generator(WebServer *web_server, void *source) {
std::string WebServer::text_all_json_generator(WebServer *web_server, void *source) {
return web_server->text_json_((text::Text *) (source), ((text::Text *) (source))->state, DETAIL_ALL);
}
json::SerializationBuffer<> WebServer::text_json_(text::Text *obj, const std::string &value, JsonDetail start_config) {
std::string WebServer::text_json_(text::Text *obj, const std::string &value, JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
@@ -1402,7 +1400,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->select_json_(obj, obj->has_state() ? obj->current_option() : StringRef(), detail);
std::string data = this->select_json_(obj, obj->has_state() ? obj->current_option() : StringRef(), detail);
request->send(200, "application/json", data.c_str());
return;
}
@@ -1421,15 +1419,15 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM
}
request->send(404);
}
json::SerializationBuffer<> WebServer::select_state_json_generator(WebServer *web_server, void *source) {
std::string WebServer::select_state_json_generator(WebServer *web_server, void *source) {
auto *obj = (select::Select *) (source);
return web_server->select_json_(obj, obj->has_state() ? obj->current_option() : StringRef(), DETAIL_STATE);
}
json::SerializationBuffer<> WebServer::select_all_json_generator(WebServer *web_server, void *source) {
std::string WebServer::select_all_json_generator(WebServer *web_server, void *source) {
auto *obj = (select::Select *) (source);
return web_server->select_json_(obj, obj->has_state() ? obj->current_option() : StringRef(), DETAIL_ALL);
}
json::SerializationBuffer<> WebServer::select_json_(select::Select *obj, StringRef value, JsonDetail start_config) {
std::string WebServer::select_json_(select::Select *obj, StringRef value, JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
@@ -1461,7 +1459,7 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->climate_json_(obj, detail);
std::string data = this->climate_json_(obj, detail);
request->send(200, "application/json", data.c_str());
return;
}
@@ -1490,15 +1488,15 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url
}
request->send(404);
}
json::SerializationBuffer<> WebServer::climate_state_json_generator(WebServer *web_server, void *source) {
std::string WebServer::climate_state_json_generator(WebServer *web_server, void *source) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
return web_server->climate_json_((climate::Climate *) (source), DETAIL_STATE);
}
json::SerializationBuffer<> WebServer::climate_all_json_generator(WebServer *web_server, void *source) {
std::string WebServer::climate_all_json_generator(WebServer *web_server, void *source) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
return web_server->climate_json_((climate::Climate *) (source), DETAIL_ALL);
}
json::SerializationBuffer<> WebServer::climate_json_(climate::Climate *obj, JsonDetail start_config) {
std::string WebServer::climate_json_(climate::Climate *obj, JsonDetail start_config) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
json::JsonBuilder builder;
JsonObject root = builder.root();
@@ -1631,7 +1629,7 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->lock_json_(obj, obj->state, detail);
std::string data = this->lock_json_(obj, obj->state, detail);
request->send(200, "application/json", data.c_str());
return;
}
@@ -1656,13 +1654,13 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat
}
request->send(404);
}
json::SerializationBuffer<> WebServer::lock_state_json_generator(WebServer *web_server, void *source) {
std::string WebServer::lock_state_json_generator(WebServer *web_server, void *source) {
return web_server->lock_json_((lock::Lock *) (source), ((lock::Lock *) (source))->state, DETAIL_STATE);
}
json::SerializationBuffer<> WebServer::lock_all_json_generator(WebServer *web_server, void *source) {
std::string WebServer::lock_all_json_generator(WebServer *web_server, void *source) {
return web_server->lock_json_((lock::Lock *) (source), ((lock::Lock *) (source))->state, DETAIL_ALL);
}
json::SerializationBuffer<> WebServer::lock_json_(lock::Lock *obj, lock::LockState value, JsonDetail start_config) {
std::string WebServer::lock_json_(lock::Lock *obj, lock::LockState value, JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
@@ -1690,7 +1688,7 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->valve_json_(obj, detail);
std::string data = this->valve_json_(obj, detail);
request->send(200, "application/json", data.c_str());
return;
}
@@ -1736,13 +1734,13 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa
}
request->send(404);
}
json::SerializationBuffer<> WebServer::valve_state_json_generator(WebServer *web_server, void *source) {
std::string WebServer::valve_state_json_generator(WebServer *web_server, void *source) {
return web_server->valve_json_((valve::Valve *) (source), DETAIL_STATE);
}
json::SerializationBuffer<> WebServer::valve_all_json_generator(WebServer *web_server, void *source) {
std::string WebServer::valve_all_json_generator(WebServer *web_server, void *source) {
return web_server->valve_json_((valve::Valve *) (source), DETAIL_ALL);
}
json::SerializationBuffer<> WebServer::valve_json_(valve::Valve *obj, JsonDetail start_config) {
std::string WebServer::valve_json_(valve::Valve *obj, JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
@@ -1775,7 +1773,7 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->alarm_control_panel_json_(obj, obj->get_state(), detail);
std::string data = this->alarm_control_panel_json_(obj, obj->get_state(), detail);
request->send(200, "application/json", data.c_str());
return;
}
@@ -1815,19 +1813,19 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques
}
request->send(404);
}
json::SerializationBuffer<> WebServer::alarm_control_panel_state_json_generator(WebServer *web_server, void *source) {
std::string WebServer::alarm_control_panel_state_json_generator(WebServer *web_server, void *source) {
return web_server->alarm_control_panel_json_((alarm_control_panel::AlarmControlPanel *) (source),
((alarm_control_panel::AlarmControlPanel *) (source))->get_state(),
DETAIL_STATE);
}
json::SerializationBuffer<> WebServer::alarm_control_panel_all_json_generator(WebServer *web_server, void *source) {
std::string WebServer::alarm_control_panel_all_json_generator(WebServer *web_server, void *source) {
return web_server->alarm_control_panel_json_((alarm_control_panel::AlarmControlPanel *) (source),
((alarm_control_panel::AlarmControlPanel *) (source))->get_state(),
DETAIL_ALL);
}
json::SerializationBuffer<> WebServer::alarm_control_panel_json_(alarm_control_panel::AlarmControlPanel *obj,
alarm_control_panel::AlarmControlPanelState value,
JsonDetail start_config) {
std::string WebServer::alarm_control_panel_json_(alarm_control_panel::AlarmControlPanel *obj,
alarm_control_panel::AlarmControlPanelState value,
JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
@@ -1856,7 +1854,7 @@ void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, cons
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->water_heater_json_(obj, detail);
std::string data = this->water_heater_json_(obj, detail);
request->send(200, "application/json", data.c_str());
return;
}
@@ -1892,14 +1890,14 @@ void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, cons
request->send(404);
}
json::SerializationBuffer<> WebServer::water_heater_state_json_generator(WebServer *web_server, void *source) {
std::string WebServer::water_heater_state_json_generator(WebServer *web_server, void *source) {
return web_server->water_heater_json_(static_cast<water_heater::WaterHeater *>(source), DETAIL_STATE);
}
json::SerializationBuffer<> WebServer::water_heater_all_json_generator(WebServer *web_server, void *source) {
std::string WebServer::water_heater_all_json_generator(WebServer *web_server, void *source) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
return web_server->water_heater_json_(static_cast<water_heater::WaterHeater *>(source), DETAIL_ALL);
}
json::SerializationBuffer<> WebServer::water_heater_json_(water_heater::WaterHeater *obj, JsonDetail start_config) {
std::string WebServer::water_heater_json_(water_heater::WaterHeater *obj, JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
char buf[PSTR_LOCAL_SIZE];
@@ -1962,7 +1960,7 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->infrared_json_(obj, detail);
std::string data = this->infrared_json_(obj, detail);
request->send(200, ESPHOME_F("application/json"), data.c_str());
return;
}
@@ -2028,12 +2026,12 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur
request->send(404);
}
json::SerializationBuffer<> WebServer::infrared_all_json_generator(WebServer *web_server, void *source) {
std::string WebServer::infrared_all_json_generator(WebServer *web_server, void *source) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
return web_server->infrared_json_(static_cast<infrared::Infrared *>(source), DETAIL_ALL);
}
json::SerializationBuffer<> WebServer::infrared_json_(infrared::Infrared *obj, JsonDetail start_config) {
std::string WebServer::infrared_json_(infrared::Infrared *obj, JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
@@ -2068,7 +2066,7 @@ void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMa
// Note: request->method() is always HTTP_GET here (canHandle ensures this)
if (entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->event_json_(obj, StringRef(), detail);
std::string data = this->event_json_(obj, StringRef(), detail);
request->send(200, "application/json", data.c_str());
return;
}
@@ -2078,16 +2076,16 @@ void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMa
static StringRef get_event_type(event::Event *event) { return event ? event->get_last_event_type() : StringRef(); }
json::SerializationBuffer<> WebServer::event_state_json_generator(WebServer *web_server, void *source) {
std::string WebServer::event_state_json_generator(WebServer *web_server, void *source) {
auto *event = static_cast<event::Event *>(source);
return web_server->event_json_(event, get_event_type(event), DETAIL_STATE);
}
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
json::SerializationBuffer<> WebServer::event_all_json_generator(WebServer *web_server, void *source) {
std::string WebServer::event_all_json_generator(WebServer *web_server, void *source) {
auto *event = static_cast<event::Event *>(source);
return web_server->event_json_(event, get_event_type(event), DETAIL_ALL);
}
json::SerializationBuffer<> WebServer::event_json_(event::Event *obj, StringRef event_type, JsonDetail start_config) {
std::string WebServer::event_json_(event::Event *obj, StringRef event_type, JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
@@ -2121,7 +2119,7 @@ void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlM
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->update_json_(obj, detail);
std::string data = this->update_json_(obj, detail);
request->send(200, "application/json", data.c_str());
return;
}
@@ -2137,15 +2135,15 @@ void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlM
}
request->send(404);
}
json::SerializationBuffer<> WebServer::update_state_json_generator(WebServer *web_server, void *source) {
std::string WebServer::update_state_json_generator(WebServer *web_server, void *source) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
return web_server->update_json_((update::UpdateEntity *) (source), DETAIL_STATE);
}
json::SerializationBuffer<> WebServer::update_all_json_generator(WebServer *web_server, void *source) {
std::string WebServer::update_all_json_generator(WebServer *web_server, void *source) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
return web_server->update_json_((update::UpdateEntity *) (source), DETAIL_STATE);
}
json::SerializationBuffer<> WebServer::update_json_(update::UpdateEntity *obj, JsonDetail start_config) {
std::string WebServer::update_json_(update::UpdateEntity *obj, JsonDetail start_config) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
json::JsonBuilder builder;
JsonObject root = builder.root();

View File

@@ -2,7 +2,6 @@
#include "list_entities.h"
#include "esphome/components/json/json_util.h"
#include "esphome/components/web_server_base/web_server_base.h"
#ifdef USE_WEBSERVER
#include "esphome/core/component.h"
@@ -104,7 +103,7 @@ enum JsonDetail { DETAIL_ALL, DETAIL_STATE };
can be forgotten.
*/
#if !defined(USE_ESP32) && defined(USE_ARDUINO)
using message_generator_t = json::SerializationBuffer<>(WebServer *, void *);
using message_generator_t = std::string(WebServer *, void *);
class DeferredUpdateEventSourceList;
class DeferredUpdateEventSource : public AsyncEventSource {
@@ -263,7 +262,7 @@ class WebServer : public Controller,
void handle_index_request(AsyncWebServerRequest *request);
/// Return the webserver configuration as JSON.
json::SerializationBuffer<> get_config_json();
std::string get_config_json();
#ifdef USE_WEBSERVER_CSS_INCLUDE
/// Handle included css request under '/0.css'.
@@ -285,8 +284,8 @@ class WebServer : public Controller,
/// Handle a sensor request under '/sensor/<id>'.
void handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match);
static json::SerializationBuffer<> sensor_state_json_generator(WebServer *web_server, void *source);
static json::SerializationBuffer<> sensor_all_json_generator(WebServer *web_server, void *source);
static std::string sensor_state_json_generator(WebServer *web_server, void *source);
static std::string sensor_all_json_generator(WebServer *web_server, void *source);
#endif
#ifdef USE_SWITCH
@@ -295,8 +294,8 @@ class WebServer : public Controller,
/// Handle a switch request under '/switch/<id>/</turn_on/turn_off/toggle>'.
void handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match);
static json::SerializationBuffer<> switch_state_json_generator(WebServer *web_server, void *source);
static json::SerializationBuffer<> switch_all_json_generator(WebServer *web_server, void *source);
static std::string switch_state_json_generator(WebServer *web_server, void *source);
static std::string switch_all_json_generator(WebServer *web_server, void *source);
#endif
#ifdef USE_BUTTON
@@ -304,7 +303,7 @@ class WebServer : public Controller,
void handle_button_request(AsyncWebServerRequest *request, const UrlMatch &match);
// Buttons are stateless, so there is no button_state_json_generator
static json::SerializationBuffer<> button_all_json_generator(WebServer *web_server, void *source);
static std::string button_all_json_generator(WebServer *web_server, void *source);
#endif
#ifdef USE_BINARY_SENSOR
@@ -313,8 +312,8 @@ class WebServer : public Controller,
/// Handle a binary sensor request under '/binary_sensor/<id>'.
void handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match);
static json::SerializationBuffer<> binary_sensor_state_json_generator(WebServer *web_server, void *source);
static json::SerializationBuffer<> binary_sensor_all_json_generator(WebServer *web_server, void *source);
static std::string binary_sensor_state_json_generator(WebServer *web_server, void *source);
static std::string binary_sensor_all_json_generator(WebServer *web_server, void *source);
#endif
#ifdef USE_FAN
@@ -323,8 +322,8 @@ class WebServer : public Controller,
/// Handle a fan request under '/fan/<id>/</turn_on/turn_off/toggle>'.
void handle_fan_request(AsyncWebServerRequest *request, const UrlMatch &match);
static json::SerializationBuffer<> fan_state_json_generator(WebServer *web_server, void *source);
static json::SerializationBuffer<> fan_all_json_generator(WebServer *web_server, void *source);
static std::string fan_state_json_generator(WebServer *web_server, void *source);
static std::string fan_all_json_generator(WebServer *web_server, void *source);
#endif
#ifdef USE_LIGHT
@@ -333,8 +332,8 @@ class WebServer : public Controller,
/// Handle a light request under '/light/<id>/</turn_on/turn_off/toggle>'.
void handle_light_request(AsyncWebServerRequest *request, const UrlMatch &match);
static json::SerializationBuffer<> light_state_json_generator(WebServer *web_server, void *source);
static json::SerializationBuffer<> light_all_json_generator(WebServer *web_server, void *source);
static std::string light_state_json_generator(WebServer *web_server, void *source);
static std::string light_all_json_generator(WebServer *web_server, void *source);
#endif
#ifdef USE_TEXT_SENSOR
@@ -343,8 +342,8 @@ class WebServer : public Controller,
/// Handle a text sensor request under '/text_sensor/<id>'.
void handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match);
static json::SerializationBuffer<> text_sensor_state_json_generator(WebServer *web_server, void *source);
static json::SerializationBuffer<> text_sensor_all_json_generator(WebServer *web_server, void *source);
static std::string text_sensor_state_json_generator(WebServer *web_server, void *source);
static std::string text_sensor_all_json_generator(WebServer *web_server, void *source);
#endif
#ifdef USE_COVER
@@ -353,8 +352,8 @@ class WebServer : public Controller,
/// Handle a cover request under '/cover/<id>/<open/close/stop/set>'.
void handle_cover_request(AsyncWebServerRequest *request, const UrlMatch &match);
static json::SerializationBuffer<> cover_state_json_generator(WebServer *web_server, void *source);
static json::SerializationBuffer<> cover_all_json_generator(WebServer *web_server, void *source);
static std::string cover_state_json_generator(WebServer *web_server, void *source);
static std::string cover_all_json_generator(WebServer *web_server, void *source);
#endif
#ifdef USE_NUMBER
@@ -362,8 +361,8 @@ class WebServer : public Controller,
/// Handle a number request under '/number/<id>'.
void handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match);
static json::SerializationBuffer<> number_state_json_generator(WebServer *web_server, void *source);
static json::SerializationBuffer<> number_all_json_generator(WebServer *web_server, void *source);
static std::string number_state_json_generator(WebServer *web_server, void *source);
static std::string number_all_json_generator(WebServer *web_server, void *source);
#endif
#ifdef USE_DATETIME_DATE
@@ -371,8 +370,8 @@ class WebServer : public Controller,
/// Handle a date request under '/date/<id>'.
void handle_date_request(AsyncWebServerRequest *request, const UrlMatch &match);
static json::SerializationBuffer<> date_state_json_generator(WebServer *web_server, void *source);
static json::SerializationBuffer<> date_all_json_generator(WebServer *web_server, void *source);
static std::string date_state_json_generator(WebServer *web_server, void *source);
static std::string date_all_json_generator(WebServer *web_server, void *source);
#endif
#ifdef USE_DATETIME_TIME
@@ -380,8 +379,8 @@ class WebServer : public Controller,
/// Handle a time request under '/time/<id>'.
void handle_time_request(AsyncWebServerRequest *request, const UrlMatch &match);
static json::SerializationBuffer<> time_state_json_generator(WebServer *web_server, void *source);
static json::SerializationBuffer<> time_all_json_generator(WebServer *web_server, void *source);
static std::string time_state_json_generator(WebServer *web_server, void *source);
static std::string time_all_json_generator(WebServer *web_server, void *source);
#endif
#ifdef USE_DATETIME_DATETIME
@@ -389,8 +388,8 @@ class WebServer : public Controller,
/// Handle a datetime request under '/datetime/<id>'.
void handle_datetime_request(AsyncWebServerRequest *request, const UrlMatch &match);
static json::SerializationBuffer<> datetime_state_json_generator(WebServer *web_server, void *source);
static json::SerializationBuffer<> datetime_all_json_generator(WebServer *web_server, void *source);
static std::string datetime_state_json_generator(WebServer *web_server, void *source);
static std::string datetime_all_json_generator(WebServer *web_server, void *source);
#endif
#ifdef USE_TEXT
@@ -398,8 +397,8 @@ class WebServer : public Controller,
/// Handle a text input request under '/text/<id>'.
void handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match);
static json::SerializationBuffer<> text_state_json_generator(WebServer *web_server, void *source);
static json::SerializationBuffer<> text_all_json_generator(WebServer *web_server, void *source);
static std::string text_state_json_generator(WebServer *web_server, void *source);
static std::string text_all_json_generator(WebServer *web_server, void *source);
#endif
#ifdef USE_SELECT
@@ -407,8 +406,8 @@ class WebServer : public Controller,
/// Handle a select request under '/select/<id>'.
void handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match);
static json::SerializationBuffer<> select_state_json_generator(WebServer *web_server, void *source);
static json::SerializationBuffer<> select_all_json_generator(WebServer *web_server, void *source);
static std::string select_state_json_generator(WebServer *web_server, void *source);
static std::string select_all_json_generator(WebServer *web_server, void *source);
#endif
#ifdef USE_CLIMATE
@@ -416,8 +415,8 @@ class WebServer : public Controller,
/// Handle a climate request under '/climate/<id>'.
void handle_climate_request(AsyncWebServerRequest *request, const UrlMatch &match);
static json::SerializationBuffer<> climate_state_json_generator(WebServer *web_server, void *source);
static json::SerializationBuffer<> climate_all_json_generator(WebServer *web_server, void *source);
static std::string climate_state_json_generator(WebServer *web_server, void *source);
static std::string climate_all_json_generator(WebServer *web_server, void *source);
#endif
#ifdef USE_LOCK
@@ -426,8 +425,8 @@ class WebServer : public Controller,
/// Handle a lock request under '/lock/<id>/</lock/unlock/open>'.
void handle_lock_request(AsyncWebServerRequest *request, const UrlMatch &match);
static json::SerializationBuffer<> lock_state_json_generator(WebServer *web_server, void *source);
static json::SerializationBuffer<> lock_all_json_generator(WebServer *web_server, void *source);
static std::string lock_state_json_generator(WebServer *web_server, void *source);
static std::string lock_all_json_generator(WebServer *web_server, void *source);
#endif
#ifdef USE_VALVE
@@ -436,8 +435,8 @@ class WebServer : public Controller,
/// Handle a valve request under '/valve/<id>/<open/close/stop/set>'.
void handle_valve_request(AsyncWebServerRequest *request, const UrlMatch &match);
static json::SerializationBuffer<> valve_state_json_generator(WebServer *web_server, void *source);
static json::SerializationBuffer<> valve_all_json_generator(WebServer *web_server, void *source);
static std::string valve_state_json_generator(WebServer *web_server, void *source);
static std::string valve_all_json_generator(WebServer *web_server, void *source);
#endif
#ifdef USE_ALARM_CONTROL_PANEL
@@ -446,8 +445,8 @@ class WebServer : public Controller,
/// Handle a alarm_control_panel request under '/alarm_control_panel/<id>'.
void handle_alarm_control_panel_request(AsyncWebServerRequest *request, const UrlMatch &match);
static json::SerializationBuffer<> alarm_control_panel_state_json_generator(WebServer *web_server, void *source);
static json::SerializationBuffer<> alarm_control_panel_all_json_generator(WebServer *web_server, void *source);
static std::string alarm_control_panel_state_json_generator(WebServer *web_server, void *source);
static std::string alarm_control_panel_all_json_generator(WebServer *web_server, void *source);
#endif
#ifdef USE_WATER_HEATER
@@ -456,22 +455,22 @@ class WebServer : public Controller,
/// Handle a water_heater request under '/water_heater/<id>/<mode/set>'.
void handle_water_heater_request(AsyncWebServerRequest *request, const UrlMatch &match);
static json::SerializationBuffer<> water_heater_state_json_generator(WebServer *web_server, void *source);
static json::SerializationBuffer<> water_heater_all_json_generator(WebServer *web_server, void *source);
static std::string water_heater_state_json_generator(WebServer *web_server, void *source);
static std::string water_heater_all_json_generator(WebServer *web_server, void *source);
#endif
#ifdef USE_INFRARED
/// Handle an infrared request under '/infrared/<id>/transmit'.
void handle_infrared_request(AsyncWebServerRequest *request, const UrlMatch &match);
static json::SerializationBuffer<> infrared_all_json_generator(WebServer *web_server, void *source);
static std::string infrared_all_json_generator(WebServer *web_server, void *source);
#endif
#ifdef USE_EVENT
void on_event(event::Event *obj) override;
static json::SerializationBuffer<> event_state_json_generator(WebServer *web_server, void *source);
static json::SerializationBuffer<> event_all_json_generator(WebServer *web_server, void *source);
static std::string event_state_json_generator(WebServer *web_server, void *source);
static std::string event_all_json_generator(WebServer *web_server, void *source);
/// Handle a event request under '/event<id>'.
void handle_event_request(AsyncWebServerRequest *request, const UrlMatch &match);
@@ -483,8 +482,8 @@ class WebServer : public Controller,
/// Handle a update request under '/update/<id>'.
void handle_update_request(AsyncWebServerRequest *request, const UrlMatch &match);
static json::SerializationBuffer<> update_state_json_generator(WebServer *web_server, void *source);
static json::SerializationBuffer<> update_all_json_generator(WebServer *web_server, void *source);
static std::string update_state_json_generator(WebServer *web_server, void *source);
static std::string update_all_json_generator(WebServer *web_server, void *source);
#endif
/// Override the web handler's canHandle method.
@@ -609,74 +608,71 @@ class WebServer : public Controller,
private:
#ifdef USE_SENSOR
json::SerializationBuffer<> sensor_json_(sensor::Sensor *obj, float value, JsonDetail start_config);
std::string sensor_json_(sensor::Sensor *obj, float value, JsonDetail start_config);
#endif
#ifdef USE_SWITCH
json::SerializationBuffer<> switch_json_(switch_::Switch *obj, bool value, JsonDetail start_config);
std::string switch_json_(switch_::Switch *obj, bool value, JsonDetail start_config);
#endif
#ifdef USE_BUTTON
json::SerializationBuffer<> button_json_(button::Button *obj, JsonDetail start_config);
std::string button_json_(button::Button *obj, JsonDetail start_config);
#endif
#ifdef USE_BINARY_SENSOR
json::SerializationBuffer<> binary_sensor_json_(binary_sensor::BinarySensor *obj, bool value,
JsonDetail start_config);
std::string binary_sensor_json_(binary_sensor::BinarySensor *obj, bool value, JsonDetail start_config);
#endif
#ifdef USE_FAN
json::SerializationBuffer<> fan_json_(fan::Fan *obj, JsonDetail start_config);
std::string fan_json_(fan::Fan *obj, JsonDetail start_config);
#endif
#ifdef USE_LIGHT
json::SerializationBuffer<> light_json_(light::LightState *obj, JsonDetail start_config);
std::string light_json_(light::LightState *obj, JsonDetail start_config);
#endif
#ifdef USE_TEXT_SENSOR
json::SerializationBuffer<> text_sensor_json_(text_sensor::TextSensor *obj, const std::string &value,
JsonDetail start_config);
std::string text_sensor_json_(text_sensor::TextSensor *obj, const std::string &value, JsonDetail start_config);
#endif
#ifdef USE_COVER
json::SerializationBuffer<> cover_json_(cover::Cover *obj, JsonDetail start_config);
std::string cover_json_(cover::Cover *obj, JsonDetail start_config);
#endif
#ifdef USE_NUMBER
json::SerializationBuffer<> number_json_(number::Number *obj, float value, JsonDetail start_config);
std::string number_json_(number::Number *obj, float value, JsonDetail start_config);
#endif
#ifdef USE_DATETIME_DATE
json::SerializationBuffer<> date_json_(datetime::DateEntity *obj, JsonDetail start_config);
std::string date_json_(datetime::DateEntity *obj, JsonDetail start_config);
#endif
#ifdef USE_DATETIME_TIME
json::SerializationBuffer<> time_json_(datetime::TimeEntity *obj, JsonDetail start_config);
std::string time_json_(datetime::TimeEntity *obj, JsonDetail start_config);
#endif
#ifdef USE_DATETIME_DATETIME
json::SerializationBuffer<> datetime_json_(datetime::DateTimeEntity *obj, JsonDetail start_config);
std::string datetime_json_(datetime::DateTimeEntity *obj, JsonDetail start_config);
#endif
#ifdef USE_TEXT
json::SerializationBuffer<> text_json_(text::Text *obj, const std::string &value, JsonDetail start_config);
std::string text_json_(text::Text *obj, const std::string &value, JsonDetail start_config);
#endif
#ifdef USE_SELECT
json::SerializationBuffer<> select_json_(select::Select *obj, StringRef value, JsonDetail start_config);
std::string select_json_(select::Select *obj, StringRef value, JsonDetail start_config);
#endif
#ifdef USE_CLIMATE
json::SerializationBuffer<> climate_json_(climate::Climate *obj, JsonDetail start_config);
std::string climate_json_(climate::Climate *obj, JsonDetail start_config);
#endif
#ifdef USE_LOCK
json::SerializationBuffer<> lock_json_(lock::Lock *obj, lock::LockState value, JsonDetail start_config);
std::string lock_json_(lock::Lock *obj, lock::LockState value, JsonDetail start_config);
#endif
#ifdef USE_VALVE
json::SerializationBuffer<> valve_json_(valve::Valve *obj, JsonDetail start_config);
std::string valve_json_(valve::Valve *obj, JsonDetail start_config);
#endif
#ifdef USE_ALARM_CONTROL_PANEL
json::SerializationBuffer<> alarm_control_panel_json_(alarm_control_panel::AlarmControlPanel *obj,
alarm_control_panel::AlarmControlPanelState value,
JsonDetail start_config);
std::string alarm_control_panel_json_(alarm_control_panel::AlarmControlPanel *obj,
alarm_control_panel::AlarmControlPanelState value, JsonDetail start_config);
#endif
#ifdef USE_EVENT
json::SerializationBuffer<> event_json_(event::Event *obj, StringRef event_type, JsonDetail start_config);
std::string event_json_(event::Event *obj, StringRef event_type, JsonDetail start_config);
#endif
#ifdef USE_WATER_HEATER
json::SerializationBuffer<> water_heater_json_(water_heater::WaterHeater *obj, JsonDetail start_config);
std::string water_heater_json_(water_heater::WaterHeater *obj, JsonDetail start_config);
#endif
#ifdef USE_INFRARED
json::SerializationBuffer<> infrared_json_(infrared::Infrared *obj, JsonDetail start_config);
std::string infrared_json_(infrared::Infrared *obj, JsonDetail start_config);
#endif
#ifdef USE_UPDATE
json::SerializationBuffer<> update_json_(update::UpdateEntity *obj, JsonDetail start_config);
std::string update_json_(update::UpdateEntity *obj, JsonDetail start_config);
#endif
};

View File

@@ -344,34 +344,14 @@ bool AsyncWebServerRequest::authenticate(const char *username, const char *passw
memcpy(user_info + user_len + 1, password, pass_len);
user_info[user_info_len] = '\0';
// Base64 output size is ceil(input_len * 4/3) + 1, with input bounded to 256 bytes
// max output is ceil(256 * 4/3) + 1 = 343 bytes, use 350 for safety
constexpr size_t max_digest_len = 350;
char digest[max_digest_len];
size_t out;
esp_crypto_base64_encode(reinterpret_cast<uint8_t *>(digest), max_digest_len, &out,
size_t n = 0, out;
esp_crypto_base64_encode(nullptr, 0, &n, reinterpret_cast<const uint8_t *>(user_info), user_info_len);
auto digest = std::unique_ptr<char[]>(new char[n + 1]);
esp_crypto_base64_encode(reinterpret_cast<uint8_t *>(digest.get()), n, &out,
reinterpret_cast<const uint8_t *>(user_info), user_info_len);
// Constant-time comparison to avoid timing side channels.
// No early return on length mismatch — the length difference is folded
// into the accumulator so any mismatch is rejected.
const char *provided = auth_str + auth_prefix_len;
size_t digest_len = out; // length from esp_crypto_base64_encode
// Derive provided_len from the already-sized std::string rather than
// rescanning with strlen (avoids attacker-controlled scan length).
size_t provided_len = auth.value().size() - auth_prefix_len;
// Use full-width XOR so any bit difference in the lengths is preserved
// (uint8_t truncation would miss differences in higher bytes, e.g.
// digest_len vs digest_len + 256).
volatile size_t result = digest_len ^ provided_len;
// Iterate over the expected digest length only — the full-width length
// XOR above already rejects any length mismatch, and bounding the loop
// prevents a long Authorization header from forcing extra work.
for (size_t i = 0; i < digest_len; i++) {
char provided_ch = (i < provided_len) ? provided[i] : 0;
result |= static_cast<uint8_t>(digest[i] ^ provided_ch);
}
return result == 0;
return strcmp(digest.get(), auth_str + auth_prefix_len) == 0;
}
void AsyncWebServerRequest::requestAuthentication(const char *realm) const {
@@ -418,9 +398,8 @@ void AsyncWebServerResponse::addHeader(const char *name, const char *value) {
void AsyncResponseStream::print(float value) {
// Use stack buffer to avoid temporary string allocation
// Size: sign (1) + digits (10) + decimal (1) + precision (6) + exponent (5) + null (1) = 24, use 32 for safety
constexpr size_t float_buf_size = 32;
char buf[float_buf_size];
int len = snprintf(buf, float_buf_size, "%f", value);
char buf[32];
int len = snprintf(buf, sizeof(buf), "%f", value);
this->content_.append(buf, len);
}
@@ -525,7 +504,7 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest *
// Configure reconnect timeout and send config
// this should always go through since the tcp send buffer is empty on connect
auto message = ws->get_config_json();
std::string message = ws->get_config_json();
this->try_send_nodefer(message.c_str(), "ping", millis(), 30000);
#ifdef USE_WEBSERVER_SORTING
@@ -579,7 +558,7 @@ void AsyncEventSourceResponse::deq_push_back_with_dedup_(void *source, message_g
void AsyncEventSourceResponse::process_deferred_queue_() {
while (!deferred_queue_.empty()) {
DeferredEvent &de = deferred_queue_.front();
auto message = de.message_generator_(web_server_, de.source_);
std::string message = de.message_generator_(web_server_, de.source_);
if (this->try_send_nodefer(message.c_str(), "state")) {
// O(n) but memory efficiency is more important than speed here which is why std::vector was chosen
deferred_queue_.erase(deferred_queue_.begin());
@@ -816,7 +795,7 @@ void AsyncEventSourceResponse::deferrable_send_state(void *source, const char *e
// trying to send first
deq_push_back_with_dedup_(source, message_generator);
} else {
auto message = message_generator(web_server_, source);
std::string message = message_generator(web_server_, source);
if (!this->try_send_nodefer(message.c_str(), "state")) {
deq_push_back_with_dedup_(source, message_generator);
}
@@ -882,12 +861,12 @@ esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *c
}
});
// Process data - use stack buffer to avoid heap allocation
char buffer[MULTIPART_CHUNK_SIZE];
// Process data
std::unique_ptr<char[]> buffer(new char[MULTIPART_CHUNK_SIZE]);
size_t bytes_since_yield = 0;
for (size_t remaining = r->content_len; remaining > 0;) {
int recv_len = httpd_req_recv(r, buffer, std::min(remaining, MULTIPART_CHUNK_SIZE));
int recv_len = httpd_req_recv(r, buffer.get(), std::min(remaining, MULTIPART_CHUNK_SIZE));
if (recv_len <= 0) {
httpd_resp_send_err(r, recv_len == HTTPD_SOCK_ERR_TIMEOUT ? HTTPD_408_REQ_TIMEOUT : HTTPD_400_BAD_REQUEST,
@@ -895,7 +874,7 @@ esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *c
return recv_len == HTTPD_SOCK_ERR_TIMEOUT ? ESP_ERR_TIMEOUT : ESP_FAIL;
}
if (reader->parse(buffer, recv_len) != static_cast<size_t>(recv_len)) {
if (reader->parse(buffer.get(), recv_len) != static_cast<size_t>(recv_len)) {
ESP_LOGW(TAG, "Multipart parser error");
httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr);
return ESP_FAIL;

View File

@@ -16,7 +16,6 @@
#include <vector>
#ifdef USE_WEBSERVER
#include "esphome/components/json/json_util.h"
#include "esphome/components/web_server/list_entities.h"
#endif
@@ -255,7 +254,7 @@ class AsyncWebHandler {
class AsyncEventSource;
class AsyncEventSourceResponse;
using message_generator_t = json::SerializationBuffer<>(esphome::web_server::WebServer *, void *);
using message_generator_t = std::string(esphome::web_server::WebServer *, void *);
/*
This class holds a pointer to the source component that wants to publish a state event, and a pointer to a function

View File

@@ -349,7 +349,7 @@ bool WiFiComponent::needs_scan_results_() const {
return this->scan_result_.empty() || !this->scan_result_[0].get_matches();
}
bool WiFiComponent::ssid_was_seen_in_scan_(const CompactString &ssid) const {
bool WiFiComponent::ssid_was_seen_in_scan_(const std::string &ssid) const {
// Check if this SSID is configured as hidden
// If explicitly marked hidden, we should always try hidden mode regardless of scan results
for (const auto &conf : this->sta_) {
@@ -641,7 +641,7 @@ void WiFiComponent::restart_adapter() {
// through start_connecting() first. Without this clear, stale errors would
// trigger spurious "failed (callback)" logs. The canonical clear location
// is in start_connecting(); this is the only exception to that pattern.
this->error_from_callback_ = 0;
this->error_from_callback_ = false;
}
void WiFiComponent::loop() {
@@ -960,12 +960,9 @@ WiFiAP WiFiComponent::get_sta() const {
return config ? *config : WiFiAP{};
}
void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &password) {
this->save_wifi_sta(ssid.c_str(), password.c_str());
}
void WiFiComponent::save_wifi_sta(const char *ssid, const char *password) {
SavedWifiSettings save{}; // zero-initialized - all bytes set to \0, guaranteeing null termination
strncpy(save.ssid, ssid, sizeof(save.ssid) - 1); // max 32 chars, byte 32 remains \0
strncpy(save.password, password, sizeof(save.password) - 1); // max 64 chars, byte 64 remains \0
strncpy(save.ssid, ssid.c_str(), sizeof(save.ssid) - 1); // max 32 chars, byte 32 remains \0
strncpy(save.password, password.c_str(), sizeof(save.password) - 1); // max 64 chars, byte 64 remains \0
this->pref_.save(&save);
// ensure it's written immediately
global_preferences->sync();
@@ -1071,7 +1068,7 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) {
// This is the canonical location for clearing the flag since all connection
// attempts go through start_connecting(). The only other clear is in
// restart_adapter() which enters COOLDOWN without calling start_connecting().
this->error_from_callback_ = 0;
this->error_from_callback_ = false;
if (!this->wifi_sta_connect_(ap)) {
ESP_LOGE(TAG, "wifi_sta_connect_ failed");
@@ -1828,11 +1825,11 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() {
}
// Get SSID for logging (use pointer to avoid copy)
const char *ssid = nullptr;
const std::string *ssid = nullptr;
if (this->retry_phase_ == WiFiRetryPhase::SCAN_CONNECTING && !this->scan_result_.empty()) {
ssid = this->scan_result_[0].get_ssid().c_str();
ssid = &this->scan_result_[0].get_ssid();
} else if (const WiFiAP *config = this->get_selected_sta_()) {
ssid = config->get_ssid().c_str();
ssid = &config->get_ssid();
}
// Only decrease priority on the last attempt for this phase
@@ -1850,11 +1847,10 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() {
(old_priority > std::numeric_limits<int8_t>::min()) ? (old_priority - 1) : std::numeric_limits<int8_t>::min();
this->set_sta_priority(failed_bssid.value(), new_priority);
}
char bssid_s[18];
format_mac_addr_upper(failed_bssid.value().data(), bssid_s);
ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", ssid != nullptr ? ssid : "",
bssid_s, old_priority, new_priority);
ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d",
ssid != nullptr ? ssid->c_str() : "", bssid_s, old_priority, new_priority);
// After adjusting priority, check if all priorities are now at minimum
// If so, clear the vector to save memory and reset for fresh start
@@ -2102,14 +2098,10 @@ void WiFiComponent::save_fast_connect_settings_() {
}
#endif
void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = CompactString(ssid.c_str(), ssid.size()); }
void WiFiAP::set_ssid(const char *ssid) { this->ssid_ = CompactString(ssid, strlen(ssid)); }
void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = ssid; }
void WiFiAP::set_bssid(const bssid_t &bssid) { this->bssid_ = bssid; }
void WiFiAP::clear_bssid() { this->bssid_ = {}; }
void WiFiAP::set_password(const std::string &password) {
this->password_ = CompactString(password.c_str(), password.size());
}
void WiFiAP::set_password(const char *password) { this->password_ = CompactString(password, strlen(password)); }
void WiFiAP::set_password(const std::string &password) { this->password_ = password; }
#ifdef USE_WIFI_WPA2_EAP
void WiFiAP::set_eap(optional<EAPAuth> eap_auth) { this->eap_ = std::move(eap_auth); }
#endif
@@ -2119,8 +2111,10 @@ void WiFiAP::clear_channel() { this->channel_ = 0; }
void WiFiAP::set_manual_ip(optional<ManualIP> manual_ip) { this->manual_ip_ = manual_ip; }
#endif
void WiFiAP::set_hidden(bool hidden) { this->hidden_ = hidden; }
const std::string &WiFiAP::get_ssid() const { return this->ssid_; }
const bssid_t &WiFiAP::get_bssid() const { return this->bssid_; }
bool WiFiAP::has_bssid() const { return this->bssid_ != bssid_t{}; }
const std::string &WiFiAP::get_password() const { return this->password_; }
#ifdef USE_WIFI_WPA2_EAP
const optional<EAPAuth> &WiFiAP::get_eap() const { return this->eap_; }
#endif
@@ -2131,12 +2125,12 @@ const optional<ManualIP> &WiFiAP::get_manual_ip() const { return this->manual_ip
#endif
bool WiFiAP::get_hidden() const { return this->hidden_; }
WiFiScanResult::WiFiScanResult(const bssid_t &bssid, const char *ssid, size_t ssid_len, uint8_t channel, int8_t rssi,
bool with_auth, bool is_hidden)
WiFiScanResult::WiFiScanResult(const bssid_t &bssid, std::string ssid, uint8_t channel, int8_t rssi, bool with_auth,
bool is_hidden)
: bssid_(bssid),
channel_(channel),
rssi_(rssi),
ssid_(ssid, ssid_len),
ssid_(std::move(ssid)),
with_auth_(with_auth),
is_hidden_(is_hidden) {}
bool WiFiScanResult::matches(const WiFiAP &config) const {
@@ -2179,6 +2173,7 @@ bool WiFiScanResult::matches(const WiFiAP &config) const {
bool WiFiScanResult::get_matches() const { return this->matches_; }
void WiFiScanResult::set_matches(bool matches) { this->matches_ = matches; }
const bssid_t &WiFiScanResult::get_bssid() const { return this->bssid_; }
const std::string &WiFiScanResult::get_ssid() const { return this->ssid_; }
uint8_t WiFiScanResult::get_channel() const { return this->channel_; }
int8_t WiFiScanResult::get_rssi() const { return this->rssi_; }
bool WiFiScanResult::get_with_auth() const { return this->with_auth_; }
@@ -2289,7 +2284,7 @@ void WiFiComponent::process_roaming_scan_() {
for (const auto &result : this->scan_result_) {
// Must be same SSID, different BSSID
if (result.get_ssid() != current_ssid.c_str() || result.get_bssid() == current_bssid)
if (current_ssid != result.get_ssid() || result.get_bssid() == current_bssid)
continue;
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE

View File

@@ -175,13 +175,9 @@ template<typename T> using wifi_scan_vector_t = FixedVector<T>;
class WiFiAP {
public:
void set_ssid(const std::string &ssid);
void set_ssid(const char *ssid);
void set_ssid(const CompactString &ssid) { this->ssid_ = ssid; }
void set_bssid(const bssid_t &bssid);
void clear_bssid();
void set_password(const std::string &password);
void set_password(const char *password);
void set_password(const CompactString &password) { this->password_ = password; }
#ifdef USE_WIFI_WPA2_EAP
void set_eap(optional<EAPAuth> eap_auth);
#endif // USE_WIFI_WPA2_EAP
@@ -192,10 +188,10 @@ class WiFiAP {
void set_manual_ip(optional<ManualIP> manual_ip);
#endif
void set_hidden(bool hidden);
const CompactString &get_ssid() const { return this->ssid_; }
const CompactString &get_password() const { return this->password_; }
const std::string &get_ssid() const;
const bssid_t &get_bssid() const;
bool has_bssid() const;
const std::string &get_password() const;
#ifdef USE_WIFI_WPA2_EAP
const optional<EAPAuth> &get_eap() const;
#endif // USE_WIFI_WPA2_EAP
@@ -208,8 +204,8 @@ class WiFiAP {
bool get_hidden() const;
protected:
CompactString ssid_;
CompactString password_;
std::string ssid_;
std::string password_;
#ifdef USE_WIFI_WPA2_EAP
optional<EAPAuth> eap_;
#endif // USE_WIFI_WPA2_EAP
@@ -225,15 +221,14 @@ class WiFiAP {
class WiFiScanResult {
public:
WiFiScanResult(const bssid_t &bssid, const char *ssid, size_t ssid_len, uint8_t channel, int8_t rssi, bool with_auth,
bool is_hidden);
WiFiScanResult(const bssid_t &bssid, std::string ssid, uint8_t channel, int8_t rssi, bool with_auth, bool is_hidden);
bool matches(const WiFiAP &config) const;
bool get_matches() const;
void set_matches(bool matches);
const bssid_t &get_bssid() const;
const CompactString &get_ssid() const { return this->ssid_; }
const std::string &get_ssid() const;
uint8_t get_channel() const;
int8_t get_rssi() const;
bool get_with_auth() const;
@@ -247,7 +242,7 @@ class WiFiScanResult {
bssid_t bssid_;
uint8_t channel_;
int8_t rssi_;
CompactString ssid_;
std::string ssid_;
int8_t priority_{0};
bool matches_{false};
bool with_auth_;
@@ -386,10 +381,6 @@ class WiFiComponent : public Component {
void set_passive_scan(bool passive);
void save_wifi_sta(const std::string &ssid, const std::string &password);
void save_wifi_sta(const char *ssid, const char *password);
void save_wifi_sta(const CompactString &ssid, const CompactString &password) {
this->save_wifi_sta(ssid.c_str(), password.c_str());
}
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
@@ -554,7 +545,7 @@ class WiFiComponent : public Component {
int8_t find_first_non_hidden_index_() const;
/// Check if an SSID was seen in the most recent scan results
/// Used to skip hidden mode for SSIDs we know are visible
bool ssid_was_seen_in_scan_(const CompactString &ssid) const;
bool ssid_was_seen_in_scan_(const std::string &ssid) const;
/// Check if full scan results are needed (captive portal active, improv, listeners)
bool needs_full_scan_results_() const;
/// Check if network matches any configured network (for scan result filtering)

Some files were not shown because too many files have changed in this diff Show More