mirror of
https://github.com/esphome/esphome.git
synced 2026-02-10 19:47:35 -07:00
Compare commits
14 Commits
api-dedup-
...
socket_rea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd683a5609 | ||
|
|
4a6eb0b16d | ||
|
|
2585779f11 | ||
|
|
b8ec3aab1d | ||
|
|
c4b109eebd | ||
|
|
03b41855f5 | ||
|
|
13a124c86d | ||
|
|
298efb5340 | ||
|
|
d4ccc64dc0 | ||
|
|
e3141211c3 | ||
|
|
e85a022c77 | ||
|
|
1c3af30299 | ||
|
|
5caed68cd9 | ||
|
|
b97a728cf1 |
@@ -1 +1 @@
|
|||||||
37ec8d5a343c8d0a485fd2118cbdabcbccd7b9bca197e4a392be75087974dced
|
8dc4dae0acfa22f26c7cde87fc24e60b27f29a73300e02189b78f0315e5d0695
|
||||||
|
|||||||
@@ -1155,9 +1155,11 @@ enum WaterHeaterCommandHasField {
|
|||||||
WATER_HEATER_COMMAND_HAS_NONE = 0;
|
WATER_HEATER_COMMAND_HAS_NONE = 0;
|
||||||
WATER_HEATER_COMMAND_HAS_MODE = 1;
|
WATER_HEATER_COMMAND_HAS_MODE = 1;
|
||||||
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE = 2;
|
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE = 2;
|
||||||
WATER_HEATER_COMMAND_HAS_STATE = 4;
|
WATER_HEATER_COMMAND_HAS_STATE = 4 [deprecated=true];
|
||||||
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8;
|
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8;
|
||||||
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH = 16;
|
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH = 16;
|
||||||
|
WATER_HEATER_COMMAND_HAS_ON_STATE = 32;
|
||||||
|
WATER_HEATER_COMMAND_HAS_AWAY_STATE = 64;
|
||||||
}
|
}
|
||||||
|
|
||||||
message WaterHeaterCommandRequest {
|
message WaterHeaterCommandRequest {
|
||||||
|
|||||||
@@ -133,8 +133,8 @@ void APIConnection::start() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Initialize client name with peername (IP address) until Hello message provides actual name
|
// Initialize client name with peername (IP address) until Hello message provides actual name
|
||||||
const char *peername = this->helper_->get_client_peername();
|
char peername[socket::SOCKADDR_STR_LEN];
|
||||||
this->helper_->set_client_name(peername, strlen(peername));
|
this->helper_->set_client_name(this->helper_->get_peername_to(peername), strlen(peername));
|
||||||
}
|
}
|
||||||
|
|
||||||
APIConnection::~APIConnection() {
|
APIConnection::~APIConnection() {
|
||||||
@@ -179,8 +179,8 @@ void APIConnection::begin_iterator_(ActiveIterator type) {
|
|||||||
|
|
||||||
void APIConnection::loop() {
|
void APIConnection::loop() {
|
||||||
if (this->flags_.next_close) {
|
if (this->flags_.next_close) {
|
||||||
// requested a disconnect
|
// requested a disconnect - don't close socket here, let APIServer::loop() do it
|
||||||
this->helper_->close();
|
// so getpeername() still works for the disconnect trigger
|
||||||
this->flags_.remove = true;
|
this->flags_.remove = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -219,8 +219,35 @@ void APIConnection::loop() {
|
|||||||
this->process_batch_();
|
this->process_batch_();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this->active_iterator_ != ActiveIterator::NONE) {
|
switch (this->active_iterator_) {
|
||||||
this->process_active_iterator_();
|
case ActiveIterator::LIST_ENTITIES:
|
||||||
|
if (this->iterator_storage_.list_entities.completed()) {
|
||||||
|
this->destroy_active_iterator_();
|
||||||
|
if (this->flags_.state_subscription) {
|
||||||
|
this->begin_iterator_(ActiveIterator::INITIAL_STATE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this->process_iterator_batch_(this->iterator_storage_.list_entities);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ActiveIterator::INITIAL_STATE:
|
||||||
|
if (this->iterator_storage_.initial_state.completed()) {
|
||||||
|
this->destroy_active_iterator_();
|
||||||
|
// Process any remaining batched messages immediately
|
||||||
|
if (!this->deferred_batch_.empty()) {
|
||||||
|
this->process_batch_();
|
||||||
|
}
|
||||||
|
// Now that everything is sent, enable immediate sending for future state changes
|
||||||
|
this->flags_.should_try_send_immediately = true;
|
||||||
|
// Release excess memory from buffers that grew during initial sync
|
||||||
|
this->deferred_batch_.release_buffer();
|
||||||
|
this->helper_->release_buffers();
|
||||||
|
} else {
|
||||||
|
this->process_iterator_batch_(this->iterator_storage_.initial_state);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ActiveIterator::NONE:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this->flags_.sent_ping) {
|
if (this->flags_.sent_ping) {
|
||||||
@@ -256,49 +283,6 @@ void APIConnection::loop() {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void APIConnection::process_active_iterator_() {
|
|
||||||
// Caller ensures active_iterator_ != NONE
|
|
||||||
if (this->active_iterator_ == ActiveIterator::LIST_ENTITIES) {
|
|
||||||
if (this->iterator_storage_.list_entities.completed()) {
|
|
||||||
this->destroy_active_iterator_();
|
|
||||||
if (this->flags_.state_subscription) {
|
|
||||||
this->begin_iterator_(ActiveIterator::INITIAL_STATE);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this->process_iterator_batch_(this->iterator_storage_.list_entities);
|
|
||||||
}
|
|
||||||
} else { // INITIAL_STATE
|
|
||||||
if (this->iterator_storage_.initial_state.completed()) {
|
|
||||||
this->destroy_active_iterator_();
|
|
||||||
// Process any remaining batched messages immediately
|
|
||||||
if (!this->deferred_batch_.empty()) {
|
|
||||||
this->process_batch_();
|
|
||||||
}
|
|
||||||
// Now that everything is sent, enable immediate sending for future state changes
|
|
||||||
this->flags_.should_try_send_immediately = true;
|
|
||||||
// Release excess memory from buffers that grew during initial sync
|
|
||||||
this->deferred_batch_.release_buffer();
|
|
||||||
this->helper_->release_buffers();
|
|
||||||
} else {
|
|
||||||
this->process_iterator_batch_(this->iterator_storage_.initial_state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void APIConnection::process_iterator_batch_(ComponentIterator &iterator) {
|
|
||||||
size_t initial_size = this->deferred_batch_.size();
|
|
||||||
size_t max_batch = this->get_max_batch_size_();
|
|
||||||
while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < max_batch) {
|
|
||||||
iterator.advance();
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the batch is full, process it immediately
|
|
||||||
// Note: iterator.advance() already calls schedule_batch_() via schedule_message_()
|
|
||||||
if (this->deferred_batch_.size() >= max_batch) {
|
|
||||||
this->process_batch_();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool APIConnection::send_disconnect_response_() {
|
bool APIConnection::send_disconnect_response_() {
|
||||||
// remote initiated disconnect_client
|
// remote initiated disconnect_client
|
||||||
// don't close yet, we still need to send the disconnect response
|
// don't close yet, we still need to send the disconnect response
|
||||||
@@ -309,7 +293,8 @@ bool APIConnection::send_disconnect_response_() {
|
|||||||
return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE);
|
return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE);
|
||||||
}
|
}
|
||||||
void APIConnection::on_disconnect_response() {
|
void APIConnection::on_disconnect_response() {
|
||||||
this->helper_->close();
|
// Don't close socket here, let APIServer::loop() do it
|
||||||
|
// so getpeername() still works for the disconnect trigger
|
||||||
this->flags_.remove = true;
|
this->flags_.remove = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1359,8 +1344,12 @@ void APIConnection::on_water_heater_command_request(const WaterHeaterCommandRequ
|
|||||||
call.set_target_temperature_low(msg.target_temperature_low);
|
call.set_target_temperature_low(msg.target_temperature_low);
|
||||||
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH)
|
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH)
|
||||||
call.set_target_temperature_high(msg.target_temperature_high);
|
call.set_target_temperature_high(msg.target_temperature_high);
|
||||||
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_STATE) {
|
if ((msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_AWAY_STATE) ||
|
||||||
|
(msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_STATE)) {
|
||||||
call.set_away((msg.state & water_heater::WATER_HEATER_STATE_AWAY) != 0);
|
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.set_on((msg.state & water_heater::WATER_HEATER_STATE_ON) != 0);
|
||||||
}
|
}
|
||||||
call.perform();
|
call.perform();
|
||||||
@@ -1481,8 +1470,11 @@ void APIConnection::complete_authentication_() {
|
|||||||
this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED);
|
this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED);
|
||||||
this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("connected"));
|
this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("connected"));
|
||||||
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
|
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
|
||||||
this->parent_->get_client_connected_trigger()->trigger(std::string(this->helper_->get_client_name()),
|
{
|
||||||
std::string(this->helper_->get_client_peername()));
|
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)));
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_HOMEASSISTANT_TIME
|
#ifdef USE_HOMEASSISTANT_TIME
|
||||||
if (homeassistant::global_homeassistant_time != nullptr) {
|
if (homeassistant::global_homeassistant_time != nullptr) {
|
||||||
@@ -1501,8 +1493,9 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) {
|
|||||||
this->helper_->set_client_name(msg.client_info.c_str(), msg.client_info.size());
|
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_major_ = msg.api_version_major;
|
||||||
this->client_api_version_minor_ = msg.api_version_minor;
|
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(),
|
ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->helper_->get_client_name(),
|
||||||
this->helper_->get_client_peername(), this->client_api_version_major_, this->client_api_version_minor_);
|
this->helper_->get_peername_to(peername), this->client_api_version_major_, this->client_api_version_minor_);
|
||||||
|
|
||||||
HelloResponse resp;
|
HelloResponse resp;
|
||||||
resp.api_version_major = 1;
|
resp.api_version_major = 1;
|
||||||
@@ -1850,7 +1843,8 @@ void APIConnection::on_no_setup_connection() {
|
|||||||
this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("no connection setup"));
|
this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("no connection setup"));
|
||||||
}
|
}
|
||||||
void APIConnection::on_fatal_error() {
|
void APIConnection::on_fatal_error() {
|
||||||
this->helper_->close();
|
// 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->flags_.remove = true;
|
this->flags_.remove = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2207,12 +2201,14 @@ void APIConnection::process_state_subscriptions_() {
|
|||||||
#endif // USE_API_HOMEASSISTANT_STATES
|
#endif // USE_API_HOMEASSISTANT_STATES
|
||||||
|
|
||||||
void APIConnection::log_client_(int level, const LogString *message) {
|
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(),
|
esp_log_printf_(level, TAG, __LINE__, ESPHOME_LOG_FORMAT("%s (%s): %s"), this->helper_->get_client_name(),
|
||||||
this->helper_->get_client_peername(), LOG_STR_ARG(message));
|
this->helper_->get_peername_to(peername), LOG_STR_ARG(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
void APIConnection::log_warning_(const LogString *message, APIError err) {
|
void APIConnection::log_warning_(const LogString *message, APIError err) {
|
||||||
ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->helper_->get_client_name(), this->helper_->get_client_peername(),
|
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),
|
||||||
LOG_STR_ARG(message), LOG_STR_ARG(api_error_to_logstr(err)), errno);
|
LOG_STR_ARG(message), LOG_STR_ARG(api_error_to_logstr(err)), errno);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,10 +15,6 @@
|
|||||||
#include <limits>
|
#include <limits>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace esphome {
|
|
||||||
class ComponentIterator;
|
|
||||||
} // namespace esphome
|
|
||||||
|
|
||||||
namespace esphome::api {
|
namespace esphome::api {
|
||||||
|
|
||||||
// Keepalive timeout in milliseconds
|
// Keepalive timeout in milliseconds
|
||||||
@@ -280,8 +276,10 @@ class APIConnection final : public APIServerConnectionBase {
|
|||||||
bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override;
|
bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override;
|
||||||
|
|
||||||
const char *get_name() const { return this->helper_->get_client_name(); }
|
const char *get_name() const { return this->helper_->get_client_name(); }
|
||||||
/// Get peer name (IP address) - cached at connection init time
|
/// Get peer name (IP address) into caller-provided buffer, returns buf for convenience
|
||||||
const char *get_peername() const { return this->helper_->get_client_peername(); }
|
const char *get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const {
|
||||||
|
return this->helper_->get_peername_to(buf);
|
||||||
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// Helper function to handle authentication completion
|
// Helper function to handle authentication completion
|
||||||
@@ -368,13 +366,20 @@ class APIConnection final : public APIServerConnectionBase {
|
|||||||
return this->client_supports_api_version(1, 14) ? MAX_INITIAL_PER_BATCH : MAX_INITIAL_PER_BATCH_LEGACY;
|
return this->client_supports_api_version(1, 14) ? MAX_INITIAL_PER_BATCH : MAX_INITIAL_PER_BATCH_LEGACY;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process active iterator (list_entities/initial_state) during connection setup.
|
// Helper method to process multiple entities from an iterator in a batch
|
||||||
// Extracted from loop() — only runs during initial handshake, NONE in steady state.
|
template<typename Iterator> void process_iterator_batch_(Iterator &iterator) {
|
||||||
void __attribute__((noinline)) process_active_iterator_();
|
size_t initial_size = this->deferred_batch_.size();
|
||||||
|
size_t max_batch = this->get_max_batch_size_();
|
||||||
|
while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < max_batch) {
|
||||||
|
iterator.advance();
|
||||||
|
}
|
||||||
|
|
||||||
// Helper method to process multiple entities from an iterator in a batch.
|
// If the batch is full, process it immediately
|
||||||
// Takes ComponentIterator base class reference to avoid duplicate template instantiations.
|
// Note: iterator.advance() already calls schedule_batch_() via schedule_message_()
|
||||||
void process_iterator_batch_(ComponentIterator &iterator);
|
if (this->deferred_batch_.size() >= max_batch) {
|
||||||
|
this->process_batch_();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef USE_BINARY_SENSOR
|
#ifdef USE_BINARY_SENSOR
|
||||||
static uint16_t try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
static uint16_t try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||||
|
|||||||
@@ -16,7 +16,12 @@ static const char *const TAG = "api.frame_helper";
|
|||||||
static constexpr size_t API_MAX_LOG_BYTES = 168;
|
static constexpr size_t API_MAX_LOG_BYTES = 168;
|
||||||
|
|
||||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||||
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
|
#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)
|
||||||
#else
|
#else
|
||||||
#define HELPER_LOG(msg, ...) ((void) 0)
|
#define HELPER_LOG(msg, ...) ((void) 0)
|
||||||
#endif
|
#endif
|
||||||
@@ -240,13 +245,20 @@ APIError APIFrameHelper::try_send_tx_buf_() {
|
|||||||
return APIError::OK; // All buffers sent successfully
|
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_() {
|
APIError APIFrameHelper::init_common_() {
|
||||||
if (state_ != State::INITIALIZE || this->socket_ == nullptr) {
|
if (state_ != State::INITIALIZE || this->socket_ == nullptr) {
|
||||||
HELPER_LOG("Bad state for init %d", (int) state_);
|
HELPER_LOG("Bad state for init %d", (int) state_);
|
||||||
return APIError::BAD_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);
|
int err = this->socket_->setblocking(false);
|
||||||
if (err != 0) {
|
if (err != 0) {
|
||||||
state_ = State::FAILED;
|
state_ = State::FAILED;
|
||||||
|
|||||||
@@ -90,8 +90,9 @@ class APIFrameHelper {
|
|||||||
|
|
||||||
// Get client name (null-terminated)
|
// Get client name (null-terminated)
|
||||||
const char *get_client_name() const { return this->client_name_; }
|
const char *get_client_name() const { return this->client_name_; }
|
||||||
// Get client peername/IP (null-terminated, cached at init time for availability after socket failure)
|
// Get client peername/IP into caller-provided buffer (fetches on-demand from socket)
|
||||||
const char *get_client_peername() const { return this->client_peername_; }
|
// Returns pointer to buf for convenience in printf-style calls
|
||||||
|
const char *get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const;
|
||||||
// Set client name from buffer with length (truncates if needed)
|
// Set client name from buffer with length (truncates if needed)
|
||||||
void set_client_name(const char *name, size_t len) {
|
void set_client_name(const char *name, size_t len) {
|
||||||
size_t copy_len = std::min(len, sizeof(this->client_name_) - 1);
|
size_t copy_len = std::min(len, sizeof(this->client_name_) - 1);
|
||||||
@@ -105,6 +106,8 @@ class APIFrameHelper {
|
|||||||
bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 0; }
|
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); }
|
int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); }
|
||||||
APIError close() {
|
APIError close() {
|
||||||
|
if (state_ == State::CLOSED)
|
||||||
|
return APIError::OK; // Already closed
|
||||||
state_ = State::CLOSED;
|
state_ = State::CLOSED;
|
||||||
int err = this->socket_->close();
|
int err = this->socket_->close();
|
||||||
if (err == -1)
|
if (err == -1)
|
||||||
@@ -231,8 +234,6 @@ class APIFrameHelper {
|
|||||||
|
|
||||||
// Client name buffer - stores name from Hello message or initial peername
|
// Client name buffer - stores name from Hello message or initial peername
|
||||||
char client_name_[CLIENT_INFO_NAME_MAX_LEN]{};
|
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
|
// Group smaller types together
|
||||||
uint16_t rx_buf_len_ = 0;
|
uint16_t rx_buf_len_ = 0;
|
||||||
|
|||||||
@@ -29,7 +29,12 @@ static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit")
|
|||||||
static constexpr size_t API_MAX_LOG_BYTES = 168;
|
static constexpr size_t API_MAX_LOG_BYTES = 168;
|
||||||
|
|
||||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||||
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
|
#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)
|
||||||
#else
|
#else
|
||||||
#define HELPER_LOG(msg, ...) ((void) 0)
|
#define HELPER_LOG(msg, ...) ((void) 0)
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -21,7 +21,12 @@ static const char *const TAG = "api.plaintext";
|
|||||||
static constexpr size_t API_MAX_LOG_BYTES = 168;
|
static constexpr size_t API_MAX_LOG_BYTES = 168;
|
||||||
|
|
||||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||||
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
|
#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)
|
||||||
#else
|
#else
|
||||||
#define HELPER_LOG(msg, ...) ((void) 0)
|
#define HELPER_LOG(msg, ...) ((void) 0)
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -147,6 +147,8 @@ enum WaterHeaterCommandHasField : uint32_t {
|
|||||||
WATER_HEATER_COMMAND_HAS_STATE = 4,
|
WATER_HEATER_COMMAND_HAS_STATE = 4,
|
||||||
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8,
|
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8,
|
||||||
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH = 16,
|
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
|
#ifdef USE_NUMBER
|
||||||
enum NumberMode : uint32_t {
|
enum NumberMode : uint32_t {
|
||||||
|
|||||||
@@ -385,6 +385,10 @@ const char *proto_enum_to_string<enums::WaterHeaterCommandHasField>(enums::Water
|
|||||||
return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW";
|
return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW";
|
||||||
case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH:
|
case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH:
|
||||||
return "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:
|
default:
|
||||||
return "UNKNOWN";
|
return "UNKNOWN";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -192,11 +192,15 @@ void APIServer::loop() {
|
|||||||
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
|
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
|
||||||
|
|
||||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||||
// Save client info before removal for the 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_name(client->get_name());
|
||||||
std::string client_peername(client->get_peername());
|
std::string client_peername(client->get_peername_to(peername_buf));
|
||||||
#endif
|
#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)
|
// Swap with the last element and pop (avoids expensive vector shifts)
|
||||||
if (client_index < this->clients_.size() - 1) {
|
if (client_index < this->clients_.size() - 1) {
|
||||||
std::swap(this->clients_[client_index], this->clients_.back());
|
std::swap(this->clients_[client_index], this->clients_.back());
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ class ListEntitiesIterator : public ComponentIterator {
|
|||||||
bool on_update(update::UpdateEntity *entity) override;
|
bool on_update(update::UpdateEntity *entity) override;
|
||||||
#endif
|
#endif
|
||||||
bool on_end() override;
|
bool on_end() override;
|
||||||
|
bool completed() { return this->state_ == IteratorState::NONE; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
APIConnection *client_;
|
APIConnection *client_;
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ class InitialStateIterator : public ComponentIterator {
|
|||||||
#ifdef USE_UPDATE
|
#ifdef USE_UPDATE
|
||||||
bool on_update(update::UpdateEntity *entity) override;
|
bool on_update(update::UpdateEntity *entity) override;
|
||||||
#endif
|
#endif
|
||||||
|
bool completed() { return this->state_ == IteratorState::NONE; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
APIConnection *client_;
|
APIConnection *client_;
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = (
|
|||||||
"esp_driver_dac", # DAC driver - only needed by esp32_dac component
|
"esp_driver_dac", # DAC driver - only needed by esp32_dac component
|
||||||
"esp_driver_i2s", # I2S driver - only needed by i2s_audio 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_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_rmt", # RMT driver - only needed by remote_transmitter/receiver, neopixelbus
|
||||||
"esp_driver_touch_sens", # Touch sensor driver - only needed by esp32_touch
|
"esp_driver_touch_sens", # Touch sensor driver - only needed by esp32_touch
|
||||||
"esp_driver_twai", # TWAI/CAN driver - only needed by esp32_can component
|
"esp_driver_twai", # TWAI/CAN driver - only needed by esp32_can component
|
||||||
|
|||||||
@@ -95,9 +95,9 @@ async def to_code(config):
|
|||||||
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
|
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
|
||||||
os.environ["ESP_IDF_VERSION"] = f"{framework_ver.major}.{framework_ver.minor}"
|
os.environ["ESP_IDF_VERSION"] = f"{framework_ver.major}.{framework_ver.minor}"
|
||||||
if framework_ver >= cv.Version(5, 5, 0):
|
if framework_ver >= cv.Version(5, 5, 0):
|
||||||
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.2.4")
|
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.3.2")
|
||||||
esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.4")
|
esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.4")
|
||||||
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.9.3")
|
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.11.5")
|
||||||
else:
|
else:
|
||||||
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0")
|
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")
|
esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0")
|
||||||
|
|||||||
@@ -7,22 +7,25 @@
|
|||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
#include <esp_attr.h>
|
#include <esp_attr.h>
|
||||||
|
#include <esp_clk_tree.h>
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace esp32_rmt_led_strip {
|
namespace esp32_rmt_led_strip {
|
||||||
|
|
||||||
static const char *const TAG = "esp32_rmt_led_strip";
|
static const char *const TAG = "esp32_rmt_led_strip";
|
||||||
|
|
||||||
#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
|
|
||||||
|
|
||||||
static const size_t RMT_SYMBOLS_PER_BYTE = 8;
|
static const size_t RMT_SYMBOLS_PER_BYTE = 8;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
|
#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,
|
static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size_t symbols_written, size_t symbols_free,
|
||||||
rmt_symbol_word_t *symbols, bool *done, void *arg) {
|
rmt_symbol_word_t *symbols, bool *done, void *arg) {
|
||||||
@@ -92,7 +95,7 @@ void ESP32RMTLEDStripLightOutput::setup() {
|
|||||||
rmt_tx_channel_config_t channel;
|
rmt_tx_channel_config_t channel;
|
||||||
memset(&channel, 0, sizeof(channel));
|
memset(&channel, 0, sizeof(channel));
|
||||||
channel.clk_src = RMT_CLK_SRC_DEFAULT;
|
channel.clk_src = RMT_CLK_SRC_DEFAULT;
|
||||||
channel.resolution_hz = RMT_CLK_FREQ / RMT_CLK_DIV;
|
channel.resolution_hz = rmt_resolution_hz();
|
||||||
channel.gpio_num = gpio_num_t(this->pin_);
|
channel.gpio_num = gpio_num_t(this->pin_);
|
||||||
channel.mem_block_symbols = this->rmt_symbols_;
|
channel.mem_block_symbols = this->rmt_symbols_;
|
||||||
channel.trans_queue_depth = 1;
|
channel.trans_queue_depth = 1;
|
||||||
@@ -137,7 +140,7 @@ void ESP32RMTLEDStripLightOutput::setup() {
|
|||||||
|
|
||||||
void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high,
|
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) {
|
uint32_t bit1_low, uint32_t reset_time_high, uint32_t reset_time_low) {
|
||||||
float ratio = (float) RMT_CLK_FREQ / RMT_CLK_DIV / 1e09f;
|
float ratio = (float) rmt_resolution_hz() / 1e09f;
|
||||||
|
|
||||||
// 0-bit
|
// 0-bit
|
||||||
this->params_.bit0.duration0 = (uint32_t) (ratio * bit0_high);
|
this->params_.bit0.duration0 = (uint32_t) (ratio * bit0_high);
|
||||||
|
|||||||
@@ -94,10 +94,7 @@ CONFIG_SCHEMA = cv.Schema(
|
|||||||
|
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
if CORE.is_esp32:
|
if CORE.is_esp32:
|
||||||
# Re-enable ESP-IDF's legacy driver component (excluded by default to save compile time)
|
include_builtin_idf_component("esp_driver_pcnt")
|
||||||
# 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])
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
await cg.register_component(var, config)
|
await cg.register_component(var, config)
|
||||||
|
|||||||
@@ -103,6 +103,42 @@ inline bool is_success(int const status) { return status >= HTTP_STATUS_OK && st
|
|||||||
* - ESP-IDF: blocking reads, 0 only returned when all content read
|
* - ESP-IDF: blocking reads, 0 only returned when all content read
|
||||||
* - Arduino: non-blocking, 0 means "no data yet" or "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:
|
* Use the helper functions below instead of checking return values directly:
|
||||||
* - http_read_loop_result(): for manual loops with per-chunk processing
|
* - http_read_loop_result(): for manual loops with per-chunk processing
|
||||||
* - http_read_fully(): for simple "read N bytes into buffer" operations
|
* - http_read_fully(): for simple "read N bytes into buffer" operations
|
||||||
@@ -204,9 +240,13 @@ class HttpContainer : public Parented<HttpRequestComponent> {
|
|||||||
|
|
||||||
size_t get_bytes_read() const { return this->bytes_read_; }
|
size_t get_bytes_read() const { return this->bytes_read_; }
|
||||||
|
|
||||||
/// Check if all expected content has been read
|
/// Check if all expected content has been read.
|
||||||
/// For chunked responses, returns false (completion detected via read() returning error/EOF)
|
/// Base implementation handles non-chunked responses and status-code-based no-body checks.
|
||||||
bool is_read_complete() const {
|
/// 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 {
|
||||||
// Per RFC 9112, these responses have no body:
|
// Per RFC 9112, these responses have no body:
|
||||||
// - 1xx (Informational), 204 No Content, 205 Reset Content, 304 Not Modified
|
// - 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 ||
|
if ((this->status_code >= 100 && this->status_code < 200) || this->status_code == HTTP_STATUS_NO_CONTENT ||
|
||||||
|
|||||||
@@ -218,32 +218,50 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
|
|||||||
return container;
|
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)
|
// ESP-IDF HTTP read implementation (blocking mode)
|
||||||
//
|
//
|
||||||
// WARNING: Return values differ from BSD sockets! See http_request.h for full documentation.
|
// WARNING: Return values differ from BSD sockets! See http_request.h for full documentation.
|
||||||
//
|
//
|
||||||
// esp_http_client_read() in blocking mode returns:
|
// esp_http_client_read() in blocking mode returns:
|
||||||
// > 0: bytes read
|
// > 0: bytes read
|
||||||
// 0: connection closed (end of stream)
|
// 0: all chunked data received (is_chunk_complete true) or connection closed
|
||||||
|
// -ESP_ERR_HTTP_EAGAIN: transport timeout, no data available yet
|
||||||
// < 0: error
|
// < 0: error
|
||||||
//
|
//
|
||||||
// We normalize to HttpContainer::read() contract:
|
// We normalize to HttpContainer::read() contract:
|
||||||
// > 0: bytes read
|
// > 0: bytes read
|
||||||
// 0: all content read (only returned when content_length is known and fully read)
|
// 0: all content read (for both content_length-based and chunked completion)
|
||||||
// < 0: error/connection closed
|
// < 0: error/connection closed
|
||||||
//
|
//
|
||||||
// Note on chunked transfer encoding:
|
// Note on chunked transfer encoding:
|
||||||
// esp_http_client_fetch_headers() returns 0 for chunked responses (no Content-Length header).
|
// esp_http_client_fetch_headers() returns 0 for chunked responses (no Content-Length header).
|
||||||
// We handle this by skipping the content_length check when content_length is 0,
|
// When esp_http_client_read() returns 0 for a chunked response, is_read_complete() calls
|
||||||
// allowing esp_http_client_read() to handle chunked decoding internally and signal EOF
|
// esp_http_client_is_complete_data_received() to distinguish successful completion from
|
||||||
// by returning 0.
|
// 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.
|
||||||
int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
|
int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
|
||||||
const uint32_t start = millis();
|
const uint32_t start = millis();
|
||||||
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
|
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
|
||||||
|
|
||||||
// Check if we've already read all expected content (non-chunked only)
|
// Check if we've already read all expected content (non-chunked and no-body only).
|
||||||
// For chunked responses (content_length == 0), esp_http_client_read() handles EOF
|
// Use the base class check here, NOT the override: esp_http_client_is_complete_data_received()
|
||||||
if (this->is_read_complete()) {
|
// 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()) {
|
||||||
return 0; // All content read successfully
|
return 0; // All content read successfully
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,15 +276,18 @@ int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
|
|||||||
return read_len_or_error;
|
return read_len_or_error;
|
||||||
}
|
}
|
||||||
|
|
||||||
// esp_http_client_read() returns 0 in two cases:
|
// esp_http_client_read() returns 0 when:
|
||||||
// 1. Known content_length: connection closed before all data received (error)
|
// - Known content_length: connection closed before all data received (error)
|
||||||
// 2. Chunked encoding (content_length == 0): end of stream reached (EOF)
|
// - Chunked encoding: all chunks received (is_chunk_complete true, genuine EOF)
|
||||||
// For case 1, returning HTTP_ERROR_CONNECTION_CLOSED is correct.
|
//
|
||||||
// For case 2, 0 indicates that all chunked data has already been delivered
|
// Return 0 in both cases. Callers use http_read_loop_result() which calls
|
||||||
// in previous successful read() calls, so treating this as a closed
|
// is_read_complete() to distinguish these:
|
||||||
// connection does not cause any loss of response data.
|
// - 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)
|
||||||
if (read_len_or_error == 0) {
|
if (read_len_or_error == 0) {
|
||||||
return HTTP_ERROR_CONNECTION_CLOSED;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Negative value - error, return the actual error code for debugging
|
// Negative value - error, return the actual error code for debugging
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class HttpContainerIDF : public HttpContainer {
|
|||||||
HttpContainerIDF(esp_http_client_handle_t client) : client_(client) {}
|
HttpContainerIDF(esp_http_client_handle_t client) : client_(client) {}
|
||||||
int read(uint8_t *buf, size_t max_len) override;
|
int read(uint8_t *buf, size_t max_len) override;
|
||||||
void end() override;
|
void end() override;
|
||||||
|
bool is_read_complete() const override;
|
||||||
|
|
||||||
/// @brief Feeds the watchdog timer if the executing task has one attached
|
/// @brief Feeds the watchdog timer if the executing task has one attached
|
||||||
void feed_wdt();
|
void feed_wdt();
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
from esphome import automation
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components import uart
|
from esphome.components import uart
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_ID, CONF_THROTTLE
|
from esphome.const import CONF_ID, CONF_ON_DATA, CONF_THROTTLE, CONF_TRIGGER_ID
|
||||||
|
|
||||||
AUTO_LOAD = ["ld24xx"]
|
AUTO_LOAD = ["ld24xx"]
|
||||||
DEPENDENCIES = ["uart"]
|
DEPENDENCIES = ["uart"]
|
||||||
@@ -11,6 +12,8 @@ MULTI_CONF = True
|
|||||||
ld2450_ns = cg.esphome_ns.namespace("ld2450")
|
ld2450_ns = cg.esphome_ns.namespace("ld2450")
|
||||||
LD2450Component = ld2450_ns.class_("LD2450Component", cg.Component, uart.UARTDevice)
|
LD2450Component = ld2450_ns.class_("LD2450Component", cg.Component, uart.UARTDevice)
|
||||||
|
|
||||||
|
LD2450DataTrigger = ld2450_ns.class_("LD2450DataTrigger", automation.Trigger.template())
|
||||||
|
|
||||||
CONF_LD2450_ID = "ld2450_id"
|
CONF_LD2450_ID = "ld2450_id"
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.All(
|
CONFIG_SCHEMA = cv.All(
|
||||||
@@ -20,6 +23,11 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
cv.Optional(CONF_THROTTLE): cv.invalid(
|
cv.Optional(CONF_THROTTLE): cv.invalid(
|
||||||
f"{CONF_THROTTLE} has been removed; use per-sensor filters, instead"
|
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)
|
.extend(uart.UART_DEVICE_SCHEMA)
|
||||||
@@ -45,3 +53,6 @@ async def to_code(config):
|
|||||||
var = cg.new_Pvariable(config[CONF_ID])
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
await cg.register_component(var, config)
|
await cg.register_component(var, config)
|
||||||
await uart.register_uart_device(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)
|
||||||
|
|||||||
@@ -413,6 +413,10 @@ void LD2450Component::restart_and_read_all_info() {
|
|||||||
this->set_timeout(1500, [this]() { this->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
|
// Send command with values to LD2450
|
||||||
void LD2450Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) {
|
void LD2450Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) {
|
||||||
ESP_LOGV(TAG, "Sending COMMAND %02X", command);
|
ESP_LOGV(TAG, "Sending COMMAND %02X", command);
|
||||||
@@ -613,6 +617,8 @@ void LD2450Component::handle_periodic_data_() {
|
|||||||
this->still_presence_millis_ = App.get_loop_component_start_time();
|
this->still_presence_millis_ = App.get_loop_component_start_time();
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
this->data_callback_.call();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool LD2450Component::handle_ack_data_() {
|
bool LD2450Component::handle_ack_data_() {
|
||||||
|
|||||||
@@ -141,6 +141,9 @@ 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 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);
|
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:
|
protected:
|
||||||
void send_command_(uint8_t command_str, const uint8_t *command_value, uint8_t command_value_len);
|
void send_command_(uint8_t command_str, const uint8_t *command_value, uint8_t command_value_len);
|
||||||
void set_config_mode_(bool enable);
|
void set_config_mode_(bool enable);
|
||||||
@@ -190,6 +193,15 @@ class LD2450Component : public Component, public uart::UARTDevice {
|
|||||||
#ifdef USE_TEXT_SENSOR
|
#ifdef USE_TEXT_SENSOR
|
||||||
std::array<text_sensor::TextSensor *, 3> direction_text_sensors_{};
|
std::array<text_sensor::TextSensor *, 3> direction_text_sensors_{};
|
||||||
#endif
|
#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
|
} // namespace esphome::ld2450
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
#include "pulse_counter_sensor.h"
|
#include "pulse_counter_sensor.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
#ifdef HAS_PCNT
|
||||||
|
#include <esp_clk_tree.h>
|
||||||
|
#include <hal/pcnt_ll.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace pulse_counter {
|
namespace pulse_counter {
|
||||||
|
|
||||||
@@ -56,103 +61,109 @@ pulse_counter_t BasicPulseCounterStorage::read_raw_value() {
|
|||||||
|
|
||||||
#ifdef HAS_PCNT
|
#ifdef HAS_PCNT
|
||||||
bool HwPulseCounterStorage::pulse_counter_setup(InternalGPIOPin *pin) {
|
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 = pin;
|
||||||
this->pin->setup();
|
this->pin->setup();
|
||||||
this->pcnt_unit = next_pcnt_unit;
|
|
||||||
this->pcnt_channel = next_pcnt_channel;
|
pcnt_unit_config_t unit_config = {
|
||||||
next_pcnt_unit = pcnt_unit_t(int(next_pcnt_unit) + 1);
|
.low_limit = INT16_MIN,
|
||||||
if (int(next_pcnt_unit) >= PCNT_UNIT_0 + PCNT_UNIT_MAX) {
|
.high_limit = INT16_MAX,
|
||||||
next_pcnt_unit = PCNT_UNIT_0;
|
.flags = {.accum_count = true},
|
||||||
next_pcnt_channel = pcnt_channel_t(int(next_pcnt_channel) + 1);
|
};
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
ESP_LOGCONFIG(TAG,
|
pcnt_chan_config_t chan_config = {
|
||||||
" PCNT Unit Number: %u\n"
|
.edge_gpio_num = this->pin->get_pin(),
|
||||||
" PCNT Channel Number: %u",
|
.level_gpio_num = -1,
|
||||||
this->pcnt_unit, this->pcnt_channel);
|
};
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
pcnt_count_mode_t rising = PCNT_COUNT_DIS, falling = PCNT_COUNT_DIS;
|
pcnt_channel_edge_action_t rising = PCNT_CHANNEL_EDGE_ACTION_HOLD;
|
||||||
|
pcnt_channel_edge_action_t falling = PCNT_CHANNEL_EDGE_ACTION_HOLD;
|
||||||
switch (this->rising_edge_mode) {
|
switch (this->rising_edge_mode) {
|
||||||
case PULSE_COUNTER_DISABLE:
|
case PULSE_COUNTER_DISABLE:
|
||||||
rising = PCNT_COUNT_DIS;
|
rising = PCNT_CHANNEL_EDGE_ACTION_HOLD;
|
||||||
break;
|
break;
|
||||||
case PULSE_COUNTER_INCREMENT:
|
case PULSE_COUNTER_INCREMENT:
|
||||||
rising = PCNT_COUNT_INC;
|
rising = PCNT_CHANNEL_EDGE_ACTION_INCREASE;
|
||||||
break;
|
break;
|
||||||
case PULSE_COUNTER_DECREMENT:
|
case PULSE_COUNTER_DECREMENT:
|
||||||
rising = PCNT_COUNT_DEC;
|
rising = PCNT_CHANNEL_EDGE_ACTION_DECREASE;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
switch (this->falling_edge_mode) {
|
switch (this->falling_edge_mode) {
|
||||||
case PULSE_COUNTER_DISABLE:
|
case PULSE_COUNTER_DISABLE:
|
||||||
falling = PCNT_COUNT_DIS;
|
falling = PCNT_CHANNEL_EDGE_ACTION_HOLD;
|
||||||
break;
|
break;
|
||||||
case PULSE_COUNTER_INCREMENT:
|
case PULSE_COUNTER_INCREMENT:
|
||||||
falling = PCNT_COUNT_INC;
|
falling = PCNT_CHANNEL_EDGE_ACTION_INCREASE;
|
||||||
break;
|
break;
|
||||||
case PULSE_COUNTER_DECREMENT:
|
case PULSE_COUNTER_DECREMENT:
|
||||||
falling = PCNT_COUNT_DEC;
|
falling = PCNT_CHANNEL_EDGE_ACTION_DECREASE;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
pcnt_config_t pcnt_config = {
|
error = pcnt_channel_set_edge_action(this->pcnt_channel, rising, falling);
|
||||||
.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) {
|
if (error != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "Configuring Pulse Counter failed: %s", esp_err_to_name(error));
|
ESP_LOGE(TAG, "Setting PCNT edge action failed: %s", esp_err_to_name(error));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this->filter_us != 0) {
|
if (this->filter_us != 0) {
|
||||||
uint16_t filter_val = std::min(static_cast<unsigned int>(this->filter_us * 80u), 1023u);
|
uint32_t apb_freq;
|
||||||
ESP_LOGCONFIG(TAG, " Filter Value: %" PRIu32 "us (val=%u)", this->filter_us, filter_val);
|
esp_clk_tree_src_get_freq_hz(SOC_MOD_CLK_APB, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &apb_freq);
|
||||||
error = pcnt_set_filter_value(this->pcnt_unit, filter_val);
|
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);
|
||||||
if (error != ESP_OK) {
|
if (error != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "Setting filter value failed: %s", esp_err_to_name(error));
|
ESP_LOGE(TAG, "Setting PCNT glitch filter 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;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
error = pcnt_counter_pause(this->pcnt_unit);
|
error = pcnt_unit_add_watch_point(this->pcnt_unit, INT16_MIN);
|
||||||
if (error != ESP_OK) {
|
if (error != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "Pausing pulse counter failed: %s", esp_err_to_name(error));
|
ESP_LOGE(TAG, "Adding PCNT low limit watch point failed: %s", esp_err_to_name(error));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
error = pcnt_counter_clear(this->pcnt_unit);
|
error = pcnt_unit_add_watch_point(this->pcnt_unit, INT16_MAX);
|
||||||
if (error != ESP_OK) {
|
if (error != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "Clearing pulse counter failed: %s", esp_err_to_name(error));
|
ESP_LOGE(TAG, "Adding PCNT high limit watch point failed: %s", esp_err_to_name(error));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
error = pcnt_counter_resume(this->pcnt_unit);
|
|
||||||
|
error = pcnt_unit_enable(this->pcnt_unit);
|
||||||
if (error != ESP_OK) {
|
if (error != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "Resuming pulse counter failed: %s", esp_err_to_name(error));
|
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));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
pulse_counter_t HwPulseCounterStorage::read_raw_value() {
|
pulse_counter_t HwPulseCounterStorage::read_raw_value() {
|
||||||
pulse_counter_t counter;
|
int count;
|
||||||
pcnt_get_counter_value(this->pcnt_unit, &counter);
|
pcnt_unit_get_count(this->pcnt_unit, &count);
|
||||||
pulse_counter_t ret = counter - this->last_value;
|
pulse_counter_t ret = count - this->last_value;
|
||||||
this->last_value = counter;
|
this->last_value = count;
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
#endif // HAS_PCNT
|
#endif // HAS_PCNT
|
||||||
|
|||||||
@@ -6,14 +6,13 @@
|
|||||||
|
|
||||||
#include <cinttypes>
|
#include <cinttypes>
|
||||||
|
|
||||||
// TODO: Migrate from legacy PCNT API (driver/pcnt.h) to new PCNT API (driver/pulse_cnt.h)
|
#if defined(USE_ESP32)
|
||||||
// The legacy PCNT API is deprecated in ESP-IDF 5.x. Migration would allow removing the
|
#include <soc/soc_caps.h>
|
||||||
// "driver" IDF component dependency. See:
|
#ifdef SOC_PCNT_SUPPORTED
|
||||||
// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/migration-guides/release-5.x/5.0/peripherals.html#id6
|
#include <driver/pulse_cnt.h>
|
||||||
#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3)
|
|
||||||
#include <driver/pcnt.h>
|
|
||||||
#define HAS_PCNT
|
#define HAS_PCNT
|
||||||
#endif // defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3)
|
#endif // SOC_PCNT_SUPPORTED
|
||||||
|
#endif // USE_ESP32
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace pulse_counter {
|
namespace pulse_counter {
|
||||||
@@ -24,11 +23,7 @@ enum PulseCounterCountMode {
|
|||||||
PULSE_COUNTER_DECREMENT,
|
PULSE_COUNTER_DECREMENT,
|
||||||
};
|
};
|
||||||
|
|
||||||
#ifdef HAS_PCNT
|
|
||||||
using pulse_counter_t = int16_t;
|
|
||||||
#else // HAS_PCNT
|
|
||||||
using pulse_counter_t = int32_t;
|
using pulse_counter_t = int32_t;
|
||||||
#endif // HAS_PCNT
|
|
||||||
|
|
||||||
struct PulseCounterStorageBase {
|
struct PulseCounterStorageBase {
|
||||||
virtual bool pulse_counter_setup(InternalGPIOPin *pin) = 0;
|
virtual bool pulse_counter_setup(InternalGPIOPin *pin) = 0;
|
||||||
@@ -58,8 +53,8 @@ struct HwPulseCounterStorage : public PulseCounterStorageBase {
|
|||||||
bool pulse_counter_setup(InternalGPIOPin *pin) override;
|
bool pulse_counter_setup(InternalGPIOPin *pin) override;
|
||||||
pulse_counter_t read_raw_value() override;
|
pulse_counter_t read_raw_value() override;
|
||||||
|
|
||||||
pcnt_unit_t pcnt_unit;
|
pcnt_unit_handle_t pcnt_unit{nullptr};
|
||||||
pcnt_channel_t pcnt_channel;
|
pcnt_channel_handle_t pcnt_channel{nullptr};
|
||||||
};
|
};
|
||||||
#endif // HAS_PCNT
|
#endif // HAS_PCNT
|
||||||
|
|
||||||
|
|||||||
@@ -129,10 +129,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
use_pcnt = config.get(CONF_USE_PCNT)
|
use_pcnt = config.get(CONF_USE_PCNT)
|
||||||
if CORE.is_esp32 and use_pcnt:
|
if CORE.is_esp32 and use_pcnt:
|
||||||
# Re-enable ESP-IDF's legacy driver component (excluded by default to save compile time)
|
include_builtin_idf_component("esp_driver_pcnt")
|
||||||
# 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)
|
var = await sensor.new_sensor(config, use_pcnt)
|
||||||
await cg.register_component(var, config)
|
await cg.register_component(var, config)
|
||||||
|
|||||||
@@ -3,15 +3,11 @@
|
|||||||
|
|
||||||
#ifdef USE_ESP32
|
#ifdef USE_ESP32
|
||||||
#include <driver/gpio.h>
|
#include <driver/gpio.h>
|
||||||
|
#include <esp_clk_tree.h>
|
||||||
|
|
||||||
namespace esphome::remote_receiver {
|
namespace esphome::remote_receiver {
|
||||||
|
|
||||||
static const char *const TAG = "remote_receiver.esp32";
|
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) {
|
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;
|
RemoteReceiverComponentStore *store = (RemoteReceiverComponentStore *) arg;
|
||||||
@@ -98,7 +94,10 @@ void RemoteReceiverComponent::setup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint32_t event_size = sizeof(rmt_rx_done_event_data_t);
|
uint32_t event_size = sizeof(rmt_rx_done_event_data_t);
|
||||||
uint32_t max_filter_ns = 255u * 1000 / (RMT_CLK_FREQ / 1000000);
|
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);
|
||||||
memset(&this->store_.config, 0, sizeof(this->store_.config));
|
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_min_ns = std::min(this->filter_us_ * 1000, max_filter_ns);
|
||||||
this->store_.config.signal_range_max_ns = this->idle_us_ * 1000;
|
this->store_.config.signal_range_max_ns = this->idle_us_ * 1000;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components import audio, esp32, speaker
|
from esphome.components import audio, esp32, socket, speaker
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_BITS_PER_SAMPLE,
|
CONF_BITS_PER_SAMPLE,
|
||||||
@@ -34,7 +34,7 @@ def _set_stream_limits(config):
|
|||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
def _validate_audio_compatability(config):
|
def _validate_audio_compatibility(config):
|
||||||
inherit_property_from(CONF_BITS_PER_SAMPLE, CONF_OUTPUT_SPEAKER)(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_NUM_CHANNELS, CONF_OUTPUT_SPEAKER)(config)
|
||||||
inherit_property_from(CONF_SAMPLE_RATE, CONF_OUTPUT_SPEAKER)(config)
|
inherit_property_from(CONF_SAMPLE_RATE, CONF_OUTPUT_SPEAKER)(config)
|
||||||
@@ -73,10 +73,13 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
FINAL_VALIDATE_SCHEMA = _validate_audio_compatability
|
FINAL_VALIDATE_SCHEMA = _validate_audio_compatibility
|
||||||
|
|
||||||
|
|
||||||
async def to_code(config):
|
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])
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
await cg.register_component(var, config)
|
await cg.register_component(var, config)
|
||||||
await speaker.register_speaker(var, config)
|
await speaker.register_speaker(var, config)
|
||||||
@@ -86,12 +89,11 @@ async def to_code(config):
|
|||||||
|
|
||||||
cg.add(var.set_buffer_duration(config[CONF_BUFFER_DURATION]))
|
cg.add(var.set_buffer_duration(config[CONF_BUFFER_DURATION]))
|
||||||
|
|
||||||
if task_stack_in_psram := config.get(CONF_TASK_STACK_IN_PSRAM):
|
if config.get(CONF_TASK_STACK_IN_PSRAM):
|
||||||
cg.add(var.set_task_stack_in_psram(task_stack_in_psram))
|
cg.add(var.set_task_stack_in_psram(True))
|
||||||
if task_stack_in_psram and config[CONF_TASK_STACK_IN_PSRAM]:
|
esp32.add_idf_sdkconfig_option(
|
||||||
esp32.add_idf_sdkconfig_option(
|
"CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True
|
||||||
"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_bits_per_sample(config[CONF_BITS_PER_SAMPLE]))
|
||||||
cg.add(var.set_target_sample_rate(config[CONF_SAMPLE_RATE]))
|
cg.add(var.set_target_sample_rate(config[CONF_SAMPLE_RATE]))
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
#include "esphome/components/audio/audio_resampler.h"
|
#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/helpers.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
@@ -17,13 +19,17 @@ static const UBaseType_t RESAMPLER_TASK_PRIORITY = 1;
|
|||||||
|
|
||||||
static const uint32_t TRANSFER_BUFFER_DURATION_MS = 50;
|
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 TASK_STACK_SIZE = 3072;
|
||||||
|
|
||||||
|
static const uint32_t STATE_TRANSITION_TIMEOUT_MS = 5000;
|
||||||
|
|
||||||
static const char *const TAG = "resampler_speaker";
|
static const char *const TAG = "resampler_speaker";
|
||||||
|
|
||||||
enum ResamplingEventGroupBits : uint32_t {
|
enum ResamplingEventGroupBits : uint32_t {
|
||||||
COMMAND_STOP = (1 << 0), // stops the resampler task
|
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
|
||||||
STATE_STARTING = (1 << 10),
|
STATE_STARTING = (1 << 10),
|
||||||
STATE_RUNNING = (1 << 11),
|
STATE_RUNNING = (1 << 11),
|
||||||
STATE_STOPPING = (1 << 12),
|
STATE_STOPPING = (1 << 12),
|
||||||
@@ -34,9 +40,16 @@ enum ResamplingEventGroupBits : uint32_t {
|
|||||||
ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits
|
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() {
|
void ResamplerSpeaker::setup() {
|
||||||
this->event_group_ = xEventGroupCreate();
|
this->event_group_ = xEventGroupCreate();
|
||||||
|
|
||||||
if (this->event_group_ == nullptr) {
|
if (this->event_group_ == nullptr) {
|
||||||
ESP_LOGE(TAG, "Failed to create event group");
|
ESP_LOGE(TAG, "Failed to create event group");
|
||||||
this->mark_failed();
|
this->mark_failed();
|
||||||
@@ -55,81 +68,155 @@ void ResamplerSpeaker::setup() {
|
|||||||
this->audio_output_callback_(new_frames, write_timestamp);
|
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() {
|
void ResamplerSpeaker::loop() {
|
||||||
uint32_t event_group_bits = xEventGroupGetBits(this->event_group_);
|
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) {
|
if (event_group_bits & ResamplingEventGroupBits::STATE_STARTING) {
|
||||||
ESP_LOGD(TAG, "Starting resampler task");
|
ESP_LOGD(TAG, "Starting");
|
||||||
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::STATE_STARTING);
|
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::STATE_STARTING);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_NO_MEM) {
|
if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_NO_MEM) {
|
||||||
this->status_set_error(LOG_STR("Resampler task failed to allocate the internal buffers"));
|
this->status_set_error(LOG_STR("Not enough memory"));
|
||||||
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_NO_MEM);
|
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_NO_MEM);
|
||||||
this->state_ = speaker::STATE_STOPPING;
|
this->enter_stopping_state_();
|
||||||
}
|
}
|
||||||
if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_NOT_SUPPORTED) {
|
if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_NOT_SUPPORTED) {
|
||||||
this->status_set_error(LOG_STR("Cannot resample due to an unsupported audio stream"));
|
this->status_set_error(LOG_STR("Unsupported stream"));
|
||||||
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_NOT_SUPPORTED);
|
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_NOT_SUPPORTED);
|
||||||
this->state_ = speaker::STATE_STOPPING;
|
this->enter_stopping_state_();
|
||||||
}
|
}
|
||||||
if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_FAIL) {
|
if (event_group_bits & ResamplingEventGroupBits::ERR_ESP_FAIL) {
|
||||||
this->status_set_error(LOG_STR("Resampler task failed"));
|
this->status_set_error(LOG_STR("Resampler failure"));
|
||||||
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_FAIL);
|
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ERR_ESP_FAIL);
|
||||||
this->state_ = speaker::STATE_STOPPING;
|
this->enter_stopping_state_();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event_group_bits & ResamplingEventGroupBits::STATE_RUNNING) {
|
if (event_group_bits & ResamplingEventGroupBits::STATE_RUNNING) {
|
||||||
ESP_LOGD(TAG, "Started resampler task");
|
ESP_LOGV(TAG, "Started");
|
||||||
this->status_clear_error();
|
this->status_clear_error();
|
||||||
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::STATE_RUNNING);
|
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::STATE_RUNNING);
|
||||||
}
|
}
|
||||||
if (event_group_bits & ResamplingEventGroupBits::STATE_STOPPING) {
|
if (event_group_bits & ResamplingEventGroupBits::STATE_STOPPING) {
|
||||||
ESP_LOGD(TAG, "Stopping resampler task");
|
ESP_LOGV(TAG, "Stopping");
|
||||||
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::STATE_STOPPING);
|
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::STATE_STOPPING);
|
||||||
}
|
}
|
||||||
if (event_group_bits & ResamplingEventGroupBits::STATE_STOPPED) {
|
if (event_group_bits & ResamplingEventGroupBits::STATE_STOPPED) {
|
||||||
if (this->delete_task_() == ESP_OK) {
|
this->delete_task_();
|
||||||
ESP_LOGD(TAG, "Stopped resampler task");
|
ESP_LOGD(TAG, "Stopped");
|
||||||
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ALL_BITS);
|
xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ALL_BITS);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (this->state_) {
|
switch (this->state_) {
|
||||||
case speaker::STATE_STARTING: {
|
case speaker::STATE_STARTING: {
|
||||||
esp_err_t err = this->start_();
|
if (!this->waiting_for_output_) {
|
||||||
if (err == ESP_OK) {
|
esp_err_t err = this->start_();
|
||||||
this->status_clear_error();
|
if (err == ESP_OK) {
|
||||||
this->state_ = speaker::STATE_RUNNING;
|
this->callback_remainder_ = 0; // reset callback remainder
|
||||||
} else {
|
this->status_clear_error();
|
||||||
switch (err) {
|
this->waiting_for_output_ = true;
|
||||||
case ESP_ERR_INVALID_STATE:
|
this->state_start_ms_ = App.get_loop_component_start_time();
|
||||||
this->status_set_error(LOG_STR("Failed to start resampler: resampler task failed to start"));
|
} else {
|
||||||
break;
|
this->set_start_error_(err);
|
||||||
case ESP_ERR_NO_MEM:
|
this->waiting_for_output_ = false;
|
||||||
this->status_set_error(LOG_STR("Failed to start resampler: not enough memory for task stack"));
|
this->enter_stopping_state_();
|
||||||
default:
|
}
|
||||||
this->status_set_error(LOG_STR("Failed to start resampler"));
|
} else {
|
||||||
break;
|
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_();
|
||||||
}
|
}
|
||||||
|
|
||||||
this->state_ = speaker::STATE_STOPPING;
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case speaker::STATE_RUNNING:
|
case speaker::STATE_RUNNING:
|
||||||
if (this->output_speaker_->is_stopped()) {
|
if (this->output_speaker_->is_stopped()) {
|
||||||
this->state_ = speaker::STATE_STOPPING;
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
break;
|
||||||
case speaker::STATE_STOPPING:
|
}
|
||||||
this->stop_();
|
|
||||||
this->state_ = speaker::STATE_STOPPED;
|
|
||||||
break;
|
|
||||||
case speaker::STATE_STOPPED:
|
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -143,16 +230,33 @@ size_t ResamplerSpeaker::play(const uint8_t *data, size_t length, TickType_t tic
|
|||||||
if ((this->output_speaker_->is_running()) && (!this->requires_resampling_())) {
|
if ((this->output_speaker_->is_running()) && (!this->requires_resampling_())) {
|
||||||
bytes_written = this->output_speaker_->play(data, length, ticks_to_wait);
|
bytes_written = this->output_speaker_->play(data, length, ticks_to_wait);
|
||||||
} else {
|
} else {
|
||||||
if (this->ring_buffer_.use_count() == 1) {
|
std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
|
||||||
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
|
||||||
bytes_written = temp_ring_buffer->write_without_replacement(data, length, ticks_to_wait);
|
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;
|
return bytes_written;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ResamplerSpeaker::start() { this->state_ = speaker::STATE_STARTING; }
|
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); }
|
||||||
|
|
||||||
esp_err_t ResamplerSpeaker::start_() {
|
esp_err_t ResamplerSpeaker::start_() {
|
||||||
this->target_stream_info_ = audio::AudioStreamInfo(
|
this->target_stream_info_ = audio::AudioStreamInfo(
|
||||||
@@ -185,7 +289,7 @@ esp_err_t ResamplerSpeaker::start_task_() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this->task_handle_ == nullptr) {
|
if (this->task_handle_ == nullptr) {
|
||||||
this->task_handle_ = xTaskCreateStatic(resample_task, "sample", TASK_STACK_SIZE, (void *) this,
|
this->task_handle_ = xTaskCreateStatic(resample_task, "resampler", TASK_STACK_SIZE, (void *) this,
|
||||||
RESAMPLER_TASK_PRIORITY, this->task_stack_buffer_, &this->task_stack_);
|
RESAMPLER_TASK_PRIORITY, this->task_stack_buffer_, &this->task_stack_);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,43 +300,47 @@ esp_err_t ResamplerSpeaker::start_task_() {
|
|||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ResamplerSpeaker::stop() { this->state_ = speaker::STATE_STOPPING; }
|
void ResamplerSpeaker::stop() { this->send_command_(ResamplingEventGroupBits::COMMAND_STOP); }
|
||||||
|
|
||||||
void ResamplerSpeaker::stop_() {
|
void ResamplerSpeaker::enter_stopping_state_() {
|
||||||
|
this->state_ = speaker::STATE_STOPPING;
|
||||||
|
this->state_start_ms_ = App.get_loop_component_start_time();
|
||||||
if (this->task_handle_ != nullptr) {
|
if (this->task_handle_ != nullptr) {
|
||||||
xEventGroupSetBits(this->event_group_, ResamplingEventGroupBits::COMMAND_STOP);
|
xEventGroupSetBits(this->event_group_, ResamplingEventGroupBits::TASK_COMMAND_STOP);
|
||||||
}
|
}
|
||||||
this->output_speaker_->stop();
|
this->output_speaker_->stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t ResamplerSpeaker::delete_task_() {
|
void ResamplerSpeaker::delete_task_() {
|
||||||
if (!this->task_created_) {
|
if (this->task_handle_ != nullptr) {
|
||||||
|
// Delete the suspended task
|
||||||
|
vTaskDelete(this->task_handle_);
|
||||||
this->task_handle_ = nullptr;
|
this->task_handle_ = nullptr;
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ESP_OK;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ESP_ERR_INVALID_STATE;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
this->task_stack_buffer_ = nullptr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ResamplerSpeaker::finish() { this->output_speaker_->finish(); }
|
void ResamplerSpeaker::finish() { this->send_command_(ResamplingEventGroupBits::COMMAND_FINISH); }
|
||||||
|
|
||||||
bool ResamplerSpeaker::has_buffered_data() const {
|
bool ResamplerSpeaker::has_buffered_data() const {
|
||||||
bool has_ring_buffer_data = false;
|
bool has_ring_buffer_data = false;
|
||||||
if (this->requires_resampling_() && (this->ring_buffer_.use_count() > 0)) {
|
if (this->requires_resampling_()) {
|
||||||
has_ring_buffer_data = (this->ring_buffer_.lock()->available() > 0);
|
std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
|
||||||
|
if (temp_ring_buffer) {
|
||||||
|
has_ring_buffer_data = (temp_ring_buffer->available() > 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return (has_ring_buffer_data || this->output_speaker_->has_buffered_data());
|
return (has_ring_buffer_data || this->output_speaker_->has_buffered_data());
|
||||||
}
|
}
|
||||||
@@ -253,9 +361,8 @@ bool ResamplerSpeaker::requires_resampling_() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ResamplerSpeaker::resample_task(void *params) {
|
void ResamplerSpeaker::resample_task(void *params) {
|
||||||
ResamplerSpeaker *this_resampler = (ResamplerSpeaker *) params;
|
ResamplerSpeaker *this_resampler = static_cast<ResamplerSpeaker *>(params);
|
||||||
|
|
||||||
this_resampler->task_created_ = true;
|
|
||||||
xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_STARTING);
|
xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_STARTING);
|
||||||
|
|
||||||
std::unique_ptr<audio::AudioResampler> resampler =
|
std::unique_ptr<audio::AudioResampler> resampler =
|
||||||
@@ -269,7 +376,7 @@ void ResamplerSpeaker::resample_task(void *params) {
|
|||||||
std::shared_ptr<RingBuffer> temp_ring_buffer =
|
std::shared_ptr<RingBuffer> temp_ring_buffer =
|
||||||
RingBuffer::create(this_resampler->audio_stream_info_.ms_to_bytes(this_resampler->buffer_duration_ms_));
|
RingBuffer::create(this_resampler->audio_stream_info_.ms_to_bytes(this_resampler->buffer_duration_ms_));
|
||||||
|
|
||||||
if (temp_ring_buffer.use_count() == 0) {
|
if (!temp_ring_buffer) {
|
||||||
err = ESP_ERR_NO_MEM;
|
err = ESP_ERR_NO_MEM;
|
||||||
} else {
|
} else {
|
||||||
this_resampler->ring_buffer_ = temp_ring_buffer;
|
this_resampler->ring_buffer_ = temp_ring_buffer;
|
||||||
@@ -291,7 +398,7 @@ void ResamplerSpeaker::resample_task(void *params) {
|
|||||||
while (err == ESP_OK) {
|
while (err == ESP_OK) {
|
||||||
uint32_t event_bits = xEventGroupGetBits(this_resampler->event_group_);
|
uint32_t event_bits = xEventGroupGetBits(this_resampler->event_group_);
|
||||||
|
|
||||||
if (event_bits & ResamplingEventGroupBits::COMMAND_STOP) {
|
if (event_bits & ResamplingEventGroupBits::TASK_COMMAND_STOP) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,8 +417,8 @@ void ResamplerSpeaker::resample_task(void *params) {
|
|||||||
xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_STOPPING);
|
xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_STOPPING);
|
||||||
resampler.reset();
|
resampler.reset();
|
||||||
xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_STOPPED);
|
xEventGroupSetBits(this_resampler->event_group_, ResamplingEventGroupBits::STATE_STOPPED);
|
||||||
this_resampler->task_created_ = false;
|
|
||||||
vTaskDelete(nullptr);
|
vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace resampler
|
} // namespace resampler
|
||||||
|
|||||||
@@ -8,14 +8,16 @@
|
|||||||
|
|
||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
|
|
||||||
#include <freertos/event_groups.h>
|
|
||||||
#include <freertos/FreeRTOS.h>
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <freertos/event_groups.h>
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace resampler {
|
namespace resampler {
|
||||||
|
|
||||||
class ResamplerSpeaker : public Component, public speaker::Speaker {
|
class ResamplerSpeaker : public Component, public speaker::Speaker {
|
||||||
public:
|
public:
|
||||||
|
float get_setup_priority() const override { return esphome::setup_priority::DATA; }
|
||||||
|
void dump_config() override;
|
||||||
void setup() override;
|
void setup() override;
|
||||||
void loop() override;
|
void loop() override;
|
||||||
|
|
||||||
@@ -65,13 +67,18 @@ class ResamplerSpeaker : public Component, public speaker::Speaker {
|
|||||||
/// ESP_ERR_INVALID_STATE if the task wasn't created
|
/// ESP_ERR_INVALID_STATE if the task wasn't created
|
||||||
esp_err_t start_task_();
|
esp_err_t start_task_();
|
||||||
|
|
||||||
/// @brief Stops the output speaker. If the resampling task is running, it sends the stop command.
|
/// @brief Transitions to STATE_STOPPING, records the stopping timestamp, sends the task stop command if the task is
|
||||||
void stop_();
|
/// running, and stops the output speaker.
|
||||||
|
void enter_stopping_state_();
|
||||||
|
|
||||||
/// @brief Deallocates the task stack and resets the pointers.
|
/// @brief Sets the appropriate status error based on the start failure reason.
|
||||||
/// @return ESP_OK if successful
|
void set_start_error_(esp_err_t err);
|
||||||
/// ESP_ERR_INVALID_STATE if the task hasn't stopped itself
|
|
||||||
esp_err_t delete_task_();
|
/// @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);
|
||||||
|
|
||||||
inline bool requires_resampling_() const;
|
inline bool requires_resampling_() const;
|
||||||
static void resample_task(void *params);
|
static void resample_task(void *params);
|
||||||
@@ -83,7 +90,7 @@ class ResamplerSpeaker : public Component, public speaker::Speaker {
|
|||||||
speaker::Speaker *output_speaker_{nullptr};
|
speaker::Speaker *output_speaker_{nullptr};
|
||||||
|
|
||||||
bool task_stack_in_psram_{false};
|
bool task_stack_in_psram_{false};
|
||||||
bool task_created_{false};
|
bool waiting_for_output_{false};
|
||||||
|
|
||||||
TaskHandle_t task_handle_{nullptr};
|
TaskHandle_t task_handle_{nullptr};
|
||||||
StaticTask_t task_stack_;
|
StaticTask_t task_stack_;
|
||||||
@@ -98,6 +105,7 @@ class ResamplerSpeaker : public Component, public speaker::Speaker {
|
|||||||
uint32_t target_sample_rate_;
|
uint32_t target_sample_rate_;
|
||||||
|
|
||||||
uint32_t buffer_duration_ms_;
|
uint32_t buffer_duration_ms_;
|
||||||
|
uint32_t state_start_ms_{0};
|
||||||
|
|
||||||
uint64_t callback_remainder_{0};
|
uint64_t callback_remainder_{0};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,19 +16,13 @@ namespace esphome::socket {
|
|||||||
|
|
||||||
class BSDSocketImpl final : public Socket {
|
class BSDSocketImpl final : public Socket {
|
||||||
public:
|
public:
|
||||||
BSDSocketImpl(int fd, bool monitor_loop = false) : fd_(fd) {
|
BSDSocketImpl(int fd, bool monitor_loop = false) {
|
||||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
this->fd_ = fd;
|
||||||
// Register new socket with the application for select() if monitoring requested
|
// Register new socket with the application for select() if monitoring requested
|
||||||
if (monitor_loop && this->fd_ >= 0) {
|
if (monitor_loop && this->fd_ >= 0) {
|
||||||
// Only set loop_monitored_ to true if registration succeeds
|
// Only set loop_monitored_ to true if registration succeeds
|
||||||
this->loop_monitored_ = App.register_socket_fd(this->fd_);
|
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 {
|
~BSDSocketImpl() override {
|
||||||
if (!this->closed_) {
|
if (!this->closed_) {
|
||||||
@@ -52,12 +46,10 @@ class BSDSocketImpl final : public Socket {
|
|||||||
int bind(const struct sockaddr *addr, socklen_t addrlen) override { return ::bind(this->fd_, addr, addrlen); }
|
int bind(const struct sockaddr *addr, socklen_t addrlen) override { return ::bind(this->fd_, addr, addrlen); }
|
||||||
int close() override {
|
int close() override {
|
||||||
if (!this->closed_) {
|
if (!this->closed_) {
|
||||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
|
||||||
// Unregister from select() before closing if monitored
|
// Unregister from select() before closing if monitored
|
||||||
if (this->loop_monitored_) {
|
if (this->loop_monitored_) {
|
||||||
App.unregister_socket_fd(this->fd_);
|
App.unregister_socket_fd(this->fd_);
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
int ret = ::close(this->fd_);
|
int ret = ::close(this->fd_);
|
||||||
this->closed_ = true;
|
this->closed_ = true;
|
||||||
return ret;
|
return ret;
|
||||||
@@ -130,23 +122,6 @@ class BSDSocketImpl final : public Socket {
|
|||||||
::fcntl(this->fd_, F_SETFL, fl);
|
::fcntl(this->fd_, F_SETFL, fl);
|
||||||
return 0;
|
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
|
// Helper to create a socket with optional monitoring
|
||||||
|
|||||||
@@ -452,6 +452,8 @@ class LWIPRawImpl : public Socket {
|
|||||||
errno = ENOSYS;
|
errno = ENOSYS;
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
bool ready() const override { return this->rx_buf_ != nullptr || this->rx_closed_ || this->pcb_ == nullptr; }
|
||||||
|
|
||||||
int setblocking(bool blocking) final {
|
int setblocking(bool blocking) final {
|
||||||
if (pcb_ == nullptr) {
|
if (pcb_ == nullptr) {
|
||||||
errno = ECONNRESET;
|
errno = ECONNRESET;
|
||||||
@@ -576,6 +578,8 @@ class LWIPRawListenImpl final : public LWIPRawImpl {
|
|||||||
tcp_err(pcb_, LWIPRawImpl::s_err_fn); // Use base class error handler
|
tcp_err(pcb_, LWIPRawImpl::s_err_fn); // Use base class error handler
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ready() const override { return this->accepted_socket_count_ > 0; }
|
||||||
|
|
||||||
std::unique_ptr<Socket> accept(struct sockaddr *addr, socklen_t *addrlen) override {
|
std::unique_ptr<Socket> accept(struct sockaddr *addr, socklen_t *addrlen) override {
|
||||||
if (pcb_ == nullptr) {
|
if (pcb_ == nullptr) {
|
||||||
errno = EBADF;
|
errno = EBADF;
|
||||||
|
|||||||
@@ -11,19 +11,13 @@ namespace esphome::socket {
|
|||||||
|
|
||||||
class LwIPSocketImpl final : public Socket {
|
class LwIPSocketImpl final : public Socket {
|
||||||
public:
|
public:
|
||||||
LwIPSocketImpl(int fd, bool monitor_loop = false) : fd_(fd) {
|
LwIPSocketImpl(int fd, bool monitor_loop = false) {
|
||||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
this->fd_ = fd;
|
||||||
// Register new socket with the application for select() if monitoring requested
|
// Register new socket with the application for select() if monitoring requested
|
||||||
if (monitor_loop && this->fd_ >= 0) {
|
if (monitor_loop && this->fd_ >= 0) {
|
||||||
// Only set loop_monitored_ to true if registration succeeds
|
// Only set loop_monitored_ to true if registration succeeds
|
||||||
this->loop_monitored_ = App.register_socket_fd(this->fd_);
|
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 {
|
~LwIPSocketImpl() override {
|
||||||
if (!this->closed_) {
|
if (!this->closed_) {
|
||||||
@@ -49,12 +43,10 @@ class LwIPSocketImpl final : public Socket {
|
|||||||
int bind(const struct sockaddr *addr, socklen_t addrlen) override { return lwip_bind(this->fd_, addr, addrlen); }
|
int bind(const struct sockaddr *addr, socklen_t addrlen) override { return lwip_bind(this->fd_, addr, addrlen); }
|
||||||
int close() override {
|
int close() override {
|
||||||
if (!this->closed_) {
|
if (!this->closed_) {
|
||||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
|
||||||
// Unregister from select() before closing if monitored
|
// Unregister from select() before closing if monitored
|
||||||
if (this->loop_monitored_) {
|
if (this->loop_monitored_) {
|
||||||
App.unregister_socket_fd(this->fd_);
|
App.unregister_socket_fd(this->fd_);
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
int ret = lwip_close(this->fd_);
|
int ret = lwip_close(this->fd_);
|
||||||
this->closed_ = true;
|
this->closed_ = true;
|
||||||
return ret;
|
return ret;
|
||||||
@@ -97,23 +89,6 @@ class LwIPSocketImpl final : public Socket {
|
|||||||
lwip_fcntl(this->fd_, F_SETFL, fl);
|
lwip_fcntl(this->fd_, F_SETFL, fl);
|
||||||
return 0;
|
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
|
// Helper to create a socket with optional monitoring
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ namespace esphome::socket {
|
|||||||
|
|
||||||
Socket::~Socket() {}
|
Socket::~Socket() {}
|
||||||
|
|
||||||
|
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||||
|
bool Socket::ready() const { return !this->loop_monitored_ || App.is_socket_ready_(this->fd_); }
|
||||||
|
#endif
|
||||||
|
|
||||||
// Platform-specific inet_ntop wrappers
|
// Platform-specific inet_ntop wrappers
|
||||||
#if defined(USE_SOCKET_IMPL_LWIP_TCP)
|
#if defined(USE_SOCKET_IMPL_LWIP_TCP)
|
||||||
// LWIP raw TCP (ESP8266) uses inet_ntoa_r which takes struct by value
|
// LWIP raw TCP (ESP8266) uses inet_ntoa_r which takes struct by value
|
||||||
|
|||||||
@@ -63,13 +63,29 @@ class Socket {
|
|||||||
virtual int setblocking(bool blocking) = 0;
|
virtual int setblocking(bool blocking) = 0;
|
||||||
virtual int loop() { return 0; };
|
virtual int loop() { return 0; };
|
||||||
|
|
||||||
/// Get the underlying file descriptor (returns -1 if not supported)
|
/// Get the underlying file descriptor (returns -1 if not supported)
|
||||||
virtual int get_fd() const { return -1; }
|
/// 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
|
||||||
|
|
||||||
/// Check if socket has data ready to read
|
/// Check if socket has data ready to read
|
||||||
/// For loop-monitored sockets, checks with the Application's select() results
|
/// For select()-based sockets: non-virtual, checks Application's select() results
|
||||||
/// For non-monitored sockets, always returns true (assumes data may be available)
|
/// For LWIP raw TCP sockets: virtual, checks internal buffer state
|
||||||
|
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||||
|
bool ready() const;
|
||||||
|
#else
|
||||||
virtual bool ready() const { return true; }
|
virtual bool ready() const { return true; }
|
||||||
|
#endif
|
||||||
|
|
||||||
|
protected:
|
||||||
|
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||||
|
int fd_{-1};
|
||||||
|
bool closed_{false};
|
||||||
|
bool loop_monitored_{false};
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Create a socket of the given domain, type and protocol.
|
/// Create a socket of the given domain, type and protocol.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import esphome.codegen as cg
|
|||||||
from esphome.components import water_heater
|
from esphome.components import water_heater
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
|
CONF_AWAY,
|
||||||
CONF_ID,
|
CONF_ID,
|
||||||
CONF_MODE,
|
CONF_MODE,
|
||||||
CONF_OPTIMISTIC,
|
CONF_OPTIMISTIC,
|
||||||
@@ -18,6 +19,7 @@ from esphome.types import ConfigType
|
|||||||
from .. import template_ns
|
from .. import template_ns
|
||||||
|
|
||||||
CONF_CURRENT_TEMPERATURE = "current_temperature"
|
CONF_CURRENT_TEMPERATURE = "current_temperature"
|
||||||
|
CONF_IS_ON = "is_on"
|
||||||
|
|
||||||
TemplateWaterHeater = template_ns.class_(
|
TemplateWaterHeater = template_ns.class_(
|
||||||
"TemplateWaterHeater", cg.Component, water_heater.WaterHeater
|
"TemplateWaterHeater", cg.Component, water_heater.WaterHeater
|
||||||
@@ -51,6 +53,8 @@ CONFIG_SCHEMA = (
|
|||||||
cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(
|
cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(
|
||||||
water_heater.validate_water_heater_mode
|
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)
|
.extend(cv.COMPONENT_SCHEMA)
|
||||||
@@ -98,6 +102,22 @@ async def to_code(config: ConfigType) -> None:
|
|||||||
if CONF_SUPPORTED_MODES in config:
|
if CONF_SUPPORTED_MODES in config:
|
||||||
cg.add(var.set_supported_modes(config[CONF_SUPPORTED_MODES]))
|
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(
|
@automation.register_action(
|
||||||
"water_heater.template.publish",
|
"water_heater.template.publish",
|
||||||
@@ -110,6 +130,8 @@ async def to_code(config: ConfigType) -> None:
|
|||||||
cv.Optional(CONF_MODE): cv.templatable(
|
cv.Optional(CONF_MODE): cv.templatable(
|
||||||
water_heater.validate_water_heater_mode
|
water_heater.validate_water_heater_mode
|
||||||
),
|
),
|
||||||
|
cv.Optional(CONF_AWAY): cv.templatable(cv.boolean),
|
||||||
|
cv.Optional(CONF_IS_ON): cv.templatable(cv.boolean),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -134,4 +156,12 @@ async def water_heater_template_publish_to_code(
|
|||||||
template_ = await cg.templatable(mode, args, water_heater.WaterHeaterMode)
|
template_ = await cg.templatable(mode, args, water_heater.WaterHeaterMode)
|
||||||
cg.add(var.set_mode(template_))
|
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
|
return var
|
||||||
|
|||||||
@@ -11,12 +11,15 @@ class TemplateWaterHeaterPublishAction : public Action<Ts...>, public Parented<T
|
|||||||
TEMPLATABLE_VALUE(float, current_temperature)
|
TEMPLATABLE_VALUE(float, current_temperature)
|
||||||
TEMPLATABLE_VALUE(float, target_temperature)
|
TEMPLATABLE_VALUE(float, target_temperature)
|
||||||
TEMPLATABLE_VALUE(water_heater::WaterHeaterMode, mode)
|
TEMPLATABLE_VALUE(water_heater::WaterHeaterMode, mode)
|
||||||
|
TEMPLATABLE_VALUE(bool, away)
|
||||||
|
TEMPLATABLE_VALUE(bool, is_on)
|
||||||
|
|
||||||
void play(const Ts &...x) override {
|
void play(const Ts &...x) override {
|
||||||
if (this->current_temperature_.has_value()) {
|
if (this->current_temperature_.has_value()) {
|
||||||
this->parent_->set_current_temperature(this->current_temperature_.value(x...));
|
this->parent_->set_current_temperature(this->current_temperature_.value(x...));
|
||||||
}
|
}
|
||||||
bool needs_call = this->target_temperature_.has_value() || this->mode_.has_value();
|
bool needs_call = this->target_temperature_.has_value() || this->mode_.has_value() || this->away_.has_value() ||
|
||||||
|
this->is_on_.has_value();
|
||||||
if (needs_call) {
|
if (needs_call) {
|
||||||
auto call = this->parent_->make_call();
|
auto call = this->parent_->make_call();
|
||||||
if (this->target_temperature_.has_value()) {
|
if (this->target_temperature_.has_value()) {
|
||||||
@@ -25,6 +28,12 @@ class TemplateWaterHeaterPublishAction : public Action<Ts...>, public Parented<T
|
|||||||
if (this->mode_.has_value()) {
|
if (this->mode_.has_value()) {
|
||||||
call.set_mode(this->mode_.value(x...));
|
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();
|
call.perform();
|
||||||
} else {
|
} else {
|
||||||
this->parent_->publish_state();
|
this->parent_->publish_state();
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ void TemplateWaterHeater::setup() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!this->current_temperature_f_.has_value() && !this->target_temperature_f_.has_value() &&
|
if (!this->current_temperature_f_.has_value() && !this->target_temperature_f_.has_value() &&
|
||||||
!this->mode_f_.has_value())
|
!this->mode_f_.has_value() && !this->away_f_.has_value() && !this->is_on_f_.has_value())
|
||||||
this->disable_loop();
|
this->disable_loop();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,6 +32,12 @@ water_heater::WaterHeaterTraits TemplateWaterHeater::traits() {
|
|||||||
if (this->target_temperature_f_.has_value()) {
|
if (this->target_temperature_f_.has_value()) {
|
||||||
traits.add_feature_flags(water_heater::WATER_HEATER_SUPPORTS_TARGET_TEMPERATURE);
|
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;
|
return traits;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +68,22 @@ 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) {
|
if (changed) {
|
||||||
this->publish_state();
|
this->publish_state();
|
||||||
}
|
}
|
||||||
@@ -90,6 +112,17 @@ 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();
|
this->set_trigger_.trigger();
|
||||||
|
|
||||||
if (this->optimistic_) {
|
if (this->optimistic_) {
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ class TemplateWaterHeater : public Component, public water_heater::WaterHeater {
|
|||||||
this->target_temperature_f_.set(std::forward<F>(f));
|
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_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_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
|
||||||
void set_restore_mode(TemplateWaterHeaterRestoreMode restore_mode) { this->restore_mode_ = restore_mode; }
|
void set_restore_mode(TemplateWaterHeaterRestoreMode restore_mode) { this->restore_mode_ = restore_mode; }
|
||||||
@@ -49,6 +51,8 @@ class TemplateWaterHeater : public Component, public water_heater::WaterHeater {
|
|||||||
TemplateLambda<float> current_temperature_f_;
|
TemplateLambda<float> current_temperature_f_;
|
||||||
TemplateLambda<float> target_temperature_f_;
|
TemplateLambda<float> target_temperature_f_;
|
||||||
TemplateLambda<water_heater::WaterHeaterMode> mode_f_;
|
TemplateLambda<water_heater::WaterHeaterMode> mode_f_;
|
||||||
|
TemplateLambda<bool> away_f_;
|
||||||
|
TemplateLambda<bool> is_on_f_;
|
||||||
TemplateWaterHeaterRestoreMode restore_mode_{WATER_HEATER_NO_RESTORE};
|
TemplateWaterHeaterRestoreMode restore_mode_{WATER_HEATER_NO_RESTORE};
|
||||||
water_heater::WaterHeaterModeMask supported_modes_;
|
water_heater::WaterHeaterModeMask supported_modes_;
|
||||||
bool optimistic_{true};
|
bool optimistic_{true};
|
||||||
|
|||||||
@@ -430,12 +430,14 @@ void VoiceAssistant::client_subscription(api::APIConnection *client, bool subscr
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this->api_client_ != nullptr) {
|
if (this->api_client_ != nullptr) {
|
||||||
|
char current_peername[socket::SOCKADDR_STR_LEN];
|
||||||
|
char new_peername[socket::SOCKADDR_STR_LEN];
|
||||||
ESP_LOGE(TAG,
|
ESP_LOGE(TAG,
|
||||||
"Multiple API Clients attempting to connect to Voice Assistant\n"
|
"Multiple API Clients attempting to connect to Voice Assistant\n"
|
||||||
"Current client: %s (%s)\n"
|
"Current client: %s (%s)\n"
|
||||||
"New client: %s (%s)",
|
"New client: %s (%s)",
|
||||||
this->api_client_->get_name(), this->api_client_->get_peername(), client->get_name(),
|
this->api_client_->get_name(), this->api_client_->get_peername_to(current_peername), client->get_name(),
|
||||||
client->get_peername());
|
client->get_peername_to(new_peername));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -605,15 +605,6 @@ 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
|
#endif
|
||||||
|
|
||||||
void Application::yield_with_select_(uint32_t delay_ms) {
|
void Application::yield_with_select_(uint32_t delay_ms) {
|
||||||
|
|||||||
@@ -101,6 +101,10 @@
|
|||||||
#include "esphome/components/update/update_entity.h"
|
#include "esphome/components/update/update_entity.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
namespace esphome::socket {
|
||||||
|
class Socket;
|
||||||
|
} // namespace esphome::socket
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
|
|
||||||
// Teardown timeout constant (in milliseconds)
|
// Teardown timeout constant (in milliseconds)
|
||||||
@@ -491,7 +495,8 @@ class Application {
|
|||||||
void unregister_socket_fd(int fd);
|
void unregister_socket_fd(int fd);
|
||||||
/// Check if there's data available on a socket without blocking
|
/// 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
|
/// This function is thread-safe for reading, but should be called after select() has run
|
||||||
bool is_socket_ready(int fd) const;
|
/// 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); }
|
||||||
|
|
||||||
#ifdef USE_WAKE_LOOP_THREADSAFE
|
#ifdef USE_WAKE_LOOP_THREADSAFE
|
||||||
/// Wake the main event loop from a FreeRTOS task
|
/// Wake the main event loop from a FreeRTOS task
|
||||||
@@ -503,6 +508,15 @@ class Application {
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
friend Component;
|
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);
|
void register_component_(Component *comp);
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ class ComponentIterator {
|
|||||||
public:
|
public:
|
||||||
void begin(bool include_internal = false);
|
void begin(bool include_internal = false);
|
||||||
void advance();
|
void advance();
|
||||||
bool completed() const { return this->state_ == IteratorState::NONE; }
|
|
||||||
virtual bool on_begin();
|
virtual bool on_begin();
|
||||||
#ifdef USE_BINARY_SENSOR
|
#ifdef USE_BINARY_SENSOR
|
||||||
virtual bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) = 0;
|
virtual bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) = 0;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ dependencies:
|
|||||||
espressif/mdns:
|
espressif/mdns:
|
||||||
version: 1.9.1
|
version: 1.9.1
|
||||||
espressif/esp_wifi_remote:
|
espressif/esp_wifi_remote:
|
||||||
version: 1.2.4
|
version: 1.3.2
|
||||||
rules:
|
rules:
|
||||||
- if: "target in [esp32h2, esp32p4]"
|
- if: "target in [esp32h2, esp32p4]"
|
||||||
espressif/eppp_link:
|
espressif/eppp_link:
|
||||||
@@ -18,7 +18,7 @@ dependencies:
|
|||||||
rules:
|
rules:
|
||||||
- if: "target in [esp32h2, esp32p4]"
|
- if: "target in [esp32h2, esp32p4]"
|
||||||
espressif/esp_hosted:
|
espressif/esp_hosted:
|
||||||
version: 2.9.3
|
version: 2.11.5
|
||||||
rules:
|
rules:
|
||||||
- if: "target in [esp32h2, esp32p4]"
|
- if: "target in [esp32h2, esp32p4]"
|
||||||
zorxx/multipart-parser:
|
zorxx/multipart-parser:
|
||||||
|
|||||||
@@ -136,6 +136,7 @@ extends = common:arduino
|
|||||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.36/platform-espressif32.zip
|
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.36/platform-espressif32.zip
|
||||||
platform_packages =
|
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-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
|
framework = arduino, espidf ; Arduino as an ESP-IDF component
|
||||||
lib_deps =
|
lib_deps =
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ pyserial==3.5
|
|||||||
platformio==6.1.19
|
platformio==6.1.19
|
||||||
esptool==5.1.0
|
esptool==5.1.0
|
||||||
click==8.1.7
|
click==8.1.7
|
||||||
esphome-dashboard==20260110.0
|
esphome-dashboard==20260210.0
|
||||||
aioesphomeapi==43.14.0
|
aioesphomeapi==44.0.0
|
||||||
zeroconf==0.148.0
|
zeroconf==0.148.0
|
||||||
puremagic==1.30
|
puremagic==1.30
|
||||||
ruamel.yaml==0.19.1 # dashboard_import
|
ruamel.yaml==0.19.1 # dashboard_import
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
ld2450:
|
ld2450:
|
||||||
- id: ld2450_radar
|
- id: ld2450_radar
|
||||||
|
on_data:
|
||||||
|
then:
|
||||||
|
- logger.log: "LD2450 Radar Data Received"
|
||||||
|
|
||||||
button:
|
button:
|
||||||
- platform: ld2450
|
- platform: ld2450
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ esphome:
|
|||||||
id: template_water_heater
|
id: template_water_heater
|
||||||
target_temperature: 50.0
|
target_temperature: 50.0
|
||||||
mode: ECO
|
mode: ECO
|
||||||
|
away: false
|
||||||
|
is_on: true
|
||||||
|
|
||||||
# Templated
|
# Templated
|
||||||
- water_heater.template.publish:
|
- water_heater.template.publish:
|
||||||
@@ -20,6 +22,8 @@ esphome:
|
|||||||
current_temperature: !lambda "return 45.0;"
|
current_temperature: !lambda "return 45.0;"
|
||||||
target_temperature: !lambda "return 55.0;"
|
target_temperature: !lambda "return 55.0;"
|
||||||
mode: !lambda "return water_heater::WATER_HEATER_MODE_GAS;"
|
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)
|
# 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.
|
# NOTE: set_template() is not intended to be a public API, but we test it to ensure it doesn't break.
|
||||||
@@ -414,6 +418,8 @@ water_heater:
|
|||||||
current_temperature: !lambda "return 42.0f;"
|
current_temperature: !lambda "return 42.0f;"
|
||||||
target_temperature: !lambda "return 60.0f;"
|
target_temperature: !lambda "return 60.0f;"
|
||||||
mode: !lambda "return water_heater::WATER_HEATER_MODE_ECO;"
|
mode: !lambda "return water_heater::WATER_HEATER_MODE_ECO;"
|
||||||
|
away: !lambda "return false;"
|
||||||
|
is_on: !lambda "return true;"
|
||||||
supported_modes:
|
supported_modes:
|
||||||
- "OFF"
|
- "OFF"
|
||||||
- ECO
|
- ECO
|
||||||
|
|||||||
@@ -4,6 +4,14 @@ host:
|
|||||||
api:
|
api:
|
||||||
logger:
|
logger:
|
||||||
|
|
||||||
|
globals:
|
||||||
|
- id: global_away
|
||||||
|
type: bool
|
||||||
|
initial_value: "false"
|
||||||
|
- id: global_is_on
|
||||||
|
type: bool
|
||||||
|
initial_value: "true"
|
||||||
|
|
||||||
water_heater:
|
water_heater:
|
||||||
- platform: template
|
- platform: template
|
||||||
id: test_boiler
|
id: test_boiler
|
||||||
@@ -11,6 +19,8 @@ water_heater:
|
|||||||
optimistic: true
|
optimistic: true
|
||||||
current_temperature: !lambda "return 45.0f;"
|
current_temperature: !lambda "return 45.0f;"
|
||||||
target_temperature: !lambda "return 60.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
|
# Note: No mode lambda - we want optimistic mode changes to stick
|
||||||
# A mode lambda would override mode changes in loop()
|
# A mode lambda would override mode changes in loop()
|
||||||
supported_modes:
|
supported_modes:
|
||||||
@@ -22,3 +32,8 @@ water_heater:
|
|||||||
min_temperature: 30.0
|
min_temperature: 30.0
|
||||||
max_temperature: 85.0
|
max_temperature: 85.0
|
||||||
target_temperature_step: 0.5
|
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();
|
||||||
|
|||||||
@@ -5,7 +5,13 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
import aioesphomeapi
|
import aioesphomeapi
|
||||||
from aioesphomeapi import WaterHeaterInfo, WaterHeaterMode, WaterHeaterState
|
from aioesphomeapi import (
|
||||||
|
WaterHeaterFeature,
|
||||||
|
WaterHeaterInfo,
|
||||||
|
WaterHeaterMode,
|
||||||
|
WaterHeaterState,
|
||||||
|
WaterHeaterStateFlag,
|
||||||
|
)
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from .state_utils import InitialStateHelper
|
from .state_utils import InitialStateHelper
|
||||||
@@ -22,18 +28,25 @@ async def test_water_heater_template(
|
|||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
async with run_compiled(yaml_config), api_client_connected() as client:
|
async with run_compiled(yaml_config), api_client_connected() as client:
|
||||||
states: dict[int, aioesphomeapi.EntityState] = {}
|
states: dict[int, aioesphomeapi.EntityState] = {}
|
||||||
gas_mode_future: asyncio.Future[WaterHeaterState] = loop.create_future()
|
state_future: asyncio.Future[WaterHeaterState] | None = None
|
||||||
eco_mode_future: asyncio.Future[WaterHeaterState] = loop.create_future()
|
|
||||||
|
|
||||||
def on_state(state: aioesphomeapi.EntityState) -> None:
|
def on_state(state: aioesphomeapi.EntityState) -> None:
|
||||||
states[state.key] = state
|
states[state.key] = state
|
||||||
if isinstance(state, WaterHeaterState):
|
if (
|
||||||
# Wait for GAS mode
|
isinstance(state, WaterHeaterState)
|
||||||
if state.mode == WaterHeaterMode.GAS and not gas_mode_future.done():
|
and state_future is not None
|
||||||
gas_mode_future.set_result(state)
|
and not state_future.done()
|
||||||
# Wait for ECO mode (we start at OFF, so test transitioning to ECO)
|
):
|
||||||
elif state.mode == WaterHeaterMode.ECO and not eco_mode_future.done():
|
state_future.set_result(state)
|
||||||
eco_mode_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
|
||||||
|
|
||||||
# Get entities and set up state synchronization
|
# Get entities and set up state synchronization
|
||||||
entities, services = await client.list_entities_services()
|
entities, services = await client.list_entities_services()
|
||||||
@@ -89,24 +102,52 @@ async def test_water_heater_template(
|
|||||||
f"Expected target temp 60.0, got {initial_state.target_temperature}"
|
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
|
# Test changing to GAS mode
|
||||||
client.water_heater_command(test_water_heater.key, mode=WaterHeaterMode.GAS)
|
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
|
assert gas_state.mode == WaterHeaterMode.GAS
|
||||||
|
|
||||||
# Test changing to ECO mode (from GAS)
|
# Test changing to ECO mode (from GAS)
|
||||||
client.water_heater_command(test_water_heater.key, mode=WaterHeaterMode.ECO)
|
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
|
assert eco_state.mode == WaterHeaterMode.ECO
|
||||||
|
|||||||
Reference in New Issue
Block a user