mirror of
https://github.com/esphome/esphome.git
synced 2026-02-04 06:29:40 -07:00
Compare commits
11 Commits
http_reque
...
beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c91d72403 | ||
|
|
0a63fc6f05 | ||
|
|
50e739ee8e | ||
|
|
6c84f20491 | ||
|
|
a68506f924 | ||
|
|
a20d42ca0b | ||
|
|
4ec8846198 | ||
|
|
40ea65b1c0 | ||
|
|
f7937ef952 | ||
|
|
d6bf137026 | ||
|
|
ed9a672f44 |
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.2
|
PROJECT_NUMBER = 2026.1.3
|
||||||
|
|
||||||
# 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
|
||||||
|
|||||||
@@ -131,6 +131,10 @@ 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.
|
||||||
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;
|
||||||
@@ -167,17 +171,23 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int available_data = stream_ptr->available();
|
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) {
|
if (bufsize == 0) {
|
||||||
this->duration_ms += (millis() - start);
|
this->duration_ms += (millis() - start);
|
||||||
// Check if we've read all expected content
|
// Check if we've read all expected content (only valid when content_length is known and not SIZE_MAX)
|
||||||
if (this->bytes_read_ >= this->content_length) {
|
// For chunked encoding (content_length == SIZE_MAX), we can't use this check
|
||||||
|
if (this->content_length > 0 && this->bytes_read_ >= this->content_length) {
|
||||||
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
|
||||||
|
// 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()) {
|
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
|
return 0; // No data yet, caller should retry
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,6 +152,8 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
|
|||||||
}
|
}
|
||||||
|
|
||||||
container->feed_wdt();
|
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->content_length = esp_http_client_fetch_headers(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);
|
||||||
@@ -220,14 +222,22 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
|
|||||||
//
|
//
|
||||||
// We normalize to HttpContainer::read() contract:
|
// We normalize to HttpContainer::read() contract:
|
||||||
// > 0: bytes read
|
// > 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
|
// < 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) {
|
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
|
||||||
if (this->bytes_read_ >= this->content_length) {
|
// Skip this check when content_length is 0 (chunked transfer encoding or unknown length)
|
||||||
|
// For chunked responses, esp_http_client_read() will return 0 when all data is received
|
||||||
|
if (this->content_length > 0 && this->bytes_read_ >= this->content_length) {
|
||||||
return 0; // All content read successfully
|
return 0; // All content read successfully
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,7 +252,13 @@ int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
|
|||||||
return read_len_or_error;
|
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) {
|
if (read_len_or_error == 0) {
|
||||||
return HTTP_ERROR_CONNECTION_CLOSED;
|
return HTTP_ERROR_CONNECTION_CLOSED;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -451,7 +451,7 @@ void LD2450Component::handle_periodic_data_() {
|
|||||||
int16_t ty = 0;
|
int16_t ty = 0;
|
||||||
int16_t td = 0;
|
int16_t td = 0;
|
||||||
int16_t ts = 0;
|
int16_t ts = 0;
|
||||||
int16_t angle = 0;
|
float angle = 0;
|
||||||
uint8_t index = 0;
|
uint8_t index = 0;
|
||||||
Direction direction{DIRECTION_UNDEFINED};
|
Direction direction{DIRECTION_UNDEFINED};
|
||||||
bool is_moving = false;
|
bool is_moving = false;
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
|
|||||||
],
|
],
|
||||||
icon=ICON_FORMAT_TEXT_ROTATION_ANGLE_UP,
|
icon=ICON_FORMAT_TEXT_ROTATION_ANGLE_UP,
|
||||||
unit_of_measurement=UNIT_DEGREES,
|
unit_of_measurement=UNIT_DEGREES,
|
||||||
|
accuracy_decimals=1,
|
||||||
),
|
),
|
||||||
cv.Optional(CONF_DISTANCE): sensor.sensor_schema(
|
cv.Optional(CONF_DISTANCE): sensor.sensor_schema(
|
||||||
device_class=DEVICE_CLASS_DISTANCE,
|
device_class=DEVICE_CLASS_DISTANCE,
|
||||||
|
|||||||
@@ -155,6 +155,9 @@ void MHZ19Component::dump_config() {
|
|||||||
case MHZ19_DETECTION_RANGE_0_10000PPM:
|
case MHZ19_DETECTION_RANGE_0_10000PPM:
|
||||||
range_str = "0 to 10000ppm";
|
range_str = "0 to 10000ppm";
|
||||||
break;
|
break;
|
||||||
|
default:
|
||||||
|
range_str = "default";
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
ESP_LOGCONFIG(TAG, " Detection range: %s", range_str);
|
ESP_LOGCONFIG(TAG, " Detection range: %s", range_str);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,14 @@ void socket_delay(uint32_t ms) {
|
|||||||
// Use esp_delay with a callback that checks if socket data arrived.
|
// 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
|
// This allows the delay to exit early when socket_wake() is called by
|
||||||
// lwip recv_fn/accept_fn callbacks, reducing socket latency.
|
// 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;
|
s_socket_woke = false;
|
||||||
esp_delay(ms, []() { return !s_socket_woke; });
|
esp_delay(ms, []() { return !s_socket_woke; });
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -527,7 +527,19 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, J
|
|||||||
memcpy(p, name.c_str(), name_len);
|
memcpy(p, name.c_str(), name_len);
|
||||||
p[name_len] = '\0';
|
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) {
|
if (start_config == DETAIL_ALL) {
|
||||||
root[ESPHOME_F("domain")] = prefix;
|
root[ESPHOME_F("domain")] = prefix;
|
||||||
|
|||||||
@@ -756,7 +756,10 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) {
|
|||||||
|
|
||||||
if (status != OK) {
|
if (status != OK) {
|
||||||
ESP_LOGV(TAG, "Scan failed: %d", status);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from enum import Enum
|
|||||||
|
|
||||||
from esphome.enum import StrEnum
|
from esphome.enum import StrEnum
|
||||||
|
|
||||||
__version__ = "2026.1.2"
|
__version__ = "2026.1.3"
|
||||||
|
|
||||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||||
|
|||||||
@@ -154,6 +154,12 @@ def check_error(data: list[int] | bytes, expect: int | list[int] | None) -> None
|
|||||||
"""
|
"""
|
||||||
if not expect:
|
if not expect:
|
||||||
return
|
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]
|
dat = data[0]
|
||||||
if dat == RESPONSE_ERROR_MAGIC:
|
if dat == RESPONSE_ERROR_MAGIC:
|
||||||
raise OTAError("Error: Invalid magic byte")
|
raise OTAError("Error: Invalid magic byte")
|
||||||
|
|||||||
@@ -192,6 +192,20 @@ def test_check_error_unexpected_response() -> None:
|
|||||||
espota2.check_error([0x7F], [espota2.RESPONSE_OK, espota2.RESPONSE_AUTH_OK])
|
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:
|
def test_send_check_with_various_data_types(mock_socket: Mock) -> None:
|
||||||
"""Test send_check handles different data types."""
|
"""Test send_check handles different data types."""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user