1
0
mirror of https://github.com/esphome/esphome.git synced 2026-02-10 01:32:06 +00:00

Compare commits

..

86 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
Jonathan Swoboda
44e624d7a7 Merge pull request #13459 from esphome/bump-2026.1.1
2026.1.1
2026-01-22 18:19:18 -05:00
Jonathan Swoboda
f938de16af Bump version to 2026.1.1 2026-01-22 16:30:52 -05:00
J. Nick Koston
ec791063b3 [time] Always call time sync callbacks even when time unchanged (#13456) 2026-01-22 16:30:52 -05:00
Jonathan Swoboda
fb984cd052 [aqi] Remove unit_of_measurement to fix Home Assistant warning (#13448)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 16:30:52 -05:00
Jonathan Swoboda
85181779d1 [fingerprint_grow] Use buffer-based dump_summary to fix deprecation warnings (#13447)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 16:30:52 -05:00
J. Nick Koston
95b23702e4 [wifi] Fix stale error_from_callback_ causing immediate connection failures (#13450) 2026-01-22 16:30:52 -05:00
J. Nick Koston
95eebcd74f [api] Limit Nagle batching for log messages to reduce LWIP buffer pressure (#13439) 2026-01-22 16:30:52 -05:00
Rene Guca
3c3d5c2fca [dht] Increase delay for DHT22 and RHT03 (#13446) 2026-01-22 16:30:52 -05:00
J. Nick Koston
811ac81320 [http_request] Fix OTA failures on ESP8266/Arduino by making read semantics consistent (#13435)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-22 16:30:52 -05:00
J. Nick Koston
f01bd68a4b [spi] Fix display init failure by marking displays as write-only for half-duplex mode (#13431) 2026-01-22 16:30:52 -05:00
J. Nick Koston
5433c0f707 [wifi] Fix bk72xx manual_ip preventing API connection (#13426) 2026-01-22 16:30:52 -05:00
Jonathan Swoboda
b06cce9eeb [esp32] Add warning for experimental 400MHz on ESP32-P4 (#13433)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 16:30:52 -05:00
Jonathan Swoboda
65bcfee035 [http_request] Fix verify_ssl: false not working on ESP32 (#13422)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 16:30:52 -05:00
Copilot
9261b9ecaa [lvgl] Validate LVGL dropdown symbols require Unicode codepoint ≥ 0x100 (#13394)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-01-22 16:30:52 -05:00
J. Nick Koston
6725e6c01e [wifi] Process scan results one at a time to avoid heap allocation (#13400) 2026-01-22 16:30:52 -05:00
Jonathan Swoboda
15f0986a59 Merge pull request #13406 from esphome/bump-2026.1.0
2026.1.0
2026-01-20 22:43:06 -05:00
Jonathan Swoboda
90edf32acf Bump version to 2026.1.0 2026-01-20 21:15:02 -05:00
Jonathan Swoboda
055c00f1ac Merge pull request #13396 from esphome/bump-2026.1.0b4
2026.1.0b4
2026-01-20 17:01:36 -05:00
Jonathan Swoboda
7dc40881e2 Bump version to 2026.1.0b4 2026-01-20 15:55:03 -05:00
J. Nick Koston
b04373687e [wifi_info] Fix missing state when both IP+DNS or SSID+BSSID configure (#13385) 2026-01-20 15:55:03 -05:00
Jonathan Swoboda
b89c127f62 [x9c] Fix potentiometer unable to decrement (#13382)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 15:55:03 -05:00
Jonathan Swoboda
47dc5d0a1f [core] Fix state leakage and module caching when processing multiple configurations (#13368)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 15:55:03 -05:00
J. Nick Koston
21886dd3ac [api] Fix truncation of Home Assistant attributes longer than 255 characters (#13348) 2026-01-20 15:55:03 -05:00
J. Nick Koston
85a5a26519 [network] Fix IPAddress::str_to() to lowercase IPv6 hex digits (#13325) 2026-01-20 15:55:03 -05:00
Jonathan Swoboda
77df3933db Merge pull request #13309 from esphome/bump-2026.1.0b3
2026.1.0b3
2026-01-16 23:45:26 -05:00
Jonathan Swoboda
19514ccdf4 Bump version to 2026.1.0b3 2026-01-16 23:05:59 -05:00
Mike Ford
2947642ca5 [http_request] Unable to handle chunked responses (#7884)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-01-16 23:05:59 -05:00
Stuart Parmenter
60e333db08 [hub75] Bump esp-hub75 version to 0.3.0 (#13243) 2026-01-16 23:05:59 -05:00
J. Nick Koston
d8463f4813 [hmac_sha256] Replace unsafe sprintf with format_hex_to (#13290) 2026-01-16 23:05:59 -05:00
mrtoy-me
e1800d2fe2 [ntc, resistance] change log level to verbose (#13268) 2026-01-16 23:05:59 -05:00
J. Nick Koston
50aa4b1992 [esp32_ble_client] Reduce GATT data event logging to prevent firmware update failures (#13252) 2026-01-16 23:05:59 -05:00
J. Nick Koston
edb303e495 [api] Fix clock conflicts when multiple clients connected to homeassistant time (#13253) 2026-01-16 23:05:59 -05:00
J. Nick Koston
973fc4c5dc [dallas_temp] Use const char* for set_timeout to fix deprecation warning and heap churn (#13250) 2026-01-16 23:05:59 -05:00
J. Nick Koston
f88e8fc43b [sprinkler] Fix scheduler deprecation warnings and heap churn with FixedVector (#13251) 2026-01-16 23:05:59 -05:00
Jonathan Swoboda
c4c31a2e8e Merge branch 'release' into beta 2026-01-16 22:49:38 -05:00
Jonathan Swoboda
e6790f0042 Merge pull request #13308 from esphome/bump-2025.12.7
2025.12.7
2026-01-16 22:49:26 -05:00
Jonathan Swoboda
ec7f72e280 Bump version to 2025.12.7 2026-01-16 22:24:05 -05:00
J. Nick Koston
6f29dbd6f1 [api] Use subtraction for protobuf bounds checking (#13306) 2026-01-16 22:24:05 -05:00
Kevin Ahrendt
9caf78aa7e [i2s_audio] Bugfix: Buffer overflow in software volume control (#13190) 2026-01-16 22:24:05 -05:00
Jonathan Swoboda
c2737ca3bb Merge pull request #13247 from esphome/bump-2026.1.0b2
2026.1.0b2
2026-01-15 16:24:08 -05:00
Jonathan Swoboda
c151b2da67 Bump version to 2026.1.0b2 2026-01-15 15:26:04 -05:00
J. Nick Koston
dacd185afb [web_server][captive_portal] Change default compression from Brotli to gzip (#13246) 2026-01-15 15:26:04 -05:00
John Stenger
f88cf1b83a [qr_code] Allocate and free memory for QR code buffer (#13161)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-01-15 15:26:04 -05:00
Jonathan Swoboda
3f6412ba07 [safe_mode] Detect bootloader rollback support at runtime (#13230)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 15:26:04 -05:00
J. Nick Koston
1ad0969099 [core] Fix ESP32-S2/S3 hardware SHA crash by aligning HashBase digest buffer (#13234)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-01-15 15:26:04 -05:00
J. Nick Koston
737c2b8732 [core] Fix platform subcomponents not filtering source files (#13208) 2026-01-15 15:26:04 -05:00
J. Nick Koston
9030dc9d4e [api] Fix state updates being sent to clients that did not subscribe (#13237) 2026-01-15 15:26:04 -05:00
J. Nick Koston
0b5a3506cc [core] Optimize and normalize entity state publishing logs with >> format (#13236) 2026-01-15 15:26:04 -05:00
Clyde Stubbs
3c63ff5e36 [image] Correctly handle dimensions in physical units (#13209) 2026-01-15 15:26:04 -05:00
159 changed files with 10591 additions and 9430 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.0b1
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

@@ -1,5 +1,6 @@
# PYTHON_ARGCOMPLETE_OK
import argparse
from collections.abc import Callable
from datetime import datetime
import functools
import getpass
@@ -931,11 +932,21 @@ def command_dashboard(args: ArgsProtocol) -> int | None:
return dashboard.start_dashboard(args)
def command_update_all(args: ArgsProtocol) -> int | None:
def run_multiple_configs(
files: list, command_builder: Callable[[str], list[str]]
) -> int:
"""Run a command for each configuration file in a subprocess.
Args:
files: List of configuration files to process.
command_builder: Callable that takes a file path and returns a command list.
Returns:
Number of failed files.
"""
import click
success = {}
files = list_yaml_files(args.configuration)
twidth = 60
def print_bar(middle_text):
@@ -945,17 +956,19 @@ def command_update_all(args: ArgsProtocol) -> int | None:
safe_print(f"{half_line}{middle_text}{half_line}")
for f in files:
safe_print(f"Updating {color(AnsiFore.CYAN, str(f))}")
f_path = Path(f) if not isinstance(f, Path) else f
if any(f_path.name == x for x in SECRETS_FILES):
_LOGGER.warning("Skipping secrets file %s", f_path)
continue
safe_print(f"Processing {color(AnsiFore.CYAN, str(f))}")
safe_print("-" * twidth)
safe_print()
if CORE.dashboard:
rc = run_external_process(
"esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA"
)
else:
rc = run_external_process(
"esphome", "run", f, "--no-logs", "--device", "OTA"
)
cmd = command_builder(f)
rc = run_external_process(*cmd)
if rc == 0:
print_bar(f"[{color(AnsiFore.BOLD_GREEN, 'SUCCESS')}] {str(f)}")
success[f] = True
@@ -970,6 +983,8 @@ def command_update_all(args: ArgsProtocol) -> int | None:
print_bar(f"[{color(AnsiFore.BOLD_WHITE, 'SUMMARY')}]")
failed = 0
for f in files:
if f not in success:
continue # Skipped file
if success[f]:
safe_print(f" - {str(f)}: {color(AnsiFore.GREEN, 'SUCCESS')}")
else:
@@ -978,6 +993,17 @@ def command_update_all(args: ArgsProtocol) -> int | None:
return failed
def command_update_all(args: ArgsProtocol) -> int | None:
files = list_yaml_files(args.configuration)
def build_command(f):
if CORE.dashboard:
return ["esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA"]
return ["esphome", "run", f, "--no-logs", "--device", "OTA"]
return run_multiple_configs(files, build_command)
def command_idedata(args: ArgsProtocol, config: ConfigType) -> int:
import json
@@ -1528,38 +1554,48 @@ def run_esphome(argv):
_LOGGER.info("ESPHome %s", const.__version__)
for conf_path in args.configuration:
conf_path = Path(conf_path)
if any(conf_path.name == x for x in SECRETS_FILES):
_LOGGER.warning("Skipping secrets file %s", conf_path)
continue
# Multiple configurations: use subprocesses to avoid state leakage
# between compilations (e.g., LVGL touchscreen state in module globals)
if len(args.configuration) > 1:
# Build command by reusing argv, replacing all configs with single file
# argv[0] is the program path, skip it since we prefix with "esphome"
def build_command(f):
return (
["esphome"]
+ [arg for arg in argv[1:] if arg not in args.configuration]
+ [str(f)]
)
CORE.config_path = conf_path
CORE.dashboard = args.dashboard
return run_multiple_configs(args.configuration, build_command)
# For logs command, skip updating external components
skip_external = args.command == "logs"
config = read_config(
dict(args.substitution) if args.substitution else {},
skip_external_update=skip_external,
)
if config is None:
return 2
CORE.config = config
# Single configuration
conf_path = Path(args.configuration[0])
if any(conf_path.name == x for x in SECRETS_FILES):
_LOGGER.warning("Skipping secrets file %s", conf_path)
return 0
if args.command not in POST_CONFIG_ACTIONS:
safe_print(f"Unknown command {args.command}")
CORE.config_path = conf_path
CORE.dashboard = args.dashboard
try:
rc = POST_CONFIG_ACTIONS[args.command](args, config)
except EsphomeError as e:
_LOGGER.error(e, exc_info=args.verbose)
return 1
if rc != 0:
return rc
# For logs command, skip updating external components
skip_external = args.command == "logs"
config = read_config(
dict(args.substitution) if args.substitution else {},
skip_external_update=skip_external,
)
if config is None:
return 2
CORE.config = config
CORE.reset()
return 0
if args.command not in POST_CONFIG_ACTIONS:
safe_print(f"Unknown command {args.command}")
return 1
try:
return POST_CONFIG_ACTIONS[args.command](args, config)
except EsphomeError as e:
_LOGGER.error(e, exc_info=args.verbose)
return 1
def main():

View File

@@ -31,7 +31,8 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) {
this->last_update_ = millis();
if (state != this->current_state_) {
auto prev_state = this->current_state_;
ESP_LOGD(TAG, "Set state to: %s, previous: %s", LOG_STR_ARG(alarm_control_panel_state_to_string(state)),
ESP_LOGD(TAG, "'%s' >> %s (was %s)", this->get_name().c_str(),
LOG_STR_ARG(alarm_control_panel_state_to_string(state)),
LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state)));
this->current_state_ = state;
// Single state callback - triggers check get_state() for specific states

View File

@@ -1712,17 +1712,16 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes
}
// Create null-terminated state for callback (parse_number needs null-termination)
// HA state max length is 255, so 256 byte buffer covers all cases
char state_buf[256];
size_t copy_len = msg.state.size();
if (copy_len >= sizeof(state_buf)) {
copy_len = sizeof(state_buf) - 1; // Truncate to leave space for null terminator
// HA state max length is 255 characters, but attributes can be much longer
// Use stack buffer for common case (states), heap fallback for large attributes
size_t state_len = msg.state.size();
SmallBufferWithHeapFallback<256> state_buf_alloc(state_len + 1);
char *state_buf = reinterpret_cast<char *>(state_buf_alloc.get());
if (state_len > 0) {
memcpy(state_buf, msg.state.c_str(), state_len);
}
if (copy_len > 0) {
memcpy(state_buf, msg.state.c_str(), copy_len);
}
state_buf[copy_len] = '\0';
it.callback(StringRef(state_buf, copy_len));
state_buf[state_len] = '\0';
it.callback(StringRef(state_buf, state_len));
}
}
#endif
@@ -1845,23 +1844,8 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
return false;
}
// Toggle Nagle's algorithm based on message type to prevent log messages from
// filling the TCP send buffer and crowding out important state updates.
//
// This honors the `no_delay` proto option - SubscribeLogsResponse is the only
// message with `option (no_delay) = false;` in api.proto, indicating it should
// allow Nagle coalescing. This option existed since 2019 but was never implemented.
//
// - Log messages: Enable Nagle (NODELAY=false) so small log packets coalesce
// into fewer, larger packets. They flush naturally via TCP delayed ACK timer
// (~200ms), buffer filling, or when a state update triggers a flush.
//
// - All other messages (state updates, responses): Disable Nagle (NODELAY=true)
// for immediate delivery. These are time-sensitive and should not be delayed.
//
// This must be done proactively BEFORE the buffer fills up - checking buffer
// state here would be too late since we'd already be in a degraded state.
this->helper_->set_nodelay(!is_log_message);
// Set TCP_NODELAY based on message type - see set_nodelay_for_message() for details
this->helper_->set_nodelay_for_message(is_log_message);
APIError err = this->helper_->write_protobuf_packet(message_type, buffer);
if (err == APIError::WOULD_BLOCK)

View File

@@ -120,26 +120,39 @@ class APIFrameHelper {
}
return APIError::OK;
}
/// Toggle TCP_NODELAY socket option to control Nagle's algorithm.
///
/// This is used to allow log messages to coalesce (Nagle enabled) while keeping
/// state updates low-latency (NODELAY enabled). Without this, many small log
/// packets fill the TCP send buffer, crowding out important state updates.
///
/// State is tracked to minimize setsockopt() overhead - on lwip_raw (ESP8266/RP2040)
/// this is just a boolean assignment; on other platforms it's a lightweight syscall.
///
/// @param enable true to enable NODELAY (disable Nagle), false to enable Nagle
/// @return true if successful or already in desired state
bool set_nodelay(bool enable) {
if (this->nodelay_enabled_ == enable)
return true;
int val = enable ? 1 : 0;
int err = this->socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &val, sizeof(int));
if (err == 0) {
this->nodelay_enabled_ = enable;
// Manage TCP_NODELAY (Nagle's algorithm) based on message type.
//
// For non-log messages (sensor data, state updates): Always disable Nagle
// (NODELAY on) for immediate delivery - these are time-sensitive.
//
// For log messages: Use Nagle to coalesce multiple small log packets into
// fewer larger packets, reducing WiFi overhead. However, we limit batching
// to 3 messages to avoid excessive LWIP buffer pressure on memory-constrained
// devices like ESP8266. LWIP's TCP_OVERSIZE option coalesces the data into
// shared pbufs, but holding data too long waiting for Nagle's timer causes
// buffer exhaustion and dropped messages.
//
// Flow: Log 1 (Nagle on) -> Log 2 (Nagle on) -> Log 3 (NODELAY, flush all)
//
void set_nodelay_for_message(bool is_log_message) {
if (!is_log_message) {
if (this->nodelay_state_ != NODELAY_ON) {
this->set_nodelay_raw_(true);
this->nodelay_state_ = NODELAY_ON;
}
return;
}
// Log messages 1-3: state transitions -1 -> 1 -> 2 -> -1 (flush on 3rd)
if (this->nodelay_state_ == NODELAY_ON) {
this->set_nodelay_raw_(false);
this->nodelay_state_ = 1;
} else if (this->nodelay_state_ >= LOG_NAGLE_COUNT) {
this->set_nodelay_raw_(true);
this->nodelay_state_ = NODELAY_ON;
} else {
this->nodelay_state_++;
}
return err == 0;
}
virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0;
// Write multiple protobuf messages in a single operation
@@ -229,10 +242,18 @@ class APIFrameHelper {
uint8_t tx_buf_head_{0};
uint8_t tx_buf_tail_{0};
uint8_t tx_buf_count_{0};
// Tracks TCP_NODELAY state to minimize setsockopt() calls. Initialized to true
// since init_common_() enables NODELAY. Used by set_nodelay() to allow log
// messages to coalesce while keeping state updates low-latency.
bool nodelay_enabled_{true};
// Nagle batching state for log messages. NODELAY_ON (-1) means NODELAY is enabled
// (immediate send). Values 1-2 count log messages in the current Nagle batch.
// After LOG_NAGLE_COUNT logs, we switch to NODELAY to flush and reset.
static constexpr int8_t NODELAY_ON = -1;
static constexpr int8_t LOG_NAGLE_COUNT = 2;
int8_t nodelay_state_{NODELAY_ON};
// Internal helper to set TCP_NODELAY socket option
void set_nodelay_raw_(bool enable) {
int val = enable ? 1 : 0;
this->socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &val, sizeof(int));
}
// Common initialization for both plaintext and noise protocols
APIError init_common_();

View File

@@ -241,8 +241,10 @@ void APIServer::handle_disconnect(APIConnection *conn) {}
void APIServer::on_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \
if (obj->is_internal()) \
return; \
for (auto &c : this->clients_) \
c->send_##entity_name##_state(obj); \
for (auto &c : this->clients_) { \
if (c->flags_.state_subscription) \
c->send_##entity_name##_state(obj); \
} \
}
#ifdef USE_BINARY_SENSOR
@@ -321,8 +323,10 @@ API_DISPATCH_UPDATE(water_heater::WaterHeater, water_heater)
void APIServer::on_event(event::Event *obj) {
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_event(obj);
for (auto &c : this->clients_) {
if (c->flags_.state_subscription)
c->send_event(obj);
}
}
#endif
@@ -331,8 +335,10 @@ void APIServer::on_event(event::Event *obj) {
void APIServer::on_update(update::UpdateEntity *obj) {
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_update_state(obj);
for (auto &c : this->clients_) {
if (c->flags_.state_subscription)
c->send_update_state(obj);
}
}
#endif
@@ -552,8 +558,10 @@ bool APIServer::clear_noise_psk(bool make_active) {
#ifdef USE_HOMEASSISTANT_TIME
void APIServer::request_time() {
for (auto &client : this->clients_) {
if (!client->flags_.remove && client->is_authenticated())
if (!client->flags_.remove && client->is_authenticated()) {
client->send_time_request();
return; // Only request from one client to avoid clock conflicts
}
}
}
#endif

View File

@@ -48,14 +48,14 @@ uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size
}
uint32_t field_length = res->as_uint32();
ptr += consumed;
if (ptr + field_length > end) {
if (field_length > static_cast<size_t>(end - ptr)) {
return count; // Out of bounds
}
ptr += field_length;
break;
}
case WIRE_TYPE_FIXED32: { // 32-bit - skip 4 bytes
if (ptr + 4 > end) {
if (end - ptr < 4) {
return count;
}
ptr += 4;
@@ -110,7 +110,7 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
}
uint32_t field_length = res->as_uint32();
ptr += consumed;
if (ptr + field_length > end) {
if (field_length > static_cast<size_t>(end - ptr)) {
ESP_LOGV(TAG, "Out-of-bounds Length Delimited at offset %ld", (long) (ptr - buffer));
return;
}
@@ -121,7 +121,7 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
break;
}
case WIRE_TYPE_FIXED32: { // 32-bit
if (ptr + 4 > end) {
if (end - ptr < 4) {
ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer));
return;
}

View File

@@ -13,14 +13,11 @@ from . import AQI_CALCULATION_TYPE, CONF_CALCULATION_TYPE, aqi_ns
CODEOWNERS = ["@jasstrong"]
DEPENDENCIES = ["sensor"]
UNIT_INDEX = "index"
AQISensor = aqi_ns.class_("AQISensor", sensor.Sensor, cg.Component)
CONFIG_SCHEMA = (
sensor.sensor_schema(
AQISensor,
unit_of_measurement=UNIT_INDEX,
accuracy_decimals=0,
device_class=DEVICE_CLASS_AQI,
state_class=STATE_CLASS_MEASUREMENT,

View File

@@ -44,7 +44,7 @@ bool BinarySensor::set_new_state(const optional<bool> &new_state) {
#if defined(USE_BINARY_SENSOR) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_binary_sensor_update(this);
#endif
ESP_LOGD(TAG, "'%s': %s", this->get_name().c_str(), ONOFFMAYBE(new_state));
ESP_LOGD(TAG, "'%s' >> %s", this->get_name().c_str(), ONOFFMAYBE(new_state));
return true;
}
return false;

View File

@@ -44,7 +44,7 @@ CONFIG_SCHEMA = cv.All(
cv.GenerateID(CONF_WEB_SERVER_BASE_ID): cv.use_id(
web_server_base.WebServerBase
),
cv.Optional(CONF_COMPRESSION, default="br"): cv.one_of("br", "gzip"),
cv.Optional(CONF_COMPRESSION, default="gzip"): cv.one_of("gzip", "br"),
}
).extend(cv.COMPONENT_SCHEMA),
cv.only_on(

View File

@@ -436,7 +436,7 @@ void Climate::save_state_() {
}
void Climate::publish_state() {
ESP_LOGD(TAG, "'%s' - Sending state:", this->name_.c_str());
ESP_LOGD(TAG, "'%s' >>", this->name_.c_str());
auto traits = this->get_traits();
ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(climate_mode_to_string(this->mode)));

View File

@@ -153,7 +153,7 @@ void Cover::publish_state(bool save) {
this->position = clamp(this->position, 0.0f, 1.0f);
this->tilt = clamp(this->tilt, 0.0f, 1.0f);
ESP_LOGD(TAG, "'%s' - Publishing:", this->name_.c_str());
ESP_LOGD(TAG, "'%s' >>", this->name_.c_str());
auto traits = this->get_traits();
if (traits.get_supports_position()) {
ESP_LOGD(TAG, " Position: %.0f%%", this->position * 100.0f);

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

@@ -44,7 +44,7 @@ void DallasTemperatureSensor::update() {
this->send_command_(DALLAS_COMMAND_START_CONVERSION);
this->set_timeout(this->get_address_name(), this->millis_to_wait_for_conversion_(), [this] {
this->set_timeout(this->get_address_name().c_str(), this->millis_to_wait_for_conversion_(), [this] {
if (!this->read_scratch_pad_() || !this->check_scratch_pad_()) {
this->publish_state(NAN);
return;

View File

@@ -30,7 +30,7 @@ void DateEntity::publish_state() {
return;
}
this->set_has_state(true);
ESP_LOGD(TAG, "'%s': Sending date %d-%d-%d", this->get_name().c_str(), this->year_, this->month_, this->day_);
ESP_LOGD(TAG, "'%s' >> %d-%d-%d", this->get_name().c_str(), this->year_, this->month_, this->day_);
this->state_callback_.call();
#if defined(USE_DATETIME_DATE) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_date_update(this);

View File

@@ -45,8 +45,8 @@ void DateTimeEntity::publish_state() {
return;
}
this->set_has_state(true);
ESP_LOGD(TAG, "'%s': Sending datetime %04u-%02u-%02u %02d:%02d:%02d", this->get_name().c_str(), this->year_,
this->month_, this->day_, this->hour_, this->minute_, this->second_);
ESP_LOGD(TAG, "'%s' >> %04u-%02u-%02u %02d:%02d:%02d", this->get_name().c_str(), this->year_, this->month_,
this->day_, this->hour_, this->minute_, this->second_);
this->state_callback_.call();
#if defined(USE_DATETIME_DATETIME) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_datetime_update(this);

View File

@@ -26,8 +26,7 @@ void TimeEntity::publish_state() {
return;
}
this->set_has_state(true);
ESP_LOGD(TAG, "'%s': Sending time %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_,
this->second_);
ESP_LOGD(TAG, "'%s' >> %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_, this->second_);
this->state_callback_.call();
#if defined(USE_DATETIME_TIME) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_time_update(this);

View File

@@ -17,7 +17,11 @@ from esphome.const import (
UNIT_PERCENT,
)
from . import CONF_DEBUG_ID, DebugComponent
from . import ( # noqa: F401 pylint: disable=unused-import
CONF_DEBUG_ID,
FILTER_SOURCE_FILES,
DebugComponent,
)
DEPENDENCIES = ["debug"]

View File

@@ -8,7 +8,11 @@ from esphome.const import (
ICON_RESTART,
)
from . import CONF_DEBUG_ID, DebugComponent
from . import ( # noqa: F401 pylint: disable=unused-import
CONF_DEBUG_ID,
FILTER_SOURCE_FILES,
DebugComponent,
)
DEPENDENCIES = ["debug"]

View File

@@ -89,10 +89,8 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
delayMicroseconds(500);
} else if (this->model_ == DHT_MODEL_DHT22_TYPE2) {
delayMicroseconds(2000);
} else if (this->model_ == DHT_MODEL_AM2120 || this->model_ == DHT_MODEL_AM2302) {
delayMicroseconds(1000);
} else {
delayMicroseconds(800);
delayMicroseconds(1000);
}
#ifdef USE_ESP32

View File

@@ -190,7 +190,7 @@ async def to_code(config):
# Rotation is handled by setting the transform
display_config = {k: v for k, v in config.items() if k != CONF_ROTATION}
await display.register_display(var, display_config)
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
cg.add(var.set_dc_pin(dc))

View File

@@ -180,6 +180,12 @@ def set_core_data(config):
path=[CONF_CPU_FREQUENCY],
)
if variant == VARIANT_ESP32P4 and cpu_frequency == "400MHZ":
_LOGGER.warning(
"400MHz on ESP32-P4 is experimental and may not boot. "
"Consider using 360MHz instead. See https://github.com/esphome/esphome/issues/13425"
)
CORE.data[KEY_ESP32] = {}
CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_ESP32
conf = config[CONF_FRAMEWORK]

View File

@@ -193,10 +193,18 @@ void BLEClientBase::log_event_(const char *name) {
ESP_LOGD(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_, name);
}
void BLEClientBase::log_gattc_event_(const char *name) {
void BLEClientBase::log_gattc_lifecycle_event_(const char *name) {
ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_%s_EVT", this->connection_index_, this->address_str_, name);
}
void BLEClientBase::log_gattc_data_event_(const char *name) {
// Data transfer events are logged at VERBOSE level because logging to UART creates
// delays that cause timing issues during time-sensitive BLE operations. This is
// especially problematic during pairing or firmware updates which require rapid
// writes to many characteristics - the log spam can cause these operations to fail.
ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_%s_EVT", this->connection_index_, this->address_str_, name);
}
void BLEClientBase::log_gattc_warning_(const char *operation, esp_gatt_status_t status) {
ESP_LOGW(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str_, operation, status);
}
@@ -280,7 +288,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
case ESP_GATTC_OPEN_EVT: {
if (!this->check_addr(param->open.remote_bda))
return false;
this->log_gattc_event_("OPEN");
this->log_gattc_lifecycle_event_("OPEN");
// conn_id was already set in ESP_GATTC_CONNECT_EVT
this->service_count_ = 0;
@@ -331,7 +339,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
case ESP_GATTC_CONNECT_EVT: {
if (!this->check_addr(param->connect.remote_bda))
return false;
this->log_gattc_event_("CONNECT");
this->log_gattc_lifecycle_event_("CONNECT");
this->conn_id_ = param->connect.conn_id;
// Start MTU negotiation immediately as recommended by ESP-IDF examples
// (gatt_client, ble_throughput) which call esp_ble_gattc_send_mtu_req in
@@ -376,7 +384,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
case ESP_GATTC_CLOSE_EVT: {
if (this->conn_id_ != param->close.conn_id)
return false;
this->log_gattc_event_("CLOSE");
this->log_gattc_lifecycle_event_("CLOSE");
this->release_services();
this->set_state(espbt::ClientState::IDLE);
this->conn_id_ = UNSET_CONN_ID;
@@ -404,7 +412,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
case ESP_GATTC_SEARCH_CMPL_EVT: {
if (this->conn_id_ != param->search_cmpl.conn_id)
return false;
this->log_gattc_event_("SEARCH_CMPL");
this->log_gattc_lifecycle_event_("SEARCH_CMPL");
// For V3_WITHOUT_CACHE, switch back to medium connection parameters after service discovery
// This balances performance with bandwidth usage after the critical discovery phase
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
@@ -431,35 +439,35 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
case ESP_GATTC_READ_DESCR_EVT: {
if (this->conn_id_ != param->write.conn_id)
return false;
this->log_gattc_event_("READ_DESCR");
this->log_gattc_data_event_("READ_DESCR");
break;
}
case ESP_GATTC_WRITE_DESCR_EVT: {
if (this->conn_id_ != param->write.conn_id)
return false;
this->log_gattc_event_("WRITE_DESCR");
this->log_gattc_data_event_("WRITE_DESCR");
break;
}
case ESP_GATTC_WRITE_CHAR_EVT: {
if (this->conn_id_ != param->write.conn_id)
return false;
this->log_gattc_event_("WRITE_CHAR");
this->log_gattc_data_event_("WRITE_CHAR");
break;
}
case ESP_GATTC_READ_CHAR_EVT: {
if (this->conn_id_ != param->read.conn_id)
return false;
this->log_gattc_event_("READ_CHAR");
this->log_gattc_data_event_("READ_CHAR");
break;
}
case ESP_GATTC_NOTIFY_EVT: {
if (this->conn_id_ != param->notify.conn_id)
return false;
this->log_gattc_event_("NOTIFY");
this->log_gattc_data_event_("NOTIFY");
break;
}
case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
this->log_gattc_event_("REG_FOR_NOTIFY");
this->log_gattc_data_event_("REG_FOR_NOTIFY");
if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
// Client is responsible for flipping the descriptor value
@@ -491,7 +499,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
esp_err_t status =
esp_ble_gattc_write_char_descr(this->gattc_if_, this->conn_id_, desc_result.handle, sizeof(notify_en),
(uint8_t *) &notify_en, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE);
ESP_LOGD(TAG, "Wrote notify descriptor %d, properties=%d", notify_en, char_result.properties);
ESP_LOGV(TAG, "Wrote notify descriptor %d, properties=%d", notify_en, char_result.properties);
if (status) {
this->log_gattc_warning_("esp_ble_gattc_write_char_descr", status);
}
@@ -499,13 +507,13 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
}
case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: {
this->log_gattc_event_("UNREG_FOR_NOTIFY");
this->log_gattc_data_event_("UNREG_FOR_NOTIFY");
break;
}
default:
// ideally would check all other events for matching conn_id
ESP_LOGD(TAG, "[%d] [%s] Event %d", this->connection_index_, this->address_str_, event);
// Unknown events logged at VERBOSE to avoid UART delays during time-sensitive operations
ESP_LOGV(TAG, "[%d] [%s] Event %d", this->connection_index_, this->address_str_, event);
break;
}
return true;

View File

@@ -127,7 +127,8 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
// 6 bytes used, 2 bytes padding
void log_event_(const char *name);
void log_gattc_event_(const char *name);
void log_gattc_lifecycle_event_(const char *name);
void log_gattc_data_event_(const char *name);
void update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout,
const char *param_type);
void set_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout,

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

@@ -11,6 +11,7 @@
#include <esp_ota_ops.h>
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
#include "esphome/components/http_request/http_request.h"
#include "esphome/components/json/json_util.h"
#include "esphome/components/network/util.h"
#endif
@@ -181,15 +182,26 @@ bool Esp32HostedUpdate::fetch_manifest_() {
}
// Read manifest JSON into string (manifest is small, ~1KB max)
// NOTE: HttpContainer::read() has non-BSD socket semantics - see http_request.h
// Use http_read_loop_result() helper instead of checking return values directly
std::string json_str;
json_str.reserve(container->content_length);
uint8_t buf[256];
uint32_t last_data_time = millis();
const uint32_t read_timeout = this->http_request_parent_->get_timeout();
while (container->get_bytes_read() < container->content_length) {
int read = container->read(buf, sizeof(buf));
if (read > 0) {
json_str.append(reinterpret_cast<char *>(buf), read);
}
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, 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; // COMPLETE, ERROR, or TIMEOUT
json_str.append(reinterpret_cast<char *>(buf), read_or_error);
}
container->end();
@@ -294,33 +306,43 @@ bool Esp32HostedUpdate::stream_firmware_to_coprocessor_() {
}
// Stream firmware to coprocessor while computing SHA256
// Hardware SHA acceleration requires 32-byte alignment on some chips (ESP32-S3 with IDF 5.5.x+)
alignas(32) sha256::SHA256 hasher;
// NOTE: HttpContainer::read() has non-BSD socket semantics - see http_request.h
// Use http_read_loop_result() helper instead of checking return values directly
sha256::SHA256 hasher;
hasher.init();
uint8_t buffer[CHUNK_SIZE];
uint32_t last_data_time = millis();
const uint32_t read_timeout = this->http_request_parent_->get_timeout();
while (container->get_bytes_read() < total_size) {
int read = container->read(buffer, sizeof(buffer));
int read_or_error = container->read(buffer, sizeof(buffer));
// Feed watchdog and give other tasks a chance to run
App.feed_wdt();
yield();
// Exit loop if no data available (stream closed or end of data)
if (read <= 0) {
if (read < 0) {
ESP_LOGE(TAG, "Stream closed with error");
esp_hosted_slave_ota_end(); // NOLINT
container->end();
this->status_set_error(LOG_STR("Download failed"));
return false;
}
// read == 0: no more data available, exit loop
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");
} else {
ESP_LOGE(TAG, "Error reading firmware data: %d", read_or_error);
}
esp_hosted_slave_ota_end(); // NOLINT
container->end();
this->status_set_error(LOG_STR("Download failed"));
return false;
}
hasher.add(buffer, read);
err = esp_hosted_slave_ota_write(buffer, read); // NOLINT
hasher.add(buffer, read_or_error);
err = esp_hosted_slave_ota_write(buffer, read_or_error); // NOLINT
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to write OTA data: %s", esp_err_to_name(err));
esp_hosted_slave_ota_end(); // NOLINT
@@ -352,8 +374,7 @@ bool Esp32HostedUpdate::write_embedded_firmware_to_coprocessor_() {
}
// Verify SHA256 before writing
// Hardware SHA acceleration requires 32-byte alignment on some chips (ESP32-S3 with IDF 5.5.x+)
alignas(32) sha256::SHA256 hasher;
sha256::SHA256 hasher;
hasher.init();
hasher.add(this->firmware_data_, this->firmware_size_);
hasher.calculate();

View File

@@ -563,11 +563,9 @@ bool ESPHomeOTAComponent::handle_auth_send_() {
// [1+hex_size...1+2*hex_size-1]: cnonce (hex_size bytes) - client's nonce
// [1+2*hex_size...1+3*hex_size-1]: response (hex_size bytes) - client's hash
// CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION: Hash object must stay in same stack frame
// CRITICAL ESP32-S2/S3 HARDWARE SHA ACCELERATION: Hash object must stay in same stack frame
// (no passing to other functions). All hash operations must happen in this function.
// NOTE: On ESP32-S3 with IDF 5.5.x, the SHA256 context must be properly aligned for
// hardware SHA acceleration DMA operations.
alignas(32) sha256::SHA256 hasher;
sha256::SHA256 hasher;
const size_t hex_size = hasher.get_size() * 2;
const size_t nonce_len = hasher.get_size() / 4;
@@ -639,11 +637,9 @@ bool ESPHomeOTAComponent::handle_auth_read_() {
const char *cnonce = nonce + hex_size;
const char *response = cnonce + hex_size;
// CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION: Hash object must stay in same stack frame
// CRITICAL ESP32-S2/S3 HARDWARE SHA ACCELERATION: Hash object must stay in same stack frame
// (no passing to other functions). All hash operations must happen in this function.
// NOTE: On ESP32-S3 with IDF 5.5.x, the SHA256 context must be properly aligned for
// hardware SHA acceleration DMA operations.
alignas(32) sha256::SHA256 hasher;
sha256::SHA256 hasher;
hasher.init();
hasher.add(this->password_.c_str(), this->password_.length());

View File

@@ -22,7 +22,7 @@ void Event::trigger(const std::string &event_type) {
return;
}
this->last_event_type_ = found;
ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), this->last_event_type_);
ESP_LOGD(TAG, "'%s' >> '%s'", this->get_name().c_str(), this->last_event_type_);
this->event_callback_.call(event_type);
#if defined(USE_EVENT) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_event(this);

View File

@@ -201,7 +201,7 @@ void Fan::publish_state() {
auto traits = this->get_traits();
ESP_LOGD(TAG,
"'%s' - Sending state:\n"
"'%s' >>\n"
" State: %s",
this->name_.c_str(), ONOFF(this->state));
if (traits.supports_speed()) {

View File

@@ -1,4 +1,5 @@
#include "fingerprint_grow.h"
#include "esphome/core/gpio.h"
#include "esphome/core/log.h"
#include <cinttypes>
@@ -532,14 +533,21 @@ void FingerprintGrowComponent::sensor_sleep_() {
}
void FingerprintGrowComponent::dump_config() {
char sensing_pin_buf[GPIO_SUMMARY_MAX_LEN];
char power_pin_buf[GPIO_SUMMARY_MAX_LEN];
if (this->has_sensing_pin_) {
this->sensing_pin_->dump_summary(sensing_pin_buf, sizeof(sensing_pin_buf));
}
if (this->has_power_pin_) {
this->sensor_power_pin_->dump_summary(power_pin_buf, sizeof(power_pin_buf));
}
ESP_LOGCONFIG(TAG,
"GROW_FINGERPRINT_READER:\n"
" System Identifier Code: 0x%.4X\n"
" Touch Sensing Pin: %s\n"
" Sensor Power Pin: %s",
this->system_identifier_code_,
this->has_sensing_pin_ ? this->sensing_pin_->dump_summary().c_str() : "None",
this->has_power_pin_ ? this->sensor_power_pin_->dump_summary().c_str() : "None");
this->system_identifier_code_, this->has_sensing_pin_ ? sensing_pin_buf : "None",
this->has_power_pin_ ? power_pin_buf : "None");
if (this->idle_period_to_sleep_ms_ < UINT32_MAX) {
ESP_LOGCONFIG(TAG, " Idle Period to Sleep: %" PRIu32 " ms", this->idle_period_to_sleep_ms_);
} else {

View File

@@ -1,4 +1,3 @@
#include <cstdio>
#include <cstring>
#include "hmac_sha256.h"
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_HOST)
@@ -26,9 +25,7 @@ void HmacSHA256::calculate() { mbedtls_md_hmac_finish(&this->ctx_, this->digest_
void HmacSHA256::get_bytes(uint8_t *output) { memcpy(output, this->digest_, SHA256_DIGEST_SIZE); }
void HmacSHA256::get_hex(char *output) {
for (size_t i = 0; i < SHA256_DIGEST_SIZE; i++) {
sprintf(output + (i * 2), "%02x", this->digest_[i]);
}
format_hex_to(output, SHA256_DIGEST_SIZE * 2 + 1, this->digest_, SHA256_DIGEST_SIZE);
}
bool HmacSHA256::equals_bytes(const uint8_t *expected) {

View File

@@ -157,6 +157,7 @@ async def to_code(config):
if CORE.is_esp32:
cg.add(var.set_buffer_size_rx(config[CONF_BUFFER_SIZE_RX]))
cg.add(var.set_buffer_size_tx(config[CONF_BUFFER_SIZE_TX]))
cg.add(var.set_verify_ssl(config[CONF_VERIFY_SSL]))
if config.get(CONF_VERIFY_SSL):
esp32.add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True)

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 */
@@ -79,22 +80,144 @@ inline bool is_redirect(int const status) {
*/
inline bool is_success(int const status) { return status >= HTTP_STATUS_OK && status < HTTP_STATUS_MULTIPLE_CHOICES; }
/*
* HTTP Container Read Semantics
* =============================
*
* IMPORTANT: These semantics differ from standard BSD sockets!
*
* BSD socket read() returns:
* > 0: bytes read
* == 0: connection closed (EOF)
* < 0: error (check errno)
*
* HttpContainer::read() returns:
* > 0: bytes read successfully
* == 0: no data available yet OR all content read
* (caller should check bytes_read vs content_length)
* < 0: error or connection closed (caller should EXIT)
* HTTP_ERROR_CONNECTION_CLOSED (-1) = connection closed prematurely
* other negative values = platform-specific errors
*
* Platform behaviors:
* - ESP-IDF: blocking reads, 0 only returned when all content read
* - Arduino: non-blocking, 0 means "no data yet" or "all content read"
*
* Use the helper functions below instead of checking return values directly:
* - http_read_loop_result(): for manual loops with per-chunk processing
* - http_read_fully(): for simple "read N bytes into buffer" operations
*/
/// Error code returned by HttpContainer::read() when connection closed prematurely
/// NOTE: Unlike BSD sockets where 0 means EOF, here 0 means "no data yet, retry"
static constexpr int HTTP_ERROR_CONNECTION_CLOSED = -1;
/// Status of a read operation
enum class HttpReadStatus : uint8_t {
OK, ///< Read completed successfully
ERROR, ///< Read error occurred
TIMEOUT, ///< Timeout waiting for data
};
/// Result of an HTTP read operation
struct HttpReadResult {
HttpReadStatus status; ///< Status of the read operation
int error_code; ///< Error code from read() on failure, 0 on success
};
/// Result of processing a non-blocking read with timeout (for manual loops)
enum class HttpReadLoopResult : uint8_t {
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
/// @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;
}
if (bytes_read_or_error < 0) {
return HttpReadLoopResult::ERROR;
}
// 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;
}
delay(1); // Small delay to prevent tight spinning
return HttpReadLoopResult::RETRY;
}
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.
*
* WARNING: These semantics differ from BSD sockets!
* BSD sockets: 0 = EOF (connection closed)
* This method: 0 = no data yet OR all content read, negative = error/closed
*
* @param buf Buffer to read data into
* @param max_len Maximum number of bytes to read
* @return
* - > 0: Number of bytes read successfully
* - 0: No data available yet OR all content read
* (check get_bytes_read() >= content_length to distinguish)
* - HTTP_ERROR_CONNECTION_CLOSED (-1): Connection closed prematurely
* - < -1: Other error (platform-specific error code)
*
* Platform notes:
* - ESP-IDF: blocking read, 0 only when all content read
* - Arduino: non-blocking, 0 can mean "no data yet" or "all content read"
*
* Use get_bytes_read() and content_length to track progress.
* When get_bytes_read() >= content_length, all data has been received.
*
* IMPORTANT: Do not use raw return values directly. Use these helpers:
* - http_read_loop_result(): for loops with per-chunk processing
* - http_read_fully(): for simple "read N bytes" operations
*/
virtual int read(uint8_t *buf, size_t max_len) = 0;
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.
*
@@ -107,9 +230,44 @@ 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_{};
};
/// Read data from HTTP container into buffer with timeout handling
/// Handles feed_wdt, yield, and timeout checking internally
/// @param container The HTTP container to read from
/// @param buffer Buffer to read into
/// @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; 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;
uint32_t last_data_time = millis();
while (read_index < total_size) {
int read_bytes_or_error = container->read(buffer + read_index, std::min(chunk_size, total_size - read_index));
App.feed_wdt();
yield();
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)
return {HttpReadStatus::TIMEOUT, 0};
read_index += read_bytes_or_error;
}
return {HttpReadStatus::OK, 0};
}
class HttpRequestResponseTrigger : public Trigger<std::shared_ptr<HttpContainer>, std::string &> {
public:
void process(const std::shared_ptr<HttpContainer> &container, std::string &response_body) {
@@ -124,6 +282,7 @@ class HttpRequestComponent : public Component {
void set_useragent(const char *useragent) { this->useragent_ = useragent; }
void set_timeout(uint32_t timeout) { this->timeout_ = timeout; }
uint32_t get_timeout() const { return this->timeout_; }
void set_watchdog_timeout(uint32_t watchdog_timeout) { this->watchdog_timeout_ = watchdog_timeout; }
uint32_t get_watchdog_timeout() const { return this->watchdog_timeout_; }
void set_follow_redirects(bool follow_redirects) { this->follow_redirects_ = follow_redirects; }
@@ -242,24 +401,29 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
return;
}
size_t content_length = container->content_length;
size_t max_length = std::min(content_length, this->max_response_buffer_size_);
size_t max_length = this->max_response_buffer_size_;
#ifdef USE_HTTP_REQUEST_RESPONSE
if (this->capture_response_.value(x...)) {
std::string response_body;
RAMAllocator<uint8_t> allocator;
uint8_t *buf = allocator.allocate(max_length);
if (buf != nullptr) {
// NOTE: HttpContainer::read() has non-BSD socket semantics - see top of this file
// Use http_read_loop_result() helper instead of checking return values directly
size_t read_index = 0;
uint32_t last_data_time = millis();
const uint32_t read_timeout = this->parent_->get_timeout();
while (container->get_bytes_read() < max_length) {
int read = container->read(buf + read_index, std::min<size_t>(max_length - read_index, 512));
if (read <= 0) {
break;
}
int read_or_error = container->read(buf + read_index, std::min<size_t>(max_length - read_index, 512));
App.feed_wdt();
yield();
read_index += read;
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; // COMPLETE, ERROR, or TIMEOUT
read_index += read_or_error;
}
response_body.reserve(read_index);
response_body.assign((char *) buf, read_index);

View File

@@ -131,14 +131,49 @@ 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;
}
// Arduino HTTP read implementation
//
// WARNING: Return values differ from BSD sockets! See http_request.h for full documentation.
//
// Arduino's WiFiClient is inherently non-blocking - available() returns 0 when
// no data is ready. We use connected() to distinguish "no data yet" from
// "connection closed".
//
// WiFiClient behavior:
// available() > 0: data ready to read
// available() == 0 && connected(): no data yet, still connected
// available() == 0 && !connected(): connection closed
//
// We normalize to HttpContainer::read() contract (NOT BSD socket semantics!):
// > 0: bytes read
// 0: no data yet, retry <-- NOTE: 0 means retry, NOT EOF!
// < 0: error/connection closed <-- connection closed returns -1, not 0
int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
const uint32_t start = millis();
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
@@ -146,15 +181,29 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
WiFiClient *stream_ptr = this->client_.getStreamPtr();
if (stream_ptr == nullptr) {
ESP_LOGE(TAG, "Stream pointer vanished!");
return -1;
return HTTP_ERROR_CONNECTION_CLOSED;
}
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);
return 0;
// 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 or EOF for chunked
}
return 0; // No data yet, caller should retry
}
App.feed_wdt();

View File

@@ -89,7 +89,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
config.max_redirection_count = this->redirect_limit_;
config.auth_type = HTTP_AUTH_TYPE_BASIC;
#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
if (secure) {
if (secure && this->verify_ssl_) {
config.crt_bundle_attach = esp_crt_bundle_attach;
}
#endif
@@ -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();
@@ -209,32 +213,70 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
return container;
}
// ESP-IDF HTTP read implementation (blocking mode)
//
// WARNING: Return values differ from BSD sockets! See http_request.h for full documentation.
//
// esp_http_client_read() in blocking mode returns:
// > 0: bytes read
// 0: connection closed (end of stream)
// < 0: error
//
// We normalize to HttpContainer::read() contract:
// > 0: bytes read
// 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());
int bufsize = std::min(max_len, this->content_length - this->bytes_read_);
if (bufsize == 0) {
this->duration_ms += (millis() - start);
return 0;
// 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
}
this->feed_wdt();
int read_len = esp_http_client_read(this->client_, (char *) buf, bufsize);
int read_len_or_error = esp_http_client_read(this->client_, (char *) buf, max_len);
this->feed_wdt();
this->bytes_read_ += read_len;
this->duration_ms += (millis() - start);
return read_len;
if (read_len_or_error > 0) {
this->bytes_read_ += read_len_or_error;
return read_len_or_error;
}
// 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;
}
// Negative value - error, return the actual error code for debugging
return read_len_or_error;
}
void HttpContainerIDF::end() {
if (this->client_ == nullptr) {
return; // Already cleaned up
}
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
esp_http_client_close(this->client_);
esp_http_client_cleanup(this->client_);
this->client_ = nullptr;
}
void HttpContainerIDF::feed_wdt() {

View File

@@ -34,6 +34,7 @@ class HttpRequestIDF : public HttpRequestComponent {
void set_buffer_size_rx(uint16_t buffer_size_rx) { this->buffer_size_rx_ = buffer_size_rx; }
void set_buffer_size_tx(uint16_t buffer_size_tx) { this->buffer_size_tx_ = buffer_size_tx; }
void set_verify_ssl(bool verify_ssl) { this->verify_ssl_ = verify_ssl; }
protected:
std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body,
@@ -42,6 +43,7 @@ class HttpRequestIDF : public HttpRequestComponent {
// if zero ESP-IDF will use DEFAULT_HTTP_BUF_SIZE
uint16_t buffer_size_rx_{};
uint16_t buffer_size_tx_{};
bool verify_ssl_{true};
/// @brief Monitors the http client events to gather response headers
static esp_err_t http_event_handler(esp_http_client_event_t *evt);

View File

@@ -115,39 +115,51 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
return error_code;
}
// NOTE: HttpContainer::read() has non-BSD socket semantics - see http_request.h
// Use http_read_loop_result() helper instead of checking return values directly
uint32_t last_data_time = millis();
const uint32_t read_timeout = this->parent_->get_timeout();
while (container->get_bytes_read() < container->content_length) {
// read a maximum of chunk_size bytes into buf. (real read size returned)
int bufsize = container->read(buf, OtaHttpRequestComponent::HTTP_RECV_BUFFER);
ESP_LOGVV(TAG, "bytes_read_ = %u, body_length_ = %u, bufsize = %i", container->get_bytes_read(),
container->content_length, bufsize);
// read a maximum of chunk_size bytes into buf. (real read size returned, or negative error code)
int bufsize_or_error = container->read(buf, OtaHttpRequestComponent::HTTP_RECV_BUFFER);
ESP_LOGVV(TAG, "bytes_read_ = %u, body_length_ = %u, bufsize_or_error = %i", container->get_bytes_read(),
container->content_length, bufsize_or_error);
// feed watchdog and give other tasks a chance to run
App.feed_wdt();
yield();
// Exit loop if no data available (stream closed or end of data)
if (bufsize <= 0) {
if (bufsize < 0) {
ESP_LOGE(TAG, "Stream closed with error");
this->cleanup_(std::move(backend), container);
return OTA_CONNECTION_ERROR;
}
// bufsize == 0: no more data available, exit loop
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");
} else {
ESP_LOGE(TAG, "Error reading data: %d", bufsize_or_error);
}
this->cleanup_(std::move(backend), container);
return OTA_CONNECTION_ERROR;
}
if (bufsize <= OtaHttpRequestComponent::HTTP_RECV_BUFFER) {
// At this point bufsize_or_error > 0, so it's a valid size
if (bufsize_or_error <= OtaHttpRequestComponent::HTTP_RECV_BUFFER) {
// add read bytes to MD5
md5_receive.add(buf, bufsize);
md5_receive.add(buf, bufsize_or_error);
// write bytes to OTA backend
this->update_started_ = true;
error_code = backend->write(buf, bufsize);
error_code = backend->write(buf, bufsize_or_error);
if (error_code != ota::OTA_RESPONSE_OK) {
// error code explanation available at
// https://github.com/esphome/esphome/blob/dev/esphome/components/ota/ota_backend.h
ESP_LOGE(TAG, "Error code (%02X) writing binary data to flash at offset %d and size %d", error_code,
container->get_bytes_read() - bufsize, container->content_length);
container->get_bytes_read() - bufsize_or_error, container->content_length);
this->cleanup_(std::move(backend), container);
return error_code;
}
@@ -244,19 +256,19 @@ bool OtaHttpRequestComponent::http_get_md5_() {
}
this->md5_expected_.resize(MD5_SIZE);
int read_len = 0;
while (container->get_bytes_read() < MD5_SIZE) {
read_len = container->read((uint8_t *) this->md5_expected_.data(), MD5_SIZE);
if (read_len <= 0) {
break;
}
App.feed_wdt();
yield();
}
auto result = http_read_fully(container.get(), (uint8_t *) this->md5_expected_.data(), MD5_SIZE, MD5_SIZE,
this->parent_->get_timeout());
container->end();
ESP_LOGV(TAG, "Read len: %u, MD5 expected: %u", read_len, MD5_SIZE);
return read_len == MD5_SIZE;
if (result.status != HttpReadStatus::OK) {
if (result.status == HttpReadStatus::TIMEOUT) {
ESP_LOGE(TAG, "Timeout reading MD5");
} else {
ESP_LOGE(TAG, "Error reading MD5: %d", result.error_code);
}
return false;
}
return true;
}
bool OtaHttpRequestComponent::validate_url_(const std::string &url) {

View File

@@ -11,7 +11,12 @@ namespace http_request {
// The update function runs in a task only on ESP32s.
#ifdef USE_ESP32
#define UPDATE_RETURN vTaskDelete(nullptr) // Delete the current update task
// vTaskDelete doesn't return, but clang-tidy doesn't know that
#define UPDATE_RETURN \
do { \
vTaskDelete(nullptr); \
__builtin_unreachable(); \
} while (0)
#else
#define UPDATE_RETURN return
#endif
@@ -70,19 +75,21 @@ void HttpRequestUpdate::update_task(void *params) {
UPDATE_RETURN;
}
size_t read_index = 0;
while (container->get_bytes_read() < container->content_length) {
int read_bytes = container->read(data + read_index, MAX_READ_SIZE);
yield();
if (read_bytes <= 0) {
// Network error or connection closed - break to avoid infinite loop
break;
auto read_result = http_read_fully(container.get(), data, container->content_length, MAX_READ_SIZE,
this_update->request_parent_->get_timeout());
if (read_result.status != HttpReadStatus::OK) {
if (read_result.status == HttpReadStatus::TIMEOUT) {
ESP_LOGE(TAG, "Timeout reading manifest");
} else {
ESP_LOGE(TAG, "Error reading manifest: %d", read_result.error_code);
}
read_index += read_bytes;
// Defer to main loop to avoid race condition on component_state_ read-modify-write
this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to read manifest")); });
allocator.deallocate(data, container->content_length);
container->end();
UPDATE_RETURN;
}
size_t read_index = container->get_bytes_read();
bool valid = false;
{ // Ensures the response string falls out of scope and deallocates before the task ends

View File

@@ -1,3 +1,4 @@
import logging
from typing import Any
from esphome import automation, pins
@@ -18,13 +19,16 @@ from esphome.const import (
CONF_ROTATION,
CONF_UPDATE_INTERVAL,
)
from esphome.core import ID
from esphome.core import ID, EnumValue
from esphome.cpp_generator import MockObj, TemplateArgsType
import esphome.final_validate as fv
from esphome.helpers import add_class_to_obj
from esphome.types import ConfigType
from . import boards, hub75_ns
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ["esp32"]
CODEOWNERS = ["@stuartparmenter"]
@@ -120,13 +124,51 @@ PANEL_LAYOUTS = {
}
Hub75ScanWiring = cg.global_ns.enum("Hub75ScanWiring", is_class=True)
SCAN_PATTERNS = {
SCAN_WIRINGS = {
"STANDARD_TWO_SCAN": Hub75ScanWiring.STANDARD_TWO_SCAN,
"FOUR_SCAN_16PX_HIGH": Hub75ScanWiring.FOUR_SCAN_16PX_HIGH,
"FOUR_SCAN_32PX_HIGH": Hub75ScanWiring.FOUR_SCAN_32PX_HIGH,
"FOUR_SCAN_64PX_HIGH": Hub75ScanWiring.FOUR_SCAN_64PX_HIGH,
"SCAN_1_4_16PX_HIGH": Hub75ScanWiring.SCAN_1_4_16PX_HIGH,
"SCAN_1_8_32PX_HIGH": Hub75ScanWiring.SCAN_1_8_32PX_HIGH,
"SCAN_1_8_40PX_HIGH": Hub75ScanWiring.SCAN_1_8_40PX_HIGH,
"SCAN_1_8_64PX_HIGH": Hub75ScanWiring.SCAN_1_8_64PX_HIGH,
}
# Deprecated scan wiring names - mapped to new names
DEPRECATED_SCAN_WIRINGS = {
"FOUR_SCAN_16PX_HIGH": "SCAN_1_4_16PX_HIGH",
"FOUR_SCAN_32PX_HIGH": "SCAN_1_8_32PX_HIGH",
"FOUR_SCAN_64PX_HIGH": "SCAN_1_8_64PX_HIGH",
}
def _validate_scan_wiring(value):
"""Validate scan_wiring with deprecation warnings for old names."""
value = cv.string(value).upper().replace(" ", "_")
# Check if using deprecated name
# Remove deprecated names in 2026.7.0
if value in DEPRECATED_SCAN_WIRINGS:
new_name = DEPRECATED_SCAN_WIRINGS[value]
_LOGGER.warning(
"Scan wiring '%s' is deprecated and will be removed in ESPHome 2026.7.0. "
"Please use '%s' instead.",
value,
new_name,
)
value = new_name
# Validate against allowed values
if value not in SCAN_WIRINGS:
raise cv.Invalid(
f"Unknown scan wiring '{value}'. "
f"Valid options are: {', '.join(sorted(SCAN_WIRINGS.keys()))}"
)
# Return as EnumValue like cv.enum does
result = add_class_to_obj(value, EnumValue)
result.enum_value = SCAN_WIRINGS[value]
return result
Hub75ClockSpeed = cg.global_ns.enum("Hub75ClockSpeed", is_class=True)
CLOCK_SPEEDS = {
"8MHZ": Hub75ClockSpeed.HZ_8M,
@@ -382,9 +424,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_LAYOUT_COLS): cv.positive_int,
cv.Optional(CONF_LAYOUT): cv.enum(PANEL_LAYOUTS, upper=True, space="_"),
# Panel hardware configuration
cv.Optional(CONF_SCAN_WIRING): cv.enum(
SCAN_PATTERNS, upper=True, space="_"
),
cv.Optional(CONF_SCAN_WIRING): _validate_scan_wiring,
cv.Optional(CONF_SHIFT_DRIVER): cv.enum(SHIFT_DRIVERS, upper=True),
# Display configuration
cv.Optional(CONF_DOUBLE_BUFFER): cv.boolean,
@@ -547,7 +587,7 @@ def _build_config_struct(
async def to_code(config: ConfigType) -> None:
add_idf_component(
name="esphome/esp-hub75",
ref="0.2.2",
ref="0.3.0",
)
# Set compile-time configuration via build flags (so external library sees them)

View File

@@ -42,8 +42,8 @@ ErrorCode I2CDevice::read_register16(uint16_t a_register, uint8_t *data, size_t
}
ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, size_t len) const {
SmallBufferWithHeapFallback<17> buffer_alloc; // Most I2C writes are <= 16 bytes
uint8_t *buffer = buffer_alloc.get(len + 1);
SmallBufferWithHeapFallback<17> buffer_alloc(len + 1); // Most I2C writes are <= 16 bytes
uint8_t *buffer = buffer_alloc.get();
buffer[0] = a_register;
std::copy(data, data + len, buffer + 1);
@@ -51,8 +51,8 @@ ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, siz
}
ErrorCode I2CDevice::write_register16(uint16_t a_register, const uint8_t *data, size_t len) const {
SmallBufferWithHeapFallback<18> buffer_alloc; // Most I2C writes are <= 16 bytes + 2 for register
uint8_t *buffer = buffer_alloc.get(len + 2);
SmallBufferWithHeapFallback<18> buffer_alloc(len + 2); // Most I2C writes are <= 16 bytes + 2 for register
uint8_t *buffer = buffer_alloc.get();
buffer[0] = a_register >> 8;
buffer[1] = a_register;

View File

@@ -11,22 +11,6 @@
namespace esphome {
namespace i2c {
/// @brief Helper class for efficient buffer allocation - uses stack for small sizes, heap for large
template<size_t STACK_SIZE> class SmallBufferWithHeapFallback {
public:
uint8_t *get(size_t size) {
if (size <= STACK_SIZE) {
return this->stack_buffer_;
}
this->heap_buffer_ = std::unique_ptr<uint8_t[]>(new uint8_t[size]);
return this->heap_buffer_.get();
}
private:
uint8_t stack_buffer_[STACK_SIZE];
std::unique_ptr<uint8_t[]> heap_buffer_;
};
/// @brief Error codes returned by I2CBus and I2CDevice methods
enum ErrorCode {
NO_ERROR = 0, ///< No error found during execution of method
@@ -92,8 +76,8 @@ class I2CBus {
total_len += read_buffers[i].len;
}
SmallBufferWithHeapFallback<128> buffer_alloc; // Most I2C reads are small
uint8_t *buffer = buffer_alloc.get(total_len);
SmallBufferWithHeapFallback<128> buffer_alloc(total_len); // Most I2C reads are small
uint8_t *buffer = buffer_alloc.get();
auto err = this->write_readv(address, nullptr, 0, buffer, total_len);
if (err != ERROR_OK)
@@ -116,8 +100,8 @@ class I2CBus {
total_len += write_buffers[i].len;
}
SmallBufferWithHeapFallback<128> buffer_alloc; // Most I2C writes are small
uint8_t *buffer = buffer_alloc.get(total_len);
SmallBufferWithHeapFallback<128> buffer_alloc(total_len); // Most I2C writes are small
uint8_t *buffer = buffer_alloc.get();
size_t pos = 0;
for (size_t i = 0; i != count; i++) {

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

@@ -223,7 +223,7 @@ async def to_code(config):
var = cg.Pvariable(config[CONF_ID], rhs)
await display.register_display(var, config)
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
cg.add(var.set_dc_pin(dc))
if init_sequences := config.get(CONF_INIT_SEQUENCE):

View File

@@ -665,15 +665,10 @@ async def write_image(config, all_frames=False):
if is_svg_file(path):
import resvg_py
if resize:
width, height = resize
# resvg-py allows rendering by width/height directly
image_data = resvg_py.svg_to_bytes(
svg_path=str(path), width=int(width), height=int(height)
)
else:
# Default size
image_data = resvg_py.svg_to_bytes(svg_path=str(path))
resize = resize or (None, None)
image_data = resvg_py.svg_to_bytes(
svg_path=str(path), width=resize[0], height=resize[1], dpi=100
)
# Convert bytes to Pillow Image
image = Image.open(io.BytesIO(image_data))

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

@@ -52,7 +52,7 @@ void Lock::publish_state(LockState state) {
this->state = state;
this->rtc_.save(&this->state);
ESP_LOGD(TAG, "'%s': Sending state %s", this->name_.c_str(), LOG_STR_ARG(lock_state_to_string(state)));
ESP_LOGD(TAG, "'%s' >> %s", this->name_.c_str(), LOG_STR_ARG(lock_state_to_string(state)));
this->state_callback_.call();
#if defined(USE_LOCK) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_lock_update(this);

View File

@@ -1,3 +1,4 @@
from esphome import codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_OPTIONS
@@ -24,6 +25,34 @@ from .label import CONF_LABEL
CONF_DROPDOWN = "dropdown"
CONF_DROPDOWN_LIST = "dropdown_list"
# Example valid dropdown symbol (left arrow) for error messages
EXAMPLE_DROPDOWN_SYMBOL = "\U00002190" # ←
def dropdown_symbol_validator(value):
"""
Validate that the dropdown symbol is a single Unicode character
with a codepoint of 0x100 (256) or greater.
This is required because LVGL uses codepoints below 0x100 for internal symbols.
"""
value = cv.string(value)
# len(value) counts Unicode code points, not grapheme clusters or bytes
if len(value) != 1:
raise cv.Invalid(
f"Dropdown symbol must be a single character, got '{value}' with length {len(value)}"
)
codepoint = ord(value)
if codepoint < 0x100:
# Format the example symbol as a Unicode escape for the error message
example_escape = f"\\U{ord(EXAMPLE_DROPDOWN_SYMBOL):08X}"
raise cv.Invalid(
f"Dropdown symbol must have a Unicode codepoint of 0x100 (256) or greater. "
f"'{value}' has codepoint {codepoint} (0x{codepoint:X}). "
f"Use a character like '{example_escape}' ({EXAMPLE_DROPDOWN_SYMBOL}) or other Unicode symbols with codepoint >= 0x100."
)
return value
lv_dropdown_t = LvSelect("LvDropdownType", parents=(LvCompound,))
lv_dropdown_list_t = LvType("lv_dropdown_list_t")
@@ -33,7 +62,7 @@ dropdown_list_spec = WidgetType(
DROPDOWN_BASE_SCHEMA = cv.Schema(
{
cv.Optional(CONF_SYMBOL): lv_text,
cv.Optional(CONF_SYMBOL): dropdown_symbol_validator,
cv.Exclusive(CONF_SELECTED_INDEX, CONF_SELECTED_TEXT): lv_int,
cv.Exclusive(CONF_SELECTED_TEXT, CONF_SELECTED_TEXT): lv_text,
cv.Optional(CONF_DROPDOWN_LIST): part_schema(dropdown_list_spec.parts),
@@ -70,7 +99,7 @@ class DropdownType(WidgetType):
if options := config.get(CONF_OPTIONS):
lv_add(w.var.set_options(options))
if symbol := config.get(CONF_SYMBOL):
lv.dropdown_set_symbol(w.var.obj, await lv_text.process(symbol))
lv.dropdown_set_symbol(w.var.obj, cg.safe_exp(symbol))
if (selected := config.get(CONF_SELECTED_INDEX)) is not None:
value = await lv_int.process(selected)
lv_add(w.var.set_selected_index(value, literal("LV_ANIM_OFF")))

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])
await spi.register_spi_device(var, config)
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

@@ -86,7 +86,7 @@ CONFIG_SCHEMA = (
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await spi.register_spi_device(var, config)
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]))

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

@@ -260,7 +260,7 @@ async def to_code(config):
cg.add(var.set_enable_pins(enable))
if CONF_SPI_ID in config:
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
sequence, madctl = model.get_sequence(config)
cg.add(var.set_init_sequence(sequence))
cg.add(var.set_madctl(madctl))

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

@@ -443,6 +443,4 @@ async def to_code(config):
)
cg.add(var.set_writer(lambda_))
await display.register_display(var, config)
await spi.register_spi_device(var, config)
# Displays are write-only, set the SPI device to write-only as well
cg.add(var.set_write_only(True))
await spi.register_spi_device(var, config, write_only=True)

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

@@ -43,6 +43,14 @@ namespace network {
/// Buffer size for IP address string (IPv6 max: 39 chars + null)
static constexpr size_t IP_ADDRESS_BUFFER_SIZE = 40;
/// Lowercase hex digits in IP address string (A-F -> a-f for IPv6 per RFC 5952)
inline void lowercase_ip_str(char *buf) {
for (char *p = buf; *p; ++p) {
if (*p >= 'A' && *p <= 'F')
*p += 32;
}
}
struct IPAddress {
public:
#ifdef USE_HOST
@@ -52,10 +60,15 @@ struct IPAddress {
}
IPAddress(const std::string &in_address) { inet_aton(in_address.c_str(), &ip_addr_); }
IPAddress(const ip_addr_t *other_ip) { ip_addr_ = *other_ip; }
std::string str() const { return str_lower_case(inet_ntoa(ip_addr_)); }
std::string str() const {
char buf[IP_ADDRESS_BUFFER_SIZE];
this->str_to(buf);
return buf;
}
/// Write IP address to buffer. Buffer must be at least IP_ADDRESS_BUFFER_SIZE bytes.
char *str_to(char *buf) const {
return const_cast<char *>(inet_ntop(AF_INET, &ip_addr_, buf, IP_ADDRESS_BUFFER_SIZE));
inet_ntop(AF_INET, &ip_addr_, buf, IP_ADDRESS_BUFFER_SIZE);
return buf; // IPv4 only, no hex letters to lowercase
}
#else
IPAddress() { ip_addr_set_zero(&ip_addr_); }
@@ -134,9 +147,18 @@ struct IPAddress {
bool is_ip4() const { return IP_IS_V4(&ip_addr_); }
bool is_ip6() const { return IP_IS_V6(&ip_addr_); }
bool is_multicast() const { return ip_addr_ismulticast(&ip_addr_); }
std::string str() const { return str_lower_case(ipaddr_ntoa(&ip_addr_)); }
std::string str() const {
char buf[IP_ADDRESS_BUFFER_SIZE];
this->str_to(buf);
return buf;
}
/// Write IP address to buffer. Buffer must be at least IP_ADDRESS_BUFFER_SIZE bytes.
char *str_to(char *buf) const { return ipaddr_ntoa_r(&ip_addr_, buf, IP_ADDRESS_BUFFER_SIZE); }
/// Output is lowercased per RFC 5952 (IPv6 hex digits a-f).
char *str_to(char *buf) const {
ipaddr_ntoa_r(&ip_addr_, buf, IP_ADDRESS_BUFFER_SIZE);
lowercase_ip_str(buf);
return buf;
}
bool operator==(const IPAddress &other) const { return ip_addr_cmp(&ip_addr_, &other.ip_addr_); }
bool operator!=(const IPAddress &other) const { return !ip_addr_cmp(&ip_addr_, &other.ip_addr_); }
IPAddress &operator+=(uint8_t increase) {

View File

@@ -11,7 +11,12 @@ from esphome.const import (
)
from esphome.core import CORE, TimePeriod
from . import Nextion, nextion_ns, nextion_ref
from . import ( # noqa: F401 pylint: disable=unused-import
FILTER_SOURCE_FILES,
Nextion,
nextion_ns,
nextion_ref,
)
from .base_component import (
CONF_AUTO_WAKE_ON_TOUCH,
CONF_COMMAND_SPACING,

View File

@@ -23,7 +23,7 @@ void NTC::process_(float value) {
double v = this->a_ + this->b_ * lr + this->c_ * lr * lr * lr;
auto temp = float(1.0 / v - 273.15);
ESP_LOGD(TAG, "'%s' - Temperature: %.1f°C", this->name_.c_str(), temp);
ESP_LOGV(TAG, "'%s' - Temperature: %.1f°C", this->name_.c_str(), temp);
this->publish_state(temp);
}

View File

@@ -31,7 +31,7 @@ void log_number(const char *tag, const char *prefix, const char *type, Number *o
void Number::publish_state(float state) {
this->set_has_state(true);
this->state = state;
ESP_LOGD(TAG, "'%s': Sending state %f", this->get_name().c_str(), state);
ESP_LOGD(TAG, "'%s' >> %.2f", this->get_name().c_str(), state);
this->state_callback_.call(state);
#if defined(USE_NUMBER) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_number_update(this);

View File

@@ -44,7 +44,7 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await display.register_display(var, config)
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
cg.add(var.set_dc_pin(dc))

View File

@@ -27,7 +27,16 @@ void QrCode::set_ecc(qrcodegen_Ecc ecc) {
void QrCode::generate_qr_code() {
ESP_LOGV(TAG, "Generating QR code");
#ifdef USE_ESP32
// ESP32 has 8KB stack, safe to allocate ~4KB buffer on stack
uint8_t tempbuffer[qrcodegen_BUFFER_LEN_MAX];
#else
// Other platforms (ESP8266: 4KB, RP2040: 2KB, LibreTiny: ~4KB) have smaller stacks
// Allocate buffer on heap to avoid stack overflow
auto tempbuffer_owner = std::make_unique<uint8_t[]>(qrcodegen_BUFFER_LEN_MAX);
uint8_t *tempbuffer = tempbuffer_owner.get();
#endif
if (!qrcodegen_encodeText(this->value_.c_str(), tempbuffer, this->qr_, this->ecc_, qrcodegen_VERSION_MIN,
qrcodegen_VERSION_MAX, qrcodegen_Mask_AUTO, true)) {

View File

@@ -161,7 +161,7 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await display.register_display(var, config)
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
chip = DriverChip.chips[config[CONF_MODEL]]
if chip.initsequence:

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,7 @@
from esphome.components import binary_sensor, remote_base
from . import FILTER_SOURCE_FILES # noqa: F401 pylint: disable=unused-import
DEPENDENCIES = ["remote_receiver"]
CONFIG_SCHEMA = remote_base.validate_binary_sensor

View File

@@ -39,7 +39,7 @@ void ResistanceSensor::process_(float value) {
}
res *= this->resistor_;
ESP_LOGD(TAG, "'%s' - Resistance %.1fΩ", this->name_.c_str(), res);
ESP_LOGV(TAG, "'%s' - Resistance %.1fΩ", this->name_.c_str(), res);
this->publish_state(res);
}

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

@@ -9,7 +9,7 @@
#include <cinttypes>
#include <cstdio>
#ifdef USE_OTA_ROLLBACK
#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK)
#include <esp_ota_ops.h>
#endif
@@ -26,6 +26,17 @@ void SafeModeComponent::dump_config() {
this->safe_mode_boot_is_good_after_ / 1000, // because milliseconds
this->safe_mode_num_attempts_,
this->safe_mode_enable_time_ / 1000); // because milliseconds
#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK)
const char *state_str;
if (this->ota_state_ == ESP_OTA_IMG_NEW) {
state_str = "not supported";
} else if (this->ota_state_ == ESP_OTA_IMG_PENDING_VERIFY) {
state_str = "supported";
} else {
state_str = "support unknown";
}
ESP_LOGCONFIG(TAG, " Bootloader rollback: %s", state_str);
#endif
if (this->safe_mode_rtc_value_ > 1 && this->safe_mode_rtc_value_ != SafeModeComponent::ENTER_SAFE_MODE_MAGIC) {
auto remaining_restarts = this->safe_mode_num_attempts_ - this->safe_mode_rtc_value_;
@@ -36,7 +47,7 @@ void SafeModeComponent::dump_config() {
}
}
#ifdef USE_OTA_ROLLBACK
#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK)
const esp_partition_t *last_invalid = esp_ota_get_last_invalid_partition();
if (last_invalid != nullptr) {
ESP_LOGW(TAG,
@@ -55,7 +66,7 @@ void SafeModeComponent::loop() {
ESP_LOGI(TAG, "Boot seems successful; resetting boot loop counter");
this->clean_rtc();
this->boot_successful_ = true;
#ifdef USE_OTA_ROLLBACK
#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK)
// Mark OTA partition as valid to prevent rollback
esp_ota_mark_app_valid_cancel_rollback();
#endif
@@ -90,6 +101,12 @@ bool SafeModeComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t en
this->safe_mode_num_attempts_ = num_attempts;
this->rtc_ = global_preferences->make_preference<uint32_t>(233825507UL, false);
#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK)
// Check partition state to detect if bootloader supports rollback
const esp_partition_t *running = esp_ota_get_running_partition();
esp_ota_get_state_partition(running, &this->ota_state_);
#endif
uint32_t rtc_val = this->read_rtc_();
this->safe_mode_rtc_value_ = rtc_val;

View File

@@ -5,6 +5,10 @@
#include "esphome/core/helpers.h"
#include "esphome/core/preferences.h"
#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK)
#include <esp_ota_ops.h>
#endif
namespace esphome::safe_mode {
/// SafeModeComponent provides a safe way to recover from repeated boot failures
@@ -42,6 +46,9 @@ class SafeModeComponent : public Component {
// Group 1-byte members together to minimize padding
bool boot_successful_{false}; ///< set to true after boot is considered successful
uint8_t safe_mode_num_attempts_{0};
#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK)
esp_ota_img_states_t ota_state_{ESP_OTA_IMG_UNDEFINED};
#endif
// Larger objects at the end
ESPPreferenceObject rtc_;
#ifdef USE_SAFE_MODE_CALLBACK

View File

@@ -31,7 +31,7 @@ void Select::publish_state(size_t index) {
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
this->state = option; // Update deprecated member for backward compatibility
#pragma GCC diagnostic pop
ESP_LOGD(TAG, "'%s': Sending state %s (index %zu)", this->get_name().c_str(), option, index);
ESP_LOGD(TAG, "'%s' >> %s (%zu)", this->get_name().c_str(), option, index);
this->state_callback_.call(index);
#if defined(USE_SELECT) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_select_update(this);

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

@@ -126,8 +126,8 @@ float Sensor::get_raw_state() const { return this->raw_state; }
void Sensor::internal_send_state_to_frontend(float state) {
this->set_has_state(true);
this->state = state;
ESP_LOGD(TAG, "'%s': Sending state %.5f %s with %d decimals of accuracy", this->get_name().c_str(), state,
this->get_unit_of_measurement_ref().c_str(), this->get_accuracy_decimals());
ESP_LOGD(TAG, "'%s' >> %.*f %s", this->get_name().c_str(), std::max(0, (int) this->get_accuracy_decimals()), state,
this->get_unit_of_measurement_ref().c_str());
this->callback_.call(state);
#if defined(USE_SENSOR) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_sensor_update(this);

View File

@@ -10,26 +10,24 @@ namespace esphome::sha256 {
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
// CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION REQUIREMENTS (IDF 5.5.x):
// CRITICAL ESP32 HARDWARE SHA ACCELERATION REQUIREMENTS (IDF 5.5.x):
//
// The ESP32-S3 uses hardware DMA for SHA acceleration. The mbedtls_sha256_context structure contains
// internal state that the DMA engine references. This imposes three critical constraints:
// ESP32 variants (except original ESP32) use DMA-based hardware SHA acceleration that requires
// 32-byte aligned digest buffers. This is handled automatically via HashBase::digest_ which has
// alignas(32) on these platforms. Two additional constraints apply:
//
// 1. ALIGNMENT: The SHA256 object MUST be declared with `alignas(32)` for proper DMA alignment.
// Without this, the DMA engine may crash with an abort in sha_hal_read_digest().
//
// 2. NO VARIABLE LENGTH ARRAYS (VLAs): VLAs corrupt the stack layout, causing the DMA engine to
// 1. NO VARIABLE LENGTH ARRAYS (VLAs): VLAs corrupt the stack layout, causing the DMA engine to
// write to incorrect memory locations. This results in null pointer dereferences and crashes.
// ALWAYS use fixed-size arrays (e.g., char buf[65], not char buf[size+1]).
//
// 3. SAME STACK FRAME ONLY: The SHA256 object must be created and used entirely within the same
// 2. SAME STACK FRAME ONLY: The SHA256 object must be created and used entirely within the same
// function. NEVER pass the SHA256 object or HashBase pointer to another function. When the stack
// frame changes (function call/return), the DMA references become invalid and will produce
// truncated hash output (20 bytes instead of 32) or corrupt memory.
//
// CORRECT USAGE:
// void my_function() {
// alignas(32) sha256::SHA256 hasher; // Created locally with proper alignment
// sha256::SHA256 hasher;
// hasher.init();
// hasher.add(data, len); // Any size, no chunking needed
// hasher.calculate();
@@ -37,9 +35,9 @@ namespace esphome::sha256 {
// // hasher destroyed when function returns
// }
//
// INCORRECT USAGE (WILL FAIL ON ESP32-S3):
// INCORRECT USAGE (WILL FAIL):
// void my_function() {
// sha256::SHA256 hasher; // WRONG: Missing alignas(32)
// sha256::SHA256 hasher;
// helper(&hasher); // WRONG: Passed to different stack frame
// }
// void helper(HashBase *h) {

View File

@@ -24,13 +24,14 @@ namespace esphome::sha256 {
/// SHA256 hash implementation.
///
/// CRITICAL for ESP32-S3 with IDF 5.5.x hardware SHA acceleration:
/// 1. SHA256 objects MUST be declared with `alignas(32)` for proper DMA alignment
/// 2. The object MUST stay in the same stack frame (no passing to other functions)
/// 3. NO Variable Length Arrays (VLAs) in the same function
/// CRITICAL for ESP32 variants (except original) with IDF 5.5.x hardware SHA acceleration:
/// 1. The object MUST stay in the same stack frame (no passing to other functions)
/// 2. NO Variable Length Arrays (VLAs) in the same function
///
/// Note: Alignment is handled automatically via the HashBase::digest_ member.
///
/// Example usage:
/// alignas(32) sha256::SHA256 hasher;
/// sha256::SHA256 hasher;
/// hasher.init();
/// hasher.add(data, len);
/// hasher.calculate();

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

@@ -39,6 +39,7 @@ from esphome.const import (
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
import esphome.final_validate as fv
from esphome.types import ConfigType
CODEOWNERS = ["@esphome/core", "@clydebarrow"]
spi_ns = cg.esphome_ns.namespace("spi")
@@ -448,9 +449,13 @@ def spi_device_schema(
)
async def register_spi_device(var, config):
async def register_spi_device(
var: cg.Pvariable, config: ConfigType, write_only: bool = False
) -> None:
parent = await cg.get_variable(config[CONF_SPI_ID])
cg.add(var.set_spi_parent(parent))
if write_only:
cg.add(var.set_write_only(True))
if cs_pin := config.get(CONF_CS_PIN):
pin = await cg.gpio_pin_expression(cs_pin)
cg.add(var.set_cs_pin(pin))

View File

@@ -195,8 +195,11 @@ class SPIDelegateHw : public SPIDelegate {
config.post_cb = nullptr;
if (this->bit_order_ == BIT_ORDER_LSB_FIRST)
config.flags |= SPI_DEVICE_BIT_LSBFIRST;
if (this->write_only_)
if (this->write_only_) {
config.flags |= SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_NO_DUMMY;
ESP_LOGD(TAG, "SPI device with CS pin %d using half-duplex mode (write-only)",
Utility::get_pin_no(this->cs_pin_));
}
esp_err_t const err = spi_bus_add_device(this->channel_, &config, &this->handle_);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Add device failed - err %X", err);

View File

@@ -332,6 +332,7 @@ Sprinkler::Sprinkler(const std::string &name) {
// The `name` is needed to set timers up, hence non-default constructor
// replaces `set_name()` method previously existed
this->name_ = name;
this->timer_.init(2);
this->timer_.push_back({this->name_ + "sm", false, 0, 0, std::bind(&Sprinkler::sm_timer_callback_, this)});
this->timer_.push_back({this->name_ + "vs", false, 0, 0, std::bind(&Sprinkler::valve_selection_callback_, this)});
}
@@ -1574,7 +1575,8 @@ const LogString *Sprinkler::state_as_str_(SprinklerState state) {
void Sprinkler::start_timer_(const SprinklerTimerIndex timer_index) {
if (this->timer_duration_(timer_index) > 0) {
this->set_timeout(this->timer_[timer_index].name, this->timer_duration_(timer_index),
// FixedVector ensures timer_ can't be resized, so .c_str() pointers remain valid
this->set_timeout(this->timer_[timer_index].name.c_str(), this->timer_duration_(timer_index),
this->timer_cbf_(timer_index));
this->timer_[timer_index].start_time = millis();
this->timer_[timer_index].active = true;
@@ -1585,7 +1587,7 @@ void Sprinkler::start_timer_(const SprinklerTimerIndex timer_index) {
bool Sprinkler::cancel_timer_(const SprinklerTimerIndex timer_index) {
this->timer_[timer_index].active = false;
return this->cancel_timeout(this->timer_[timer_index].name);
return this->cancel_timeout(this->timer_[timer_index].name.c_str());
}
bool Sprinkler::timer_active_(const SprinklerTimerIndex timer_index) { return this->timer_[timer_index].active; }

View File

@@ -3,6 +3,7 @@
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/components/number/number.h"
#include "esphome/components/switch/switch.h"
@@ -553,8 +554,8 @@ class Sprinkler : public Component {
/// Sprinkler valve operator objects
std::vector<SprinklerValveOperator> valve_op_{2};
/// Valve control timers
std::vector<SprinklerTimer> timer_{};
/// Valve control timers - FixedVector enforces that this can never grow beyond init() size
FixedVector<SprinklerTimer> timer_;
/// Other Sprinkler instances we should be aware of (used to check if pumps are in use)
std::vector<Sprinkler *> other_controllers_;

View File

@@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await ssd1306_base.setup_ssd1306(var, config)
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
cg.add(var.set_dc_pin(dc))

View File

@@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await ssd1322_base.setup_ssd1322(var, config)
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
cg.add(var.set_dc_pin(dc))

View File

@@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await ssd1325_base.setup_ssd1325(var, config)
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
cg.add(var.set_dc_pin(dc))

View File

@@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await ssd1327_base.setup_ssd1327(var, config)
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
cg.add(var.set_dc_pin(dc))

View File

@@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await ssd1331_base.setup_ssd1331(var, config)
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
cg.add(var.set_dc_pin(dc))

View File

@@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await ssd1351_base.setup_ssd1351(var, config)
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
cg.add(var.set_dc_pin(dc))

View File

@@ -32,7 +32,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await st7567_base.setup_st7567(var, config)
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
cg.add(var.set_dc_pin(dc))

View File

@@ -173,7 +173,7 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await display.register_display(var, config)
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
sequence = []
for seq in config[CONF_INIT_SEQUENCE]:

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));
}

Some files were not shown because too many files have changed in this diff Show More