mirror of
https://github.com/esphome/esphome.git
synced 2026-02-08 00:31:58 +00:00
@@ -528,7 +528,7 @@ esphome/components/uart/packet_transport/* @clydebarrow
|
|||||||
esphome/components/udp/* @clydebarrow
|
esphome/components/udp/* @clydebarrow
|
||||||
esphome/components/ufire_ec/* @pvizeli
|
esphome/components/ufire_ec/* @pvizeli
|
||||||
esphome/components/ufire_ise/* @pvizeli
|
esphome/components/ufire_ise/* @pvizeli
|
||||||
esphome/components/ultrasonic/* @OttoWinter
|
esphome/components/ultrasonic/* @ssieb @swoboda1337
|
||||||
esphome/components/update/* @jesserockz
|
esphome/components/update/* @jesserockz
|
||||||
esphome/components/uponor_smatrix/* @kroimon
|
esphome/components/uponor_smatrix/* @kroimon
|
||||||
esphome/components/usb_cdc_acm/* @kbx81
|
esphome/components/usb_cdc_acm/* @kbx81
|
||||||
|
|||||||
2
Doxyfile
2
Doxyfile
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
|||||||
# could be handy for archiving the generated documentation or if some version
|
# could be handy for archiving the generated documentation or if some version
|
||||||
# control system is used.
|
# control system is used.
|
||||||
|
|
||||||
PROJECT_NUMBER = 2026.1.3
|
PROJECT_NUMBER = 2026.1.4
|
||||||
|
|
||||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
||||||
# for a project that appears at the top of each page and should give viewer a
|
# for a project that appears at the top of each page and should give viewer a
|
||||||
|
|||||||
@@ -152,6 +152,10 @@ void CSE7766Component::parse_data_() {
|
|||||||
if (this->power_sensor_ != nullptr) {
|
if (this->power_sensor_ != nullptr) {
|
||||||
this->power_sensor_->publish_state(power);
|
this->power_sensor_->publish_state(power);
|
||||||
}
|
}
|
||||||
|
} else if (this->power_sensor_ != nullptr) {
|
||||||
|
// No valid power measurement from chip - publish 0W to avoid stale readings
|
||||||
|
// This typically happens when current is below the measurable threshold (~50mA)
|
||||||
|
this->power_sensor_->publish_state(0.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
float current = 0.0f;
|
float current = 0.0f;
|
||||||
|
|||||||
@@ -193,11 +193,14 @@ bool Esp32HostedUpdate::fetch_manifest_() {
|
|||||||
int read_or_error = container->read(buf, sizeof(buf));
|
int read_or_error = container->read(buf, sizeof(buf));
|
||||||
App.feed_wdt();
|
App.feed_wdt();
|
||||||
yield();
|
yield();
|
||||||
auto result = http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout);
|
auto result =
|
||||||
|
http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout, container->is_read_complete());
|
||||||
if (result == http_request::HttpReadLoopResult::RETRY)
|
if (result == http_request::HttpReadLoopResult::RETRY)
|
||||||
continue;
|
continue;
|
||||||
|
// Note: COMPLETE is currently unreachable since the loop condition checks bytes_read < content_length,
|
||||||
|
// but this is defensive code in case chunked transfer encoding support is added in the future.
|
||||||
if (result != http_request::HttpReadLoopResult::DATA)
|
if (result != http_request::HttpReadLoopResult::DATA)
|
||||||
break; // ERROR or TIMEOUT
|
break; // COMPLETE, ERROR, or TIMEOUT
|
||||||
json_str.append(reinterpret_cast<char *>(buf), read_or_error);
|
json_str.append(reinterpret_cast<char *>(buf), read_or_error);
|
||||||
}
|
}
|
||||||
container->end();
|
container->end();
|
||||||
@@ -318,9 +321,14 @@ bool Esp32HostedUpdate::stream_firmware_to_coprocessor_() {
|
|||||||
App.feed_wdt();
|
App.feed_wdt();
|
||||||
yield();
|
yield();
|
||||||
|
|
||||||
auto result = http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout);
|
auto result =
|
||||||
|
http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout, container->is_read_complete());
|
||||||
if (result == http_request::HttpReadLoopResult::RETRY)
|
if (result == http_request::HttpReadLoopResult::RETRY)
|
||||||
continue;
|
continue;
|
||||||
|
// Note: COMPLETE is currently unreachable since the loop condition checks bytes_read < content_length,
|
||||||
|
// but this is defensive code in case chunked transfer encoding support is added in the future.
|
||||||
|
if (result == http_request::HttpReadLoopResult::COMPLETE)
|
||||||
|
break;
|
||||||
if (result != http_request::HttpReadLoopResult::DATA) {
|
if (result != http_request::HttpReadLoopResult::DATA) {
|
||||||
if (result == http_request::HttpReadLoopResult::TIMEOUT) {
|
if (result == http_request::HttpReadLoopResult::TIMEOUT) {
|
||||||
ESP_LOGE(TAG, "Timeout reading firmware data");
|
ESP_LOGE(TAG, "Timeout reading firmware data");
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ struct Header {
|
|||||||
enum HttpStatus {
|
enum HttpStatus {
|
||||||
HTTP_STATUS_OK = 200,
|
HTTP_STATUS_OK = 200,
|
||||||
HTTP_STATUS_NO_CONTENT = 204,
|
HTTP_STATUS_NO_CONTENT = 204,
|
||||||
|
HTTP_STATUS_RESET_CONTENT = 205,
|
||||||
HTTP_STATUS_PARTIAL_CONTENT = 206,
|
HTTP_STATUS_PARTIAL_CONTENT = 206,
|
||||||
|
|
||||||
/* 3xx - Redirection */
|
/* 3xx - Redirection */
|
||||||
@@ -126,19 +127,21 @@ struct HttpReadResult {
|
|||||||
|
|
||||||
/// Result of processing a non-blocking read with timeout (for manual loops)
|
/// Result of processing a non-blocking read with timeout (for manual loops)
|
||||||
enum class HttpReadLoopResult : uint8_t {
|
enum class HttpReadLoopResult : uint8_t {
|
||||||
DATA, ///< Data was read, process it
|
DATA, ///< Data was read, process it
|
||||||
RETRY, ///< No data yet, already delayed, caller should continue loop
|
COMPLETE, ///< All content has been read, caller should exit loop
|
||||||
ERROR, ///< Read error, caller should exit loop
|
RETRY, ///< No data yet, already delayed, caller should continue loop
|
||||||
TIMEOUT, ///< Timeout waiting for data, caller should exit loop
|
ERROR, ///< Read error, caller should exit loop
|
||||||
|
TIMEOUT, ///< Timeout waiting for data, caller should exit loop
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Process a read result with timeout tracking and delay handling
|
/// Process a read result with timeout tracking and delay handling
|
||||||
/// @param bytes_read_or_error Return value from read() - positive for bytes read, negative for error
|
/// @param bytes_read_or_error Return value from read() - positive for bytes read, negative for error
|
||||||
/// @param last_data_time Time of last successful read, updated when data received
|
/// @param last_data_time Time of last successful read, updated when data received
|
||||||
/// @param timeout_ms Maximum time to wait for data
|
/// @param timeout_ms Maximum time to wait for data
|
||||||
/// @return DATA if data received, RETRY if should continue loop, ERROR/TIMEOUT if should exit
|
/// @param is_read_complete Whether all expected content has been read (from HttpContainer::is_read_complete())
|
||||||
inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_t &last_data_time,
|
/// @return How the caller should proceed - see HttpReadLoopResult enum
|
||||||
uint32_t timeout_ms) {
|
inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_t &last_data_time, uint32_t timeout_ms,
|
||||||
|
bool is_read_complete) {
|
||||||
if (bytes_read_or_error > 0) {
|
if (bytes_read_or_error > 0) {
|
||||||
last_data_time = millis();
|
last_data_time = millis();
|
||||||
return HttpReadLoopResult::DATA;
|
return HttpReadLoopResult::DATA;
|
||||||
@@ -146,7 +149,10 @@ inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_
|
|||||||
if (bytes_read_or_error < 0) {
|
if (bytes_read_or_error < 0) {
|
||||||
return HttpReadLoopResult::ERROR;
|
return HttpReadLoopResult::ERROR;
|
||||||
}
|
}
|
||||||
// bytes_read_or_error == 0: no data available yet
|
// bytes_read_or_error == 0: either "no data yet" or "all content read"
|
||||||
|
if (is_read_complete) {
|
||||||
|
return HttpReadLoopResult::COMPLETE;
|
||||||
|
}
|
||||||
if (millis() - last_data_time >= timeout_ms) {
|
if (millis() - last_data_time >= timeout_ms) {
|
||||||
return HttpReadLoopResult::TIMEOUT;
|
return HttpReadLoopResult::TIMEOUT;
|
||||||
}
|
}
|
||||||
@@ -159,9 +165,9 @@ class HttpRequestComponent;
|
|||||||
class HttpContainer : public Parented<HttpRequestComponent> {
|
class HttpContainer : public Parented<HttpRequestComponent> {
|
||||||
public:
|
public:
|
||||||
virtual ~HttpContainer() = default;
|
virtual ~HttpContainer() = default;
|
||||||
size_t content_length;
|
size_t content_length{0};
|
||||||
int status_code;
|
int status_code{-1}; ///< -1 indicates no response received yet
|
||||||
uint32_t duration_ms;
|
uint32_t duration_ms{0};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Read data from the HTTP response body.
|
* @brief Read data from the HTTP response body.
|
||||||
@@ -194,9 +200,24 @@ class HttpContainer : public Parented<HttpRequestComponent> {
|
|||||||
virtual void end() = 0;
|
virtual void end() = 0;
|
||||||
|
|
||||||
void set_secure(bool secure) { this->secure_ = secure; }
|
void set_secure(bool secure) { this->secure_ = secure; }
|
||||||
|
void set_chunked(bool chunked) { this->is_chunked_ = chunked; }
|
||||||
|
|
||||||
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
|
||||||
|
/// For chunked responses, returns false (completion detected via read() returning error/EOF)
|
||||||
|
bool is_read_complete() const {
|
||||||
|
// Per RFC 9112, these responses have no body:
|
||||||
|
// - 1xx (Informational), 204 No Content, 205 Reset Content, 304 Not Modified
|
||||||
|
if ((this->status_code >= 100 && this->status_code < 200) || this->status_code == HTTP_STATUS_NO_CONTENT ||
|
||||||
|
this->status_code == HTTP_STATUS_RESET_CONTENT || this->status_code == HTTP_STATUS_NOT_MODIFIED) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// For non-chunked responses, complete when bytes_read >= content_length
|
||||||
|
// This handles both Content-Length: 0 and Content-Length: N cases
|
||||||
|
return !this->is_chunked_ && this->bytes_read_ >= this->content_length;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get response headers.
|
* @brief Get response headers.
|
||||||
*
|
*
|
||||||
@@ -209,6 +230,7 @@ class HttpContainer : public Parented<HttpRequestComponent> {
|
|||||||
protected:
|
protected:
|
||||||
size_t bytes_read_{0};
|
size_t bytes_read_{0};
|
||||||
bool secure_{false};
|
bool secure_{false};
|
||||||
|
bool is_chunked_{false}; ///< True if response uses chunked transfer encoding
|
||||||
std::map<std::string, std::list<std::string>> response_headers_{};
|
std::map<std::string, std::list<std::string>> response_headers_{};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -219,7 +241,7 @@ class HttpContainer : public Parented<HttpRequestComponent> {
|
|||||||
/// @param total_size Total bytes to read
|
/// @param total_size Total bytes to read
|
||||||
/// @param chunk_size Maximum bytes per read call
|
/// @param chunk_size Maximum bytes per read call
|
||||||
/// @param timeout_ms Read timeout in milliseconds
|
/// @param timeout_ms Read timeout in milliseconds
|
||||||
/// @return HttpReadResult with status and error_code on failure
|
/// @return HttpReadResult with status and error_code on failure; use container->get_bytes_read() for total bytes read
|
||||||
inline HttpReadResult http_read_fully(HttpContainer *container, uint8_t *buffer, size_t total_size, size_t chunk_size,
|
inline HttpReadResult http_read_fully(HttpContainer *container, uint8_t *buffer, size_t total_size, size_t chunk_size,
|
||||||
uint32_t timeout_ms) {
|
uint32_t timeout_ms) {
|
||||||
size_t read_index = 0;
|
size_t read_index = 0;
|
||||||
@@ -231,9 +253,11 @@ inline HttpReadResult http_read_fully(HttpContainer *container, uint8_t *buffer,
|
|||||||
App.feed_wdt();
|
App.feed_wdt();
|
||||||
yield();
|
yield();
|
||||||
|
|
||||||
auto result = http_read_loop_result(read_bytes_or_error, last_data_time, timeout_ms);
|
auto result = http_read_loop_result(read_bytes_or_error, last_data_time, timeout_ms, container->is_read_complete());
|
||||||
if (result == HttpReadLoopResult::RETRY)
|
if (result == HttpReadLoopResult::RETRY)
|
||||||
continue;
|
continue;
|
||||||
|
if (result == HttpReadLoopResult::COMPLETE)
|
||||||
|
break; // Server sent less data than requested, but transfer is complete
|
||||||
if (result == HttpReadLoopResult::ERROR)
|
if (result == HttpReadLoopResult::ERROR)
|
||||||
return {HttpReadStatus::ERROR, read_bytes_or_error};
|
return {HttpReadStatus::ERROR, read_bytes_or_error};
|
||||||
if (result == HttpReadLoopResult::TIMEOUT)
|
if (result == HttpReadLoopResult::TIMEOUT)
|
||||||
@@ -393,11 +417,12 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
|
|||||||
int read_or_error = container->read(buf + read_index, std::min<size_t>(max_length - read_index, 512));
|
int read_or_error = container->read(buf + read_index, std::min<size_t>(max_length - read_index, 512));
|
||||||
App.feed_wdt();
|
App.feed_wdt();
|
||||||
yield();
|
yield();
|
||||||
auto result = http_read_loop_result(read_or_error, last_data_time, read_timeout);
|
auto result =
|
||||||
|
http_read_loop_result(read_or_error, last_data_time, read_timeout, container->is_read_complete());
|
||||||
if (result == HttpReadLoopResult::RETRY)
|
if (result == HttpReadLoopResult::RETRY)
|
||||||
continue;
|
continue;
|
||||||
if (result != HttpReadLoopResult::DATA)
|
if (result != HttpReadLoopResult::DATA)
|
||||||
break; // ERROR or TIMEOUT
|
break; // COMPLETE, ERROR, or TIMEOUT
|
||||||
read_index += read_or_error;
|
read_index += read_or_error;
|
||||||
}
|
}
|
||||||
response_body.reserve(read_index);
|
response_body.reserve(read_index);
|
||||||
|
|||||||
@@ -135,9 +135,23 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
|
|||||||
// When cast to size_t, -1 becomes SIZE_MAX (4294967295 on 32-bit).
|
// When cast to size_t, -1 becomes SIZE_MAX (4294967295 on 32-bit).
|
||||||
// The read() method handles this: bytes_read_ can never reach SIZE_MAX, so the
|
// The read() method handles this: bytes_read_ can never reach SIZE_MAX, so the
|
||||||
// early return check (bytes_read_ >= content_length) will never trigger.
|
// early return check (bytes_read_ >= content_length) will never trigger.
|
||||||
|
//
|
||||||
|
// TODO: Chunked transfer encoding is NOT properly supported on Arduino.
|
||||||
|
// The implementation in #7884 was incomplete - it only works correctly on ESP-IDF where
|
||||||
|
// esp_http_client_read() decodes chunks internally. On Arduino, using getStreamPtr()
|
||||||
|
// returns raw TCP data with chunk framing (e.g., "12a\r\n{json}\r\n0\r\n\r\n") instead
|
||||||
|
// of decoded content. This wasn't noticed because requests would complete and payloads
|
||||||
|
// were only examined on IDF. The long transfer times were also masked by the misleading
|
||||||
|
// "HTTP on Arduino version >= 3.1 is **very** slow" warning above. This causes two issues:
|
||||||
|
// 1. Response body is corrupted - contains chunk size headers mixed with data
|
||||||
|
// 2. Cannot detect end of transfer - connection stays open (keep-alive), causing timeout
|
||||||
|
// The proper fix would be to use getString() for chunked responses, which decodes chunks
|
||||||
|
// internally, but this buffers the entire response in memory.
|
||||||
int content_length = container->client_.getSize();
|
int content_length = container->client_.getSize();
|
||||||
ESP_LOGD(TAG, "Content-Length: %d", content_length);
|
ESP_LOGD(TAG, "Content-Length: %d", content_length);
|
||||||
container->content_length = (size_t) content_length;
|
container->content_length = (size_t) content_length;
|
||||||
|
// -1 (SIZE_MAX when cast to size_t) means chunked transfer encoding
|
||||||
|
container->set_chunked(content_length == -1);
|
||||||
container->duration_ms = millis() - start;
|
container->duration_ms = millis() - start;
|
||||||
|
|
||||||
return container;
|
return container;
|
||||||
@@ -178,9 +192,9 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
|
|||||||
|
|
||||||
if (bufsize == 0) {
|
if (bufsize == 0) {
|
||||||
this->duration_ms += (millis() - start);
|
this->duration_ms += (millis() - start);
|
||||||
// Check if we've read all expected content (only valid when content_length is known and not SIZE_MAX)
|
// Check if we've read all expected content (non-chunked only)
|
||||||
// For chunked encoding (content_length == SIZE_MAX), we can't use this check
|
// For chunked encoding (content_length == SIZE_MAX), is_read_complete() returns false
|
||||||
if (this->content_length > 0 && this->bytes_read_ >= this->content_length) {
|
if (this->is_read_complete()) {
|
||||||
return 0; // All content read successfully
|
return 0; // All content read successfully
|
||||||
}
|
}
|
||||||
// No data available - check if connection is still open
|
// No data available - check if connection is still open
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
|
|||||||
// esp_http_client_fetch_headers() returns 0 for chunked transfer encoding (no Content-Length header).
|
// esp_http_client_fetch_headers() returns 0 for chunked transfer encoding (no Content-Length header).
|
||||||
// The read() method handles content_length == 0 specially to support chunked responses.
|
// The read() method handles content_length == 0 specially to support chunked responses.
|
||||||
container->content_length = esp_http_client_fetch_headers(client);
|
container->content_length = esp_http_client_fetch_headers(client);
|
||||||
|
container->set_chunked(esp_http_client_is_chunked_response(client));
|
||||||
container->feed_wdt();
|
container->feed_wdt();
|
||||||
container->status_code = esp_http_client_get_status_code(client);
|
container->status_code = esp_http_client_get_status_code(client);
|
||||||
container->feed_wdt();
|
container->feed_wdt();
|
||||||
@@ -190,6 +191,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
|
|||||||
|
|
||||||
container->feed_wdt();
|
container->feed_wdt();
|
||||||
container->content_length = esp_http_client_fetch_headers(client);
|
container->content_length = esp_http_client_fetch_headers(client);
|
||||||
|
container->set_chunked(esp_http_client_is_chunked_response(client));
|
||||||
container->feed_wdt();
|
container->feed_wdt();
|
||||||
container->status_code = esp_http_client_get_status_code(client);
|
container->status_code = esp_http_client_get_status_code(client);
|
||||||
container->feed_wdt();
|
container->feed_wdt();
|
||||||
@@ -234,10 +236,9 @@ 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
|
// Check if we've already read all expected content (non-chunked only)
|
||||||
// Skip this check when content_length is 0 (chunked transfer encoding or unknown length)
|
// For chunked responses (content_length == 0), esp_http_client_read() handles EOF
|
||||||
// For chunked responses, esp_http_client_read() will return 0 when all data is received
|
if (this->is_read_complete()) {
|
||||||
if (this->content_length > 0 && this->bytes_read_ >= this->content_length) {
|
|
||||||
return 0; // All content read successfully
|
return 0; // All content read successfully
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -130,9 +130,13 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
|
|||||||
App.feed_wdt();
|
App.feed_wdt();
|
||||||
yield();
|
yield();
|
||||||
|
|
||||||
auto result = http_read_loop_result(bufsize_or_error, last_data_time, read_timeout);
|
auto result = http_read_loop_result(bufsize_or_error, last_data_time, read_timeout, container->is_read_complete());
|
||||||
if (result == HttpReadLoopResult::RETRY)
|
if (result == HttpReadLoopResult::RETRY)
|
||||||
continue;
|
continue;
|
||||||
|
// Note: COMPLETE is currently unreachable since the loop condition checks bytes_read < content_length,
|
||||||
|
// but this is defensive code in case chunked transfer encoding support is added for OTA in the future.
|
||||||
|
if (result == HttpReadLoopResult::COMPLETE)
|
||||||
|
break;
|
||||||
if (result != HttpReadLoopResult::DATA) {
|
if (result != HttpReadLoopResult::DATA) {
|
||||||
if (result == HttpReadLoopResult::TIMEOUT) {
|
if (result == HttpReadLoopResult::TIMEOUT) {
|
||||||
ESP_LOGE(TAG, "Timeout reading data");
|
ESP_LOGE(TAG, "Timeout reading data");
|
||||||
|
|||||||
@@ -28,11 +28,10 @@ CONFIG_SCHEMA = (
|
|||||||
|
|
||||||
|
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
var = cg.new_Pvariable(config[CONF_ID])
|
var = cg.new_Pvariable(config[CONF_ID], config[CONF_NUM_CHIPS])
|
||||||
await spi.register_spi_device(var, config, write_only=True)
|
await spi.register_spi_device(var, config, write_only=True)
|
||||||
await display.register_display(var, config)
|
await display.register_display(var, config)
|
||||||
|
|
||||||
cg.add(var.set_num_chips(config[CONF_NUM_CHIPS]))
|
|
||||||
cg.add(var.set_intensity(config[CONF_INTENSITY]))
|
cg.add(var.set_intensity(config[CONF_INTENSITY]))
|
||||||
cg.add(var.set_reverse(config[CONF_REVERSE_ENABLE]))
|
cg.add(var.set_reverse(config[CONF_REVERSE_ENABLE]))
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,7 @@
|
|||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome::max7219 {
|
||||||
namespace max7219 {
|
|
||||||
|
|
||||||
static const char *const TAG = "max7219";
|
static const char *const TAG = "max7219";
|
||||||
|
|
||||||
@@ -115,12 +114,14 @@ const uint8_t MAX7219_ASCII_TO_RAW[95] PROGMEM = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
float MAX7219Component::get_setup_priority() const { return setup_priority::PROCESSOR; }
|
float MAX7219Component::get_setup_priority() const { return setup_priority::PROCESSOR; }
|
||||||
|
|
||||||
|
MAX7219Component::MAX7219Component(uint8_t num_chips) : num_chips_(num_chips) {
|
||||||
|
this->buffer_ = new uint8_t[this->num_chips_ * 8]; // NOLINT
|
||||||
|
memset(this->buffer_, 0, this->num_chips_ * 8);
|
||||||
|
}
|
||||||
|
|
||||||
void MAX7219Component::setup() {
|
void MAX7219Component::setup() {
|
||||||
this->spi_setup();
|
this->spi_setup();
|
||||||
this->buffer_ = new uint8_t[this->num_chips_ * 8]; // NOLINT
|
|
||||||
for (uint8_t i = 0; i < this->num_chips_ * 8; i++)
|
|
||||||
this->buffer_[i] = 0;
|
|
||||||
|
|
||||||
// let's assume the user has all 8 digits connected, only important in daisy chained setups anyway
|
// let's assume the user has all 8 digits connected, only important in daisy chained setups anyway
|
||||||
this->send_to_all_(MAX7219_REGISTER_SCAN_LIMIT, 7);
|
this->send_to_all_(MAX7219_REGISTER_SCAN_LIMIT, 7);
|
||||||
// let's use our own ASCII -> led pattern encoding
|
// let's use our own ASCII -> led pattern encoding
|
||||||
@@ -229,7 +230,6 @@ void MAX7219Component::set_intensity(uint8_t intensity) {
|
|||||||
this->intensity_ = intensity;
|
this->intensity_ = intensity;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
void MAX7219Component::set_num_chips(uint8_t num_chips) { this->num_chips_ = num_chips; }
|
|
||||||
|
|
||||||
uint8_t MAX7219Component::strftime(uint8_t pos, const char *format, ESPTime time) {
|
uint8_t MAX7219Component::strftime(uint8_t pos, const char *format, ESPTime time) {
|
||||||
char buffer[64];
|
char buffer[64];
|
||||||
@@ -240,5 +240,4 @@ uint8_t MAX7219Component::strftime(uint8_t pos, const char *format, ESPTime time
|
|||||||
}
|
}
|
||||||
uint8_t MAX7219Component::strftime(const char *format, ESPTime time) { return this->strftime(0, format, time); }
|
uint8_t MAX7219Component::strftime(const char *format, ESPTime time) { return this->strftime(0, format, time); }
|
||||||
|
|
||||||
} // namespace max7219
|
} // namespace esphome::max7219
|
||||||
} // namespace esphome
|
|
||||||
|
|||||||
@@ -6,8 +6,7 @@
|
|||||||
#include "esphome/components/spi/spi.h"
|
#include "esphome/components/spi/spi.h"
|
||||||
#include "esphome/components/display/display.h"
|
#include "esphome/components/display/display.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome::max7219 {
|
||||||
namespace max7219 {
|
|
||||||
|
|
||||||
class MAX7219Component;
|
class MAX7219Component;
|
||||||
|
|
||||||
@@ -17,6 +16,8 @@ class MAX7219Component : public PollingComponent,
|
|||||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
|
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
|
||||||
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_1MHZ> {
|
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_1MHZ> {
|
||||||
public:
|
public:
|
||||||
|
explicit MAX7219Component(uint8_t num_chips);
|
||||||
|
|
||||||
void set_writer(max7219_writer_t &&writer);
|
void set_writer(max7219_writer_t &&writer);
|
||||||
|
|
||||||
void setup() override;
|
void setup() override;
|
||||||
@@ -30,7 +31,6 @@ class MAX7219Component : public PollingComponent,
|
|||||||
void display();
|
void display();
|
||||||
|
|
||||||
void set_intensity(uint8_t intensity);
|
void set_intensity(uint8_t intensity);
|
||||||
void set_num_chips(uint8_t num_chips);
|
|
||||||
void set_reverse(bool reverse) { this->reverse_ = reverse; };
|
void set_reverse(bool reverse) { this->reverse_ = reverse; };
|
||||||
|
|
||||||
/// Evaluate the printf-format and print the result at the given position.
|
/// Evaluate the printf-format and print the result at the given position.
|
||||||
@@ -56,10 +56,9 @@ class MAX7219Component : public PollingComponent,
|
|||||||
uint8_t intensity_{15}; // Intensity of the display from 0 to 15 (most)
|
uint8_t intensity_{15}; // Intensity of the display from 0 to 15 (most)
|
||||||
bool intensity_changed_{}; // True if we need to re-send the intensity
|
bool intensity_changed_{}; // True if we need to re-send the intensity
|
||||||
uint8_t num_chips_{1};
|
uint8_t num_chips_{1};
|
||||||
uint8_t *buffer_;
|
uint8_t *buffer_{nullptr};
|
||||||
bool reverse_{false};
|
bool reverse_{false};
|
||||||
max7219_writer_t writer_{};
|
max7219_writer_t writer_{};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace max7219
|
} // namespace esphome::max7219
|
||||||
} // namespace esphome
|
|
||||||
|
|||||||
@@ -1,6 +1,39 @@
|
|||||||
#include "mipi_spi.h"
|
#include "mipi_spi.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome::mipi_spi {
|
||||||
namespace mipi_spi {} // namespace mipi_spi
|
|
||||||
} // namespace esphome
|
void internal_dump_config(const char *model, int width, int height, int offset_width, int offset_height, uint8_t madctl,
|
||||||
|
bool invert_colors, int display_bits, bool is_big_endian, const optional<uint8_t> &brightness,
|
||||||
|
GPIOPin *cs, GPIOPin *reset, GPIOPin *dc, int spi_mode, uint32_t data_rate, int bus_width) {
|
||||||
|
ESP_LOGCONFIG(TAG,
|
||||||
|
"MIPI_SPI Display\n"
|
||||||
|
" Model: %s\n"
|
||||||
|
" Width: %d\n"
|
||||||
|
" Height: %d\n"
|
||||||
|
" Swap X/Y: %s\n"
|
||||||
|
" Mirror X: %s\n"
|
||||||
|
" Mirror Y: %s\n"
|
||||||
|
" Invert colors: %s\n"
|
||||||
|
" Color order: %s\n"
|
||||||
|
" Display pixels: %d bits\n"
|
||||||
|
" Endianness: %s\n"
|
||||||
|
" SPI Mode: %d\n"
|
||||||
|
" SPI Data rate: %uMHz\n"
|
||||||
|
" SPI Bus width: %d",
|
||||||
|
model, width, height, YESNO(madctl & MADCTL_MV), YESNO(madctl & (MADCTL_MX | MADCTL_XFLIP)),
|
||||||
|
YESNO(madctl & (MADCTL_MY | MADCTL_YFLIP)), YESNO(invert_colors), (madctl & MADCTL_BGR) ? "BGR" : "RGB",
|
||||||
|
display_bits, is_big_endian ? "Big" : "Little", spi_mode, static_cast<unsigned>(data_rate / 1000000),
|
||||||
|
bus_width);
|
||||||
|
LOG_PIN(" CS Pin: ", cs);
|
||||||
|
LOG_PIN(" Reset Pin: ", reset);
|
||||||
|
LOG_PIN(" DC Pin: ", dc);
|
||||||
|
if (offset_width != 0)
|
||||||
|
ESP_LOGCONFIG(TAG, " Offset width: %d", offset_width);
|
||||||
|
if (offset_height != 0)
|
||||||
|
ESP_LOGCONFIG(TAG, " Offset height: %d", offset_height);
|
||||||
|
if (brightness.has_value())
|
||||||
|
ESP_LOGCONFIG(TAG, " Brightness: %u", brightness.value());
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace esphome::mipi_spi
|
||||||
|
|||||||
@@ -63,6 +63,11 @@ enum BusType {
|
|||||||
BUS_TYPE_SINGLE_16 = 16, // Single bit bus, but 16 bits per transfer
|
BUS_TYPE_SINGLE_16 = 16, // Single bit bus, but 16 bits per transfer
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper function for dump_config - defined in mipi_spi.cpp to allow use of LOG_PIN macro
|
||||||
|
void internal_dump_config(const char *model, int width, int height, int offset_width, int offset_height, uint8_t madctl,
|
||||||
|
bool invert_colors, int display_bits, bool is_big_endian, const optional<uint8_t> &brightness,
|
||||||
|
GPIOPin *cs, GPIOPin *reset, GPIOPin *dc, int spi_mode, uint32_t data_rate, int bus_width);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class for MIPI SPI displays.
|
* Base class for MIPI SPI displays.
|
||||||
* All the methods are defined here in the header file, as it is not possible to define templated methods in a cpp file.
|
* All the methods are defined here in the header file, as it is not possible to define templated methods in a cpp file.
|
||||||
@@ -201,40 +206,9 @@ class MipiSpi : public display::Display,
|
|||||||
}
|
}
|
||||||
|
|
||||||
void dump_config() override {
|
void dump_config() override {
|
||||||
esph_log_config(TAG,
|
internal_dump_config(this->model_, WIDTH, HEIGHT, OFFSET_WIDTH, OFFSET_HEIGHT, this->madctl_, this->invert_colors_,
|
||||||
"MIPI_SPI Display\n"
|
DISPLAYPIXEL * 8, IS_BIG_ENDIAN, this->brightness_, this->cs_, this->reset_pin_, this->dc_pin_,
|
||||||
" Model: %s\n"
|
this->mode_, this->data_rate_, BUS_TYPE);
|
||||||
" Width: %u\n"
|
|
||||||
" Height: %u",
|
|
||||||
this->model_, WIDTH, HEIGHT);
|
|
||||||
if constexpr (OFFSET_WIDTH != 0)
|
|
||||||
esph_log_config(TAG, " Offset width: %u", OFFSET_WIDTH);
|
|
||||||
if constexpr (OFFSET_HEIGHT != 0)
|
|
||||||
esph_log_config(TAG, " Offset height: %u", OFFSET_HEIGHT);
|
|
||||||
esph_log_config(TAG,
|
|
||||||
" Swap X/Y: %s\n"
|
|
||||||
" Mirror X: %s\n"
|
|
||||||
" Mirror Y: %s\n"
|
|
||||||
" Invert colors: %s\n"
|
|
||||||
" Color order: %s\n"
|
|
||||||
" Display pixels: %d bits\n"
|
|
||||||
" Endianness: %s\n",
|
|
||||||
YESNO(this->madctl_ & MADCTL_MV), YESNO(this->madctl_ & (MADCTL_MX | MADCTL_XFLIP)),
|
|
||||||
YESNO(this->madctl_ & (MADCTL_MY | MADCTL_YFLIP)), YESNO(this->invert_colors_),
|
|
||||||
this->madctl_ & MADCTL_BGR ? "BGR" : "RGB", DISPLAYPIXEL * 8, IS_BIG_ENDIAN ? "Big" : "Little");
|
|
||||||
if (this->brightness_.has_value())
|
|
||||||
esph_log_config(TAG, " Brightness: %u", this->brightness_.value());
|
|
||||||
if (this->cs_ != nullptr)
|
|
||||||
esph_log_config(TAG, " CS Pin: %s", this->cs_->dump_summary().c_str());
|
|
||||||
if (this->reset_pin_ != nullptr)
|
|
||||||
esph_log_config(TAG, " Reset Pin: %s", this->reset_pin_->dump_summary().c_str());
|
|
||||||
if (this->dc_pin_ != nullptr)
|
|
||||||
esph_log_config(TAG, " DC Pin: %s", this->dc_pin_->dump_summary().c_str());
|
|
||||||
esph_log_config(TAG,
|
|
||||||
" SPI Mode: %d\n"
|
|
||||||
" SPI Data rate: %dMHz\n"
|
|
||||||
" SPI Bus width: %d",
|
|
||||||
this->mode_, static_cast<unsigned>(this->data_rate_ / 1000000), BUS_TYPE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
|||||||
@@ -139,7 +139,8 @@ class MQTTBackendESP32 final : public MQTTBackend {
|
|||||||
this->lwt_retain_ = retain;
|
this->lwt_retain_ = retain;
|
||||||
}
|
}
|
||||||
void set_server(network::IPAddress ip, uint16_t port) final {
|
void set_server(network::IPAddress ip, uint16_t port) final {
|
||||||
this->host_ = ip.str();
|
char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||||
|
this->host_ = ip.str_to(ip_buf);
|
||||||
this->port_ = port;
|
this->port_ = port;
|
||||||
}
|
}
|
||||||
void set_server(const char *host, uint16_t port) final {
|
void set_server(const char *host, uint16_t port) final {
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
CODEOWNERS = ["@OttoWinter"]
|
CODEOWNERS = ["@swoboda1337", "@ssieb"]
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ CONFIG_SCHEMA = (
|
|||||||
{
|
{
|
||||||
cv.Required(CONF_TRIGGER_PIN): pins.internal_gpio_output_pin_schema,
|
cv.Required(CONF_TRIGGER_PIN): pins.internal_gpio_output_pin_schema,
|
||||||
cv.Required(CONF_ECHO_PIN): pins.internal_gpio_input_pin_schema,
|
cv.Required(CONF_ECHO_PIN): pins.internal_gpio_input_pin_schema,
|
||||||
cv.Optional(CONF_TIMEOUT): cv.distance,
|
cv.Optional(CONF_TIMEOUT, default="2m"): cv.distance,
|
||||||
cv.Optional(
|
cv.Optional(
|
||||||
CONF_PULSE_TIME, default="10us"
|
CONF_PULSE_TIME, default="10us"
|
||||||
): cv.positive_time_period_microseconds,
|
): cv.positive_time_period_microseconds,
|
||||||
@@ -52,12 +52,5 @@ async def to_code(config):
|
|||||||
cg.add(var.set_trigger_pin(trigger))
|
cg.add(var.set_trigger_pin(trigger))
|
||||||
echo = await cg.gpio_pin_expression(config[CONF_ECHO_PIN])
|
echo = await cg.gpio_pin_expression(config[CONF_ECHO_PIN])
|
||||||
cg.add(var.set_echo_pin(echo))
|
cg.add(var.set_echo_pin(echo))
|
||||||
|
cg.add(var.set_timeout_us(config[CONF_TIMEOUT] / (0.000343 / 2)))
|
||||||
# Remove before 2026.8.0
|
|
||||||
if CONF_TIMEOUT in config:
|
|
||||||
_LOGGER.warning(
|
|
||||||
"'timeout' option is deprecated and will be removed in 2026.8.0. "
|
|
||||||
"The option has no effect and can be safely removed."
|
|
||||||
)
|
|
||||||
|
|
||||||
cg.add(var.set_pulse_time_us(config[CONF_PULSE_TIME]))
|
cg.add(var.set_pulse_time_us(config[CONF_PULSE_TIME]))
|
||||||
|
|||||||
@@ -6,12 +6,11 @@ namespace esphome::ultrasonic {
|
|||||||
|
|
||||||
static const char *const TAG = "ultrasonic.sensor";
|
static const char *const TAG = "ultrasonic.sensor";
|
||||||
|
|
||||||
static constexpr uint32_t DEBOUNCE_US = 50; // Ignore edges within 50us (noise filtering)
|
static constexpr uint32_t START_TIMEOUT_US = 40000; // Maximum time to wait for echo pulse to start
|
||||||
static constexpr uint32_t MEASUREMENT_TIMEOUT_US = 80000; // Maximum time to wait for measurement completion
|
|
||||||
|
|
||||||
void IRAM_ATTR UltrasonicSensorStore::gpio_intr(UltrasonicSensorStore *arg) {
|
void IRAM_ATTR UltrasonicSensorStore::gpio_intr(UltrasonicSensorStore *arg) {
|
||||||
uint32_t now = micros();
|
uint32_t now = micros();
|
||||||
if (!arg->echo_start || (now - arg->echo_start_us) <= DEBOUNCE_US) {
|
if (arg->echo_pin_isr.digital_read()) {
|
||||||
arg->echo_start_us = now;
|
arg->echo_start_us = now;
|
||||||
arg->echo_start = true;
|
arg->echo_start = true;
|
||||||
} else {
|
} else {
|
||||||
@@ -38,6 +37,7 @@ void UltrasonicSensorComponent::setup() {
|
|||||||
this->trigger_pin_->digital_write(false);
|
this->trigger_pin_->digital_write(false);
|
||||||
this->trigger_pin_isr_ = this->trigger_pin_->to_isr();
|
this->trigger_pin_isr_ = this->trigger_pin_->to_isr();
|
||||||
this->echo_pin_->setup();
|
this->echo_pin_->setup();
|
||||||
|
this->store_.echo_pin_isr = this->echo_pin_->to_isr();
|
||||||
this->echo_pin_->attach_interrupt(UltrasonicSensorStore::gpio_intr, &this->store_, gpio::INTERRUPT_ANY_EDGE);
|
this->echo_pin_->attach_interrupt(UltrasonicSensorStore::gpio_intr, &this->store_, gpio::INTERRUPT_ANY_EDGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,29 +53,55 @@ void UltrasonicSensorComponent::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this->store_.echo_start) {
|
||||||
|
uint32_t elapsed = micros() - this->measurement_start_us_;
|
||||||
|
if (elapsed >= START_TIMEOUT_US) {
|
||||||
|
ESP_LOGW(TAG, "'%s' - Measurement start timed out", this->name_.c_str());
|
||||||
|
this->publish_state(NAN);
|
||||||
|
this->measurement_pending_ = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
uint32_t elapsed;
|
||||||
|
if (this->store_.echo_end) {
|
||||||
|
elapsed = this->store_.echo_end_us - this->store_.echo_start_us;
|
||||||
|
} else {
|
||||||
|
elapsed = micros() - this->store_.echo_start_us;
|
||||||
|
}
|
||||||
|
if (elapsed >= this->timeout_us_) {
|
||||||
|
ESP_LOGD(TAG, "'%s' - Measurement pulse timed out after %" PRIu32 "us", this->name_.c_str(), elapsed);
|
||||||
|
this->publish_state(NAN);
|
||||||
|
this->measurement_pending_ = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this->store_.echo_end) {
|
if (this->store_.echo_end) {
|
||||||
uint32_t pulse_duration = this->store_.echo_end_us - this->store_.echo_start_us;
|
float result;
|
||||||
ESP_LOGV(TAG, "Echo took %" PRIu32 "us", pulse_duration);
|
if (this->store_.echo_start) {
|
||||||
float result = UltrasonicSensorComponent::us_to_m(pulse_duration);
|
uint32_t pulse_duration = this->store_.echo_end_us - this->store_.echo_start_us;
|
||||||
ESP_LOGD(TAG, "'%s' - Got distance: %.3f m", this->name_.c_str(), result);
|
ESP_LOGV(TAG, "pulse start took %" PRIu32 "us, echo took %" PRIu32 "us",
|
||||||
|
this->store_.echo_start_us - this->measurement_start_us_, pulse_duration);
|
||||||
|
result = UltrasonicSensorComponent::us_to_m(pulse_duration);
|
||||||
|
ESP_LOGD(TAG, "'%s' - Got distance: %.3f m", this->name_.c_str(), result);
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG, "'%s' - pulse end before pulse start, does the echo pin need to be inverted?", this->name_.c_str());
|
||||||
|
result = NAN;
|
||||||
|
}
|
||||||
this->publish_state(result);
|
this->publish_state(result);
|
||||||
this->measurement_pending_ = false;
|
this->measurement_pending_ = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t elapsed = micros() - this->measurement_start_us_;
|
|
||||||
if (elapsed >= MEASUREMENT_TIMEOUT_US) {
|
|
||||||
ESP_LOGD(TAG, "'%s' - Measurement timed out after %" PRIu32 "us", this->name_.c_str(), elapsed);
|
|
||||||
this->publish_state(NAN);
|
|
||||||
this->measurement_pending_ = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void UltrasonicSensorComponent::dump_config() {
|
void UltrasonicSensorComponent::dump_config() {
|
||||||
LOG_SENSOR("", "Ultrasonic Sensor", this);
|
LOG_SENSOR("", "Ultrasonic Sensor", this);
|
||||||
LOG_PIN(" Echo Pin: ", this->echo_pin_);
|
LOG_PIN(" Echo Pin: ", this->echo_pin_);
|
||||||
LOG_PIN(" Trigger Pin: ", this->trigger_pin_);
|
LOG_PIN(" Trigger Pin: ", this->trigger_pin_);
|
||||||
ESP_LOGCONFIG(TAG, " Pulse time: %" PRIu32 " us", this->pulse_time_us_);
|
ESP_LOGCONFIG(TAG,
|
||||||
|
" Pulse time: %" PRIu32 " µs\n"
|
||||||
|
" Timeout: %" PRIu32 " µs",
|
||||||
|
this->pulse_time_us_, this->timeout_us_);
|
||||||
LOG_UPDATE_INTERVAL(this);
|
LOG_UPDATE_INTERVAL(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ namespace esphome::ultrasonic {
|
|||||||
struct UltrasonicSensorStore {
|
struct UltrasonicSensorStore {
|
||||||
static void gpio_intr(UltrasonicSensorStore *arg);
|
static void gpio_intr(UltrasonicSensorStore *arg);
|
||||||
|
|
||||||
|
ISRInternalGPIOPin echo_pin_isr;
|
||||||
|
volatile uint32_t wait_start_us{0};
|
||||||
volatile uint32_t echo_start_us{0};
|
volatile uint32_t echo_start_us{0};
|
||||||
volatile uint32_t echo_end_us{0};
|
volatile uint32_t echo_end_us{0};
|
||||||
volatile bool echo_start{false};
|
volatile bool echo_start{false};
|
||||||
@@ -29,6 +31,8 @@ class UltrasonicSensorComponent : public sensor::Sensor, public PollingComponent
|
|||||||
|
|
||||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||||
|
|
||||||
|
/// Set the maximum time in µs to wait for the echo to return
|
||||||
|
void set_timeout_us(uint32_t timeout_us) { this->timeout_us_ = timeout_us; }
|
||||||
/// Set the time in µs the trigger pin should be enabled for in µs, defaults to 10µs (for HC-SR04)
|
/// Set the time in µs the trigger pin should be enabled for in µs, defaults to 10µs (for HC-SR04)
|
||||||
void set_pulse_time_us(uint32_t pulse_time_us) { this->pulse_time_us_ = pulse_time_us; }
|
void set_pulse_time_us(uint32_t pulse_time_us) { this->pulse_time_us_ = pulse_time_us; }
|
||||||
|
|
||||||
@@ -41,6 +45,7 @@ class UltrasonicSensorComponent : public sensor::Sensor, public PollingComponent
|
|||||||
ISRInternalGPIOPin trigger_pin_isr_;
|
ISRInternalGPIOPin trigger_pin_isr_;
|
||||||
InternalGPIOPin *echo_pin_;
|
InternalGPIOPin *echo_pin_;
|
||||||
UltrasonicSensorStore store_;
|
UltrasonicSensorStore store_;
|
||||||
|
uint32_t timeout_us_{};
|
||||||
uint32_t pulse_time_us_{};
|
uint32_t pulse_time_us_{};
|
||||||
|
|
||||||
uint32_t measurement_start_us_{0};
|
uint32_t measurement_start_us_{0};
|
||||||
|
|||||||
@@ -1383,6 +1383,12 @@ void WiFiComponent::check_connecting_finished(uint32_t now) {
|
|||||||
|
|
||||||
this->release_scan_results_();
|
this->release_scan_results_();
|
||||||
|
|
||||||
|
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
|
||||||
|
// Notify listeners now that state machine has reached STA_CONNECTED
|
||||||
|
// This ensures wifi.connected condition returns true in listener automations
|
||||||
|
this->notify_connect_state_listeners_();
|
||||||
|
#endif
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2090,6 +2096,21 @@ void WiFiComponent::release_scan_results_() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
|
||||||
|
void WiFiComponent::notify_connect_state_listeners_() {
|
||||||
|
if (!this->pending_.connect_state)
|
||||||
|
return;
|
||||||
|
this->pending_.connect_state = false;
|
||||||
|
// Get current SSID and BSSID from the WiFi driver
|
||||||
|
char ssid_buf[SSID_BUFFER_SIZE];
|
||||||
|
const char *ssid = this->wifi_ssid_to(ssid_buf);
|
||||||
|
bssid_t bssid = this->wifi_bssid();
|
||||||
|
for (auto *listener : this->connect_state_listeners_) {
|
||||||
|
listener->on_wifi_connect_state(StringRef(ssid, strlen(ssid)), bssid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif // USE_WIFI_CONNECT_STATE_LISTENERS
|
||||||
|
|
||||||
void WiFiComponent::check_roaming_(uint32_t now) {
|
void WiFiComponent::check_roaming_(uint32_t now) {
|
||||||
// Guard: not for hidden networks (may not appear in scan)
|
// Guard: not for hidden networks (may not appear in scan)
|
||||||
const WiFiAP *selected = this->get_selected_sta_();
|
const WiFiAP *selected = this->get_selected_sta_();
|
||||||
|
|||||||
@@ -618,6 +618,11 @@ class WiFiComponent : public Component {
|
|||||||
/// Free scan results memory unless a component needs them
|
/// Free scan results memory unless a component needs them
|
||||||
void release_scan_results_();
|
void release_scan_results_();
|
||||||
|
|
||||||
|
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
|
||||||
|
/// Notify connect state listeners (called after state machine reaches STA_CONNECTED)
|
||||||
|
void notify_connect_state_listeners_();
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef USE_ESP8266
|
#ifdef USE_ESP8266
|
||||||
static void wifi_event_callback(System_Event_t *event);
|
static void wifi_event_callback(System_Event_t *event);
|
||||||
void wifi_scan_done_callback_(void *arg, STATUS status);
|
void wifi_scan_done_callback_(void *arg, STATUS status);
|
||||||
@@ -721,6 +726,16 @@ class WiFiComponent : public Component {
|
|||||||
SemaphoreHandle_t high_performance_semaphore_{nullptr};
|
SemaphoreHandle_t high_performance_semaphore_{nullptr};
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
|
||||||
|
// Pending listener notifications deferred until state machine reaches appropriate state.
|
||||||
|
// Listeners are notified after state transitions complete so conditions like
|
||||||
|
// wifi.connected return correct values in automations.
|
||||||
|
// Uses bitfields to minimize memory; more flags may be added as needed.
|
||||||
|
struct {
|
||||||
|
bool connect_state : 1; // Notify connect state listeners after STA_CONNECTED
|
||||||
|
} pending_{};
|
||||||
|
#endif
|
||||||
|
|
||||||
// Pointers at the end (naturally aligned)
|
// Pointers at the end (naturally aligned)
|
||||||
Trigger<> *connect_trigger_{new Trigger<>()};
|
Trigger<> *connect_trigger_{new Trigger<>()};
|
||||||
Trigger<> *disconnect_trigger_{new Trigger<>()};
|
Trigger<> *disconnect_trigger_{new Trigger<>()};
|
||||||
|
|||||||
@@ -500,6 +500,10 @@ const LogString *get_disconnect_reason_str(uint8_t reason) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: This callback runs in ESP8266 system context with limited stack (~2KB).
|
||||||
|
// All listener notifications should be deferred to wifi_loop_() via pending_ flags
|
||||||
|
// to avoid stack overflow. Currently only connect_state is deferred; disconnect,
|
||||||
|
// IP, and scan listeners still run in this context and should be migrated.
|
||||||
void WiFiComponent::wifi_event_callback(System_Event_t *event) {
|
void WiFiComponent::wifi_event_callback(System_Event_t *event) {
|
||||||
switch (event->event) {
|
switch (event->event) {
|
||||||
case EVENT_STAMODE_CONNECTED: {
|
case EVENT_STAMODE_CONNECTED: {
|
||||||
@@ -512,9 +516,9 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
|
|||||||
#endif
|
#endif
|
||||||
s_sta_connected = true;
|
s_sta_connected = true;
|
||||||
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
|
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
|
||||||
for (auto *listener : global_wifi_component->connect_state_listeners_) {
|
// Defer listener notification until state machine reaches STA_CONNECTED
|
||||||
listener->on_wifi_connect_state(StringRef(it.ssid, it.ssid_len), it.bssid);
|
// This ensures wifi.connected condition returns true in listener automations
|
||||||
}
|
global_wifi_component->pending_.connect_state = true;
|
||||||
#endif
|
#endif
|
||||||
// For static IP configurations, GOT_IP event may not fire, so notify IP listeners here
|
// For static IP configurations, GOT_IP event may not fire, so notify IP listeners here
|
||||||
#if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP)
|
#if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP)
|
||||||
|
|||||||
@@ -710,6 +710,9 @@ void WiFiComponent::wifi_loop_() {
|
|||||||
delete data; // NOLINT(cppcoreguidelines-owning-memory)
|
delete data; // NOLINT(cppcoreguidelines-owning-memory)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Events are processed from queue in main loop context, but listener notifications
|
||||||
|
// must be deferred until after the state machine transitions (in check_connecting_finished)
|
||||||
|
// so that conditions like wifi.connected return correct values in automations.
|
||||||
void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
||||||
esp_err_t err;
|
esp_err_t err;
|
||||||
if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_START) {
|
if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_START) {
|
||||||
@@ -743,9 +746,9 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
|||||||
#endif
|
#endif
|
||||||
s_sta_connected = true;
|
s_sta_connected = true;
|
||||||
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
|
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
|
||||||
for (auto *listener : this->connect_state_listeners_) {
|
// Defer listener notification until state machine reaches STA_CONNECTED
|
||||||
listener->on_wifi_connect_state(StringRef(it.ssid, it.ssid_len), it.bssid);
|
// This ensures wifi.connected condition returns true in listener automations
|
||||||
}
|
this->pending_.connect_state = true;
|
||||||
#endif
|
#endif
|
||||||
// For static IP configurations, GOT_IP event may not fire, so notify IP listeners here
|
// For static IP configurations, GOT_IP event may not fire, so notify IP listeners here
|
||||||
#if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP)
|
#if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP)
|
||||||
|
|||||||
@@ -423,7 +423,10 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process a single event from the queue - runs in main loop context
|
// Process a single event from the queue - runs in main loop context.
|
||||||
|
// Listener notifications must be deferred until after the state machine transitions
|
||||||
|
// (in check_connecting_finished) so that conditions like wifi.connected return
|
||||||
|
// correct values in automations.
|
||||||
void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) {
|
void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) {
|
||||||
switch (event->event_id) {
|
switch (event->event_id) {
|
||||||
case ESPHOME_EVENT_ID_WIFI_READY: {
|
case ESPHOME_EVENT_ID_WIFI_READY: {
|
||||||
@@ -456,9 +459,9 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) {
|
|||||||
// This matches ESP32 IDF behavior where s_sta_connected is set but
|
// This matches ESP32 IDF behavior where s_sta_connected is set but
|
||||||
// wifi_sta_connect_status_() also checks got_ipv4_address_
|
// wifi_sta_connect_status_() also checks got_ipv4_address_
|
||||||
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
|
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
|
||||||
for (auto *listener : this->connect_state_listeners_) {
|
// Defer listener notification until state machine reaches STA_CONNECTED
|
||||||
listener->on_wifi_connect_state(StringRef(it.ssid, it.ssid_len), it.bssid);
|
// This ensures wifi.connected condition returns true in listener automations
|
||||||
}
|
this->pending_.connect_state = true;
|
||||||
#endif
|
#endif
|
||||||
// For static IP configurations, GOT_IP event may not fire, so set connected state here
|
// For static IP configurations, GOT_IP event may not fire, so set connected state here
|
||||||
#ifdef USE_WIFI_MANUAL_IP
|
#ifdef USE_WIFI_MANUAL_IP
|
||||||
|
|||||||
@@ -240,6 +240,10 @@ network::IPAddress WiFiComponent::wifi_dns_ip_(int num) {
|
|||||||
return network::IPAddress(dns_ip);
|
return network::IPAddress(dns_ip);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pico W uses polling for connection state detection.
|
||||||
|
// Connect state listener notifications are deferred until after the state machine
|
||||||
|
// transitions (in check_connecting_finished) so that conditions like wifi.connected
|
||||||
|
// return correct values in automations.
|
||||||
void WiFiComponent::wifi_loop_() {
|
void WiFiComponent::wifi_loop_() {
|
||||||
// Handle scan completion
|
// Handle scan completion
|
||||||
if (this->state_ == WIFI_COMPONENT_STATE_STA_SCANNING && !cyw43_wifi_scan_active(&cyw43_state)) {
|
if (this->state_ == WIFI_COMPONENT_STATE_STA_SCANNING && !cyw43_wifi_scan_active(&cyw43_state)) {
|
||||||
@@ -264,11 +268,9 @@ void WiFiComponent::wifi_loop_() {
|
|||||||
s_sta_was_connected = true;
|
s_sta_was_connected = true;
|
||||||
ESP_LOGV(TAG, "Connected");
|
ESP_LOGV(TAG, "Connected");
|
||||||
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
|
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
|
||||||
String ssid = WiFi.SSID();
|
// Defer listener notification until state machine reaches STA_CONNECTED
|
||||||
bssid_t bssid = this->wifi_bssid();
|
// This ensures wifi.connected condition returns true in listener automations
|
||||||
for (auto *listener : this->connect_state_listeners_) {
|
this->pending_.connect_state = true;
|
||||||
listener->on_wifi_connect_state(StringRef(ssid.c_str(), ssid.length()), bssid);
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
// For static IP configurations, notify IP listeners immediately as the IP is already configured
|
// For static IP configurations, notify IP listeners immediately as the IP is already configured
|
||||||
#if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP)
|
#if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from enum import Enum
|
|||||||
|
|
||||||
from esphome.enum import StrEnum
|
from esphome.enum import StrEnum
|
||||||
|
|
||||||
__version__ = "2026.1.3"
|
__version__ = "2026.1.4"
|
||||||
|
|
||||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||||
|
|||||||
@@ -356,6 +356,10 @@ void Component::defer(const std::string &name, std::function<void()> &&f) { //
|
|||||||
void Component::defer(const char *name, std::function<void()> &&f) { // NOLINT
|
void Component::defer(const char *name, std::function<void()> &&f) { // NOLINT
|
||||||
App.scheduler.set_timeout(this, name, 0, std::move(f));
|
App.scheduler.set_timeout(this, name, 0, std::move(f));
|
||||||
}
|
}
|
||||||
|
void Component::defer(uint32_t id, std::function<void()> &&f) { // NOLINT
|
||||||
|
App.scheduler.set_timeout(this, id, 0, std::move(f));
|
||||||
|
}
|
||||||
|
bool Component::cancel_defer(uint32_t id) { return App.scheduler.cancel_timeout(this, id); }
|
||||||
void Component::set_timeout(uint32_t timeout, std::function<void()> &&f) { // NOLINT
|
void Component::set_timeout(uint32_t timeout, std::function<void()> &&f) { // NOLINT
|
||||||
App.scheduler.set_timeout(this, static_cast<const char *>(nullptr), timeout, std::move(f));
|
App.scheduler.set_timeout(this, static_cast<const char *>(nullptr), timeout, std::move(f));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -494,11 +494,15 @@ class Component {
|
|||||||
/// Defer a callback to the next loop() call.
|
/// Defer a callback to the next loop() call.
|
||||||
void defer(std::function<void()> &&f); // NOLINT
|
void defer(std::function<void()> &&f); // NOLINT
|
||||||
|
|
||||||
|
/// Defer a callback with a numeric ID (zero heap allocation)
|
||||||
|
void defer(uint32_t id, std::function<void()> &&f); // NOLINT
|
||||||
|
|
||||||
/// Cancel a defer callback using the specified name, name must not be empty.
|
/// Cancel a defer callback using the specified name, name must not be empty.
|
||||||
// Remove before 2026.7.0
|
// Remove before 2026.7.0
|
||||||
ESPDEPRECATED("Use const char* overload instead. Removed in 2026.7.0", "2026.1.0")
|
ESPDEPRECATED("Use const char* overload instead. Removed in 2026.7.0", "2026.1.0")
|
||||||
bool cancel_defer(const std::string &name); // NOLINT
|
bool cancel_defer(const std::string &name); // NOLINT
|
||||||
bool cancel_defer(const char *name); // NOLINT
|
bool cancel_defer(const char *name); // NOLINT
|
||||||
|
bool cancel_defer(uint32_t id); // NOLINT
|
||||||
|
|
||||||
// Ordered for optimal packing on 32-bit systems
|
// Ordered for optimal packing on 32-bit systems
|
||||||
const LogString *component_source_{nullptr};
|
const LogString *component_source_{nullptr};
|
||||||
|
|||||||
10
tests/components/mipi_spi/test.esp8266-ard.yaml
Normal file
10
tests/components/mipi_spi/test.esp8266-ard.yaml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
substitutions:
|
||||||
|
dc_pin: GPIO15
|
||||||
|
cs_pin: GPIO5
|
||||||
|
enable_pin: GPIO4
|
||||||
|
reset_pin: GPIO16
|
||||||
|
|
||||||
|
packages:
|
||||||
|
spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml
|
||||||
|
|
||||||
|
<<: !include common.yaml
|
||||||
@@ -20,6 +20,9 @@ globals:
|
|||||||
- id: retry_counter
|
- id: retry_counter
|
||||||
type: int
|
type: int
|
||||||
initial_value: '0'
|
initial_value: '0'
|
||||||
|
- id: defer_counter
|
||||||
|
type: int
|
||||||
|
initial_value: '0'
|
||||||
- id: tests_done
|
- id: tests_done
|
||||||
type: bool
|
type: bool
|
||||||
initial_value: 'false'
|
initial_value: 'false'
|
||||||
@@ -136,11 +139,49 @@ script:
|
|||||||
App.scheduler.cancel_retry(component1, 6002U);
|
App.scheduler.cancel_retry(component1, 6002U);
|
||||||
ESP_LOGI("test", "Cancelled numeric retry 6002");
|
ESP_LOGI("test", "Cancelled numeric retry 6002");
|
||||||
|
|
||||||
|
// Test 12: defer with numeric ID (Component method)
|
||||||
|
class TestDeferComponent : public Component {
|
||||||
|
public:
|
||||||
|
void test_defer_methods() {
|
||||||
|
// Test defer with uint32_t ID - should execute on next loop
|
||||||
|
this->defer(7001U, []() {
|
||||||
|
ESP_LOGI("test", "Component numeric defer 7001 fired");
|
||||||
|
id(defer_counter) += 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test another defer with numeric ID
|
||||||
|
this->defer(7002U, []() {
|
||||||
|
ESP_LOGI("test", "Component numeric defer 7002 fired");
|
||||||
|
id(defer_counter) += 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static TestDeferComponent test_defer_component;
|
||||||
|
test_defer_component.test_defer_methods();
|
||||||
|
|
||||||
|
// Test 13: cancel_defer with numeric ID (Component method)
|
||||||
|
class TestCancelDeferComponent : public Component {
|
||||||
|
public:
|
||||||
|
void test_cancel_defer() {
|
||||||
|
// Set a defer that should be cancelled
|
||||||
|
this->defer(8001U, []() {
|
||||||
|
ESP_LOGE("test", "ERROR: Numeric defer 8001 should have been cancelled");
|
||||||
|
});
|
||||||
|
// Cancel it immediately
|
||||||
|
bool cancelled = this->cancel_defer(8001U);
|
||||||
|
ESP_LOGI("test", "Cancelled numeric defer 8001: %s", cancelled ? "true" : "false");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static TestCancelDeferComponent test_cancel_defer_component;
|
||||||
|
test_cancel_defer_component.test_cancel_defer();
|
||||||
|
|
||||||
- id: report_results
|
- id: report_results
|
||||||
then:
|
then:
|
||||||
- lambda: |-
|
- lambda: |-
|
||||||
ESP_LOGI("test", "Final results - Timeouts: %d, Intervals: %d, Retries: %d",
|
ESP_LOGI("test", "Final results - Timeouts: %d, Intervals: %d, Retries: %d, Defers: %d",
|
||||||
id(timeout_counter), id(interval_counter), id(retry_counter));
|
id(timeout_counter), id(interval_counter), id(retry_counter), id(defer_counter));
|
||||||
|
|
||||||
sensor:
|
sensor:
|
||||||
- platform: template
|
- platform: template
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ async def test_scheduler_numeric_id_test(
|
|||||||
timeout_count = 0
|
timeout_count = 0
|
||||||
interval_count = 0
|
interval_count = 0
|
||||||
retry_count = 0
|
retry_count = 0
|
||||||
|
defer_count = 0
|
||||||
|
|
||||||
# Events for each test completion
|
# Events for each test completion
|
||||||
numeric_timeout_1001_fired = asyncio.Event()
|
numeric_timeout_1001_fired = asyncio.Event()
|
||||||
@@ -33,6 +34,9 @@ async def test_scheduler_numeric_id_test(
|
|||||||
max_id_timeout_fired = asyncio.Event()
|
max_id_timeout_fired = asyncio.Event()
|
||||||
numeric_retry_done = asyncio.Event()
|
numeric_retry_done = asyncio.Event()
|
||||||
numeric_retry_cancelled = asyncio.Event()
|
numeric_retry_cancelled = asyncio.Event()
|
||||||
|
numeric_defer_7001_fired = asyncio.Event()
|
||||||
|
numeric_defer_7002_fired = asyncio.Event()
|
||||||
|
numeric_defer_cancelled = asyncio.Event()
|
||||||
final_results_logged = asyncio.Event()
|
final_results_logged = asyncio.Event()
|
||||||
|
|
||||||
# Track interval counts
|
# Track interval counts
|
||||||
@@ -40,7 +44,7 @@ async def test_scheduler_numeric_id_test(
|
|||||||
numeric_retry_count = 0
|
numeric_retry_count = 0
|
||||||
|
|
||||||
def on_log_line(line: str) -> None:
|
def on_log_line(line: str) -> None:
|
||||||
nonlocal timeout_count, interval_count, retry_count
|
nonlocal timeout_count, interval_count, retry_count, defer_count
|
||||||
nonlocal numeric_interval_count, numeric_retry_count
|
nonlocal numeric_interval_count, numeric_retry_count
|
||||||
|
|
||||||
# Strip ANSI color codes
|
# Strip ANSI color codes
|
||||||
@@ -105,15 +109,27 @@ async def test_scheduler_numeric_id_test(
|
|||||||
elif "Cancelled numeric retry 6002" in clean_line:
|
elif "Cancelled numeric retry 6002" in clean_line:
|
||||||
numeric_retry_cancelled.set()
|
numeric_retry_cancelled.set()
|
||||||
|
|
||||||
|
# Check for numeric defer tests
|
||||||
|
elif "Component numeric defer 7001 fired" in clean_line:
|
||||||
|
numeric_defer_7001_fired.set()
|
||||||
|
|
||||||
|
elif "Component numeric defer 7002 fired" in clean_line:
|
||||||
|
numeric_defer_7002_fired.set()
|
||||||
|
|
||||||
|
elif "Cancelled numeric defer 8001: true" in clean_line:
|
||||||
|
numeric_defer_cancelled.set()
|
||||||
|
|
||||||
# Check for final results
|
# Check for final results
|
||||||
elif "Final results" in clean_line:
|
elif "Final results" in clean_line:
|
||||||
match = re.search(
|
match = re.search(
|
||||||
r"Timeouts: (\d+), Intervals: (\d+), Retries: (\d+)", clean_line
|
r"Timeouts: (\d+), Intervals: (\d+), Retries: (\d+), Defers: (\d+)",
|
||||||
|
clean_line,
|
||||||
)
|
)
|
||||||
if match:
|
if match:
|
||||||
timeout_count = int(match.group(1))
|
timeout_count = int(match.group(1))
|
||||||
interval_count = int(match.group(2))
|
interval_count = int(match.group(2))
|
||||||
retry_count = int(match.group(3))
|
retry_count = int(match.group(3))
|
||||||
|
defer_count = int(match.group(4))
|
||||||
final_results_logged.set()
|
final_results_logged.set()
|
||||||
|
|
||||||
async with (
|
async with (
|
||||||
@@ -201,6 +217,23 @@ async def test_scheduler_numeric_id_test(
|
|||||||
"Numeric retry 6002 should have been cancelled"
|
"Numeric retry 6002 should have been cancelled"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Wait for numeric defer tests
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(numeric_defer_7001_fired.wait(), timeout=0.5)
|
||||||
|
except TimeoutError:
|
||||||
|
pytest.fail("Numeric defer 7001 did not fire within 0.5 seconds")
|
||||||
|
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(numeric_defer_7002_fired.wait(), timeout=0.5)
|
||||||
|
except TimeoutError:
|
||||||
|
pytest.fail("Numeric defer 7002 did not fire within 0.5 seconds")
|
||||||
|
|
||||||
|
# Verify numeric defer was cancelled
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(numeric_defer_cancelled.wait(), timeout=0.5)
|
||||||
|
except TimeoutError:
|
||||||
|
pytest.fail("Numeric defer 8001 cancel confirmation not received")
|
||||||
|
|
||||||
# Wait for final results
|
# Wait for final results
|
||||||
try:
|
try:
|
||||||
await asyncio.wait_for(final_results_logged.wait(), timeout=3.0)
|
await asyncio.wait_for(final_results_logged.wait(), timeout=3.0)
|
||||||
@@ -215,3 +248,4 @@ async def test_scheduler_numeric_id_test(
|
|||||||
assert retry_count >= 2, (
|
assert retry_count >= 2, (
|
||||||
f"Expected at least 2 retry attempts, got {retry_count}"
|
f"Expected at least 2 retry attempts, got {retry_count}"
|
||||||
)
|
)
|
||||||
|
assert defer_count >= 2, f"Expected at least 2 defer fires, got {defer_count}"
|
||||||
|
|||||||
Reference in New Issue
Block a user