Compare commits

..
26 changed files with 105 additions and 286 deletions

No files matched your search

+1 -19
View File
@@ -395,7 +395,7 @@ file does, and it is the authority when they disagree. The most useful starting
* **Triggers -- Callback method (preferred):**
Use `build_callback_automation()` for simple triggers. The automation is registered directly as a callback on the parent, so there is no C++ Trigger class and nothing is allocated. No `CONF_TRIGGER_ID` in the schema.
Use `build_callback_automation()` for simple triggers. This eliminates the need for a C++ Trigger class by using a lightweight pointer-sized forwarder struct registered directly as a callback. No `CONF_TRIGGER_ID` in the schema.
**Python:**
```python
@@ -448,24 +448,6 @@ file does, and it is the authority when they disagree. The most useful starting
forward=[cg.StringRef(parent.option_at(index)), index],
)
```
**Choosing the filter:** a filter that many automations share, such as entering one value of a state enum,
is a small forwarder struct templated on that value. Every automation for that value then shares one
function and passes only its `Automation *`. `when` generates a separate lambda per automation, which cost
16 to 64 bytes more flash when lock, media_player, alarm_control_panel and fusb302b were tried with it.
```cpp
template<LockState State> struct LockStateForwarder {
Automation<> *automation;
void operator()(LockState state) const {
if (state == State)
this->automation->trigger();
}
};
```
Use `when` for a test that is not a fixed value shared across automations, e.g. one that reads the parent:
`when="{parent}->is_fully_open()"` (valve). Only a trigger that must remember something between calls
(the last position, a click count) keeps a Trigger class.
`build_parent_callback_automation`, `build_trigger_callback`, `build_callback_automations` and
`build_trigger_automations` cover the other shapes; see their docstrings in `esphome/automation.py`.
+1 -1
View File
@@ -188,7 +188,7 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) {
// Expose the next chunk of file data. Every decoder buffers internally and consumes only what it
// processed, so the source does not need to accumulate or stitch chunks across fill() calls.
this->input_buffer_->fill(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS));
this->input_buffer_->fill(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), false);
const size_t available_before_decode = this->input_buffer_->available();
+3 -2
View File
@@ -129,8 +129,9 @@ AudioResamplerState AudioResampler::resample(bool stop_gracefully, int32_t *ms_d
delay(READ_WRITE_TIMEOUT_MS);
}
// Expose a chunk of the ring buffer's internal storage.
this->audio_source_->fill(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS));
// Expose a chunk of the ring buffer's internal storage. pre_shift is ignored by RingBufferAudioSource
// (there is no intermediate transfer buffer to compact).
this->audio_source_->fill(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), false);
if (this->audio_source_->available() == 0) {
// No samples available to process
@@ -20,6 +20,16 @@ std::unique_ptr<AudioSinkTransferBuffer> AudioSinkTransferBuffer::create(size_t
return sink_buffer;
}
std::unique_ptr<AudioSourceTransferBuffer> AudioSourceTransferBuffer::create(size_t buffer_size) {
std::unique_ptr<AudioSourceTransferBuffer> source_buffer = make_unique<AudioSourceTransferBuffer>();
if (!source_buffer->allocate_buffer_(buffer_size)) {
return nullptr;
}
return source_buffer;
}
size_t AudioTransferBuffer::free() const {
if (this->buffer_size_ == 0) {
return 0;
@@ -122,6 +132,27 @@ void AudioTransferBuffer::deallocate_buffer_() {
this->buffer_length_ = 0;
}
size_t AudioSourceTransferBuffer::transfer_data_from_source(TickType_t ticks_to_wait, bool pre_shift) {
if (pre_shift) {
// Shift data in buffer to start
if (this->buffer_length_ > 0) {
std::memmove(this->buffer_, this->data_start_, this->buffer_length_);
}
this->data_start_ = this->buffer_;
}
size_t bytes_to_read = AudioTransferBuffer::free();
size_t bytes_read = 0;
if (bytes_to_read > 0) {
if (this->ring_buffer_ != nullptr) {
bytes_read = this->ring_buffer_->read((void *) this->get_buffer_end(), bytes_to_read, ticks_to_wait);
}
this->increase_buffer_length(bytes_read);
}
return bytes_read;
}
size_t AudioSinkTransferBuffer::transfer_data_to_sink(TickType_t ticks_to_wait, bool post_shift) {
size_t bytes_written = 0;
if (this->available()) {
@@ -161,44 +192,9 @@ bool AudioSinkTransferBuffer::has_buffered_data() const {
return (this->available() > 0);
}
// AudioSourceTransferBuffer is deprecated. Remove before 2027.5.0.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
std::unique_ptr<AudioSourceTransferBuffer> AudioSourceTransferBuffer::create(size_t buffer_size) {
std::unique_ptr<AudioSourceTransferBuffer> source_buffer = make_unique<AudioSourceTransferBuffer>();
if (!source_buffer->allocate_buffer_(buffer_size)) {
return nullptr;
}
return source_buffer;
}
size_t AudioSourceTransferBuffer::transfer_data_from_source(TickType_t ticks_to_wait, bool pre_shift) {
if (pre_shift) {
// Shift data in buffer to start
if (this->buffer_length_ > 0) {
std::memmove(this->buffer_, this->data_start_, this->buffer_length_);
}
this->data_start_ = this->buffer_;
}
size_t bytes_to_read = AudioTransferBuffer::free();
size_t bytes_read = 0;
if (bytes_to_read > 0) {
if (this->ring_buffer_ != nullptr) {
bytes_read = this->ring_buffer_->read((void *) this->get_buffer_end(), bytes_to_read, ticks_to_wait);
}
this->increase_buffer_length(bytes_read);
}
return bytes_read;
}
size_t AudioSourceTransferBuffer::free() const { return AudioTransferBuffer::free(); }
bool AudioSourceTransferBuffer::has_buffered_data() const { return AudioTransferBuffer::has_buffered_data(); }
#pragma GCC diagnostic pop
void ConstAudioSourceBuffer::set_data(const uint8_t *data, size_t length) {
this->data_start_ = data;
@@ -272,23 +268,6 @@ void RingBufferAudioSource::clear_buffered_data() {
this->ring_buffer_->reset();
}
size_t RingBufferAudioSource::expose_padded_partial(size_t frame_bytes) {
if (this->current_available_ > 0 || this->queued_length_ > 0) {
return 0;
}
// Moves any trailing bytes still inside a held item into the splice buffer
this->release_item_();
const size_t real_bytes = this->splice_length_;
if (real_bytes == 0 || frame_bytes == 0 || real_bytes % frame_bytes != 0) {
return 0;
}
std::memset(this->splice_buffer_ + real_bytes, 0, this->alignment_bytes_ - real_bytes);
this->splice_length_ = 0;
this->current_data_ = this->splice_buffer_;
this->current_available_ = this->alignment_bytes_;
return real_bytes;
}
bool RingBufferAudioSource::has_buffered_data() const {
// splice_length_ is deliberately not considered here. It holds an incomplete frame whose completion
// bytes must still arrive through the ring buffer, which ring_buffer_->available() already reports.
@@ -297,7 +276,7 @@ bool RingBufferAudioSource::has_buffered_data() const {
return (this->current_available_ > 0) || (this->queued_length_ > 0) || (this->ring_buffer_->available() > 0);
}
size_t RingBufferAudioSource::fill(TickType_t ticks_to_wait) {
size_t RingBufferAudioSource::fill(TickType_t ticks_to_wait, bool /*pre_shift*/) {
if (this->current_available_ > 0) {
// Caller has not finished consuming the current exposure
return 0;
@@ -3,7 +3,6 @@
#ifdef USE_ESP32
#include "esphome/components/ring_buffer/ring_buffer.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h" // for ESPDEPRECATED
#ifdef USE_SPEAKER
#include "esphome/components/speaker/speaker.h"
@@ -153,20 +152,13 @@ class AudioReadableBuffer {
/// @brief Refills the buffer from its source. No-op by default for read-only buffers.
/// @param ticks_to_wait FreeRTOS ticks to block while waiting for data
/// @param pre_shift If true, shifts existing data to the start of the buffer before reading
/// @return Number of bytes read
virtual size_t fill(TickType_t ticks_to_wait) { return 0; }
// Remove before 2027.5.0
ESPDEPRECATED("Use fill(ticks_to_wait) instead. Removed in 2027.5.0.", "2026.11.0")
virtual size_t fill(TickType_t ticks_to_wait, bool pre_shift) { return this->fill(ticks_to_wait); }
virtual size_t fill(TickType_t ticks_to_wait, bool pre_shift) { return 0; }
size_t fill(TickType_t ticks_to_wait) { return this->fill(ticks_to_wait, true); }
};
// Remove before 2027.5.0
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
class ESPDEPRECATED("Use RingBufferAudioSource instead. Removed in 2027.5.0.", "2026.11.0") AudioSourceTransferBuffer
: public AudioTransferBuffer,
public AudioReadableBuffer {
class AudioSourceTransferBuffer : public AudioTransferBuffer, public AudioReadableBuffer {
/*
* @brief A class that implements a transfer buffer for audio sources.
* Supports reading audio data from a ring buffer into the transfer buffer for processing.
@@ -197,12 +189,10 @@ class ESPDEPRECATED("Use RingBufferAudioSource instead. Removed in 2027.5.0.", "
size_t free() const override;
void consume(size_t bytes) override { this->decrease_buffer_length(bytes); }
bool has_buffered_data() const override;
size_t fill(TickType_t ticks_to_wait) override { return this->transfer_data_from_source(ticks_to_wait); }
size_t fill(TickType_t ticks_to_wait, bool pre_shift) override {
return this->transfer_data_from_source(ticks_to_wait, pre_shift);
}
};
#pragma GCC diagnostic pop
/// @brief A lightweight read-only audio buffer for const data sources (e.g., flash memory).
/// Does not allocate memory or transfer data from external sources.
@@ -256,21 +246,14 @@ class RingBufferAudioSource : public AudioReadableBuffer {
size_t available() const override { return this->current_available_; }
void consume(size_t bytes) override;
bool has_buffered_data() const override;
/// An unconsumed exposure stays in place and fill() returns 0 until it is fully consumed.
size_t fill(TickType_t ticks_to_wait) override;
// Keeps the deprecated fill(ticks_to_wait, pre_shift) overload visible. Remove before 2027.5.0.
using AudioReadableBuffer::fill;
/// pre_shift is ignored: there is no intermediate transfer buffer to compact, so an unconsumed
/// exposure stays in place and fill() returns 0 until it is fully consumed.
size_t fill(TickType_t ticks_to_wait, bool pre_shift) override;
/// @brief Discards all buffered audio: releases any held ring buffer item, clears the source's in-flight
/// state, and resets the underlying ring buffer. Must be invoked from the ring buffer's consumer thread.
void clear_buffered_data();
/// @brief Zero-pads held whole frames short of an alignment unit and exposes them, so a stream's tail is not
/// stranded. A held partial frame is left to wait for its remaining bytes.
/// @param frame_bytes Bytes per audio frame
/// @return Real (unpadded) bytes exposed, or 0 if nothing is exposable
size_t expose_padded_partial(size_t frame_bytes);
/// @brief Returns a mutable pointer to the currently exposed audio data.
/// The pointer may reference the ring buffer's internal storage or, when exposing a stitched frame
/// across a wrap boundary, an internal splice buffer. In either case mutations are safe but data
@@ -270,7 +270,6 @@ HTTP_REQUEST_ACTION_SCHEMA = cv.Schema(
cv.Optional(CONF_ON_RESPONSE): automation.validate_automation(single=True),
cv.Optional(CONF_ON_ERROR): automation.validate_automation(single=True),
cv.Optional(CONF_MAX_RESPONSE_BUFFER_SIZE, default="1kB"): cv.validate_bytes,
cv.Optional(CONF_TIMEOUT): cv.templatable(cv.positive_time_period_milliseconds),
}
)
HTTP_REQUEST_GET_ACTION_SCHEMA = automation.maybe_conf(
@@ -348,10 +347,6 @@ async def http_request_action_to_code(
cg.add(var.set_max_response_buffer_size(config[CONF_MAX_RESPONSE_BUFFER_SIZE]))
if CONF_TIMEOUT in config:
template_ = await cg.templatable(config[CONF_TIMEOUT], args, cg.uint32)
cg.add(var.set_timeout(template_))
if CONF_BODY in config:
template_ = await cg.templatable(config[CONF_BODY], args, cg.std_string)
cg.add(var.set_body(template_))
+8 -25
View File
@@ -252,8 +252,6 @@ class HttpContainer : public Parented<HttpRequestComponent> {
virtual void end() = 0;
void set_secure(bool secure) { this->secure_ = secure; }
void set_watchdog_timeout(uint32_t watchdog_timeout) { this->watchdog_timeout_ = watchdog_timeout; }
uint32_t get_watchdog_timeout() const { return this->watchdog_timeout_; }
void set_chunked(bool chunked) { this->is_chunked_ = chunked; }
size_t get_bytes_read() const { return this->bytes_read_; }
@@ -285,7 +283,6 @@ class HttpContainer : public Parented<HttpRequestComponent> {
protected:
size_t bytes_read_{0};
uint32_t watchdog_timeout_{0};
bool secure_{false};
bool is_chunked_{false}; ///< True if response uses chunked transfer encoding
std::vector<Header> response_headers_{};
@@ -342,11 +339,6 @@ class HttpRequestComponent : public Component {
uint32_t get_timeout() const { return this->timeout_; }
void set_watchdog_timeout(uint32_t watchdog_timeout) { this->watchdog_timeout_ = watchdog_timeout; }
uint32_t get_watchdog_timeout() const { return this->watchdog_timeout_; }
uint32_t get_watchdog_timeout(uint32_t timeout_ms) const {
if (this->watchdog_timeout_ == 0 || timeout_ms <= this->timeout_)
return this->watchdog_timeout_;
return this->watchdog_timeout_ + 3 * (timeout_ms - this->timeout_);
}
void set_follow_redirects(bool follow_redirects) { this->follow_redirects_ = follow_redirects; }
void set_redirect_limit(uint16_t limit) { this->redirect_limit_ = limit; }
@@ -420,7 +412,7 @@ class HttpRequestComponent : public Component {
std::shared_ptr<HttpContainer> start(const char *url, const char *method, const std::string &body,
const std::vector<Header> &request_headers) {
// Call perform() directly to avoid ambiguity with the deprecated overloads
return this->perform(url, method, body, request_headers, {}, this->timeout_);
return this->perform(url, method, body, request_headers, {});
}
std::shared_ptr<HttpContainer> start(const std::string &url, const std::string &method, const std::string &body,
const std::vector<Header> &request_headers) {
@@ -445,7 +437,7 @@ class HttpRequestComponent : public Component {
for (const auto &h : collect_headers) {
lower.push_back(str_lower_case(h)); // NOLINT
}
return this->perform(url.c_str(), method.c_str(), body, request_headers, lower, this->timeout_);
return this->perform(url.c_str(), method.c_str(), body, request_headers, lower);
}
// Remove before 2027.1.0
@@ -461,7 +453,7 @@ class HttpRequestComponent : public Component {
lower.push_back(str_lower_case(h)); // NOLINT
}
return this->perform(url.c_str(), method.c_str(), body,
std::vector<Header>(request_headers.begin(), request_headers.end()), lower, this->timeout_);
std::vector<Header>(request_headers.begin(), request_headers.end()), lower);
}
// Remove before 2027.1.0
@@ -471,19 +463,13 @@ class HttpRequestComponent : public Component {
const std::vector<std::string> &lower_case_collect_headers) {
return this->perform(url.c_str(), method.c_str(), body,
std::vector<Header>(request_headers.begin(), request_headers.end()),
lower_case_collect_headers, this->timeout_);
lower_case_collect_headers);
}
std::shared_ptr<HttpContainer> start(const char *url, const char *method, const std::string &body,
const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers) {
return this->perform(url, method, body, request_headers, lower_case_collect_headers, this->timeout_);
}
std::shared_ptr<HttpContainer> start(const char *url, const char *method, const std::string &body,
const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers,
uint32_t timeout_ms) {
return this->perform(url, method, body, request_headers, lower_case_collect_headers, timeout_ms);
return this->perform(url, method, body, request_headers, lower_case_collect_headers);
}
std::shared_ptr<HttpContainer> start(const std::string &url, const std::string &method, const std::string &body,
const std::vector<Header> &request_headers,
@@ -494,8 +480,7 @@ class HttpRequestComponent : public Component {
protected:
virtual std::shared_ptr<HttpContainer> perform(const char *url, const char *method, const std::string &body,
const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers,
uint32_t timeout_ms) = 0;
const std::vector<std::string> &lower_case_collect_headers) = 0;
const char *useragent_{nullptr};
bool follow_redirects_{};
uint16_t redirect_limit_{};
@@ -509,7 +494,6 @@ template<typename... Ts> class HttpRequestSendAction final : public Action<Ts...
TEMPLATABLE_VALUE(std::string, url)
TEMPLATABLE_VALUE(const char *, method)
TEMPLATABLE_VALUE(std::string, body)
TEMPLATABLE_VALUE(uint32_t, timeout)
#ifdef USE_HTTP_REQUEST_RESPONSE
TEMPLATABLE_VALUE(bool, capture_response)
#endif
@@ -559,9 +543,8 @@ template<typename... Ts> class HttpRequestSendAction final : public Action<Ts...
request_headers.push_back({header.name, header.value(x...)});
}
const uint32_t timeout = this->timeout_.has_value() ? this->timeout_.value(x...) : this->parent_->get_timeout();
auto container = this->parent_->start(this->url_.value(x...).c_str(), this->method_.value(x...), body,
request_headers, this->lower_case_collect_headers_, timeout);
request_headers, this->lower_case_collect_headers_);
auto captured_args = std::make_tuple(x...);
@@ -582,7 +565,7 @@ template<typename... Ts> class HttpRequestSendAction final : public Action<Ts...
// Use http_read_loop_result() helper instead of checking return values directly
size_t read_index = 0;
uint32_t last_data_time = millis();
const uint32_t read_timeout = timeout;
const uint32_t read_timeout = this->parent_->get_timeout();
while (container->get_bytes_read() < max_length) {
int read_or_error = container->read(buf + read_index, std::min<size_t>(max_length - read_index, 512));
App.feed_wdt();
@@ -26,8 +26,7 @@ static constexpr int ESP8266_SSL_ERR_OOM = -1000;
std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const char *url, const char *method, const std::string &body,
const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers,
uint32_t timeout_ms) {
const std::vector<std::string> &lower_case_collect_headers) {
if (!network::is_connected()) {
this->status_momentary_error(1000);
ESP_LOGW(TAG, "HTTP Request failed; Not connected to network");
@@ -41,9 +40,8 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const char *url, cons
bool secure = strstr(url, "https:") != nullptr;
container->set_secure(secure);
container->set_watchdog_timeout(this->get_watchdog_timeout(timeout_ms));
watchdog::WatchdogManager wdm(container->get_watchdog_timeout());
watchdog::WatchdogManager wdm(this->get_watchdog_timeout());
if (this->follow_redirects_) {
container->client_.setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS);
@@ -92,7 +90,7 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const char *url, cons
}
container->client_.setReuse(true);
container->client_.setTimeout(timeout_ms);
container->client_.setTimeout(this->timeout_);
if (this->useragent_ != nullptr) {
container->client_.setUserAgent(this->useragent_);
@@ -205,7 +203,7 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const char *url, cons
// and sets content_length = bytes_read_ so is_read_complete() returns true.
int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
const uint32_t start = millis();
watchdog::WatchdogManager wdm(this->get_watchdog_timeout());
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
WiFiClient *stream_ptr = this->client_.getStreamPtr();
if (stream_ptr == nullptr) {
@@ -400,7 +398,7 @@ int HttpContainerArduino::read_chunked_(uint8_t *buf, size_t max_len, WiFiClient
}
void HttpContainerArduino::end() {
watchdog::WatchdogManager wdm(this->get_watchdog_timeout());
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
this->client_.end();
}
@@ -56,8 +56,7 @@ class HttpRequestArduino final : public HttpRequestComponent {
protected:
std::shared_ptr<HttpContainer> perform(const char *url, const char *method, const std::string &body,
const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers,
uint32_t timeout_ms) override;
const std::vector<std::string> &lower_case_collect_headers) override;
#ifdef USE_ESP8266
uint16_t tls_buffer_size_rx_{512};
uint16_t tls_buffer_size_tx_{512};
@@ -20,8 +20,7 @@ ESPHOME_LOG_TAG(TAG, "http_request");
std::shared_ptr<HttpContainer> HttpRequestHost::perform(const char *url, const char *method, const std::string &body,
const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers,
uint32_t) {
const std::vector<std::string> &lower_case_collect_headers) {
if (!network::is_connected()) {
this->status_momentary_error(1000);
ESP_LOGW(TAG, "HTTP Request failed; Not connected to network");
@@ -20,8 +20,7 @@ class HttpRequestHost final : public HttpRequestComponent {
public:
std::shared_ptr<HttpContainer> perform(const char *url, const char *method, const std::string &body,
const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers,
uint32_t timeout_ms) override;
const std::vector<std::string> &lower_case_collect_headers) override;
void set_ca_path(const char *ca_path) { this->ca_path_ = ca_path; }
protected:
@@ -52,8 +52,7 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) {
std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const char *url, const char *method, const std::string &body,
const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers,
uint32_t timeout_ms) {
const std::vector<std::string> &lower_case_collect_headers) {
if (!network::is_connected()) {
this->status_momentary_error(ERROR_DURATION_MS);
ESP_LOGE(TAG, "HTTP Request failed; Not connected to network");
@@ -83,7 +82,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const char *url, const ch
config.url = url;
config.method = method_idf;
config.timeout_ms = timeout_ms;
config.timeout_ms = this->timeout_;
config.disable_auto_redirect = !this->follow_redirects_;
config.max_redirection_count = this->redirect_limit_;
config.auth_type = HTTP_AUTH_TYPE_BASIC;
@@ -105,8 +104,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const char *url, const ch
config.buffer_size_tx = this->buffer_size_tx_;
const uint32_t start = millis();
const uint32_t watchdog_timeout = this->get_watchdog_timeout(timeout_ms);
watchdog::WatchdogManager wdm(watchdog_timeout);
watchdog::WatchdogManager wdm(this->get_watchdog_timeout());
config.event_handler = http_event_handler;
@@ -119,7 +117,6 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const char *url, const ch
std::shared_ptr<HttpContainerIDF> container = std::make_shared<HttpContainerIDF>(client);
container->set_parent(this);
container->set_watchdog_timeout(watchdog_timeout);
container->set_secure(secure);
@@ -264,7 +261,7 @@ bool HttpContainerIDF::is_read_complete() const {
// 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) {
const uint32_t start = millis();
watchdog::WatchdogManager wdm(this->get_watchdog_timeout());
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
// Check if we've already read all expected content (non-chunked and no-body only).
// Use the base class check here, NOT the override: esp_http_client_is_complete_data_received()
@@ -307,7 +304,7 @@ void HttpContainerIDF::end() {
if (this->client_ == nullptr) {
return; // Already cleaned up
}
watchdog::WatchdogManager wdm(this->get_watchdog_timeout());
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
esp_http_client_close(this->client_);
esp_http_client_cleanup(this->client_);
@@ -43,8 +43,7 @@ class HttpRequestIDF final : public HttpRequestComponent {
protected:
std::shared_ptr<HttpContainer> perform(const char *url, const char *method, const std::string &body,
const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers,
uint32_t timeout_ms) override;
const std::vector<std::string> &lower_case_collect_headers) override;
// if zero ESP-IDF will use DEFAULT_HTTP_BUF_SIZE
uint16_t buffer_size_rx_{};
uint16_t buffer_size_tx_{};
@@ -3,9 +3,8 @@ import esphome.codegen as cg
from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_PASSWORD, CONF_URL, CONF_USERNAME
from esphome.core import ID, coroutine_with_priority
from esphome.core import coroutine_with_priority
from esphome.coroutine import CoroPriority
from esphome.cpp_generator import MockObj, TemplateArgsType
from esphome.types import ConfigType
from .. import CONF_HTTP_REQUEST_ID, HttpRequestComponent, http_request_ns
@@ -21,9 +20,6 @@ CONF_MD5_URL = "md5_url"
OtaHttpRequestComponent = http_request_ns.class_(
"OtaHttpRequestComponent", OTAComponent
)
OtaHttpRequestComponentFlashAction = http_request_ns.class_(
"OtaHttpRequestComponentFlashAction", automation.Action
)
CONFIG_SCHEMA = cv.All(
cv.Schema(
@@ -68,38 +64,13 @@ OTA_HTTP_REQUEST_FLASH_ACTION_SCHEMA = cv.All(
)
@automation.register_action(
automation.register_apply_action(
"ota.http_request.flash",
OtaHttpRequestComponentFlashAction,
OTA_HTTP_REQUEST_FLASH_ACTION_SCHEMA,
synchronous=True,
automation.ApplyField(CONF_MD5_URL, "set_md5_url", cg.std_string),
automation.ApplyField(CONF_MD5, "set_md5", cg.std_string),
automation.ApplyField(CONF_PASSWORD, "set_password", cg.std_string),
automation.ApplyField(CONF_USERNAME, "set_username", cg.std_string),
automation.ApplyField(CONF_URL, "set_url", cg.std_string),
automation.ApplyCall("flash()"),
)
async def ota_http_request_action_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
if md5_url := config.get(CONF_MD5_URL):
template_ = await cg.templatable(md5_url, args, cg.std_string)
cg.add(var.set_md5_url(template_))
if md5_str := config.get(CONF_MD5):
template_ = await cg.templatable(md5_str, args, cg.std_string)
cg.add(var.set_md5(template_))
if password_str := config.get(CONF_PASSWORD):
template_ = await cg.templatable(password_str, args, cg.std_string)
cg.add(var.set_password(template_))
if username_str := config.get(CONF_USERNAME):
template_ = await cg.templatable(username_str, args, cg.std_string)
cg.add(var.set_username(template_))
template_ = await cg.templatable(config[CONF_URL], args, cg.std_string)
cg.add(var.set_url(template_))
return var
@@ -1,40 +0,0 @@
#pragma once
#include "ota_http_request.h"
#include "esphome/core/automation.h"
namespace esphome::http_request {
template<typename... Ts> class OtaHttpRequestComponentFlashAction final : public Action<Ts...> {
public:
OtaHttpRequestComponentFlashAction(OtaHttpRequestComponent *parent) : parent_(parent) {}
TEMPLATABLE_VALUE(std::string, md5_url)
TEMPLATABLE_VALUE(std::string, md5)
TEMPLATABLE_VALUE(std::string, password)
TEMPLATABLE_VALUE(std::string, url)
TEMPLATABLE_VALUE(std::string, username)
void play(const Ts &...x) override {
if (this->md5_url_.has_value()) {
this->parent_->set_md5_url(this->md5_url_.value(x...));
}
if (this->md5_.has_value()) {
this->parent_->set_md5(this->md5_.value(x...));
}
if (this->password_.has_value()) {
this->parent_->set_password(this->password_.value(x...));
}
if (this->username_.has_value()) {
this->parent_->set_username(this->username_.value(x...));
}
this->parent_->set_url(this->url_.value(x...));
this->parent_->flash();
// Normally never reached due to reboot
}
protected:
OtaHttpRequestComponent *parent_;
};
} // namespace esphome::http_request
@@ -334,7 +334,7 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() {
if (!this->pause_state_) {
while (real_frames_in_block < SPDIF_BLOCK_SAMPLES) {
if (audio_source->available() == 0) {
size_t bytes_read = audio_source->fill(read_timeout_ticks);
size_t bytes_read = audio_source->fill(read_timeout_ticks, false);
if (bytes_read == 0) {
break; // No upstream data within the read budget; silence-pad the remainder.
}
@@ -372,18 +372,10 @@ void I2SAudioSpeakerBase::apply_software_volume_(uint8_t *data, size_t bytes_rea
this->current_stream_info_.bytes_to_samples(bytes_read));
}
bool I2SAudioSpeakerBase::esp32_mono_swap_needed_() const {
#ifdef USE_ESP32_VARIANT_ESP32
return this->output_stream_info_.get_channels() == 1 && this->output_stream_info_.get_bits_per_sample() == 16;
#else
return false;
#endif // USE_ESP32_VARIANT_ESP32
}
void I2SAudioSpeakerBase::swap_esp32_mono_samples_(uint8_t *data, size_t bytes_read) {
#ifdef USE_ESP32_VARIANT_ESP32
// For ESP32 16-bit mono mode, adjacent samples need to be swapped.
if (this->esp32_mono_swap_needed_()) {
if (this->output_stream_info_.get_channels() == 1 && this->output_stream_info_.get_bits_per_sample() == 16) {
int16_t *samples = reinterpret_cast<int16_t *>(data);
size_t sample_count = bytes_read / sizeof(int16_t);
for (size_t i = 0; i + 1 < sample_count; i += 2) {
@@ -160,11 +160,9 @@ class I2SAudioSpeakerBase : public I2SAudioOut, public speaker::Speaker, public
/// @param bytes_read Number of bytes of audio data
void apply_software_volume_(uint8_t *data, size_t bytes_read);
/// @brief Whether the output is 16-bit mono on the original ESP32, which sends each pair of samples swapped.
bool esp32_mono_swap_needed_() const;
/// @brief Swap adjacent 16-bit mono samples for ESP32 (non-variant) hardware quirk.
/// Only applies when esp32_mono_swap_needed_() is true. `data` must start on a DMA word and hold whole pairs.
/// Only applies when running on original ESP32 with 16-bit mono output. Operates on the data that is
/// handed to the I2S peripheral, so the check uses the output (post-narrowing) stream info.
/// @param data Pointer to audio sample data (modified in place)
/// @param bytes_read Number of bytes of audio data
void swap_esp32_mono_samples_(uint8_t *data, size_t bytes_read);
@@ -44,10 +44,10 @@ static constexpr TickType_t WRITE_TIMEOUT_TICKS = pdMS_TO_TICKS(DMA_BUFFER_DURAT
// it (e.g. cache-line rounding on some targets), so the speaker task reads the size actually allocated back
// from the driver instead of assuming this value. Clamping here keeps the request in range and avoids a
// noisy ESP-IDF "dma frame num is out of dma buffer size" warning at high sample rates or bit depths.
static uint32_t dma_buffer_frames(const audio::AudioStreamInfo &stream_info, uint32_t frame_multiple) {
static uint32_t dma_buffer_frames(const audio::AudioStreamInfo &stream_info) {
const uint32_t frames_from_duration = stream_info.ms_to_frames(DMA_BUFFER_DURATION_MS);
const uint32_t max_frames = I2S_DMA_BUFFER_MAX_SIZE / stream_info.frames_to_bytes(1);
return (std::min(frames_from_duration, max_frames) / frame_multiple) * frame_multiple;
return std::min(frames_from_duration, max_frames);
}
void I2SAudioSpeaker::dump_config() {
@@ -85,12 +85,10 @@ void I2SAudioSpeaker::run_speaker_task() {
// The ring buffer holds input-format audio (what play() receives), so size it from the input stream info.
const size_t bytes_per_frame = this->current_stream_info_.frames_to_bytes(1);
// Hand over whole ESP32 mono sample pairs so writes stay word-aligned
const size_t source_alignment = bytes_per_frame * this->dma_frame_multiple_();
const bool pair_aligned = source_alignment > bytes_per_frame;
// Round the ring buffer size down to a multiple of source_alignment so the wrap boundary avoids splices.
// Round the ring buffer size down to a multiple of bytes_per_frame so the wrap boundary stays frame-aligned and
// avoids unnecessary single-frame splices.
const size_t ring_buffer_size =
(this->current_stream_info_.ms_to_bytes(ring_buffer_duration) / source_alignment) * source_alignment;
(this->current_stream_info_.ms_to_bytes(ring_buffer_duration) / bytes_per_frame) * bytes_per_frame;
// Per-frame byte widths and whether the task must narrow the bit depth before writing to the I2S peripheral.
const uint8_t channels = this->current_stream_info_.get_channels();
@@ -110,8 +108,7 @@ void I2SAudioSpeaker::run_speaker_task() {
dma_buffer_bytes = chan_info.total_dma_buf_size / DMA_BUFFERS_COUNT;
} else {
// Should not happen for a READY channel; fall back to the requested size.
dma_buffer_bytes = this->output_stream_info_.frames_to_bytes(
dma_buffer_frames(this->output_stream_info_, this->dma_frame_multiple_()));
dma_buffer_bytes = this->output_stream_info_.frames_to_bytes(dma_buffer_frames(this->output_stream_info_));
}
// dma_buffer_bytes counts output-format bytes; convert with the output stream info.
const uint32_t frames_per_dma_buffer = this->output_stream_info_.bytes_to_frames(dma_buffer_bytes);
@@ -131,7 +128,7 @@ void I2SAudioSpeaker::run_speaker_task() {
std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = ring_buffer::RingBuffer::create(ring_buffer_size);
audio_source = audio::RingBufferAudioSource::create(temp_ring_buffer, dma_buffer_input_bytes,
static_cast<uint8_t>(source_alignment));
static_cast<uint8_t>(bytes_per_frame));
if (audio_source != nullptr) {
// audio_source is nullptr if the ring buffer fails to allocate
@@ -289,7 +286,7 @@ void I2SAudioSpeaker::run_speaker_task() {
if (!this->pause_state_) {
while (bytes_written_total < dma_buffer_bytes) {
size_t bytes_read = audio_source->fill(pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS) / 2);
size_t bytes_read = audio_source->fill(pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS) / 2, false);
if (bytes_read > 0) {
// Apply volume at the input bit depth, before any narrowing, so the full precision is scaled.
uint8_t *new_data = audio_source->mutable_data() + audio_source->available() - bytes_read;
@@ -298,20 +295,9 @@ void I2SAudioSpeaker::run_speaker_task() {
// Convert as many whole frames as fit in the remaining DMA space, bounded by what the source
// currently exposes. Frame counts are shared between input and output; only the byte widths differ.
uint32_t frames_available = this->current_stream_info_.bytes_to_frames(audio_source->available());
const uint32_t frames_available = this->current_stream_info_.bytes_to_frames(audio_source->available());
const uint32_t frames_room =
this->output_stream_info_.bytes_to_frames(dma_buffer_bytes - bytes_written_total);
uint32_t padding_frames = 0;
if (pair_aligned && frames_available == 0 && frames_room >= 2 && !audio_source->has_buffered_data()) {
// Silence padding follows, so write a held half pair now with a silent partner rather than strand it.
// Only the real frame is reported as played.
const size_t real_bytes = audio_source->expose_padded_partial(bytes_per_frame);
if (real_bytes > 0) {
this->apply_software_volume_(audio_source->mutable_data(), real_bytes);
frames_available = this->current_stream_info_.bytes_to_frames(audio_source->available());
padding_frames = frames_available - this->current_stream_info_.bytes_to_frames(real_bytes);
}
}
const uint32_t frames_to_write = std::min(frames_available, frames_room);
if (frames_to_write == 0) {
// Ring buffer has nothing more to hand over right now; pad the rest of this DMA buffer
@@ -340,17 +326,13 @@ void I2SAudioSpeaker::run_speaker_task() {
ESP_LOGE(TAG, "Partial DMA write (%u of %u bytes), resyncing DMA lockstep", (unsigned) bw,
(unsigned) output_bytes);
audio_source->consume(input_bytes);
real_frames_total += frames_to_write - padding_frames;
real_frames_total += frames_to_write;
partial_write_failure = true;
break;
}
audio_source->consume(input_bytes);
bytes_written_total += output_bytes;
real_frames_total += frames_to_write - padding_frames;
if (padding_frames > 0) {
// The source was already empty; skip waiting on another fill() before the silence padding
break;
}
real_frames_total += frames_to_write;
}
if (real_frames_total > 0) {
last_data_received_time = millis();
@@ -458,7 +440,7 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver(audio::AudioStreamInfo &audio_stream
this->current_stream_info_ = audio_stream_info;
// The DMA buffers hold output-format (post-narrowing) samples, so size them from the output stream info.
uint32_t dma_buffer_length = dma_buffer_frames(this->output_stream_info_, this->dma_frame_multiple_());
uint32_t dma_buffer_length = dma_buffer_frames(this->output_stream_info_);
i2s_role_t i2s_role = this->i2s_role_;
i2s_clock_src_t clk_src = I2S_CLK_SRC_DEFAULT;
@@ -24,9 +24,6 @@ class I2SAudioSpeaker final : public I2SAudioSpeakerBase {
void run_speaker_task() override;
esp_err_t start_i2s_driver(audio::AudioStreamInfo &audio_stream_info) override;
/// @brief Frame multiple for DMA buffers and writes; 2 keeps ESP32 mono sample pairs word-aligned.
uint32_t dma_frame_multiple_() const { return this->esp32_mono_swap_needed_() ? 2 : 1; }
I2SCommFmt i2s_comm_fmt_{I2SCommFmt::STANDARD};
};
@@ -221,7 +221,7 @@ void MicroWakeWord::inference_task(void *params) {
xEventGroupClearBits(this_mww->event_group_, EventGroupBits::COMMAND_RESET_RING_BUFFER);
}
audio_source->fill(pdMS_TO_TICKS(DATA_TIMEOUT_MS));
audio_source->fill(pdMS_TO_TICKS(DATA_TIMEOUT_MS), false);
// The frontend buffers samples internally and only emits a feature once it has a full window, so we can
// hand it whatever the source exposes. The frontend consumes at least one sample per call, so available()
@@ -302,7 +302,7 @@ size_t SourceSpeaker::process_data_from_source(std::shared_ptr<audio::RingBuffer
return 0;
}
size_t bytes_read = audio_source->fill(ticks_to_wait);
size_t bytes_read = audio_source->fill(ticks_to_wait, false);
uint32_t samples_to_duck = this->audio_stream_info_.bytes_to_samples(bytes_read);
if (samples_to_duck > 0) {
@@ -81,7 +81,8 @@ void SoundLevelComponent::loop() {
}
// Expose a chunk of the ring buffer's internal storage - don't block to avoid slowing the main loop.
this->audio_source_->fill(0);
// pre_shift is ignored by RingBufferAudioSource (no intermediate transfer buffer to compact).
this->audio_source_->fill(0, false);
if (this->audio_source_->available() == 0) {
// No new audio available for processing
@@ -224,11 +224,11 @@ void VoiceAssistant::stream_api_audio_() {
while (true) {
// fill() exposes a new chunk, or returns 0 if a previous chunk is still exposed; available()
// reports the currently exposed bytes either way.
this->audio_source_->fill(0);
this->audio_source_->fill(0, false);
size_t available = this->audio_source_->available();
size_t available2 = 0;
if (this->audio_source2_ != nullptr) {
this->audio_source2_->fill(0);
this->audio_source2_->fill(0, false);
available2 = this->audio_source2_->available();
}
@@ -383,13 +383,14 @@ void VoiceAssistant::loop() {
break; // State changed when udp server port received
}
case State::STREAMING_MICROPHONE: {
// pre_shift is ignored by RingBufferAudioSource (no intermediate transfer buffer to compact).
if (this->audio_mode_ == AUDIO_MODE_API) {
this->stream_api_audio_();
} else {
// UDP (will eventually be deprecated)
// Only the primary microphone channel is used
while (true) {
this->audio_source_->fill(0);
this->audio_source_->fill(0, false);
size_t available = this->audio_source_->available();
if (available == 0) {
break;
@@ -42,6 +42,13 @@ button:
md5: 0123456789abcdef0123456789abcdef
url: http://my.ha.net:8123/local/esphome/firmware.bin
- ota.http_request.flash:
id: http_request_ota
md5_url: !lambda 'return std::string("http://my.ha.net:8123/local/esphome/firmware.md5");'
username: user
password: !lambda 'return std::string("pass");'
url: http://my.ha.net:8123/local/esphome/firmware.bin
- logger.log: "This message should be not displayed (reboot)"
update:
@@ -31,10 +31,6 @@ esphome:
request_headers:
Content-Type: application/json
body: "Some data"
timeout: 15s
- http_request.get:
url: https://esphome.io
timeout: !lambda "return 10000;"
- http_request.post:
url: https://esphome.io
request_headers: