1
0
mirror of https://github.com/esphome/esphome.git synced 2026-02-08 16:51:52 +00:00

Compare commits

...

37 Commits

Author SHA1 Message Date
Jesse Hills
ab8ac72c4f Merge pull request #13757 from esphome/bump-2026.1.4
2026.1.4
2026-02-05 00:01:14 +13:00
Jesse Hills
1b3c9aa98e Bump version to 2026.1.4 2026-02-04 11:01:32 +01:00
Samuel Sieb
bafbd4235a [ultrasonic] adjust timeouts and bring the parameter back (#13738)
Co-authored-by: Samuel Sieb <samuel@sieb.net>
2026-02-04 11:01:31 +01:00
J. Nick Koston
900aab45f1 [wifi] Fix wifi.connected condition returning false in connect state listener automations (#13733) 2026-02-04 11:01:29 +01:00
J. Nick Koston
bc41d25657 [cse7766] Fix power reading stuck when load switches off (#13734) 2026-02-04 10:56:42 +01:00
J. Nick Koston
094d64f872 [http_request] Fix requests taking full timeout when response is already complete (#13649) 2026-02-04 10:56:42 +01:00
J. Nick Koston
b085585461 [core] Add missing uint32_t ID overloads for defer() and cancel_defer() (#13720) 2026-02-04 10:56:42 +01:00
rwrozelle
49ef4e00df [mqtt] resolve warnings related to use of ip.str() (#13719) 2026-02-04 10:56:42 +01:00
Jonathan Swoboda
8314ad9ca0 [max7219] Allocate buffer in constructor (#13660)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 10:56:42 +01:00
J0k3r2k1
5544f0d346 [mipi_spi] Fix log_pin() FlashStringHelper compatibility (#13624)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-02-04 10:56:34 +01:00
Jonathan Swoboda
3c91d72403 Merge pull request #13632 from esphome/bump-2026.1.3
2026.1.3
2026-01-29 22:22:10 -05:00
Jonathan Swoboda
0a63fc6f05 Bump version to 2026.1.3 2026-01-29 21:11:09 -05:00
J. Nick Koston
50e739ee8e [http_request] Fix empty body for chunked transfer encoding responses (#13599) 2026-01-29 21:11:09 -05:00
J. Nick Koston
6c84f20491 [wifi] Fix ESP8266 yield panic when WiFi scan fails (#13603) 2026-01-29 21:11:09 -05:00
Cody Cutrer
a68506f924 [ld2450] preserve precision of angle (#13600) 2026-01-29 21:11:08 -05:00
esphomebot
a20d42ca0b Update webserver local assets to 20260127-190637 (#13573)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-01-29 21:11:08 -05:00
J. Nick Koston
4ec8846198 [web_server] Add name_id to SSE for entity ID format migration (#13535) 2026-01-29 21:11:08 -05:00
J. Nick Koston
40ea65b1c0 [socket] ESP8266: call delay(0) instead of esp_delay(0, cb) for zero timeout (#13530) 2026-01-29 21:11:08 -05:00
J. Nick Koston
f7937ef952 [ota] Improve error message when device closes connection without responding (#13562) 2026-01-29 21:11:08 -05:00
sebcaps
d6bf137026 [mhz19] Fix Uninitialized var warning message (#13526) 2026-01-29 21:11:08 -05:00
esphomebot
ed9a672f44 Update webserver local assets to 20260122-204614 (#13455)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-01-29 21:11:08 -05:00
Jonathan Swoboda
1141e83a7c Merge pull request #13529 from esphome/bump-2026.1.2
2026.1.2
2026-01-25 13:21:26 -05:00
Jonathan Swoboda
214ce95cf3 Bump version to 2026.1.2 2026-01-25 12:22:18 -05:00
J. Nick Koston
3a7b83ba93 [wifi] Fix scan flag race condition causing reconnect failure on ESP8266/LibreTiny (#13514) 2026-01-25 12:22:18 -05:00
Clyde Stubbs
cc2f3d85dc [wifi] Fix watchdog timeout on P4 WiFi scan (#13520) 2026-01-25 12:22:18 -05:00
Jonathan Swoboda
723f67d5e2 [i2c] Increase ESP-IDF I2C transaction timeout from 20ms to 100ms (#13483)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 12:22:18 -05:00
Jonathan Swoboda
70e45706d9 [modbus_controller] Fix YAML serialization error with custom_command (#13482)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 12:22:18 -05:00
Jas Strong
56a2a2269f [rd03d] Fix speed and resolution field order (#13495)
Co-authored-by: jas <jas@asspa.in>
2026-01-25 12:22:18 -05:00
Keith Burzinski
d6841ba33a [light] Fix cwww state restore (#13493) 2026-01-25 12:22:18 -05:00
Clyde Stubbs
10cbd0164a [lvgl] Fix setting empty text (#13494) 2026-01-25 12:22:18 -05:00
Big Mike
d285706b41 [sen5x] Fix store baseline functionality (#13469) 2026-01-25 12:22:18 -05:00
J. Nick Koston
ef469c20df [slow_pwm] Fix dump_summary deprecation warning (#13460) 2026-01-25 12:22:18 -05:00
Clyde Stubbs
6870d3dc50 [mipi_rgb] Add software reset command to st7701s init sequence (#13470) 2026-01-25 12:22:18 -05:00
Keith Burzinski
9cc39621a6 [ir_rf_proxy] Remove unnecessary headers, add tests (#13464) 2026-01-25 12:22:18 -05:00
J. Nick Koston
c4f7d09553 [rpi_dpi_rgb] Fix dump_summary deprecation warning (#13461) 2026-01-25 12:22:18 -05:00
J. Nick Koston
ab1661ef22 [mipi_rgb] Fix dump_summary deprecation warning (#13463) 2026-01-25 12:22:18 -05:00
J. Nick Koston
ccbf17d5ab [st7701s] Fix dump_summary deprecation warning (#13462) 2026-01-25 12:22:18 -05:00
69 changed files with 9614 additions and 9082 deletions

View File

@@ -528,7 +528,7 @@ esphome/components/uart/packet_transport/* @clydebarrow
esphome/components/udp/* @clydebarrow
esphome/components/ufire_ec/* @pvizeli
esphome/components/ufire_ise/* @pvizeli
esphome/components/ultrasonic/* @OttoWinter
esphome/components/ultrasonic/* @ssieb @swoboda1337
esphome/components/update/* @jesserockz
esphome/components/uponor_smatrix/* @kroimon
esphome/components/usb_cdc_acm/* @kbx81

View File

@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version
# control system is used.
PROJECT_NUMBER = 2026.1.1
PROJECT_NUMBER = 2026.1.4
# 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

View File

@@ -152,6 +152,10 @@ void CSE7766Component::parse_data_() {
if (this->power_sensor_ != nullptr) {
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;

View File

@@ -12,6 +12,7 @@ from esphome.const import (
KEY_FRAMEWORK_VERSION,
)
from esphome.core import CORE
from esphome.cpp_generator import add_define
CODEOWNERS = ["@swoboda1337"]
@@ -42,6 +43,7 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
add_define("USE_ESP32_HOSTED")
if config[CONF_ACTIVE_HIGH]:
esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_HOSTED_SDIO_RESET_ACTIVE_HIGH",

View File

@@ -193,11 +193,14 @@ bool Esp32HostedUpdate::fetch_manifest_() {
int read_or_error = container->read(buf, sizeof(buf));
App.feed_wdt();
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)
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)
break; // ERROR or TIMEOUT
break; // COMPLETE, ERROR, or TIMEOUT
json_str.append(reinterpret_cast<char *>(buf), read_or_error);
}
container->end();
@@ -318,9 +321,14 @@ bool Esp32HostedUpdate::stream_firmware_to_coprocessor_() {
App.feed_wdt();
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)
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::TIMEOUT) {
ESP_LOGE(TAG, "Timeout reading firmware data");

View File

@@ -26,6 +26,7 @@ struct Header {
enum HttpStatus {
HTTP_STATUS_OK = 200,
HTTP_STATUS_NO_CONTENT = 204,
HTTP_STATUS_RESET_CONTENT = 205,
HTTP_STATUS_PARTIAL_CONTENT = 206,
/* 3xx - Redirection */
@@ -126,19 +127,21 @@ struct HttpReadResult {
/// Result of processing a non-blocking read with timeout (for manual loops)
enum class HttpReadLoopResult : uint8_t {
DATA, ///< Data was read, process it
RETRY, ///< No data yet, already delayed, caller should continue loop
ERROR, ///< Read error, caller should exit loop
TIMEOUT, ///< Timeout waiting for data, caller should exit loop
DATA, ///< Data was read, process it
COMPLETE, ///< All content has been read, caller should exit loop
RETRY, ///< No data yet, already delayed, caller should continue 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
/// @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 timeout_ms Maximum time to wait for data
/// @return DATA if data received, RETRY if should continue loop, ERROR/TIMEOUT if should exit
inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_t &last_data_time,
uint32_t timeout_ms) {
/// @param is_read_complete Whether all expected content has been read (from HttpContainer::is_read_complete())
/// @return How the caller should proceed - see HttpReadLoopResult enum
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) {
last_data_time = millis();
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) {
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) {
return HttpReadLoopResult::TIMEOUT;
}
@@ -159,9 +165,9 @@ class HttpRequestComponent;
class HttpContainer : public Parented<HttpRequestComponent> {
public:
virtual ~HttpContainer() = default;
size_t content_length;
int status_code;
uint32_t duration_ms;
size_t content_length{0};
int status_code{-1}; ///< -1 indicates no response received yet
uint32_t duration_ms{0};
/**
* @brief Read data from the HTTP response body.
@@ -194,9 +200,24 @@ class HttpContainer : public Parented<HttpRequestComponent> {
virtual void end() = 0;
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_; }
/// 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.
*
@@ -209,6 +230,7 @@ class HttpContainer : public Parented<HttpRequestComponent> {
protected:
size_t bytes_read_{0};
bool secure_{false};
bool is_chunked_{false}; ///< True if response uses chunked transfer encoding
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 chunk_size Maximum bytes per read call
/// @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,
uint32_t timeout_ms) {
size_t read_index = 0;
@@ -231,9 +253,11 @@ inline HttpReadResult http_read_fully(HttpContainer *container, uint8_t *buffer,
App.feed_wdt();
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)
continue;
if (result == HttpReadLoopResult::COMPLETE)
break; // Server sent less data than requested, but transfer is complete
if (result == HttpReadLoopResult::ERROR)
return {HttpReadStatus::ERROR, read_bytes_or_error};
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));
App.feed_wdt();
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)
continue;
if (result != HttpReadLoopResult::DATA)
break; // ERROR or TIMEOUT
break; // COMPLETE, ERROR, or TIMEOUT
read_index += read_or_error;
}
response_body.reserve(read_index);

View File

@@ -131,9 +131,27 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
}
}
// HTTPClient::getSize() returns -1 for chunked transfer encoding (no Content-Length).
// 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
// 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();
ESP_LOGD(TAG, "Content-Length: %d", 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;
return container;
@@ -167,17 +185,23 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
}
int available_data = stream_ptr->available();
int bufsize = std::min(max_len, std::min(this->content_length - this->bytes_read_, (size_t) available_data));
// For chunked transfer encoding, HTTPClient::getSize() returns -1, which becomes SIZE_MAX when
// cast to size_t. SIZE_MAX - bytes_read_ is still huge, so it won't limit the read.
size_t remaining = (this->content_length > 0) ? (this->content_length - this->bytes_read_) : max_len;
int bufsize = std::min(max_len, std::min(remaining, (size_t) available_data));
if (bufsize == 0) {
this->duration_ms += (millis() - start);
// Check if we've read all expected content
if (this->bytes_read_ >= this->content_length) {
// Check if we've read all expected content (non-chunked only)
// For chunked encoding (content_length == SIZE_MAX), is_read_complete() returns false
if (this->is_read_complete()) {
return 0; // All content read successfully
}
// No data available - check if connection is still open
// For chunked encoding, !connected() after reading means EOF (all chunks received)
// For known content_length with bytes_read_ < content_length, it means connection dropped
if (!stream_ptr->connected()) {
return HTTP_ERROR_CONNECTION_CLOSED; // Connection closed prematurely
return HTTP_ERROR_CONNECTION_CLOSED; // Connection closed or EOF for chunked
}
return 0; // No data yet, caller should retry
}

View File

@@ -152,7 +152,10 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
}
container->feed_wdt();
// 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.
container->content_length = esp_http_client_fetch_headers(client);
container->set_chunked(esp_http_client_is_chunked_response(client));
container->feed_wdt();
container->status_code = esp_http_client_get_status_code(client);
container->feed_wdt();
@@ -188,6 +191,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
container->feed_wdt();
container->content_length = esp_http_client_fetch_headers(client);
container->set_chunked(esp_http_client_is_chunked_response(client));
container->feed_wdt();
container->status_code = esp_http_client_get_status_code(client);
container->feed_wdt();
@@ -220,14 +224,21 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
//
// We normalize to HttpContainer::read() contract:
// > 0: bytes read
// 0: no data yet / all content read (caller should check bytes_read vs content_length)
// 0: all content read (only returned when content_length is known and fully read)
// < 0: error/connection closed
//
// Note on chunked transfer encoding:
// esp_http_client_fetch_headers() returns 0 for chunked responses (no Content-Length header).
// We handle this by skipping the content_length check when content_length is 0,
// allowing esp_http_client_read() to handle chunked decoding internally and signal EOF
// by returning 0.
int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
const uint32_t start = millis();
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
// Check if we've already read all expected content
if (this->bytes_read_ >= this->content_length) {
// Check if we've already read all expected content (non-chunked only)
// For chunked responses (content_length == 0), esp_http_client_read() handles EOF
if (this->is_read_complete()) {
return 0; // All content read successfully
}
@@ -242,7 +253,13 @@ int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
return read_len_or_error;
}
// Connection closed by server before all content received
// esp_http_client_read() returns 0 in two cases:
// 1. Known content_length: connection closed before all data received (error)
// 2. Chunked encoding (content_length == 0): end of stream reached (EOF)
// For case 1, returning HTTP_ERROR_CONNECTION_CLOSED is correct.
// For case 2, 0 indicates that all chunked data has already been delivered
// in previous successful read() calls, so treating this as a closed
// connection does not cause any loss of response data.
if (read_len_or_error == 0) {
return HTTP_ERROR_CONNECTION_CLOSED;
}

View File

@@ -130,9 +130,13 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
App.feed_wdt();
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)
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::TIMEOUT) {
ESP_LOGE(TAG, "Timeout reading data");

View File

@@ -185,7 +185,7 @@ ErrorCode IDFI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, s
}
jobs[num_jobs++].command = I2C_MASTER_CMD_STOP;
ESP_LOGV(TAG, "Sending %zu jobs", num_jobs);
esp_err_t err = i2c_master_execute_defined_operations(this->dev_, jobs, num_jobs, 20);
esp_err_t err = i2c_master_execute_defined_operations(this->dev_, jobs, num_jobs, 100);
if (err == ESP_ERR_INVALID_STATE) {
ESP_LOGV(TAG, "TX to %02X failed: not acked", address);
return ERROR_NOT_ACKNOWLEDGED;

View File

@@ -5,8 +5,6 @@
// Once the API is considered stable, this warning will be removed.
#include "esphome/components/infrared/infrared.h"
#include "esphome/components/remote_transmitter/remote_transmitter.h"
#include "esphome/components/remote_receiver/remote_receiver.h"
namespace esphome::ir_rf_proxy {

View File

@@ -451,7 +451,7 @@ void LD2450Component::handle_periodic_data_() {
int16_t ty = 0;
int16_t td = 0;
int16_t ts = 0;
int16_t angle = 0;
float angle = 0;
uint8_t index = 0;
Direction direction{DIRECTION_UNDEFINED};
bool is_moving = false;

View File

@@ -143,6 +143,7 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
],
icon=ICON_FORMAT_TEXT_ROTATION_ANGLE_UP,
unit_of_measurement=UNIT_DEGREES,
accuracy_decimals=1,
),
cv.Optional(CONF_DISTANCE): sensor.sensor_schema(
device_class=DEVICE_CLASS_DISTANCE,

View File

@@ -391,7 +391,10 @@ void LightCall::transform_parameters_() {
min_mireds > 0.0f && max_mireds > 0.0f) {
ESP_LOGD(TAG, "'%s': setting cold/warm white channels using white/color temperature values",
this->parent_->get_name().c_str());
if (this->has_color_temperature()) {
// Only compute cold_white/warm_white from color_temperature if they're not already explicitly set.
// This is important for state restoration, where both color_temperature and cold_white/warm_white
// are restored from flash - we want to preserve the saved cold_white/warm_white values.
if (this->has_color_temperature() && !this->has_cold_white() && !this->has_warm_white()) {
const float color_temp = clamp(this->color_temperature_, min_mireds, max_mireds);
const float range = max_mireds - min_mireds;
const float ww_fraction = (color_temp - min_mireds) / range;

View File

@@ -32,7 +32,7 @@ class LabelType(WidgetType):
async def to_code(self, w: Widget, config):
"""For a text object, create and set text"""
if value := config.get(CONF_TEXT):
if (value := config.get(CONF_TEXT)) is not None:
await w.set_property(CONF_TEXT, await lv_text.process(value))
await w.set_property(CONF_LONG_MODE, config)
await w.set_property(CONF_RECOLOR, config)

View File

@@ -28,11 +28,10 @@ CONFIG_SCHEMA = (
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 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_reverse(config[CONF_REVERSE_ENABLE]))

View File

@@ -3,8 +3,7 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
namespace max7219 {
namespace esphome::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; }
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() {
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
this->send_to_all_(MAX7219_REGISTER_SCAN_LIMIT, 7);
// let's use our own ASCII -> led pattern encoding
@@ -229,7 +230,6 @@ void MAX7219Component::set_intensity(uint8_t 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) {
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); }
} // namespace max7219
} // namespace esphome
} // namespace esphome::max7219

View File

@@ -6,8 +6,7 @@
#include "esphome/components/spi/spi.h"
#include "esphome/components/display/display.h"
namespace esphome {
namespace max7219 {
namespace esphome::max7219 {
class MAX7219Component;
@@ -17,6 +16,8 @@ class MAX7219Component : public PollingComponent,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_1MHZ> {
public:
explicit MAX7219Component(uint8_t num_chips);
void set_writer(max7219_writer_t &&writer);
void setup() override;
@@ -30,7 +31,6 @@ class MAX7219Component : public PollingComponent,
void display();
void set_intensity(uint8_t intensity);
void set_num_chips(uint8_t num_chips);
void set_reverse(bool reverse) { this->reverse_ = reverse; };
/// 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)
bool intensity_changed_{}; // True if we need to re-send the intensity
uint8_t num_chips_{1};
uint8_t *buffer_;
uint8_t *buffer_{nullptr};
bool reverse_{false};
max7219_writer_t writer_{};
};
} // namespace max7219
} // namespace esphome
} // namespace esphome::max7219

View File

@@ -155,6 +155,9 @@ void MHZ19Component::dump_config() {
case MHZ19_DETECTION_RANGE_0_10000PPM:
range_str = "0 to 10000ppm";
break;
default:
range_str = "default";
break;
}
ESP_LOGCONFIG(TAG, " Detection range: %s", range_str);
}

View File

@@ -1,9 +1,11 @@
#ifdef USE_ESP32_VARIANT_ESP32S3
#include "mipi_rgb.h"
#include "esphome/core/gpio.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include "esp_lcd_panel_rgb.h"
#include <span>
namespace esphome {
namespace mipi_rgb {
@@ -343,19 +345,27 @@ int MipiRgb::get_height() {
}
}
static std::string get_pin_name(GPIOPin *pin) {
static const char *get_pin_name(GPIOPin *pin, std::span<char, GPIO_SUMMARY_MAX_LEN> buffer) {
if (pin == nullptr)
return "None";
return pin->dump_summary();
pin->dump_summary(buffer.data(), buffer.size());
return buffer.data();
}
void MipiRgb::dump_pins_(uint8_t start, uint8_t end, const char *name, uint8_t offset) {
char pin_summary[GPIO_SUMMARY_MAX_LEN];
for (uint8_t i = start; i != end; i++) {
ESP_LOGCONFIG(TAG, " %s pin %d: %s", name, offset++, this->data_pins_[i]->dump_summary().c_str());
this->data_pins_[i]->dump_summary(pin_summary, sizeof(pin_summary));
ESP_LOGCONFIG(TAG, " %s pin %d: %s", name, offset++, pin_summary);
}
}
void MipiRgb::dump_config() {
char reset_buf[GPIO_SUMMARY_MAX_LEN];
char de_buf[GPIO_SUMMARY_MAX_LEN];
char pclk_buf[GPIO_SUMMARY_MAX_LEN];
char hsync_buf[GPIO_SUMMARY_MAX_LEN];
char vsync_buf[GPIO_SUMMARY_MAX_LEN];
ESP_LOGCONFIG(TAG,
"MIPI_RGB LCD"
"\n Model: %s"
@@ -379,9 +389,9 @@ void MipiRgb::dump_config() {
this->model_, this->width_, this->height_, this->rotation_, YESNO(this->pclk_inverted_),
this->hsync_pulse_width_, this->hsync_back_porch_, this->hsync_front_porch_, this->vsync_pulse_width_,
this->vsync_back_porch_, this->vsync_front_porch_, YESNO(this->invert_colors_),
(unsigned) (this->pclk_frequency_ / 1000000), get_pin_name(this->reset_pin_).c_str(),
get_pin_name(this->de_pin_).c_str(), get_pin_name(this->pclk_pin_).c_str(),
get_pin_name(this->hsync_pin_).c_str(), get_pin_name(this->vsync_pin_).c_str());
(unsigned) (this->pclk_frequency_ / 1000000), get_pin_name(this->reset_pin_, reset_buf),
get_pin_name(this->de_pin_, de_buf), get_pin_name(this->pclk_pin_, pclk_buf),
get_pin_name(this->hsync_pin_, hsync_buf), get_pin_name(this->vsync_pin_, vsync_buf));
this->dump_pins_(8, 13, "Blue", 0);
this->dump_pins_(13, 16, "Green", 0);

View File

@@ -55,6 +55,7 @@ st7701s = ST7701S(
pclk_frequency="16MHz",
pclk_inverted=True,
initsequence=(
(0x01,), # Software Reset
(0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), # Page 0
(0xC0, 0x3B, 0x00), (0xC1, 0x0D, 0x02), (0xC2, 0x31, 0x05),
(0xB0, 0x00, 0x11, 0x18, 0x0E, 0x11, 0x06, 0x07, 0x08, 0x07, 0x22, 0x04, 0x12, 0x0F, 0xAA, 0x31, 0x18,),

View File

@@ -1,6 +1,39 @@
#include "mipi_spi.h"
#include "esphome/core/log.h"
namespace esphome {
namespace mipi_spi {} // namespace mipi_spi
} // namespace esphome
namespace esphome::mipi_spi {
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

View File

@@ -63,6 +63,11 @@ enum BusType {
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.
* 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 {
esph_log_config(TAG,
"MIPI_SPI Display\n"
" Model: %s\n"
" 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);
internal_dump_config(this->model_, WIDTH, HEIGHT, OFFSET_WIDTH, OFFSET_HEIGHT, this->madctl_, this->invert_colors_,
DISPLAYPIXEL * 8, IS_BIG_ENDIAN, this->brightness_, this->cs_, this->reset_pin_, this->dc_pin_,
this->mode_, this->data_rate_, BUS_TYPE);
}
protected:

View File

@@ -279,7 +279,7 @@ def modbus_calc_properties(config):
if isinstance(value, str):
value = value.encode()
config[CONF_ADDRESS] = binascii.crc_hqx(value, 0)
config[CONF_REGISTER_TYPE] = ModbusRegisterType.CUSTOM
config[CONF_REGISTER_TYPE] = cv.enum(MODBUS_REGISTER_TYPE)("custom")
config[CONF_FORCE_NEW_RANGE] = True
return byte_offset, reg_count

View File

@@ -139,7 +139,8 @@ class MQTTBackendESP32 final : public MQTTBackend {
this->lwt_retain_ = retain;
}
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;
}
void set_server(const char *host, uint16_t port) final {

View File

@@ -133,14 +133,17 @@ void RD03DComponent::process_frame_() {
uint8_t offset = FRAME_HEADER_SIZE + (i * TARGET_DATA_SIZE);
// Extract raw bytes for this target
// Note: Despite datasheet Table 5-2 showing order as X, Y, Speed, Resolution,
// actual radar output has Resolution before Speed (verified empirically -
// stationary targets were showing non-zero speed with original field order)
uint8_t x_low = this->buffer_[offset + 0];
uint8_t x_high = this->buffer_[offset + 1];
uint8_t y_low = this->buffer_[offset + 2];
uint8_t y_high = this->buffer_[offset + 3];
uint8_t speed_low = this->buffer_[offset + 4];
uint8_t speed_high = this->buffer_[offset + 5];
uint8_t res_low = this->buffer_[offset + 6];
uint8_t res_high = this->buffer_[offset + 7];
uint8_t res_low = this->buffer_[offset + 4];
uint8_t res_high = this->buffer_[offset + 5];
uint8_t speed_low = this->buffer_[offset + 6];
uint8_t speed_high = this->buffer_[offset + 7];
// Decode values per RD-03D format
int16_t x = decode_value(x_low, x_high);

View File

@@ -1,5 +1,6 @@
#ifdef USE_ESP32_VARIANT_ESP32S3
#include "rpi_dpi_rgb.h"
#include "esphome/core/gpio.h"
#include "esphome/core/log.h"
namespace esphome {
@@ -134,8 +135,11 @@ void RpiDpiRgb::dump_config() {
LOG_PIN(" Enable Pin: ", this->enable_pin_);
LOG_PIN(" Reset Pin: ", this->reset_pin_);
size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]);
for (size_t i = 0; i != data_pin_count; i++)
ESP_LOGCONFIG(TAG, " Data pin %d: %s", i, (this->data_pins_[i])->dump_summary().c_str());
char pin_summary[GPIO_SUMMARY_MAX_LEN];
for (size_t i = 0; i != data_pin_count; i++) {
this->data_pins_[i]->dump_summary(pin_summary, sizeof(pin_summary));
ESP_LOGCONFIG(TAG, " Data pin %d: %s", i, pin_summary);
}
}
void RpiDpiRgb::reset_display_() const {

View File

@@ -124,8 +124,8 @@ void SEN5XComponent::setup() {
sen5x_type = SEN55;
}
}
ESP_LOGD(TAG, "Product name: %s", this->product_name_.c_str());
}
ESP_LOGD(TAG, "Product name: %s", this->product_name_.c_str());
if (this->humidity_sensor_ && sen5x_type == SEN50) {
ESP_LOGE(TAG, "Relative humidity requires a SEN54 or SEN55");
this->humidity_sensor_ = nullptr; // mark as not used
@@ -159,28 +159,14 @@ void SEN5XComponent::setup() {
// This ensures the baseline storage is cleared after OTA
// Serial numbers are unique to each sensor, so multiple sensors can be used without conflict
uint32_t hash = fnv1a_hash_extend(App.get_config_version_hash(), combined_serial);
this->pref_ = global_preferences->make_preference<Sen5xBaselines>(hash, true);
if (this->pref_.load(&this->voc_baselines_storage_)) {
ESP_LOGI(TAG, "Loaded VOC baseline state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32,
this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1);
}
// Initialize storage timestamp
this->seconds_since_last_store_ = 0;
if (this->voc_baselines_storage_.state0 > 0 && this->voc_baselines_storage_.state1 > 0) {
ESP_LOGI(TAG, "Setting VOC baseline from save state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32,
this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1);
uint16_t states[4];
states[0] = this->voc_baselines_storage_.state0 >> 16;
states[1] = this->voc_baselines_storage_.state0 & 0xFFFF;
states[2] = this->voc_baselines_storage_.state1 >> 16;
states[3] = this->voc_baselines_storage_.state1 & 0xFFFF;
if (!this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE, states, 4)) {
ESP_LOGE(TAG, "Failed to set VOC baseline from saved state");
this->pref_ = global_preferences->make_preference<uint16_t[4]>(hash, true);
this->voc_baseline_time_ = App.get_loop_component_start_time();
if (this->pref_.load(&this->voc_baseline_state_)) {
if (!this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE, this->voc_baseline_state_, 4)) {
ESP_LOGE(TAG, "VOC Baseline State write to sensor failed");
} else {
ESP_LOGV(TAG, "VOC Baseline State loaded");
delay(20);
}
}
}
@@ -288,6 +274,14 @@ void SEN5XComponent::dump_config() {
ESP_LOGCONFIG(TAG, " RH/T acceleration mode: %s",
LOG_STR_ARG(rht_accel_mode_to_string(this->acceleration_mode_.value())));
}
if (this->voc_sensor_) {
char hex_buf[5 * 4];
format_hex_pretty_to(hex_buf, this->voc_baseline_state_, 4, 0);
ESP_LOGCONFIG(TAG,
" Store Baseline: %s\n"
" State: %s\n",
TRUEFALSE(this->store_baseline_), hex_buf);
}
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "PM 1.0", this->pm_1_0_sensor_);
LOG_SENSOR(" ", "PM 2.5", this->pm_2_5_sensor_);
@@ -304,36 +298,6 @@ void SEN5XComponent::update() {
return;
}
// Store baselines after defined interval or if the difference between current and stored baseline becomes too
// much
if (this->store_baseline_ && this->seconds_since_last_store_ > SHORTEST_BASELINE_STORE_INTERVAL) {
if (this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE)) {
// run it a bit later to avoid adding a delay here
this->set_timeout(550, [this]() {
uint16_t states[4];
if (this->read_data(states, 4)) {
uint32_t state0 = states[0] << 16 | states[1];
uint32_t state1 = states[2] << 16 | states[3];
if ((uint32_t) std::abs(static_cast<int32_t>(this->voc_baselines_storage_.state0 - state0)) >
MAXIMUM_STORAGE_DIFF ||
(uint32_t) std::abs(static_cast<int32_t>(this->voc_baselines_storage_.state1 - state1)) >
MAXIMUM_STORAGE_DIFF) {
this->seconds_since_last_store_ = 0;
this->voc_baselines_storage_.state0 = state0;
this->voc_baselines_storage_.state1 = state1;
if (this->pref_.save(&this->voc_baselines_storage_)) {
ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32,
this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1);
} else {
ESP_LOGW(TAG, "Could not store VOC baselines");
}
}
}
});
}
}
if (!this->write_command(SEN5X_CMD_READ_MEASUREMENT)) {
this->status_set_warning();
ESP_LOGD(TAG, "Write error: read measurement (%d)", this->last_error_);
@@ -402,7 +366,29 @@ void SEN5XComponent::update() {
if (this->nox_sensor_ != nullptr) {
this->nox_sensor_->publish_state(nox);
}
this->status_clear_warning();
if (!this->voc_sensor_ || !this->store_baseline_ ||
(App.get_loop_component_start_time() - this->voc_baseline_time_) < SHORTEST_BASELINE_STORE_INTERVAL) {
this->status_clear_warning();
} else {
this->voc_baseline_time_ = App.get_loop_component_start_time();
if (!this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE)) {
this->status_set_warning();
ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL);
} else {
this->set_timeout(20, [this]() {
if (!this->read_data(this->voc_baseline_state_, 4)) {
this->status_set_warning();
ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL);
} else {
if (this->pref_.save(&this->voc_baseline_state_)) {
ESP_LOGD(TAG, "VOC Baseline State saved");
}
this->status_clear_warning();
}
});
}
}
});
}

View File

@@ -24,11 +24,6 @@ enum RhtAccelerationMode : uint16_t {
HIGH_ACCELERATION = 2,
};
struct Sen5xBaselines {
int32_t state0;
int32_t state1;
} PACKED; // NOLINT
struct GasTuning {
uint16_t index_offset;
uint16_t learning_time_offset_hours;
@@ -44,11 +39,9 @@ struct TemperatureCompensation {
uint16_t time_constant;
};
// Shortest time interval of 3H for storing baseline values.
// Shortest time interval of 2H (in milliseconds) for storing baseline values.
// Prevents wear of the flash because of too many write operations
static const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 10800;
// Store anyway if the baseline difference exceeds the max storage diff value
static const uint32_t MAXIMUM_STORAGE_DIFF = 50;
static const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 2 * 60 * 60 * 1000;
class SEN5XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice {
public:
@@ -107,7 +100,8 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri
bool write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning);
bool write_temperature_compensation_(const TemperatureCompensation &compensation);
uint32_t seconds_since_last_store_;
uint16_t voc_baseline_state_[4]{0};
uint32_t voc_baseline_time_;
uint16_t firmware_version_;
ERRORCODE error_code_;
uint8_t serial_number_[4];
@@ -132,7 +126,6 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri
optional<TemperatureCompensation> temperature_compensation_;
ESPPreferenceObject pref_;
std::string product_name_;
Sen5xBaselines voc_baselines_storage_;
};
} // namespace sen5x

View File

@@ -210,6 +210,7 @@ SENSOR_MAP = {
SETTING_MAP = {
CONF_AUTO_CLEANING_INTERVAL: "set_auto_cleaning_interval",
CONF_ACCELERATION_MODE: "set_acceleration_mode",
CONF_STORE_BASELINE: "set_store_baseline",
}

View File

@@ -1,6 +1,7 @@
#include "slow_pwm_output.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#include "esphome/core/gpio.h"
#include "esphome/core/log.h"
namespace esphome {
namespace slow_pwm {
@@ -20,7 +21,9 @@ void SlowPWMOutput::set_output_state_(bool new_state) {
}
if (new_state != current_state_) {
if (this->pin_) {
ESP_LOGV(TAG, "Switching output pin %s to %s", this->pin_->dump_summary().c_str(), ONOFF(new_state));
char pin_summary[GPIO_SUMMARY_MAX_LEN];
this->pin_->dump_summary(pin_summary, sizeof(pin_summary));
ESP_LOGV(TAG, "Switching output pin %s to %s", pin_summary, ONOFF(new_state));
} else {
ESP_LOGV(TAG, "Switching to %s", ONOFF(new_state));
}

View File

@@ -29,6 +29,14 @@ void socket_delay(uint32_t ms) {
// Use esp_delay with a callback that checks if socket data arrived.
// This allows the delay to exit early when socket_wake() is called by
// lwip recv_fn/accept_fn callbacks, reducing socket latency.
//
// When ms is 0, we must use delay(0) because esp_delay(0, callback)
// exits immediately without yielding, which can cause watchdog timeouts
// when the main loop runs in high-frequency mode (e.g., during light effects).
if (ms == 0) {
delay(0);
return;
}
s_socket_woke = false;
esp_delay(ms, []() { return !s_socket_woke; });
}

View File

@@ -1,5 +1,6 @@
#ifdef USE_ESP32_VARIANT_ESP32S3
#include "st7701s.h"
#include "esphome/core/gpio.h"
#include "esphome/core/log.h"
namespace esphome {
@@ -183,8 +184,11 @@ void ST7701S::dump_config() {
LOG_PIN(" DE Pin: ", this->de_pin_);
LOG_PIN(" Reset Pin: ", this->reset_pin_);
size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]);
for (size_t i = 0; i != data_pin_count; i++)
ESP_LOGCONFIG(TAG, " Data pin %d: %s", i, (this->data_pins_[i])->dump_summary().c_str());
char pin_summary[GPIO_SUMMARY_MAX_LEN];
for (size_t i = 0; i != data_pin_count; i++) {
this->data_pins_[i]->dump_summary(pin_summary, sizeof(pin_summary));
ESP_LOGCONFIG(TAG, " Data pin %d: %s", i, pin_summary);
}
ESP_LOGCONFIG(TAG, " SPI Data rate: %dMHz", (unsigned) (this->data_rate_ / 1000000));
}

View File

@@ -1 +1 @@
CODEOWNERS = ["@OttoWinter"]
CODEOWNERS = ["@swoboda1337", "@ssieb"]

View File

@@ -34,7 +34,7 @@ CONFIG_SCHEMA = (
{
cv.Required(CONF_TRIGGER_PIN): pins.internal_gpio_output_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(
CONF_PULSE_TIME, default="10us"
): cv.positive_time_period_microseconds,
@@ -52,12 +52,5 @@ async def to_code(config):
cg.add(var.set_trigger_pin(trigger))
echo = await cg.gpio_pin_expression(config[CONF_ECHO_PIN])
cg.add(var.set_echo_pin(echo))
# 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_timeout_us(config[CONF_TIMEOUT] / (0.000343 / 2)))
cg.add(var.set_pulse_time_us(config[CONF_PULSE_TIME]))

View File

@@ -6,12 +6,11 @@ namespace esphome::ultrasonic {
static const char *const TAG = "ultrasonic.sensor";
static constexpr uint32_t DEBOUNCE_US = 50; // Ignore edges within 50us (noise filtering)
static constexpr uint32_t MEASUREMENT_TIMEOUT_US = 80000; // Maximum time to wait for measurement completion
static constexpr uint32_t START_TIMEOUT_US = 40000; // Maximum time to wait for echo pulse to start
void IRAM_ATTR UltrasonicSensorStore::gpio_intr(UltrasonicSensorStore *arg) {
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 = true;
} else {
@@ -38,6 +37,7 @@ void UltrasonicSensorComponent::setup() {
this->trigger_pin_->digital_write(false);
this->trigger_pin_isr_ = this->trigger_pin_->to_isr();
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);
}
@@ -53,29 +53,55 @@ void UltrasonicSensorComponent::loop() {
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) {
uint32_t pulse_duration = this->store_.echo_end_us - this->store_.echo_start_us;
ESP_LOGV(TAG, "Echo took %" PRIu32 "us", pulse_duration);
float result = UltrasonicSensorComponent::us_to_m(pulse_duration);
ESP_LOGD(TAG, "'%s' - Got distance: %.3f m", this->name_.c_str(), result);
float result;
if (this->store_.echo_start) {
uint32_t pulse_duration = this->store_.echo_end_us - this->store_.echo_start_us;
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->measurement_pending_ = false;
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() {
LOG_SENSOR("", "Ultrasonic Sensor", this);
LOG_PIN(" Echo Pin: ", this->echo_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);
}

View File

@@ -11,6 +11,8 @@ namespace esphome::ultrasonic {
struct UltrasonicSensorStore {
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_end_us{0};
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; }
/// 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)
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_;
InternalGPIOPin *echo_pin_;
UltrasonicSensorStore store_;
uint32_t timeout_us_{};
uint32_t pulse_time_us_{};
uint32_t measurement_start_us_{0};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -527,7 +527,19 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, J
memcpy(p, name.c_str(), name_len);
p[name_len] = '\0';
root[ESPHOME_F("id")] = id_buf;
// name_id: new format {prefix}/{device?}/{name} - frontend should prefer this
// Remove in 2026.8.0 when id switches to new format permanently
root[ESPHOME_F("name_id")] = id_buf;
// id: old format {prefix}-{object_id} for backward compatibility
// Will switch to new format in 2026.8.0
char legacy_buf[ESPHOME_DOMAIN_MAX_LEN + 1 + OBJECT_ID_MAX_LEN];
char *lp = legacy_buf;
memcpy(lp, prefix, prefix_len);
lp += prefix_len;
*lp++ = '-';
obj->write_object_id_to(lp, sizeof(legacy_buf) - (lp - legacy_buf));
root[ESPHOME_F("id")] = legacy_buf;
if (start_config == DETAIL_ALL) {
root[ESPHOME_F("domain")] = prefix;

View File

@@ -1383,6 +1383,12 @@ void WiFiComponent::check_connecting_finished(uint32_t now) {
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;
}
@@ -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) {
// Guard: not for hidden networks (may not appear in scan)
const WiFiAP *selected = this->get_selected_sta_();

View File

@@ -618,6 +618,11 @@ class WiFiComponent : public Component {
/// Free scan results memory unless a component needs them
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
static void wifi_event_callback(System_Event_t *event);
void wifi_scan_done_callback_(void *arg, STATUS status);
@@ -721,6 +726,16 @@ class WiFiComponent : public Component {
SemaphoreHandle_t high_performance_semaphore_{nullptr};
#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)
Trigger<> *connect_trigger_{new Trigger<>()};
Trigger<> *disconnect_trigger_{new Trigger<>()};

View File

@@ -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) {
switch (event->event) {
case EVENT_STAMODE_CONNECTED: {
@@ -512,9 +516,9 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
#endif
s_sta_connected = true;
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
for (auto *listener : global_wifi_component->connect_state_listeners_) {
listener->on_wifi_connect_state(StringRef(it.ssid, it.ssid_len), it.bssid);
}
// Defer listener notification until state machine reaches STA_CONNECTED
// This ensures wifi.connected condition returns true in listener automations
global_wifi_component->pending_.connect_state = true;
#endif
// 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)
@@ -698,6 +702,10 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
if (!this->wifi_mode_(true, {}))
return false;
// Reset scan_done_ before starting new scan to prevent stale flag from previous scan
// (e.g., roaming scan completed just before unexpected disconnect)
this->scan_done_ = false;
struct scan_config config {};
memset(&config, 0, sizeof(config));
config.ssid = nullptr;
@@ -752,7 +760,10 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) {
if (status != OK) {
ESP_LOGV(TAG, "Scan failed: %d", status);
this->retry_connect();
// Don't call retry_connect() here - this callback runs in SDK system context
// where yield() cannot be called. Instead, just set scan_done_ and let
// check_scanning_finished() handle the empty scan_result_ from loop context.
this->scan_done_ = true;
return;
}

View File

@@ -14,6 +14,7 @@
#include <algorithm>
#include <cinttypes>
#include <memory>
#include <utility>
#ifdef USE_WIFI_WPA2_EAP
#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1)
@@ -709,6 +710,9 @@ void WiFiComponent::wifi_loop_() {
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) {
esp_err_t err;
if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_START) {
@@ -742,9 +746,9 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
#endif
s_sta_connected = true;
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
for (auto *listener : this->connect_state_listeners_) {
listener->on_wifi_connect_state(StringRef(it.ssid, it.ssid_len), it.bssid);
}
// Defer listener notification until state machine reaches STA_CONNECTED
// This ensures wifi.connected condition returns true in listener automations
this->pending_.connect_state = true;
#endif
// 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)
@@ -828,16 +832,29 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
uint16_t number = it.number;
scan_result_.init(number);
// Process one record at a time to avoid large buffer allocation
wifi_ap_record_t record;
#ifdef USE_ESP32_HOSTED
// getting records one at a time fails on P4 with hosted esp32 WiFi coprocessor
// Presumably an upstream bug, work-around by getting all records at once
auto records = std::make_unique<wifi_ap_record_t[]>(number);
err = esp_wifi_scan_get_ap_records(&number, records.get());
if (err != ESP_OK) {
esp_wifi_clear_ap_list();
ESP_LOGW(TAG, "esp_wifi_scan_get_ap_records failed: %s", esp_err_to_name(err));
return;
}
for (uint16_t i = 0; i < number; i++) {
wifi_ap_record_t &record = records[i];
#else
// Process one record at a time to avoid large buffer allocation
for (uint16_t i = 0; i < number; i++) {
wifi_ap_record_t record;
err = esp_wifi_scan_get_ap_record(&record);
if (err != ESP_OK) {
ESP_LOGW(TAG, "esp_wifi_scan_get_ap_record failed: %s", esp_err_to_name(err));
esp_wifi_clear_ap_list(); // Free remaining records not yet retrieved
break;
}
#endif // USE_ESP32_HOSTED
bssid_t bssid;
std::copy(record.bssid, record.bssid + 6, bssid.begin());
std::string ssid(reinterpret_cast<const char *>(record.ssid));

View File

@@ -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) {
switch (event->event_id) {
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
// wifi_sta_connect_status_() also checks got_ipv4_address_
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
for (auto *listener : this->connect_state_listeners_) {
listener->on_wifi_connect_state(StringRef(it.ssid, it.ssid_len), it.bssid);
}
// Defer listener notification until state machine reaches STA_CONNECTED
// This ensures wifi.connected condition returns true in listener automations
this->pending_.connect_state = true;
#endif
// For static IP configurations, GOT_IP event may not fire, so set connected state here
#ifdef USE_WIFI_MANUAL_IP
@@ -649,6 +652,10 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
if (!this->wifi_mode_(true, {}))
return false;
// Reset scan_done_ before starting new scan to prevent stale flag from previous scan
// (e.g., roaming scan completed just before unexpected disconnect)
this->scan_done_ = false;
// need to use WiFi because of WiFiScanClass allocations :(
int16_t err = WiFi.scanNetworks(true, true, passive, 200);
if (err != WIFI_SCAN_RUNNING) {

View File

@@ -240,6 +240,10 @@ network::IPAddress WiFiComponent::wifi_dns_ip_(int num) {
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_() {
// Handle scan completion
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;
ESP_LOGV(TAG, "Connected");
#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
String ssid = WiFi.SSID();
bssid_t bssid = this->wifi_bssid();
for (auto *listener : this->connect_state_listeners_) {
listener->on_wifi_connect_state(StringRef(ssid.c_str(), ssid.length()), bssid);
}
// Defer listener notification until state machine reaches STA_CONNECTED
// This ensures wifi.connected condition returns true in listener automations
this->pending_.connect_state = true;
#endif
// 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)

View File

@@ -4,7 +4,7 @@ from enum import Enum
from esphome.enum import StrEnum
__version__ = "2026.1.1"
__version__ = "2026.1.4"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = (

View File

@@ -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
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
App.scheduler.set_timeout(this, static_cast<const char *>(nullptr), timeout, std::move(f));
}

View File

@@ -494,11 +494,15 @@ class Component {
/// Defer a callback to the next loop() call.
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.
// Remove before 2026.7.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 char *name); // NOLINT
bool cancel_defer(uint32_t id); // NOLINT
// Ordered for optimal packing on 32-bit systems
const LogString *component_source_{nullptr};

View File

@@ -42,6 +42,7 @@
#define USE_DEVICES
#define USE_DISPLAY
#define USE_ENTITY_ICON
#define USE_ESP32_HOSTED
#define USE_ESP32_IMPROV_STATE_CALLBACK
#define USE_EVENT
#define USE_FAN

View File

@@ -154,6 +154,12 @@ def check_error(data: list[int] | bytes, expect: int | list[int] | None) -> None
"""
if not expect:
return
if not data:
raise OTAError(
"Error: Device closed connection without responding. "
"This may indicate the device ran out of memory, "
"a network issue, or the connection was interrupted."
)
dat = data[0]
if dat == RESPONSE_ERROR_MAGIC:
raise OTAError("Error: Invalid magic byte")

View File

@@ -0,0 +1,18 @@
remote_receiver:
id: ir_receiver
pin: ${rx_pin}
# Test various hardware types with transmitter/receiver using infrared platform
infrared:
# Infrared receiver
- platform: ir_rf_proxy
id: ir_rx
name: "IR Receiver"
remote_receiver_id: ir_receiver
# RF 900MHz receiver
- platform: ir_rf_proxy
id: rf_900_rx
name: "RF 900 Receiver"
frequency: 900 MHz
remote_receiver_id: ir_receiver

View File

@@ -0,0 +1,19 @@
remote_transmitter:
id: ir_transmitter
pin: ${tx_pin}
carrier_duty_percent: 50%
# Test various hardware types with transmitter/receiver using infrared platform
infrared:
# Infrared transmitter
- platform: ir_rf_proxy
id: ir_tx
name: "IR Transmitter"
remote_transmitter_id: ir_transmitter
# RF 433MHz transmitter
- platform: ir_rf_proxy
id: rf_433_tx
name: "RF 433 Transmitter"
frequency: 433 MHz
remote_transmitter_id: ir_transmitter

View File

@@ -1,42 +1,7 @@
network:
wifi:
ssid: MySSID
password: password1
api:
remote_transmitter:
id: ir_transmitter
pin: ${tx_pin}
carrier_duty_percent: 50%
remote_receiver:
id: ir_receiver
pin: ${rx_pin}
# Test various hardware types with transmitter/receiver using infrared platform
infrared:
# Infrared transmitter
- platform: ir_rf_proxy
id: ir_tx
name: "IR Transmitter"
remote_transmitter_id: ir_transmitter
# Infrared receiver
- platform: ir_rf_proxy
id: ir_rx
name: "IR Receiver"
remote_receiver_id: ir_receiver
# RF 433MHz transmitter
- platform: ir_rf_proxy
id: rf_433_tx
name: "RF 433 Transmitter"
frequency: 433 MHz
remote_transmitter_id: ir_transmitter
# RF 900MHz receiver
- platform: ir_rf_proxy
id: rf_900_rx
name: "RF 900 Receiver"
frequency: 900 MHz
remote_receiver_id: ir_receiver

View File

@@ -0,0 +1,7 @@
substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
packages:
common: !include common.yaml
rx: !include common-rx.yaml

View File

@@ -0,0 +1,7 @@
substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
packages:
common: !include common.yaml
rx: !include common-rx.yaml

View File

@@ -0,0 +1,7 @@
substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
packages:
common: !include common.yaml
rx: !include common-rx.yaml

View File

@@ -0,0 +1,7 @@
substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
packages:
common: !include common.yaml
tx: !include common-tx.yaml

View File

@@ -0,0 +1,7 @@
substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
packages:
common: !include common.yaml
tx: !include common-tx.yaml

View File

@@ -0,0 +1,7 @@
substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
packages:
common: !include common.yaml
tx: !include common-tx.yaml

View File

@@ -0,0 +1,8 @@
substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
packages:
common: !include common.yaml
rx: !include common-rx.yaml
tx: !include common-tx.yaml

View File

@@ -2,4 +2,7 @@ substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
<<: !include common.yaml
packages:
common: !include common.yaml
rx: !include common-rx.yaml
tx: !include common-tx.yaml

View File

@@ -2,4 +2,7 @@ substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
<<: !include common.yaml
packages:
common: !include common.yaml
rx: !include common-rx.yaml
tx: !include common-tx.yaml

View File

@@ -2,4 +2,7 @@ substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
<<: !include common.yaml
packages:
common: !include common.yaml
rx: !include common-rx.yaml
tx: !include common-tx.yaml

View File

@@ -197,6 +197,9 @@ lvgl:
- lvgl.label.update:
id: msgbox_label
text: Unloaded
- lvgl.label.update:
id: msgbox_label
text: "" # Empty text
on_all_events:
logger.log:
format: "Event %s"

View 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

View File

@@ -20,6 +20,9 @@ globals:
- id: retry_counter
type: int
initial_value: '0'
- id: defer_counter
type: int
initial_value: '0'
- id: tests_done
type: bool
initial_value: 'false'
@@ -136,11 +139,49 @@ script:
App.scheduler.cancel_retry(component1, 6002U);
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
then:
- lambda: |-
ESP_LOGI("test", "Final results - Timeouts: %d, Intervals: %d, Retries: %d",
id(timeout_counter), id(interval_counter), id(retry_counter));
ESP_LOGI("test", "Final results - Timeouts: %d, Intervals: %d, Retries: %d, Defers: %d",
id(timeout_counter), id(interval_counter), id(retry_counter), id(defer_counter));
sensor:
- platform: template

View File

@@ -19,6 +19,7 @@ async def test_scheduler_numeric_id_test(
timeout_count = 0
interval_count = 0
retry_count = 0
defer_count = 0
# Events for each test completion
numeric_timeout_1001_fired = asyncio.Event()
@@ -33,6 +34,9 @@ async def test_scheduler_numeric_id_test(
max_id_timeout_fired = asyncio.Event()
numeric_retry_done = 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()
# Track interval counts
@@ -40,7 +44,7 @@ async def test_scheduler_numeric_id_test(
numeric_retry_count = 0
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
# Strip ANSI color codes
@@ -105,15 +109,27 @@ async def test_scheduler_numeric_id_test(
elif "Cancelled numeric retry 6002" in clean_line:
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
elif "Final results" in clean_line:
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:
timeout_count = int(match.group(1))
interval_count = int(match.group(2))
retry_count = int(match.group(3))
defer_count = int(match.group(4))
final_results_logged.set()
async with (
@@ -201,6 +217,23 @@ async def test_scheduler_numeric_id_test(
"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
try:
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, (
f"Expected at least 2 retry attempts, got {retry_count}"
)
assert defer_count >= 2, f"Expected at least 2 defer fires, got {defer_count}"

View File

@@ -192,6 +192,20 @@ def test_check_error_unexpected_response() -> None:
espota2.check_error([0x7F], [espota2.RESPONSE_OK, espota2.RESPONSE_AUTH_OK])
def test_check_error_empty_data() -> None:
"""Test check_error raises error when device closes connection without responding."""
with pytest.raises(
espota2.OTAError, match="Device closed connection without responding"
):
espota2.check_error([], [espota2.RESPONSE_OK])
# Also test with empty bytes
with pytest.raises(
espota2.OTAError, match="Device closed connection without responding"
):
espota2.check_error(b"", [espota2.RESPONSE_OK])
def test_send_check_with_various_data_types(mock_socket: Mock) -> None:
"""Test send_check handles different data types."""