Compare commits

..

10 Commits

Author SHA1 Message Date
J. Nick Koston
a671f6ea85 Use if/else instead of continue in client loop 2026-02-09 20:42:25 -06:00
J. Nick Koston
0c62781539 Extract remove_client_() from APIServer::loop() hot path 2026-02-09 20:42:09 -06:00
J. Nick Koston
e6c743ea67 [api] Extract accept_new_connections_() from APIServer::loop() hot path 2026-02-09 20:34:11 -06:00
J. Nick Koston
4c006d98af Merge remote-tracking branch 'upstream/dev' into peername_no_double_ram
# Conflicts:
#	esphome/components/api/api_connection.cpp
2026-02-09 18:38:02 -06:00
J. Nick Koston
c08726036e Merge branch 'dev' into peername_no_double_ram 2026-01-30 20:13:13 -06:00
J. Nick Koston
d602a2e5e4 compile tmie safety at higheer level 2026-01-26 08:44:06 -10:00
J. Nick Koston
dcab12adae isra 2026-01-25 20:03:44 -10:00
J. Nick Koston
fb714636e3 missed 2026-01-25 20:02:46 -10:00
J. Nick Koston
05a431ea54 fixup 2026-01-25 20:02:46 -10:00
J. Nick Koston
1a34b4e7d7 [api] Remove duplicate peername storage to save RAM 2026-01-25 18:17:47 -10:00
56 changed files with 410 additions and 737 deletions

View File

@@ -1 +1 @@
8dc4dae0acfa22f26c7cde87fc24e60b27f29a73300e02189b78f0315e5d0695
37ec8d5a343c8d0a485fd2118cbdabcbccd7b9bca197e4a392be75087974dced

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

@@ -1344,12 +1344,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();

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,37 +117,7 @@ void APIServer::setup() {
void APIServer::loop() {
// Accept new clients only if the socket exists and has incoming connections
if (this->socket_ && this->socket_->ready()) {
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();
}
}
this->accept_new_connections_();
}
if (this->clients_.empty()) {
@@ -178,46 +148,84 @@ void APIServer::loop() {
while (client_index < this->clients_.size()) {
auto &client = this->clients_[client_index];
if (!client->flags_.remove) {
if (client->flags_.remove) {
// Rare case: handle disconnection (don't increment - swapped element needs processing)
this->remove_client_(client_index);
} else {
// 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;
}
// 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());
ESP_LOGD(TAG, "Accept %s", peername);
#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
auto *conn = new APIConnection(std::move(sock), this);
this->clients_.emplace_back(conn);
conn->start();
// 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();
// 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();
}
#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,6 +234,11 @@ 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

@@ -16,8 +16,8 @@ void CSE7766Component::loop() {
}
// Early return prevents updating last_transmission_ when no data is available.
size_t avail = this->available();
if (avail == 0) {
int avail = this->available();
if (avail <= 0) {
return;
}
@@ -27,7 +27,7 @@ void CSE7766Component::loop() {
// At 4800 baud (~480 bytes/sec) with ~122 Hz loop rate, typically ~4 bytes per call.
uint8_t buf[CSE7766_RAW_DATA_SIZE];
while (avail > 0) {
size_t to_read = std::min(avail, sizeof(buf));
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}

View File

@@ -133,10 +133,10 @@ void DFPlayer::send_cmd_(uint8_t cmd, uint16_t argument) {
void DFPlayer::loop() {
// Read all available bytes in batches to reduce UART call overhead.
size_t avail = this->available();
int avail = this->available();
uint8_t buf[64];
while (avail > 0) {
size_t to_read = std::min(avail, sizeof(buf));
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}

View File

@@ -120,9 +120,9 @@ void Dsmr::stop_requesting_data_() {
void Dsmr::drain_rx_buffer_() {
uint8_t buf[64];
size_t avail;
int avail;
while ((avail = this->available()) > 0) {
if (!this->read_array(buf, std::min(avail, sizeof(buf)))) {
if (!this->read_array(buf, std::min(static_cast<size_t>(avail), sizeof(buf)))) {
break;
}
}
@@ -140,9 +140,9 @@ void Dsmr::receive_telegram_() {
while (this->available_within_timeout_()) {
// Read all available bytes in batches to reduce UART call overhead.
uint8_t buf[64];
size_t avail = this->available();
int avail = this->available();
while (avail > 0) {
size_t to_read = std::min(avail, sizeof(buf));
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read))
return;
avail -= to_read;
@@ -206,9 +206,9 @@ void Dsmr::receive_encrypted_telegram_() {
while (this->available_within_timeout_()) {
// Read all available bytes in batches to reduce UART call overhead.
uint8_t buf[64];
size_t avail = this->available();
int avail = this->available();
while (avail > 0) {
size_t to_read = std::min(avail, sizeof(buf));
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read))
return;
avail -= to_read;

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

@@ -95,9 +95,9 @@ async def to_code(config):
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
os.environ["ESP_IDF_VERSION"] = f"{framework_ver.major}.{framework_ver.minor}"
if framework_ver >= cv.Version(5, 5, 0):
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.3.2")
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.2.4")
esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.4")
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.11.5")
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.9.3")
else:
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0")
esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0")

View File

@@ -7,24 +7,21 @@
#include "esphome/core/log.h"
#include <esp_attr.h>
#include <esp_clk_tree.h>
namespace esphome {
namespace esp32_rmt_led_strip {
static const char *const TAG = "esp32_rmt_led_strip";
static const size_t RMT_SYMBOLS_PER_BYTE = 8;
#ifdef USE_ESP32_VARIANT_ESP32H2
static const uint32_t RMT_CLK_FREQ = 32000000;
static const uint8_t RMT_CLK_DIV = 1;
#else
static const uint32_t RMT_CLK_FREQ = 80000000;
static const uint8_t RMT_CLK_DIV = 2;
#endif
// Query the RMT default clock source frequency. This varies by variant:
// APB (80MHz) on ESP32/S2/S3/C3, PLL_F80M (80MHz) on C6/P4, XTAL (32MHz) on H2.
// Worst-case reset time is WS2811 at 300µs = 24000 ticks at 80MHz, well within
// the 15-bit rmt_symbol_word_t duration field max of 32767.
static uint32_t rmt_resolution_hz() {
uint32_t freq;
esp_clk_tree_src_get_freq_hz((soc_module_clk_t) RMT_CLK_SRC_DEFAULT, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &freq);
return freq;
}
static const size_t RMT_SYMBOLS_PER_BYTE = 8;
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size_t symbols_written, size_t symbols_free,
@@ -95,7 +92,7 @@ void ESP32RMTLEDStripLightOutput::setup() {
rmt_tx_channel_config_t channel;
memset(&channel, 0, sizeof(channel));
channel.clk_src = RMT_CLK_SRC_DEFAULT;
channel.resolution_hz = rmt_resolution_hz();
channel.resolution_hz = RMT_CLK_FREQ / RMT_CLK_DIV;
channel.gpio_num = gpio_num_t(this->pin_);
channel.mem_block_symbols = this->rmt_symbols_;
channel.trans_queue_depth = 1;
@@ -140,7 +137,7 @@ void ESP32RMTLEDStripLightOutput::setup() {
void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high,
uint32_t bit1_low, uint32_t reset_time_high, uint32_t reset_time_low) {
float ratio = (float) rmt_resolution_hz() / 1e09f;
float ratio = (float) RMT_CLK_FREQ / RMT_CLK_DIV / 1e09f;
// 0-bit
this->params_.bit0.duration0 = (uint32_t) (ratio * bit0_high);

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

@@ -276,10 +276,10 @@ void LD2410Component::restart_and_read_all_info() {
void LD2410Component::loop() {
// Read all available bytes in batches to reduce UART call overhead.
size_t avail = this->available();
int avail = this->available();
uint8_t buf[MAX_LINE_LENGTH];
while (avail > 0) {
size_t to_read = std::min(avail, sizeof(buf));
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}

View File

@@ -311,10 +311,10 @@ void LD2412Component::restart_and_read_all_info() {
void LD2412Component::loop() {
// Read all available bytes in batches to reduce UART call overhead.
size_t avail = this->available();
int avail = this->available();
uint8_t buf[MAX_LINE_LENGTH];
while (avail > 0) {
size_t to_read = std::min(avail, sizeof(buf));
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}

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

@@ -277,10 +277,10 @@ void LD2450Component::dump_config() {
void LD2450Component::loop() {
// Read all available bytes in batches to reduce UART call overhead.
size_t avail = this->available();
int avail = this->available();
uint8_t buf[MAX_LINE_LENGTH];
while (avail > 0) {
size_t to_read = std::min(avail, sizeof(buf));
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}
@@ -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

@@ -20,10 +20,10 @@ void Modbus::loop() {
const uint32_t now = App.get_loop_component_start_time();
// Read all available bytes in batches to reduce UART call overhead.
size_t avail = this->available();
int avail = this->available();
uint8_t buf[64];
while (avail > 0) {
size_t to_read = std::min(avail, sizeof(buf));
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}

View File

@@ -398,10 +398,10 @@ bool Nextion::remove_from_q_(bool report_empty) {
void Nextion::process_serial_() {
// Read all available bytes in batches to reduce UART call overhead.
size_t avail = this->available();
int avail = this->available();
uint8_t buf[64];
while (avail > 0) {
size_t to_read = std::min(avail, sizeof(buf));
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}

View File

@@ -14,9 +14,9 @@ void Pipsolar::setup() {
void Pipsolar::empty_uart_buffer_() {
uint8_t buf[64];
size_t avail;
int avail;
while ((avail = this->available()) > 0) {
if (!this->read_array(buf, std::min(avail, sizeof(buf)))) {
if (!this->read_array(buf, std::min(static_cast<size_t>(avail), sizeof(buf)))) {
break;
}
}
@@ -97,10 +97,10 @@ void Pipsolar::loop() {
}
if (this->state_ == STATE_COMMAND || this->state_ == STATE_POLL) {
size_t avail = this->available();
int avail = this->available();
while (avail > 0) {
uint8_t buf[64];
size_t to_read = std::min(avail, sizeof(buf));
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}

View File

@@ -1,11 +1,6 @@
#include "pulse_counter_sensor.h"
#include "esphome/core/log.h"
#ifdef HAS_PCNT
#include <esp_clk_tree.h>
#include <hal/pcnt_ll.h>
#endif
namespace esphome {
namespace pulse_counter {
@@ -61,109 +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 apb_freq;
esp_clk_tree_src_get_freq_hz(SOC_MOD_CLK_APB, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &apb_freq);
uint32_t max_glitch_ns = PCNT_LL_MAX_GLITCH_WIDTH * 1000000u / 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

@@ -56,14 +56,14 @@ void PylontechComponent::setup() {
void PylontechComponent::update() { this->write_str("pwr\n"); }
void PylontechComponent::loop() {
size_t avail = this->available();
int avail = this->available();
if (avail > 0) {
// pylontech sends a lot of data very suddenly
// we need to quickly put it all into our own buffer, otherwise the uart's buffer will overflow
int recv = 0;
uint8_t buf[64];
while (avail > 0) {
size_t to_read = std::min(avail, sizeof(buf));
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}

View File

@@ -82,10 +82,10 @@ void RD03DComponent::dump_config() {
void RD03DComponent::loop() {
// Read all available bytes in batches to reduce UART call overhead.
size_t avail = this->available();
int avail = this->available();
uint8_t buf[64];
while (avail > 0) {
size_t to_read = std::min(avail, sizeof(buf));
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}

View File

@@ -3,11 +3,15 @@
#ifdef USE_ESP32
#include <driver/gpio.h>
#include <esp_clk_tree.h>
namespace esphome::remote_receiver {
static const char *const TAG = "remote_receiver.esp32";
#ifdef USE_ESP32_VARIANT_ESP32H2
static const uint32_t RMT_CLK_FREQ = 32000000;
#else
static const uint32_t RMT_CLK_FREQ = 80000000;
#endif
static bool IRAM_ATTR HOT rmt_callback(rmt_channel_handle_t channel, const rmt_rx_done_event_data_t *event, void *arg) {
RemoteReceiverComponentStore *store = (RemoteReceiverComponentStore *) arg;
@@ -94,10 +98,7 @@ void RemoteReceiverComponent::setup() {
}
uint32_t event_size = sizeof(rmt_rx_done_event_data_t);
uint32_t rmt_freq;
esp_clk_tree_src_get_freq_hz((soc_module_clk_t) RMT_CLK_SRC_DEFAULT, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED,
&rmt_freq);
uint32_t max_filter_ns = UINT8_MAX * 1000u / (rmt_freq / 1000000);
uint32_t max_filter_ns = 255u * 1000 / (RMT_CLK_FREQ / 1000000);
memset(&this->store_.config, 0, sizeof(this->store_.config));
this->store_.config.signal_range_min_ns = std::min(this->filter_us_ * 1000, max_filter_ns);
this->store_.config.signal_range_max_ns = this->idle_us_ * 1000;

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

@@ -136,10 +136,10 @@ void RFBridgeComponent::loop() {
this->last_bridge_byte_ = now;
}
size_t avail = this->available();
int avail = this->available();
while (avail > 0) {
uint8_t buf[64];
size_t to_read = std::min(avail, sizeof(buf));
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}

View File

@@ -107,10 +107,10 @@ void MR24HPC1Component::update_() {
// main loop
void MR24HPC1Component::loop() {
// Read all available bytes in batches to reduce UART call overhead.
size_t avail = this->available();
int avail = this->available();
uint8_t buf[64];
while (avail > 0) {
size_t to_read = std::min(avail, sizeof(buf));
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}

View File

@@ -31,10 +31,10 @@ void MR60BHA2Component::dump_config() {
// main loop
void MR60BHA2Component::loop() {
// Read all available bytes in batches to reduce UART call overhead.
size_t avail = this->available();
int avail = this->available();
uint8_t buf[64];
while (avail > 0) {
size_t to_read = std::min(avail, sizeof(buf));
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}

View File

@@ -50,10 +50,10 @@ void MR60FDA2Component::setup() {
// main loop
void MR60FDA2Component::loop() {
// Read all available bytes in batches to reduce UART call overhead.
size_t avail = this->available();
int avail = this->available();
uint8_t buf[64];
while (avail > 0) {
size_t to_read = std::min(avail, sizeof(buf));
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}

View File

@@ -16,13 +16,19 @@ namespace esphome::socket {
class BSDSocketImpl final : public Socket {
public:
BSDSocketImpl(int fd, bool monitor_loop = false) {
this->fd_ = fd;
BSDSocketImpl(int fd, bool monitor_loop = false) : fd_(fd) {
#ifdef USE_SOCKET_SELECT_SUPPORT
// Register new socket with the application for select() if monitoring requested
if (monitor_loop && this->fd_ >= 0) {
// Only set loop_monitored_ to true if registration succeeds
this->loop_monitored_ = App.register_socket_fd(this->fd_);
} else {
this->loop_monitored_ = false;
}
#else
// Without select support, ignore monitor_loop parameter
(void) monitor_loop;
#endif
}
~BSDSocketImpl() override {
if (!this->closed_) {
@@ -46,10 +52,12 @@ class BSDSocketImpl final : public Socket {
int bind(const struct sockaddr *addr, socklen_t addrlen) override { return ::bind(this->fd_, addr, addrlen); }
int close() override {
if (!this->closed_) {
#ifdef USE_SOCKET_SELECT_SUPPORT
// Unregister from select() before closing if monitored
if (this->loop_monitored_) {
App.unregister_socket_fd(this->fd_);
}
#endif
int ret = ::close(this->fd_);
this->closed_ = true;
return ret;
@@ -122,6 +130,23 @@ class BSDSocketImpl final : public Socket {
::fcntl(this->fd_, F_SETFL, fl);
return 0;
}
int get_fd() const override { return this->fd_; }
#ifdef USE_SOCKET_SELECT_SUPPORT
bool ready() const override {
if (!this->loop_monitored_)
return true;
return App.is_socket_ready(this->fd_);
}
#endif
protected:
int fd_;
bool closed_{false};
#ifdef USE_SOCKET_SELECT_SUPPORT
bool loop_monitored_{false};
#endif
};
// Helper to create a socket with optional monitoring

View File

@@ -11,13 +11,19 @@ namespace esphome::socket {
class LwIPSocketImpl final : public Socket {
public:
LwIPSocketImpl(int fd, bool monitor_loop = false) {
this->fd_ = fd;
LwIPSocketImpl(int fd, bool monitor_loop = false) : fd_(fd) {
#ifdef USE_SOCKET_SELECT_SUPPORT
// Register new socket with the application for select() if monitoring requested
if (monitor_loop && this->fd_ >= 0) {
// Only set loop_monitored_ to true if registration succeeds
this->loop_monitored_ = App.register_socket_fd(this->fd_);
} else {
this->loop_monitored_ = false;
}
#else
// Without select support, ignore monitor_loop parameter
(void) monitor_loop;
#endif
}
~LwIPSocketImpl() override {
if (!this->closed_) {
@@ -43,10 +49,12 @@ class LwIPSocketImpl final : public Socket {
int bind(const struct sockaddr *addr, socklen_t addrlen) override { return lwip_bind(this->fd_, addr, addrlen); }
int close() override {
if (!this->closed_) {
#ifdef USE_SOCKET_SELECT_SUPPORT
// Unregister from select() before closing if monitored
if (this->loop_monitored_) {
App.unregister_socket_fd(this->fd_);
}
#endif
int ret = lwip_close(this->fd_);
this->closed_ = true;
return ret;
@@ -89,6 +97,23 @@ class LwIPSocketImpl final : public Socket {
lwip_fcntl(this->fd_, F_SETFL, fl);
return 0;
}
int get_fd() const override { return this->fd_; }
#ifdef USE_SOCKET_SELECT_SUPPORT
bool ready() const override {
if (!this->loop_monitored_)
return true;
return App.is_socket_ready(this->fd_);
}
#endif
protected:
int fd_;
bool closed_{false};
#ifdef USE_SOCKET_SELECT_SUPPORT
bool loop_monitored_{false};
#endif
};
// Helper to create a socket with optional monitoring

View File

@@ -10,14 +10,6 @@ namespace esphome::socket {
Socket::~Socket() {}
bool Socket::ready() const {
#ifdef USE_SOCKET_SELECT_SUPPORT
return !this->loop_monitored_ || App.is_socket_ready_(this->fd_);
#else
return true;
#endif
}
// Platform-specific inet_ntop wrappers
#if defined(USE_SOCKET_IMPL_LWIP_TCP)
// LWIP raw TCP (ESP8266) uses inet_ntoa_r which takes struct by value

View File

@@ -63,25 +63,13 @@ class Socket {
virtual int setblocking(bool blocking) = 0;
virtual int loop() { return 0; };
/// Get the underlying file descriptor (returns -1 if not supported)
/// Non-virtual: only one socket implementation is active per build.
#ifdef USE_SOCKET_SELECT_SUPPORT
int get_fd() const { return this->fd_; }
#else
int get_fd() const { return -1; }
#endif
/// Get the underlying file descriptor (returns -1 if not supported)
virtual int get_fd() const { return -1; }
/// Check if socket has data ready to read (non-virtual for direct call)
/// For loop-monitored sockets, checks the Application's select() results
/// Check if socket has data ready to read
/// For loop-monitored sockets, checks with the Application's select() results
/// For non-monitored sockets, always returns true (assumes data may be available)
bool ready() const;
protected:
#ifdef USE_SOCKET_SELECT_SUPPORT
int fd_{-1};
bool closed_{false};
bool loop_monitored_{false};
#endif
virtual bool ready() const { return true; }
};
/// Create a socket of the given domain, type and protocol.

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

@@ -32,10 +32,10 @@ void Tuya::setup() {
void Tuya::loop() {
// Read all available bytes in batches to reduce UART call overhead.
size_t avail = this->available();
int avail = this->available();
uint8_t buf[64];
while (avail > 0) {
size_t to_read = std::min(avail, sizeof(buf));
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}

View File

@@ -605,6 +605,15 @@ void Application::unregister_socket_fd(int fd) {
}
}
bool Application::is_socket_ready(int fd) const {
// This function is thread-safe for reading the result of select()
// However, it should only be called after select() has been executed in the main loop
// The read_fds_ is only modified by select() in the main loop
if (fd < 0 || fd >= FD_SETSIZE)
return false;
return FD_ISSET(fd, &this->read_fds_);
}
#endif
void Application::yield_with_select_(uint32_t delay_ms) {

View File

@@ -101,10 +101,6 @@
#include "esphome/components/update/update_entity.h"
#endif
namespace esphome::socket {
class Socket;
} // namespace esphome::socket
namespace esphome {
// Teardown timeout constant (in milliseconds)
@@ -495,8 +491,7 @@ class Application {
void unregister_socket_fd(int fd);
/// Check if there's data available on a socket without blocking
/// This function is thread-safe for reading, but should be called after select() has run
/// The read_fds_ is only modified by select() in the main loop
bool is_socket_ready(int fd) const { return fd >= 0 && this->is_socket_ready_(fd); }
bool is_socket_ready(int fd) const;
#ifdef USE_WAKE_LOOP_THREADSAFE
/// Wake the main event loop from a FreeRTOS task
@@ -508,15 +503,6 @@ class Application {
protected:
friend Component;
friend class socket::Socket;
#ifdef USE_SOCKET_SELECT_SUPPORT
/// Fast path for Socket::ready() via friendship - skips negative fd check.
/// Safe because: fd was validated in register_socket_fd() at registration time,
/// and Socket::ready() only calls this when loop_monitored_ is true (registration succeeded).
/// FD_ISSET may include its own upper bounds check depending on platform.
bool is_socket_ready_(int fd) const { return FD_ISSET(fd, &this->read_fds_); }
#endif
void register_component_(Component *comp);

View File

@@ -10,7 +10,7 @@ dependencies:
espressif/mdns:
version: 1.9.1
espressif/esp_wifi_remote:
version: 1.3.2
version: 1.2.4
rules:
- if: "target in [esp32h2, esp32p4]"
espressif/eppp_link:
@@ -18,7 +18,7 @@ dependencies:
rules:
- if: "target in [esp32h2, esp32p4]"
espressif/esp_hosted:
version: 2.11.5
version: 2.9.3
rules:
- if: "target in [esp32h2, esp32p4]"
zorxx/multipart-parser:

View File

@@ -136,7 +136,6 @@ extends = common:arduino
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.36/platform-espressif32.zip
platform_packages =
pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.6/esp32-core-3.3.6.tar.xz
pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.2/esp-idf-v5.5.2.tar.xz
framework = arduino, espidf ; Arduino as an ESP-IDF component
lib_deps =

View File

@@ -11,8 +11,8 @@ pyserial==3.5
platformio==6.1.19
esptool==5.1.0
click==8.1.7
esphome-dashboard==20260210.0
aioesphomeapi==44.0.0
esphome-dashboard==20260110.0
aioesphomeapi==43.14.0
zeroconf==0.148.0
puremagic==1.30
ruamel.yaml==0.19.1 # dashboard_import

View File

@@ -1,8 +1,5 @@
ld2450:
- id: ld2450_radar
on_data:
then:
- logger.log: "LD2450 Radar Data Received"
button:
- platform: ld2450

View File

@@ -13,8 +13,6 @@ esphome:
id: template_water_heater
target_temperature: 50.0
mode: ECO
away: false
is_on: true
# Templated
- water_heater.template.publish:
@@ -22,8 +20,6 @@ esphome:
current_temperature: !lambda "return 45.0;"
target_temperature: !lambda "return 55.0;"
mode: !lambda "return water_heater::WATER_HEATER_MODE_GAS;"
away: !lambda "return true;"
is_on: !lambda "return false;"
# Test C++ API: set_template() with stateless lambda (no captures)
# NOTE: set_template() is not intended to be a public API, but we test it to ensure it doesn't break.
@@ -418,8 +414,6 @@ water_heater:
current_temperature: !lambda "return 42.0f;"
target_temperature: !lambda "return 60.0f;"
mode: !lambda "return water_heater::WATER_HEATER_MODE_ECO;"
away: !lambda "return false;"
is_on: !lambda "return true;"
supported_modes:
- "OFF"
- ECO

View File

@@ -4,14 +4,6 @@ host:
api:
logger:
globals:
- id: global_away
type: bool
initial_value: "false"
- id: global_is_on
type: bool
initial_value: "true"
water_heater:
- platform: template
id: test_boiler
@@ -19,8 +11,6 @@ water_heater:
optimistic: true
current_temperature: !lambda "return 45.0f;"
target_temperature: !lambda "return 60.0f;"
away: !lambda "return id(global_away);"
is_on: !lambda "return id(global_is_on);"
# Note: No mode lambda - we want optimistic mode changes to stick
# A mode lambda would override mode changes in loop()
supported_modes:
@@ -32,8 +22,3 @@ water_heater:
min_temperature: 30.0
max_temperature: 85.0
target_temperature_step: 0.5
set_action:
- lambda: |-
// Sync optimistic state back to globals so lambdas reflect the change
id(global_away) = id(test_boiler).is_away();
id(global_is_on) = id(test_boiler).is_on();

View File

@@ -5,13 +5,7 @@ from __future__ import annotations
import asyncio
import aioesphomeapi
from aioesphomeapi import (
WaterHeaterFeature,
WaterHeaterInfo,
WaterHeaterMode,
WaterHeaterState,
WaterHeaterStateFlag,
)
from aioesphomeapi import WaterHeaterInfo, WaterHeaterMode, WaterHeaterState
import pytest
from .state_utils import InitialStateHelper
@@ -28,25 +22,18 @@ async def test_water_heater_template(
loop = asyncio.get_running_loop()
async with run_compiled(yaml_config), api_client_connected() as client:
states: dict[int, aioesphomeapi.EntityState] = {}
state_future: asyncio.Future[WaterHeaterState] | None = None
gas_mode_future: asyncio.Future[WaterHeaterState] = loop.create_future()
eco_mode_future: asyncio.Future[WaterHeaterState] = loop.create_future()
def on_state(state: aioesphomeapi.EntityState) -> None:
states[state.key] = state
if (
isinstance(state, WaterHeaterState)
and state_future is not None
and not state_future.done()
):
state_future.set_result(state)
async def wait_for_state(timeout: float = 5.0) -> WaterHeaterState:
"""Wait for next water heater state change."""
nonlocal state_future
state_future = loop.create_future()
try:
return await asyncio.wait_for(state_future, timeout)
finally:
state_future = None
if isinstance(state, WaterHeaterState):
# Wait for GAS mode
if state.mode == WaterHeaterMode.GAS and not gas_mode_future.done():
gas_mode_future.set_result(state)
# Wait for ECO mode (we start at OFF, so test transitioning to ECO)
elif state.mode == WaterHeaterMode.ECO and not eco_mode_future.done():
eco_mode_future.set_result(state)
# Get entities and set up state synchronization
entities, services = await client.list_entities_services()
@@ -102,52 +89,24 @@ async def test_water_heater_template(
f"Expected target temp 60.0, got {initial_state.target_temperature}"
)
# Verify supported features: away mode and on/off (fixture has away + is_on lambdas)
assert (
test_water_heater.supported_features & WaterHeaterFeature.SUPPORTS_AWAY_MODE
) != 0, "Expected SUPPORTS_AWAY_MODE in supported_features"
assert (
test_water_heater.supported_features & WaterHeaterFeature.SUPPORTS_ON_OFF
) != 0, "Expected SUPPORTS_ON_OFF in supported_features"
# Verify initial state: on (is_on lambda returns true), not away (away lambda returns false)
assert (initial_state.state & WaterHeaterStateFlag.ON) != 0, (
"Expected initial state to include ON flag"
)
assert (initial_state.state & WaterHeaterStateFlag.AWAY) == 0, (
"Expected initial state to not include AWAY flag"
)
# Test turning on away mode
client.water_heater_command(test_water_heater.key, away=True)
away_on_state = await wait_for_state()
assert (away_on_state.state & WaterHeaterStateFlag.AWAY) != 0
# ON flag should still be set (is_on lambda returns true)
assert (away_on_state.state & WaterHeaterStateFlag.ON) != 0
# Test turning off away mode
client.water_heater_command(test_water_heater.key, away=False)
away_off_state = await wait_for_state()
assert (away_off_state.state & WaterHeaterStateFlag.AWAY) == 0
assert (away_off_state.state & WaterHeaterStateFlag.ON) != 0
# Test turning off (on=False)
client.water_heater_command(test_water_heater.key, on=False)
off_state = await wait_for_state()
assert (off_state.state & WaterHeaterStateFlag.ON) == 0
assert (off_state.state & WaterHeaterStateFlag.AWAY) == 0
# Test turning back on (on=True)
client.water_heater_command(test_water_heater.key, on=True)
on_state = await wait_for_state()
assert (on_state.state & WaterHeaterStateFlag.ON) != 0
# Test changing to GAS mode
client.water_heater_command(test_water_heater.key, mode=WaterHeaterMode.GAS)
gas_state = await wait_for_state()
try:
gas_state = await asyncio.wait_for(gas_mode_future, timeout=5.0)
except TimeoutError:
pytest.fail("GAS mode change not received within 5 seconds")
assert isinstance(gas_state, WaterHeaterState)
assert gas_state.mode == WaterHeaterMode.GAS
# Test changing to ECO mode (from GAS)
client.water_heater_command(test_water_heater.key, mode=WaterHeaterMode.ECO)
eco_state = await wait_for_state()
try:
eco_state = await asyncio.wait_for(eco_mode_future, timeout=5.0)
except TimeoutError:
pytest.fail("ECO mode change not received within 5 seconds")
assert isinstance(eco_state, WaterHeaterState)
assert eco_state.mode == WaterHeaterMode.ECO