Compare commits

...

59 Commits

Author SHA1 Message Date
J. Nick Koston
133cf0be1e remove unnecessary duration_ms update on early return 2026-01-21 15:38:33 -10:00
J. Nick Koston
d708dc648b fix cleanup crash (existing bug)
[13:35:00.591][I][http_request.ota:175]: Done in 542 seconds
[13:35:00.696][V][esp-idf:000]: E (669073) boot_comm: mismatch chip ID, expected 9, found 3424
[13:35:00.698][V][esp-idf:000]: E (669076) esp_ota_ops: New image failed verification
[13:35:00.699][W][http_request.ota:198]: Error ending update! error_code: 132
[13:35:00.701][V][http_request.ota:073]: Aborting OTA backend
[13:35:00.702][V][http_request.ota:076]: Aborting HTTP connection
[13:35:00.703]Guru Meditation Error: Core  1 panic'ed (InstrFetchProhibited). Exception was unhandled.

[13:35:00.703]Core  1 register dump:
[13:35:00.703]PC      : 0x11101080  PS      : 0x00060630  A0      : 0x8203e461  A1      : 0x3fceded0
[13:35:00.703]A2      : 0x3fc9e45c  A3      : 0x3fcee840  A4      : 0x0000003f  A5      : 0x3fcee840
[13:35:00.703]A6      : 0x0000003e  A7      : 0x3fcafccc  A8      : 0x8203e43a  A9      : 0x3fcede40
[13:35:00.703]A10     : 0x3fc9e45c  A11     : 0x00000001  A12     : 0x0000003f  A13     : 0x3fc9ee08
[13:35:00.704]A14     : 0x0000006d  A15     : 0x3fcee674  SAR     : 0x00000008  EXCCAUSE: 0x00000014
[13:35:00.704]EXCVADDR: 0x11101080  LBEG    : 0x40056f08  LEND    : 0x40056f12  LCOUNT  : 0x00000000

[13:35:00.706]Backtrace: 0x1110107d:0x3fceded0 0x4203e45e:0x3fcedef0 0x4203e46e:0x3fcedf10 0x4201f561:0x3fcedf30 0x420078fc:0x3fcedf50 0x4200817f:0x3fcedf80 0x42008c89:0x3fcedfa0 0x42008da9:0x3fcee1d0 0x42014e45:0x3fcee1f0 0x4209e323:0x3fcee230 0x4209e337:0x3fcee250 0x4209e505:0x3fcee270 0x4201402e:0x3fcee290 0x42006555:0x3fcee2b0 0x4200376c:0x3fcee2d0 0x4209d809:0x3fcee2f0 0x42005b6d:0x3fcee310 0x42005cb9:0x3fcee350 0x420042b4:0x3fcee370 0x4200620f:0x3fcee3a0 0x4209e0ed:0x3fcee3f0 0x42012889:0x3fcee410 0x4201243e:0x3fcee430 0x420140d2:0x3fcee490 0x420070fe:0x3fcee4b0
WARNING Found stack trace! Trying to decode it
WARNING Decoded 0x4203e45e: esp_transport_list_clean at /Users/bdraco/.platformio/packages/framework-espidf/components/tcp_transport/transport.c:85
WARNING Decoded 0x4203e46e: esp_transport_list_destroy at /Users/bdraco/.platformio/packages/framework-espidf/components/tcp_transport/transport.c:74
WARNING Decoded 0x4201f561: esp_http_client_cleanup at /Users/bdraco/.platformio/packages/framework-espidf/components/esp_http_client/esp_http_client.c:1027
WARNING Decoded 0x420078fc: esphome::http_request::HttpContainerIDF::end() at /Users/bdraco/esphome/.esphome/build/test-http-ota-s3/src/esphome/components/http_request/http_request_idf.cpp:270
WARNING Decoded 0x4200817f: esphome::http_request::OtaHttpRequestComponent::cleanup_(std::unique_ptr<esphome::ota::OTABackend, std::default_delete<esphome::ota::OTABackend> >, std::shared_ptr<esphome::http_request::HttpContainer> const&) at /Users/bdraco/esphome/.esphome/build/test-http-ota-s3/src/esphome/components/http_request/ota/ota_http_request.cpp:77 (discriminator 1)
WARNING Decoded 0x42008c89: esphome::http_request::OtaHttpRequestComponent::do_ota_() at /Users/bdraco/esphome/.esphome/build/test-http-ota-s3/src/esphome/components/http_request/ota/ota_http_request.cpp:199 (discriminator 1)
WARNING Decoded 0x42008da9: esphome::http_request::OtaHttpRequestComponent::flash() at /Users/bdraco/esphome/.esphome/build/test-http-ota-s3/src/esphome/components/http_request/ota/ota_http_request.cpp:49
WARNING Decoded 0x42014e45: esphome::http_request::OtaHttpRequestComponentFlashAction<>::play() at /Users/bdraco/esphome/.esphome/build/test-http-ota-s3/src/esphome/components/http_request/ota/automation.h:33
WARNING Decoded 0x4209e323: esphome::Action<>::play_complex() at /Users/bdraco/esphome/.esphome/build/test-http-ota-s3/src/esphome/core/automation.h:268
WARNING Decoded 0x4209e337: esphome::Action<>::play_next_() at /Users/bdraco/esphome/.esphome/build/test-http-ota-s3/src/esphome/core/automation.h:299
 (inlined by) esphome::Action<>::play_complex() at /Users/bdraco/esphome/.esphome/build/test-http-ota-s3/src/esphome/core/automation.h:269
WARNING Decoded 0x4209e505: esphome::ActionList<>::play() at /Users/bdraco/esphome/.esphome/build/test-http-ota-s3/src/esphome/core/automation.h:347
 (inlined by) esphome::Automation<>::trigger() at /Users/bdraco/esphome/.esphome/build/test-http-ota-s3/src/esphome/core/automation.h:389
 (inlined by) esphome::Trigger<>::trigger() at /Users/bdraco/esphome/.esphome/build/test-http-ota-s3/src/esphome/core/automation.h:241
WARNING Decoded 0x4201402e: esphome::button::ButtonPressTrigger::ButtonPressTrigger(esphome::button::Button*)::{lambda()#1}::operator()() const at /Users/bdraco/esphome/.esphome/build/test-http-ota-s3/src/esphome/components/button/automation.h:22
 (inlined by) void std::__invoke_impl<void, esphome::button::ButtonPressTrigger::ButtonPressTrigger(esphome::button::Button*)::{lambda()#1}&>(std::__invoke_other, esphome::button::ButtonPressTrigger::ButtonPressTrigger(esphome::button::Button*)::{lambda()#1}&) at /Users/bdraco/.platformio/packages/toolchain-xtensa-esp-elf/xtensa-esp-elf/include/c++/14.2.0/bits/invoke.h:61
 (inlined by) std::enable_if<is_invocable_r_v<void, esphome::button::ButtonPressTrigger::ButtonPressTrigger(esphome::button::Button*)::{lambda()#1}&>, void>::type std::__invoke_r<void, esphome::button::ButtonPressTrigger::ButtonPressTrigger(esphome::button::Button*)::{lambda()#1}&>(esphome::button::ButtonPressTrigger::ButtonPressTrigger(esphome::button::Button*)::{lambda()#1}&) at /Users/bdraco/.platformio/packages/toolchain-xtensa-esp-elf/xtensa-esp-elf/include/c++/14.2.0/bits/invoke.h:111
 (inlined by) std::_Function_handler<void (), esphome::button::ButtonPressTrigger::ButtonPressTrigger(esphome::button::Button*)::{lambda()#1}>::_M_invoke(std::_Any_data const&) at /Users/bdraco/.platformio/packages/toolchain-xtensa-esp-elf/xtensa-esp-elf/include/c++/14.2.0/bits/std_function.h:290
WARNING Decoded 0x42006555: std::function<void ()>::operator()() const at /Users/bdraco/.platformio/packages/toolchain-xtensa-esp-elf/xtensa-esp-elf/include/c++/14.2.0/bits/std_function.h:591
 (inlined by) esphome::CallbackManager<void ()>::call() at /Users/bdraco/esphome/.esphome/build/test-http-ota-s3/src/esphome/core/helpers.h:1335
 (inlined by) esphome::LazyCallbackManager<void ()>::call() at /Users/bdraco/esphome/.esphome/build/test-http-ota-s3/src/esphome/core/helpers.h:1387
 (inlined by) esphome::button::Button::press() at /Users/bdraco/esphome/.esphome/build/test-http-ota-s3/src/esphome/components/button/button.cpp:24
WARNING Decoded 0x4200376c: esphome::api::APIConnection::button_command(esphome::api::ButtonCommandRequest const&) at /Users/bdraco/esphome/.esphome/build/test-http-ota-s3/src/esphome/components/api/api_connection.cpp:941
WARNING Decoded 0x4209d809: esphome::api::APIServerConnection::on_button_command_request(esphome::api::ButtonCommandRequest const&) at /Users/bdraco/esphome/.esphome/build/test-http-ota-s3/src/esphome/components/api/api_pb2_service.cpp:692
WARNING Decoded 0x42005b6d: esphome::api::APIServerConnectionBase::read_message(unsigned long, unsigned long, unsigned char const*) at /Users/bdraco/esphome/.esphome/build/test-http-ota-s3/src/esphome/components/api/api_pb2_service.cpp:526
WARNING Decoded 0x42005cb9: esphome::api::APIServerConnection::read_message(unsigned long, unsigned long, unsigned char const*) at /Users/bdraco/esphome/.esphome/build/test-http-ota-s3/src/esphome/components/api/api_pb2_service.cpp:864
WARNING Decoded 0x420042b4: esphome::api::APIConnection::loop() at /Users/bdraco/esphome/.esphome/build/test-http-ota-s3/src/esphome/components/api/api_connection.cpp:210
WARNING Decoded 0x4200620f: esphome::api::APIServer::loop() at /Users/bdraco/esphome/.esphome/build/test-http-ota-s3/src/esphome/components/api/api_server.cpp:183 (discriminator 1)
WARNING Decoded 0x4209e0ed: esphome::Component::call_loop() at /Users/bdraco/esphome/.esphome/build/test-http-ota-s3/src/esphome/core/component.cpp:211
WARNING Decoded 0x42012889: esphome::Component::call() at /Users/bdraco/esphome/.esphome/build/test-http-ota-s3/src/esphome/core/component.cpp:266
WARNING Decoded 0x4201243e: esphome::Application::loop() at /Users/bdraco/esphome/.esphome/build/test-http-ota-s3/src/esphome/core/application.cpp:164
WARNING Decoded 0x420140d2: loop() at /Users/bdraco/esphome/.esphome/build/test-http-ota-s3/test_http_ota_esp32s3.yaml:162
WARNING Decoded 0x420070fe: esphome::loop_task(void*) at /Users/bdraco/esphome/.esphome/build/test-http-ota-s3/src/esphome/components/esp32/core.cpp:62 (discriminator 1)

[13:35:00.706]ELF file SHA256: 84cefc24a

[13:35:00.706]Rebooting...
[13:35:02.193]ESP-ROM:esp32s3-20210327
[13:35:02.193]Build:Mar 27 2021
[13:35:02.193]rst:0xc (RTC_SW_CPU_RST),boot:0x8 (SPI_FAST_FLASH_BOOT)
[13:35:02.193]Saved PC:0x40378c02
WARNING Decoded 0x40378c02: esp_cpu_wait_for_intr at /Users/bdraco/.platformio/packages/framework-espidf/components/esp_hw_support/cpu.c:64
[13:35:02.193]SPIWP:0xee
[13:35:02.193]mode:DIO, clock div:1
[13:35:02.193]load:0x3fce2820,len:0x15c8
[13:35:02.193]load:0x403c8700,len:0xce4
[13:35:02.193]load:0x403cb700,len:0x2f98
2026-01-21 13:56:37 -10:00
J. Nick Koston
802549362f help clang-tidy 2026-01-21 12:57:59 -10:00
J. Nick Koston
dd4bfc7b0b unify, make consistant 2026-01-21 12:52:39 -10:00
J. Nick Koston
0d0899b10e unify, make consistant 2026-01-21 12:52:15 -10:00
J. Nick Koston
9b155a3126 document document document 2026-01-21 12:50:39 -10:00
J. Nick Koston
371a1f71a8 document document document 2026-01-21 12:50:20 -10:00
J. Nick Koston
d56554100b document document document 2026-01-21 12:50:06 -10:00
J. Nick Koston
5efe5ff9fd fix all the use 2026-01-21 12:35:00 -10:00
J. Nick Koston
af76ddeda4 unify, make consistant 2026-01-21 12:31:18 -10:00
J. Nick Koston
6a8bae5b1c unify, make consistant 2026-01-21 12:30:42 -10:00
J. Nick Koston
dffc9257dd Update esphome/components/http_request/http_request_idf.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-21 12:25:54 -10:00
J. Nick Koston
68b328c019 match difficult ard behavior 2026-01-21 12:25:28 -10:00
J. Nick Koston
81df19dd4b handle failure 2026-01-21 12:18:52 -10:00
J. Nick Koston
cbcd2b2a70 [http_request] Fix OTA failures on ESP8266/Arduino by making read semantics consistent 2026-01-21 12:14:27 -10:00
Joakim Plate
9d967b01c8 Expose sockaddr to string formatter (#12351) 2026-01-21 10:32:39 -10:00
tomaszduda23
11e0d536e4 [debug] Print reg0 value from config if mismatched on nrf52 (#11867)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-01-21 20:15:51 +00:00
dependabot[bot]
673f46f761 Bump peter-evans/create-pull-request from 8.0.0 to 8.1.0 (#13430)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-21 09:37:18 -10:00
dependabot[bot]
4abae8d445 Bump setuptools from 80.9.0 to 80.10.1 (#13429)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-21 09:37:04 -10:00
Jonathan Swoboda
e62368e058 [heatpumpir] Add ESP-IDF support, bump to 1.0.40 (#13042) 2026-01-21 13:19:36 -05:00
Jonathan Swoboda
5345c96ff3 [http_request] Fix verify_ssl: false not working on ESP32 (#13422)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 13:18:37 -05:00
tomaszduda23
333ace25c9 [adc] Fix indent (#11933) 2026-01-21 12:41:56 -05:00
Dawid
6014bba3d1 [zephyr] Small build fixes for the logger/gpio subsystems (#13242)
Co-authored-by: dawret <dawret@dawret.me>
2026-01-21 12:37:10 -05:00
maikeljkwak
5f2394ef80 [hc8, mhz19] Moving constant CONF_WARMUP_TIME to const.py (#13392)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-01-21 12:34:52 -05:00
Copilot
29555c0ddc [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-21 12:32:55 -05:00
Kevin Ahrendt
37eaf10f75 [audio] Bump esp-audio-libs to 2.0.3 (#13346) 2026-01-21 07:40:41 -05:00
J. Nick Koston
0b60fd0c8c [core] Avoid heap allocation in str_equals_case_insensitive with string literals (#13312) 2026-01-20 21:49:14 -10:00
J. Nick Koston
fc16ad806a [ci] Block sprintf/vsprintf usage, suggest snprintf alternatives (#13305) 2026-01-20 17:53:36 -10:00
J. Nick Koston
7e43abd86f [web_server_idf] Use direct member for ListEntitiesIterator instead of unique_ptr (#13405) 2026-01-20 17:53:23 -10:00
J. Nick Koston
7a2734fae9 [libretiny] Disable unused LWIP statistics to save RAM and flash (#13404) 2026-01-20 17:53:10 -10:00
J. Nick Koston
346f3d38d5 [logger] Use raw pointer for task log buffer to match tx_buffer pattern (#13402) 2026-01-20 17:52:58 -10:00
J. Nick Koston
fbde91358c [mdns] Use stack buffer for txt records on ESP32 (#13401) 2026-01-20 17:52:43 -10:00
J. Nick Koston
54d6825323 [esp32] [libretiny] Use stack buffer for preference comparison (#13398) 2026-01-20 17:52:28 -10:00
J. Nick Koston
307c3e1061 [core] Simplify LazyCallbackManager memory management (#13387) 2026-01-20 17:52:12 -10:00
Jonathan Swoboda
df74d307c8 [esp32] Add support for native ESP-IDF builds (#13272)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-01-20 22:52:04 -05:00
Jonathan Swoboda
acdc7bd892 [json] Use ESP-IDF component registry for ArduinoJson on ESP32 (#13280)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 22:51:54 -05:00
Jasper van der Neut - Stulen
1095bde2db [cc1101] Add on_packet listener callback code (packet_transport) (#13344) 2026-01-20 22:51:39 -05:00
J. Nick Koston
258b73d7f6 [core] Eliminate global constructor overhead for component vectors (#13386) 2026-01-20 17:51:06 -10:00
J. Nick Koston
31608543c2 [esp32_ble_tracker] Optimize loop with state change tracking for ~85% CPU reduction (#13337) 2026-01-20 17:50:53 -10:00
J. Nick Koston
41a060668c [api] Use stack buffers for noise handshake messages (#13399)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-20 17:50:39 -10:00
J. Nick Koston
6bad697fc6 [debug] ESP8266: Eliminate heap allocations from Arduino String functions (#13352) 2026-01-20 17:50:27 -10:00
J. Nick Koston
3ca5e5e4e4 [wifi] ESP8266: Use direct SDK calls to reduce flash and heap allocation (#13349) 2026-01-20 17:50:13 -10:00
J. Nick Koston
cd4cb8b3ec [datetime] Add const char * overloads for string parsing to avoid heap allocation (#13363) 2026-01-20 17:50:01 -10:00
J. Nick Koston
1f3a0490a7 [wifi] Process scan results one at a time to avoid heap allocation (#13400) 2026-01-20 17:49:40 -10:00
Jonathan Swoboda
b08d871add Merge branch 'release' into dev 2026-01-20 22:43:22 -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
polyfloyd
3c0f43db9e Add the max_delta filter (#12605)
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2026-01-21 10:58:47 +11:00
Jonathan Swoboda
6edecd3d45 Merge branch 'beta' into dev 2026-01-20 17:01:47 -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
Clyde Stubbs
79ccacd6d6 [helpers] Allow reading capacity of FixedVector (#13391) 2026-01-20 09:24:42 -10:00
J. Nick Koston
e2319ba651 [wifi_info] Fix missing state when both IP+DNS or SSID+BSSID configure (#13385) 2026-01-20 07:55:59 -10:00
Jonathan Swoboda
ed4ebffa74 [x9c] Fix potentiometer unable to decrement (#13382)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 22:57:54 -05:00
77 changed files with 1779 additions and 435 deletions

View File

@@ -1 +1 @@
d272a88e8ca28ae9340a9a03295a566432a52cb696501908f57764475bf7ca65
c0335c9688ce9defb4a7d4446b93460547e22df055668bacd7d963c770f0c65f

View File

@@ -41,7 +41,7 @@ jobs:
python script/run-in-env.py pre-commit run --all-files
- name: Commit changes
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
commit-message: "Synchronise Device Classes from Home Assistant"
committer: esphomebot <esphome@openhomefoundation.org>

View File

@@ -43,6 +43,7 @@ from esphome.const import (
CONF_SUBSTITUTIONS,
CONF_TOPIC,
ENV_NOGITIGNORE,
KEY_NATIVE_IDF,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_RP2040,
@@ -116,6 +117,7 @@ class ArgsProtocol(Protocol):
configuration: str
name: str
upload_speed: str | None
native_idf: bool
def choose_prompt(options, purpose: str = None):
@@ -500,12 +502,15 @@ def wrap_to_code(name, comp):
return wrapped
def write_cpp(config: ConfigType) -> int:
def write_cpp(config: ConfigType, native_idf: bool = False) -> int:
if not get_bool_env(ENV_NOGITIGNORE):
writer.write_gitignore()
# Store native_idf flag so esp32 component can check it
CORE.data[KEY_NATIVE_IDF] = native_idf
generate_cpp_contents(config)
return write_cpp_file()
return write_cpp_file(native_idf=native_idf)
def generate_cpp_contents(config: ConfigType) -> None:
@@ -519,32 +524,54 @@ def generate_cpp_contents(config: ConfigType) -> None:
CORE.flush_tasks()
def write_cpp_file() -> int:
def write_cpp_file(native_idf: bool = False) -> int:
code_s = indent(CORE.cpp_main_section)
writer.write_cpp(code_s)
from esphome.build_gen import platformio
if native_idf and CORE.is_esp32 and CORE.target_framework == "esp-idf":
from esphome.build_gen import espidf
platformio.write_project()
espidf.write_project()
else:
from esphome.build_gen import platformio
platformio.write_project()
return 0
def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
from esphome import platformio_api
native_idf = getattr(args, "native_idf", False)
# NOTE: "Build path:" format is parsed by script/ci_memory_impact_extract.py
# If you change this format, update the regex in that script as well
_LOGGER.info("Compiling app... Build path: %s", CORE.build_path)
rc = platformio_api.run_compile(config, CORE.verbose)
if rc != 0:
return rc
if native_idf and CORE.is_esp32 and CORE.target_framework == "esp-idf":
from esphome import espidf_api
rc = espidf_api.run_compile(config, CORE.verbose)
if rc != 0:
return rc
# Create factory.bin and ota.bin
espidf_api.create_factory_bin()
espidf_api.create_ota_bin()
else:
from esphome import platformio_api
rc = platformio_api.run_compile(config, CORE.verbose)
if rc != 0:
return rc
idedata = platformio_api.get_idedata(config)
if idedata is None:
return 1
# Check if firmware was rebuilt and emit build_info + create manifest
_check_and_emit_build_info()
idedata = platformio_api.get_idedata(config)
return 0 if idedata is not None else 1
return 0
def _check_and_emit_build_info() -> None:
@@ -801,7 +828,8 @@ def command_vscode(args: ArgsProtocol) -> int | None:
def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
exit_code = write_cpp(config)
native_idf = getattr(args, "native_idf", False)
exit_code = write_cpp(config, native_idf=native_idf)
if exit_code != 0:
return exit_code
if args.only_generate:
@@ -856,7 +884,8 @@ def command_logs(args: ArgsProtocol, config: ConfigType) -> int | None:
def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
exit_code = write_cpp(config)
native_idf = getattr(args, "native_idf", False)
exit_code = write_cpp(config, native_idf=native_idf)
if exit_code != 0:
return exit_code
exit_code = compile_program(args, config)
@@ -1310,6 +1339,11 @@ def parse_args(argv):
help="Only generate source code, do not compile.",
action="store_true",
)
parser_compile.add_argument(
"--native-idf",
help="Build with native ESP-IDF instead of PlatformIO (ESP32 esp-idf framework only).",
action="store_true",
)
parser_upload = subparsers.add_parser(
"upload",
@@ -1391,6 +1425,11 @@ def parse_args(argv):
help="Reset the device before starting serial logs.",
default=os.getenv("ESPHOME_SERIAL_LOGGING_RESET"),
)
parser_run.add_argument(
"--native-idf",
help="Build with native ESP-IDF instead of PlatformIO (ESP32 esp-idf framework only).",
action="store_true",
)
parser_clean = subparsers.add_parser(
"clean-mqtt",

139
esphome/build_gen/espidf.py Normal file
View File

@@ -0,0 +1,139 @@
"""ESP-IDF direct build generator for ESPHome."""
import json
from pathlib import Path
from esphome.components.esp32 import get_esp32_variant
from esphome.core import CORE
from esphome.helpers import mkdir_p, write_file_if_changed
def get_available_components() -> list[str] | None:
"""Get list of available ESP-IDF components from project_description.json.
Returns only internal ESP-IDF components, excluding external/managed
components (from idf_component.yml).
"""
project_desc = Path(CORE.build_path) / "build" / "project_description.json"
if not project_desc.exists():
return None
try:
with open(project_desc, encoding="utf-8") as f:
data = json.load(f)
component_info = data.get("build_component_info", {})
result = []
for name, info in component_info.items():
# Exclude our own src component
if name == "src":
continue
# Exclude managed/external components
comp_dir = info.get("dir", "")
if "managed_components" in comp_dir:
continue
result.append(name)
return result
except (json.JSONDecodeError, OSError):
return None
def has_discovered_components() -> bool:
"""Check if we have discovered components from a previous configure."""
return get_available_components() is not None
def get_project_cmakelists() -> str:
"""Generate the top-level CMakeLists.txt for ESP-IDF project."""
# Get IDF target from ESP32 variant (e.g., ESP32S3 -> esp32s3)
variant = get_esp32_variant()
idf_target = variant.lower().replace("-", "")
return f"""\
# Auto-generated by ESPHome
cmake_minimum_required(VERSION 3.16)
set(IDF_TARGET {idf_target})
set(EXTRA_COMPONENT_DIRS ${{CMAKE_SOURCE_DIR}}/src)
include($ENV{{IDF_PATH}}/tools/cmake/project.cmake)
project({CORE.name})
"""
def get_component_cmakelists(minimal: bool = False) -> str:
"""Generate the main component CMakeLists.txt."""
idf_requires = [] if minimal else (get_available_components() or [])
requires_str = " ".join(idf_requires)
# Extract compile definitions from build flags (-DXXX -> XXX)
compile_defs = [flag[2:] for flag in CORE.build_flags if flag.startswith("-D")]
compile_defs_str = "\n ".join(compile_defs) if compile_defs else ""
# Extract compile options (-W flags, excluding linker flags)
compile_opts = [
flag
for flag in CORE.build_flags
if flag.startswith("-W") and not flag.startswith("-Wl,")
]
compile_opts_str = "\n ".join(compile_opts) if compile_opts else ""
# Extract linker options (-Wl, flags)
link_opts = [flag for flag in CORE.build_flags if flag.startswith("-Wl,")]
link_opts_str = "\n ".join(link_opts) if link_opts else ""
return f"""\
# Auto-generated by ESPHome
file(GLOB_RECURSE app_sources
"${{CMAKE_CURRENT_SOURCE_DIR}}/*.cpp"
"${{CMAKE_CURRENT_SOURCE_DIR}}/*.c"
"${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.cpp"
"${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.c"
)
idf_component_register(
SRCS ${{app_sources}}
INCLUDE_DIRS "." "esphome"
REQUIRES {requires_str}
)
# Apply C++ standard
target_compile_features(${{COMPONENT_LIB}} PUBLIC cxx_std_20)
# ESPHome compile definitions
target_compile_definitions(${{COMPONENT_LIB}} PUBLIC
{compile_defs_str}
)
# ESPHome compile options
target_compile_options(${{COMPONENT_LIB}} PUBLIC
{compile_opts_str}
)
# ESPHome linker options
target_link_options(${{COMPONENT_LIB}} PUBLIC
{link_opts_str}
)
"""
def write_project(minimal: bool = False) -> None:
"""Write ESP-IDF project files."""
mkdir_p(CORE.build_path)
mkdir_p(CORE.relative_src_path())
# Write top-level CMakeLists.txt
write_file_if_changed(
CORE.relative_build_path("CMakeLists.txt"),
get_project_cmakelists(),
)
# Write component CMakeLists.txt in src/
write_file_if_changed(
CORE.relative_src_path("CMakeLists.txt"),
get_component_cmakelists(minimal=minimal),
)

View File

@@ -160,21 +160,21 @@ async def to_code(config):
zephyr_add_user("io-channels", f"<&adc {channel_id}>")
zephyr_add_overlay(
f"""
&adc {{
#address-cells = <1>;
#size-cells = <0>;
&adc {{
#address-cells = <1>;
#size-cells = <0>;
channel@{channel_id} {{
reg = <{channel_id}>;
zephyr,gain = "{gain}";
zephyr,reference = "ADC_REF_INTERNAL";
zephyr,acquisition-time = <ADC_ACQ_TIME_DEFAULT>;
zephyr,input-positive = <NRF_SAADC_{pin_number}>;
zephyr,resolution = <14>;
zephyr,oversampling = <8>;
}};
}};
"""
channel@{channel_id} {{
reg = <{channel_id}>;
zephyr,gain = "{gain}";
zephyr,reference = "ADC_REF_INTERNAL";
zephyr,acquisition-time = <ADC_ACQ_TIME_DEFAULT>;
zephyr,input-positive = <NRF_SAADC_{pin_number}>;
zephyr,resolution = <14>;
zephyr,oversampling = <8>;
}};
}};
"""
)

View File

@@ -3,6 +3,7 @@
#ifdef USE_API_NOISE
#include "api_connection.h" // For ClientInfo struct
#include "esphome/core/application.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -256,28 +257,30 @@ APIError APINoiseFrameHelper::state_action_() {
}
if (state_ == State::SERVER_HELLO) {
// send server hello
constexpr size_t mac_len = 13; // 12 hex chars + null terminator
const std::string &name = App.get_name();
char mac[mac_len];
char mac[MAC_ADDRESS_BUFFER_SIZE];
get_mac_address_into_buffer(mac);
// Calculate positions and sizes
size_t name_len = name.size() + 1; // including null terminator
size_t name_offset = 1;
size_t mac_offset = name_offset + name_len;
size_t total_size = 1 + name_len + mac_len;
size_t total_size = 1 + name_len + MAC_ADDRESS_BUFFER_SIZE;
auto msg = std::make_unique<uint8_t[]>(total_size);
// 1 (proto) + name (max ESPHOME_DEVICE_NAME_MAX_LEN) + 1 (name null)
// + mac (MAC_ADDRESS_BUFFER_SIZE - 1) + 1 (mac null)
constexpr size_t max_msg_size = 1 + ESPHOME_DEVICE_NAME_MAX_LEN + 1 + MAC_ADDRESS_BUFFER_SIZE;
uint8_t msg[max_msg_size];
// chosen proto
msg[0] = 0x01;
// node name, terminated by null byte
std::memcpy(msg.get() + name_offset, name.c_str(), name_len);
std::memcpy(msg + name_offset, name.c_str(), name_len);
// node mac, terminated by null byte
std::memcpy(msg.get() + mac_offset, mac, mac_len);
std::memcpy(msg + mac_offset, mac, MAC_ADDRESS_BUFFER_SIZE);
aerr = write_frame_(msg.get(), total_size);
aerr = write_frame_(msg, total_size);
if (aerr != APIError::OK)
return aerr;
@@ -353,35 +356,32 @@ APIError APINoiseFrameHelper::state_action_() {
return APIError::OK;
}
void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reason) {
// Max reject message: "Bad handshake packet len" (24) + 1 (failure byte) = 25 bytes
uint8_t data[32];
data[0] = 0x01; // failure
#ifdef USE_STORE_LOG_STR_IN_FLASH
// On ESP8266 with flash strings, we need to use PROGMEM-aware functions
size_t reason_len = strlen_P(reinterpret_cast<PGM_P>(reason));
size_t data_size = reason_len + 1;
auto data = std::make_unique<uint8_t[]>(data_size);
data[0] = 0x01; // failure
// Copy error message from PROGMEM
if (reason_len > 0) {
memcpy_P(data.get() + 1, reinterpret_cast<PGM_P>(reason), reason_len);
memcpy_P(data + 1, reinterpret_cast<PGM_P>(reason), reason_len);
}
#else
// Normal memory access
const char *reason_str = LOG_STR_ARG(reason);
size_t reason_len = strlen(reason_str);
size_t data_size = reason_len + 1;
auto data = std::make_unique<uint8_t[]>(data_size);
data[0] = 0x01; // failure
// Copy error message in bulk
if (reason_len > 0) {
std::memcpy(data.get() + 1, reason_str, reason_len);
// NOLINTNEXTLINE(bugprone-not-null-terminated-result) - binary protocol, not a C string
std::memcpy(data + 1, reason_str, reason_len);
}
#endif
size_t data_size = reason_len + 1;
// temporarily remove failed state
auto orig_state = state_;
state_ = State::EXPLICIT_REJECT;
write_frame_(data.get(), data_size);
write_frame_(data, data_size);
state_ = orig_state;
}
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {

View File

@@ -1,4 +1,5 @@
import esphome.codegen as cg
from esphome.components.esp32 import add_idf_component
import esphome.config_validation as cv
from esphome.const import CONF_BITS_PER_SAMPLE, CONF_NUM_CHANNELS, CONF_SAMPLE_RATE
import esphome.final_validate as fv
@@ -165,4 +166,7 @@ def final_validate_audio_schema(
async def to_code(config):
cg.add_library("esphome/esp-audio-libs", "2.0.1")
add_idf_component(
name="esphome/esp-audio-libs",
ref="2.0.3",
)

View File

@@ -300,7 +300,7 @@ FileDecoderState AudioDecoder::decode_mp3_() {
// Advance read pointer to match the offset for the syncword
this->input_transfer_buffer_->decrease_buffer_length(offset);
uint8_t *buffer_start = this->input_transfer_buffer_->get_buffer_start();
const uint8_t *buffer_start = this->input_transfer_buffer_->get_buffer_start();
buffer_length = (int) this->input_transfer_buffer_->available();
int err = esp_audio_libs::helix_decoder::MP3Decode(this->mp3_decoder_, &buffer_start, &buffer_length,

View File

@@ -135,8 +135,8 @@ void BluetoothConnection::loop() {
// - For V3_WITH_CACHE: Services are never sent, disable after INIT state
// - For V3_WITHOUT_CACHE: Disable only after service discovery is complete
// (send_service_ == DONE_SENDING_SERVICES, which is only set after services are sent)
if (this->state_ != espbt::ClientState::INIT && (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
this->send_service_ == DONE_SENDING_SERVICES)) {
if (this->state() != espbt::ClientState::INIT && (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
this->send_service_ == DONE_SENDING_SERVICES)) {
this->disable_loop();
}
}

View File

@@ -152,6 +152,13 @@ void CC1101Component::setup() {
}
}
void CC1101Component::call_listeners_(const std::vector<uint8_t> &packet, float freq_offset, float rssi, uint8_t lqi) {
for (auto &listener : this->listeners_) {
listener->on_packet(packet, freq_offset, rssi, lqi);
}
this->packet_trigger_->trigger(packet, freq_offset, rssi, lqi);
}
void CC1101Component::loop() {
if (this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO) || this->gdo0_pin_ == nullptr ||
!this->gdo0_pin_->digital_read()) {
@@ -198,7 +205,7 @@ void CC1101Component::loop() {
bool crc_ok = (this->state_.LQI & STATUS_CRC_OK_MASK) != 0;
uint8_t lqi = this->state_.LQI & STATUS_LQI_MASK;
if (this->state_.CRC_EN == 0 || crc_ok) {
this->packet_trigger_->trigger(this->packet_, freq_offset, rssi, lqi);
this->call_listeners_(this->packet_, freq_offset, rssi, lqi);
}
// Return to rx

View File

@@ -11,6 +11,11 @@ namespace esphome::cc1101 {
enum class CC1101Error { NONE = 0, TIMEOUT, PARAMS, CRC_ERROR, FIFO_OVERFLOW, PLL_LOCK };
class CC1101Listener {
public:
virtual void on_packet(const std::vector<uint8_t> &packet, float freq_offset, float rssi, uint8_t lqi) = 0;
};
class CC1101Component : public Component,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_1MHZ> {
@@ -73,6 +78,7 @@ class CC1101Component : public Component,
// Packet mode operations
CC1101Error transmit_packet(const std::vector<uint8_t> &packet);
void register_listener(CC1101Listener *listener) { this->listeners_.push_back(listener); }
Trigger<std::vector<uint8_t>, float, float, uint8_t> *get_packet_trigger() const { return this->packet_trigger_; }
protected:
@@ -89,9 +95,11 @@ class CC1101Component : public Component,
InternalGPIOPin *gdo0_pin_{nullptr};
// Packet handling
void call_listeners_(const std::vector<uint8_t> &packet, float freq_offset, float rssi, uint8_t lqi);
Trigger<std::vector<uint8_t>, float, float, uint8_t> *packet_trigger_{
new Trigger<std::vector<uint8_t>, float, float, uint8_t>()};
std::vector<uint8_t> packet_;
std::vector<CC1101Listener *> listeners_;
// Low-level Helpers
uint8_t strobe_(Command cmd);

View File

@@ -106,9 +106,9 @@ DateCall &DateCall::set_date(uint16_t year, uint8_t month, uint8_t day) {
DateCall &DateCall::set_date(ESPTime time) { return this->set_date(time.year, time.month, time.day_of_month); };
DateCall &DateCall::set_date(const std::string &date) {
DateCall &DateCall::set_date(const char *date, size_t len) {
ESPTime val{};
if (!ESPTime::strptime(date, val)) {
if (!ESPTime::strptime(date, len, val)) {
ESP_LOGE(TAG, "Could not convert the date string to an ESPTime object");
return *this;
}

View File

@@ -67,7 +67,9 @@ class DateCall {
void perform();
DateCall &set_date(uint16_t year, uint8_t month, uint8_t day);
DateCall &set_date(ESPTime time);
DateCall &set_date(const std::string &date);
DateCall &set_date(const char *date, size_t len);
DateCall &set_date(const char *date) { return this->set_date(date, strlen(date)); }
DateCall &set_date(const std::string &date) { return this->set_date(date.c_str(), date.size()); }
DateCall &set_year(uint16_t year) {
this->year_ = year;

View File

@@ -163,9 +163,9 @@ DateTimeCall &DateTimeCall::set_datetime(ESPTime datetime) {
datetime.second);
};
DateTimeCall &DateTimeCall::set_datetime(const std::string &datetime) {
DateTimeCall &DateTimeCall::set_datetime(const char *datetime, size_t len) {
ESPTime val{};
if (!ESPTime::strptime(datetime, val)) {
if (!ESPTime::strptime(datetime, len, val)) {
ESP_LOGE(TAG, "Could not convert the time string to an ESPTime object");
return *this;
}

View File

@@ -71,7 +71,11 @@ class DateTimeCall {
void perform();
DateTimeCall &set_datetime(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second);
DateTimeCall &set_datetime(ESPTime datetime);
DateTimeCall &set_datetime(const std::string &datetime);
DateTimeCall &set_datetime(const char *datetime, size_t len);
DateTimeCall &set_datetime(const char *datetime) { return this->set_datetime(datetime, strlen(datetime)); }
DateTimeCall &set_datetime(const std::string &datetime) {
return this->set_datetime(datetime.c_str(), datetime.size());
}
DateTimeCall &set_datetime(time_t epoch_seconds);
DateTimeCall &set_year(uint16_t year) {

View File

@@ -74,9 +74,9 @@ TimeCall &TimeCall::set_time(uint8_t hour, uint8_t minute, uint8_t second) {
TimeCall &TimeCall::set_time(ESPTime time) { return this->set_time(time.hour, time.minute, time.second); };
TimeCall &TimeCall::set_time(const std::string &time) {
TimeCall &TimeCall::set_time(const char *time, size_t len) {
ESPTime val{};
if (!ESPTime::strptime(time, val)) {
if (!ESPTime::strptime(time, len, val)) {
ESP_LOGE(TAG, "Could not convert the time string to an ESPTime object");
return *this;
}

View File

@@ -69,7 +69,9 @@ class TimeCall {
void perform();
TimeCall &set_time(uint8_t hour, uint8_t minute, uint8_t second);
TimeCall &set_time(ESPTime time);
TimeCall &set_time(const std::string &time);
TimeCall &set_time(const char *time, size_t len);
TimeCall &set_time(const char *time) { return this->set_time(time, strlen(time)); }
TimeCall &set_time(const std::string &time) { return this->set_time(time.c_str(), time.size()); }
TimeCall &set_hour(uint8_t hour) {
this->hour_ = hour;

View File

@@ -3,21 +3,80 @@
#include "esphome/core/log.h"
#include <Esp.h>
extern "C" {
#include <user_interface.h>
// Global reset info struct populated by SDK at boot
extern struct rst_info resetInfo;
// Core version - either a string pointer or a version number to format as hex
extern uint32_t core_version;
extern const char *core_release;
}
namespace esphome {
namespace debug {
static const char *const TAG = "debug";
// Get reset reason string from reason code (no heap allocation)
// Returns LogString* pointing to flash (PROGMEM) on ESP8266
static const LogString *get_reset_reason_str(uint32_t reason) {
switch (reason) {
case REASON_DEFAULT_RST:
return LOG_STR("Power On");
case REASON_WDT_RST:
return LOG_STR("Hardware Watchdog");
case REASON_EXCEPTION_RST:
return LOG_STR("Exception");
case REASON_SOFT_WDT_RST:
return LOG_STR("Software Watchdog");
case REASON_SOFT_RESTART:
return LOG_STR("Software/System restart");
case REASON_DEEP_SLEEP_AWAKE:
return LOG_STR("Deep-Sleep Wake");
case REASON_EXT_SYS_RST:
return LOG_STR("External System");
default:
return LOG_STR("Unknown");
}
}
// Size for core version hex buffer
static constexpr size_t CORE_VERSION_BUFFER_SIZE = 12;
// Get core version string (no heap allocation)
// Returns either core_release directly or formats core_version as hex into provided buffer
static const char *get_core_version_str(std::span<char, CORE_VERSION_BUFFER_SIZE> buffer) {
if (core_release != nullptr) {
return core_release;
}
snprintf_P(buffer.data(), CORE_VERSION_BUFFER_SIZE, PSTR("%08x"), core_version);
return buffer.data();
}
// Size for reset info buffer
static constexpr size_t RESET_INFO_BUFFER_SIZE = 200;
// Get detailed reset info string (no heap allocation)
// For watchdog/exception resets, includes detailed exception info
static const char *get_reset_info_str(std::span<char, RESET_INFO_BUFFER_SIZE> buffer, uint32_t reason) {
if (reason >= REASON_WDT_RST && reason <= REASON_SOFT_WDT_RST) {
snprintf_P(buffer.data(), RESET_INFO_BUFFER_SIZE,
PSTR("Fatal exception:%d flag:%d (%s) epc1:0x%08x epc2:0x%08x epc3:0x%08x excvaddr:0x%08x depc:0x%08x"),
static_cast<int>(resetInfo.exccause), static_cast<int>(reason),
LOG_STR_ARG(get_reset_reason_str(reason)), resetInfo.epc1, resetInfo.epc2, resetInfo.epc3,
resetInfo.excvaddr, resetInfo.depc);
return buffer.data();
}
return LOG_STR_ARG(get_reset_reason_str(reason));
}
const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) {
char *buf = buffer.data();
#if !defined(CLANG_TIDY)
String reason = ESP.getResetReason(); // NOLINT
snprintf_P(buf, RESET_REASON_BUFFER_SIZE, PSTR("%s"), reason.c_str());
return buf;
#else
buf[0] = '\0';
return buf;
#endif
// Copy from flash to provided buffer
strncpy_P(buffer.data(), (PGM_P) get_reset_reason_str(resetInfo.reason), RESET_REASON_BUFFER_SIZE - 1);
buffer[RESET_REASON_BUFFER_SIZE - 1] = '\0';
return buffer.data();
}
const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) {
@@ -33,37 +92,42 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
constexpr size_t size = DEVICE_INFO_BUFFER_SIZE;
char *buf = buffer.data();
const char *flash_mode;
const LogString *flash_mode;
switch (ESP.getFlashChipMode()) { // NOLINT(readability-static-accessed-through-instance)
case FM_QIO:
flash_mode = "QIO";
flash_mode = LOG_STR("QIO");
break;
case FM_QOUT:
flash_mode = "QOUT";
flash_mode = LOG_STR("QOUT");
break;
case FM_DIO:
flash_mode = "DIO";
flash_mode = LOG_STR("DIO");
break;
case FM_DOUT:
flash_mode = "DOUT";
flash_mode = LOG_STR("DOUT");
break;
default:
flash_mode = "UNKNOWN";
flash_mode = LOG_STR("UNKNOWN");
}
uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT
uint32_t flash_speed = ESP.getFlashChipSpeed() / 1000000; // NOLINT
ESP_LOGD(TAG, "Flash Chip: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed, flash_mode);
uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT(readability-static-accessed-through-instance)
uint32_t flash_speed = ESP.getFlashChipSpeed() / 1000000; // NOLINT(readability-static-accessed-through-instance)
ESP_LOGD(TAG, "Flash Chip: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed,
LOG_STR_ARG(flash_mode));
pos = buf_append_printf(buf, size, pos, "|Flash: %" PRIu32 "kB Speed:%" PRIu32 "MHz Mode:%s", flash_size, flash_speed,
flash_mode);
LOG_STR_ARG(flash_mode));
#if !defined(CLANG_TIDY)
char reason_buffer[RESET_REASON_BUFFER_SIZE];
const char *reset_reason = get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE>(reason_buffer));
const char *reset_reason = get_reset_reason_(reason_buffer);
char core_version_buffer[CORE_VERSION_BUFFER_SIZE];
char reset_info_buffer[RESET_INFO_BUFFER_SIZE];
// NOLINTBEGIN(readability-static-accessed-through-instance)
uint32_t chip_id = ESP.getChipId();
uint8_t boot_version = ESP.getBootVersion();
uint8_t boot_mode = ESP.getBootMode();
uint8_t cpu_freq = ESP.getCpuFreqMHz();
uint32_t flash_chip_id = ESP.getFlashChipId();
const char *sdk_version = ESP.getSdkVersion();
// NOLINTEND(readability-static-accessed-through-instance)
ESP_LOGD(TAG,
"Chip ID: 0x%08" PRIX32 "\n"
@@ -74,19 +138,18 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
"Flash Chip ID=0x%08" PRIX32 "\n"
"Reset Reason: %s\n"
"Reset Info: %s",
chip_id, ESP.getSdkVersion(), ESP.getCoreVersion().c_str(), boot_version, boot_mode, cpu_freq, flash_chip_id,
reset_reason, ESP.getResetInfo().c_str());
chip_id, sdk_version, get_core_version_str(core_version_buffer), boot_version, boot_mode, cpu_freq,
flash_chip_id, reset_reason, get_reset_info_str(reset_info_buffer, resetInfo.reason));
pos = buf_append_printf(buf, size, pos, "|Chip: 0x%08" PRIX32, chip_id);
pos = buf_append_printf(buf, size, pos, "|SDK: %s", ESP.getSdkVersion());
pos = buf_append_printf(buf, size, pos, "|Core: %s", ESP.getCoreVersion().c_str());
pos = buf_append_printf(buf, size, pos, "|SDK: %s", sdk_version);
pos = buf_append_printf(buf, size, pos, "|Core: %s", get_core_version_str(core_version_buffer));
pos = buf_append_printf(buf, size, pos, "|Boot: %u", boot_version);
pos = buf_append_printf(buf, size, pos, "|Mode: %u", boot_mode);
pos = buf_append_printf(buf, size, pos, "|CPU: %u", cpu_freq);
pos = buf_append_printf(buf, size, pos, "|Flash: 0x%08" PRIX32, flash_chip_id);
pos = buf_append_printf(buf, size, pos, "|Reset: %s", reset_reason);
pos = buf_append_printf(buf, size, pos, "|%s", ESP.getResetInfo().c_str());
#endif
pos = buf_append_printf(buf, size, pos, "|%s", get_reset_info_str(reset_info_buffer, resetInfo.reason));
return pos;
}

View File

@@ -132,6 +132,26 @@ void DebugComponent::log_partition_info_() {
flash_area_foreach(fa_cb, nullptr);
}
static const char *regout0_to_str(uint32_t value) {
switch (value) {
case (UICR_REGOUT0_VOUT_DEFAULT):
return "1.8V (default)";
case (UICR_REGOUT0_VOUT_1V8):
return "1.8V";
case (UICR_REGOUT0_VOUT_2V1):
return "2.1V";
case (UICR_REGOUT0_VOUT_2V4):
return "2.4V";
case (UICR_REGOUT0_VOUT_2V7):
return "2.7V";
case (UICR_REGOUT0_VOUT_3V0):
return "3.0V";
case (UICR_REGOUT0_VOUT_3V3):
return "3.3V";
}
return "???V";
}
size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE> buffer, size_t pos) {
constexpr size_t size = DEVICE_INFO_BUFFER_SIZE;
char *buf = buffer.data();
@@ -145,34 +165,14 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
// Regulator stage 0
if (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_HIGH) {
const char *reg0_type = nrf_power_dcdcen_vddh_get(NRF_POWER) ? "DC/DC" : "LDO";
const char *reg0_voltage;
switch (NRF_UICR->REGOUT0 & UICR_REGOUT0_VOUT_Msk) {
case (UICR_REGOUT0_VOUT_DEFAULT << UICR_REGOUT0_VOUT_Pos):
reg0_voltage = "1.8V (default)";
break;
case (UICR_REGOUT0_VOUT_1V8 << UICR_REGOUT0_VOUT_Pos):
reg0_voltage = "1.8V";
break;
case (UICR_REGOUT0_VOUT_2V1 << UICR_REGOUT0_VOUT_Pos):
reg0_voltage = "2.1V";
break;
case (UICR_REGOUT0_VOUT_2V4 << UICR_REGOUT0_VOUT_Pos):
reg0_voltage = "2.4V";
break;
case (UICR_REGOUT0_VOUT_2V7 << UICR_REGOUT0_VOUT_Pos):
reg0_voltage = "2.7V";
break;
case (UICR_REGOUT0_VOUT_3V0 << UICR_REGOUT0_VOUT_Pos):
reg0_voltage = "3.0V";
break;
case (UICR_REGOUT0_VOUT_3V3 << UICR_REGOUT0_VOUT_Pos):
reg0_voltage = "3.3V";
break;
default:
reg0_voltage = "???V";
}
const char *reg0_voltage = regout0_to_str((NRF_UICR->REGOUT0 & UICR_REGOUT0_VOUT_Msk) >> UICR_REGOUT0_VOUT_Pos);
ESP_LOGD(TAG, "Regulator stage 0: %s, %s", reg0_type, reg0_voltage);
pos = buf_append_printf(buf, size, pos, "|Regulator stage 0: %s, %s", reg0_type, reg0_voltage);
#ifdef USE_NRF52_REG0_VOUT
if ((NRF_UICR->REGOUT0 & UICR_REGOUT0_VOUT_Msk) >> UICR_REGOUT0_VOUT_Pos != USE_NRF52_REG0_VOUT) {
ESP_LOGE(TAG, "Regulator stage 0: expected %s", regout0_to_str(USE_NRF52_REG0_VOUT));
}
#endif
} else {
ESP_LOGD(TAG, "Regulator stage 0: disabled");
pos = buf_append_printf(buf, size, pos, "|Regulator stage 0: disabled");

View File

@@ -34,6 +34,7 @@ from esphome.const import (
KEY_CORE,
KEY_FRAMEWORK_VERSION,
KEY_NAME,
KEY_NATIVE_IDF,
KEY_TARGET_FRAMEWORK,
KEY_TARGET_PLATFORM,
PLATFORM_ESP32,
@@ -53,6 +54,7 @@ from .const import ( # noqa
KEY_COMPONENTS,
KEY_ESP32,
KEY_EXTRA_BUILD_FILES,
KEY_FLASH_SIZE,
KEY_PATH,
KEY_REF,
KEY_REPO,
@@ -199,6 +201,7 @@ def set_core_data(config):
)
CORE.data[KEY_ESP32][KEY_BOARD] = config[CONF_BOARD]
CORE.data[KEY_ESP32][KEY_FLASH_SIZE] = config[CONF_FLASH_SIZE]
CORE.data[KEY_ESP32][KEY_VARIANT] = variant
CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES] = {}
@@ -962,12 +965,54 @@ async def _add_yaml_idf_components(components: list[ConfigType]):
async def to_code(config):
cg.add_platformio_option("board", config[CONF_BOARD])
cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE])
cg.add_platformio_option(
"board_upload.maximum_size",
int(config[CONF_FLASH_SIZE].removesuffix("MB")) * 1024 * 1024,
)
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
conf = config[CONF_FRAMEWORK]
# Check if using native ESP-IDF build (--native-idf)
use_platformio = not CORE.data.get(KEY_NATIVE_IDF, False)
if use_platformio:
# Clear IDF environment variables to avoid conflicts with PlatformIO's ESP-IDF
# but keep them when using --native-idf for native ESP-IDF builds
for clean_var in ("IDF_PATH", "IDF_TOOLS_PATH"):
os.environ.pop(clean_var, None)
cg.add_platformio_option("lib_ldf_mode", "off")
cg.add_platformio_option("lib_compat_mode", "strict")
cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION])
cg.add_platformio_option("board", config[CONF_BOARD])
cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE])
cg.add_platformio_option(
"board_upload.maximum_size",
int(config[CONF_FLASH_SIZE].removesuffix("MB")) * 1024 * 1024,
)
if CONF_SOURCE in conf:
cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]])
add_extra_script(
"pre",
"pre_build.py",
Path(__file__).parent / "pre_build.py.script",
)
add_extra_script(
"post",
"post_build.py",
Path(__file__).parent / "post_build.py.script",
)
# In testing mode, add IRAM fix script to allow linking grouped component tests
# Similar to ESP8266's approach but for ESP-IDF
if CORE.testing_mode:
cg.add_build_flag("-DESPHOME_TESTING_MODE")
add_extra_script(
"pre",
"iram_fix.py",
Path(__file__).parent / "iram_fix.py.script",
)
else:
cg.add_build_flag("-Wno-error=format")
cg.set_cpp_standard("gnu++20")
cg.add_build_flag("-DUSE_ESP32")
cg.add_build_flag("-Wl,-z,noexecstack")
@@ -977,79 +1022,49 @@ async def to_code(config):
cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[variant])
cg.add_define(ThreadModel.MULTI_ATOMICS)
cg.add_platformio_option("lib_ldf_mode", "off")
cg.add_platformio_option("lib_compat_mode", "strict")
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
conf = config[CONF_FRAMEWORK]
cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION])
if CONF_SOURCE in conf:
cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]])
if conf[CONF_ADVANCED][CONF_IGNORE_EFUSE_CUSTOM_MAC]:
cg.add_define("USE_ESP32_IGNORE_EFUSE_CUSTOM_MAC")
for clean_var in ("IDF_PATH", "IDF_TOOLS_PATH"):
os.environ.pop(clean_var, None)
# Set the location of the IDF component manager cache
os.environ["IDF_COMPONENT_CACHE_PATH"] = str(
CORE.relative_internal_path(".espressif")
)
add_extra_script(
"pre",
"pre_build.py",
Path(__file__).parent / "pre_build.py.script",
)
add_extra_script(
"post",
"post_build.py",
Path(__file__).parent / "post_build.py.script",
)
# In testing mode, add IRAM fix script to allow linking grouped component tests
# Similar to ESP8266's approach but for ESP-IDF
if CORE.testing_mode:
cg.add_build_flag("-DESPHOME_TESTING_MODE")
add_extra_script(
"pre",
"iram_fix.py",
Path(__file__).parent / "iram_fix.py.script",
)
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
cg.add_platformio_option("framework", "espidf")
cg.add_build_flag("-DUSE_ESP_IDF")
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF")
if use_platformio:
cg.add_platformio_option("framework", "espidf")
else:
cg.add_platformio_option("framework", "arduino, espidf")
cg.add_build_flag("-DUSE_ARDUINO")
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO")
if use_platformio:
cg.add_platformio_option("framework", "arduino, espidf")
# Add IDF framework source for Arduino builds to ensure it uses the same version as
# the ESP-IDF framework
if (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is not None:
cg.add_platformio_option(
"platform_packages",
[_format_framework_espidf_version(idf_ver, None)],
)
# ESP32-S2 Arduino: Disable USB Serial on boot to avoid TinyUSB dependency
if get_esp32_variant() == VARIANT_ESP32S2:
cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=1")
cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=0")
cg.add_build_flag("-DARDUINO_USB_CDC_ON_BOOT=0")
cg.add_define(
"USE_ARDUINO_VERSION_CODE",
cg.RawExpression(
f"VERSION_CODE({framework_ver.major}, {framework_ver.minor}, {framework_ver.patch})"
),
)
add_idf_sdkconfig_option("CONFIG_MBEDTLS_PSK_MODES", True)
add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True)
# Add IDF framework source for Arduino builds to ensure it uses the same version as
# the ESP-IDF framework
if (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is not None:
cg.add_platformio_option(
"platform_packages", [_format_framework_espidf_version(idf_ver, None)]
)
# ESP32-S2 Arduino: Disable USB Serial on boot to avoid TinyUSB dependency
if get_esp32_variant() == VARIANT_ESP32S2:
cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=1")
cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=0")
cg.add_build_flag("-DARDUINO_USB_CDC_ON_BOOT=0")
cg.add_build_flag("-Wno-nonnull-compare")
add_idf_sdkconfig_option(f"CONFIG_IDF_TARGET_{variant}", True)
@@ -1196,7 +1211,8 @@ async def to_code(config):
"CONFIG_VFS_SUPPORT_DIR", not advanced[CONF_DISABLE_VFS_SUPPORT_DIR]
)
cg.add_platformio_option("board_build.partitions", "partitions.csv")
if use_platformio:
cg.add_platformio_option("board_build.partitions", "partitions.csv")
if CONF_PARTITIONS in config:
add_extra_build_file(
"partitions.csv", CORE.relative_config_path(config[CONF_PARTITIONS])
@@ -1361,19 +1377,16 @@ def copy_files():
_write_idf_component_yml()
if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]:
flash_size = CORE.data[KEY_ESP32][KEY_FLASH_SIZE]
if CORE.using_arduino:
write_file_if_changed(
CORE.relative_build_path("partitions.csv"),
get_arduino_partition_csv(
CORE.platformio_options.get("board_upload.flash_size")
),
get_arduino_partition_csv(flash_size),
)
else:
write_file_if_changed(
CORE.relative_build_path("partitions.csv"),
get_idf_partition_csv(
CORE.platformio_options.get("board_upload.flash_size")
),
get_idf_partition_csv(flash_size),
)
# IDF build scripts look for version string to put in the build.
# However, if the build path does not have an initialized git repo,

View File

@@ -2,6 +2,7 @@ import esphome.codegen as cg
KEY_ESP32 = "esp32"
KEY_BOARD = "board"
KEY_FLASH_SIZE = "flash_size"
KEY_VARIANT = "variant"
KEY_SDKCONFIG_OPTIONS = "sdkconfig_options"
KEY_COMPONENTS = "components"

View File

@@ -181,7 +181,8 @@ class ESP32Preferences : public ESPPreferences {
if (actual_len != to_save.len) {
return true;
}
auto stored_data = std::make_unique<uint8_t[]>(actual_len);
// Most preferences are small, use stack buffer with heap fallback for large ones
SmallBufferWithHeapFallback<256> stored_data(actual_len);
err = nvs_get_blob(nvs_handle, key_str, stored_data.get(), &actual_len);
if (err != 0) {
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err));

View File

@@ -50,7 +50,7 @@ void BLEClientBase::loop() {
this->set_state(espbt::ClientState::INIT);
return;
}
if (this->state_ == espbt::ClientState::INIT) {
if (this->state() == espbt::ClientState::INIT) {
auto ret = esp_ble_gattc_app_register(this->app_id);
if (ret) {
ESP_LOGE(TAG, "gattc app register failed. app_id=%d code=%d", this->app_id, ret);
@@ -60,7 +60,7 @@ void BLEClientBase::loop() {
}
// If idle, we can disable the loop as connect()
// will enable it again when a connection is needed.
else if (this->state_ == espbt::ClientState::IDLE) {
else if (this->state() == espbt::ClientState::IDLE) {
this->disable_loop();
}
}
@@ -86,7 +86,7 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) {
return false;
if (this->address_ == 0 || device.address_uint64() != this->address_)
return false;
if (this->state_ != espbt::ClientState::IDLE)
if (this->state() != espbt::ClientState::IDLE)
return false;
this->log_event_("Found device");
@@ -102,10 +102,10 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) {
void BLEClientBase::connect() {
// Prevent duplicate connection attempts
if (this->state_ == espbt::ClientState::CONNECTING || this->state_ == espbt::ClientState::CONNECTED ||
this->state_ == espbt::ClientState::ESTABLISHED) {
if (this->state() == espbt::ClientState::CONNECTING || this->state() == espbt::ClientState::CONNECTED ||
this->state() == espbt::ClientState::ESTABLISHED) {
ESP_LOGW(TAG, "[%d] [%s] Connection already in progress, state=%s", this->connection_index_, this->address_str_,
espbt::client_state_to_string(this->state_));
espbt::client_state_to_string(this->state()));
return;
}
ESP_LOGI(TAG, "[%d] [%s] 0x%02x Connecting", this->connection_index_, this->address_str_, this->remote_addr_type_);
@@ -133,12 +133,12 @@ void BLEClientBase::connect() {
esp_err_t BLEClientBase::pair() { return esp_ble_set_encryption(this->remote_bda_, ESP_BLE_SEC_ENCRYPT); }
void BLEClientBase::disconnect() {
if (this->state_ == espbt::ClientState::IDLE || this->state_ == espbt::ClientState::DISCONNECTING) {
if (this->state() == espbt::ClientState::IDLE || this->state() == espbt::ClientState::DISCONNECTING) {
ESP_LOGI(TAG, "[%d] [%s] Disconnect requested, but already %s", this->connection_index_, this->address_str_,
espbt::client_state_to_string(this->state_));
espbt::client_state_to_string(this->state()));
return;
}
if (this->state_ == espbt::ClientState::CONNECTING || this->conn_id_ == UNSET_CONN_ID) {
if (this->state() == espbt::ClientState::CONNECTING || this->conn_id_ == UNSET_CONN_ID) {
ESP_LOGD(TAG, "[%d] [%s] Disconnect before connected, disconnect scheduled", this->connection_index_,
this->address_str_);
this->want_disconnect_ = true;
@@ -150,7 +150,7 @@ void BLEClientBase::disconnect() {
void BLEClientBase::unconditional_disconnect() {
// Disconnect without checking the state.
ESP_LOGI(TAG, "[%d] [%s] Disconnecting (conn_id: %d).", this->connection_index_, this->address_str_, this->conn_id_);
if (this->state_ == espbt::ClientState::DISCONNECTING) {
if (this->state() == espbt::ClientState::DISCONNECTING) {
this->log_error_("Already disconnecting");
return;
}
@@ -170,7 +170,7 @@ void BLEClientBase::unconditional_disconnect() {
this->log_gattc_warning_("esp_ble_gattc_close", err);
}
if (this->state_ == espbt::ClientState::DISCOVERED) {
if (this->state() == espbt::ClientState::DISCOVERED) {
this->set_address(0);
this->set_state(espbt::ClientState::IDLE);
} else {
@@ -295,18 +295,18 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
// ESP-IDF's BLE stack may send ESP_GATTC_OPEN_EVT after esp_ble_gattc_open() returns an
// error, if the error occurred at the BTA/GATT layer. This can result in the event
// arriving after we've already transitioned to IDLE state.
if (this->state_ == espbt::ClientState::IDLE) {
if (this->state() == espbt::ClientState::IDLE) {
ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in IDLE state (status=%d), ignoring", this->connection_index_,
this->address_str_, param->open.status);
break;
}
if (this->state_ != espbt::ClientState::CONNECTING) {
if (this->state() != espbt::ClientState::CONNECTING) {
// This should not happen but lets log it in case it does
// because it means we have a bad assumption about how the
// ESP BT stack works.
ESP_LOGE(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in %s state (status=%d)", this->connection_index_,
this->address_str_, espbt::client_state_to_string(this->state_), param->open.status);
this->address_str_, espbt::client_state_to_string(this->state()), param->open.status);
}
if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) {
this->log_gattc_warning_("Connection open", param->open.status);
@@ -327,7 +327,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
// Cached connections already connected with medium parameters, no update needed
// only set our state, subclients might have more stuff to do yet.
this->state_ = espbt::ClientState::ESTABLISHED;
this->set_state_internal_(espbt::ClientState::ESTABLISHED);
break;
}
// For V3_WITHOUT_CACHE, we already set fast params before connecting
@@ -356,7 +356,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
return false;
// Check if we were disconnected while waiting for service discovery
if (param->disconnect.reason == ESP_GATT_CONN_TERMINATE_PEER_USER &&
this->state_ == espbt::ClientState::CONNECTED) {
this->state() == espbt::ClientState::CONNECTED) {
this->log_warning_("Remote closed during discovery");
} else {
ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason 0x%02x", this->connection_index_, this->address_str_,
@@ -433,7 +433,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
#endif
}
ESP_LOGI(TAG, "[%d] [%s] Service discovery complete", this->connection_index_, this->address_str_);
this->state_ = espbt::ClientState::ESTABLISHED;
this->set_state_internal_(espbt::ClientState::ESTABLISHED);
break;
}
case ESP_GATTC_READ_DESCR_EVT: {

View File

@@ -44,7 +44,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
void unconditional_disconnect();
void release_services();
bool connected() { return this->state_ == espbt::ClientState::ESTABLISHED; }
bool connected() { return this->state() == espbt::ClientState::ESTABLISHED; }
void set_auto_connect(bool auto_connect) { this->auto_connect_ = auto_connect; }

View File

@@ -105,15 +105,13 @@ void ESP32BLETracker::loop() {
}
// Check for scan timeout - moved here from scheduler to avoid false reboots
// when the loop is blocked
// when the loop is blocked. This must run every iteration for safety.
if (this->scanner_state_ == ScannerState::RUNNING) {
switch (this->scan_timeout_state_) {
case ScanTimeoutState::MONITORING: {
uint32_t now = App.get_loop_component_start_time();
uint32_t timeout_ms = this->scan_duration_ * 2000;
// Robust time comparison that handles rollover correctly
// This works because unsigned arithmetic wraps around predictably
if ((now - this->scan_start_time_) > timeout_ms) {
if ((App.get_loop_component_start_time() - this->scan_start_time_) > this->scan_timeout_ms_) {
// First time we've seen the timeout exceeded - wait one more loop iteration
// This ensures all components have had a chance to process pending events
// This is because esp32_ble may not have run yet and called
@@ -128,13 +126,31 @@ void ESP32BLETracker::loop() {
ESP_LOGE(TAG, "Scan never terminated, rebooting");
App.reboot();
break;
case ScanTimeoutState::INACTIVE:
// This case should be unreachable - scanner and timeout states are always synchronized
break;
}
}
// Fast path: skip expensive client state counting and processing
// if no state has changed since last loop iteration.
//
// How state changes ensure we reach the code below:
// - handle_scanner_failure_(): scanner_state_ becomes FAILED via set_scanner_state_(), or
// scan_set_param_failed_ requires scanner_state_==RUNNING which can only be reached via
// set_scanner_state_(RUNNING) in gap_scan_start_complete_() (scan params are set during
// STARTING, not RUNNING, so version is always incremented before this condition is true)
// - start_scan_(): scanner_state_ becomes IDLE via set_scanner_state_() in cleanup_scan_state_()
// - try_promote_discovered_clients_(): client enters DISCOVERED via set_state(), or
// connecting client finishes (state change), or scanner reaches RUNNING/IDLE
//
// All conditions that affect the logic below are tied to state changes that increment
// state_version_, so the fast path is safe.
if (this->state_version_ == this->last_processed_version_) {
return;
}
this->last_processed_version_ = this->state_version_;
// State changed - do full processing
ClientStateCounts counts = this->count_client_states_();
if (counts != this->client_state_counts_) {
this->client_state_counts_ = counts;
@@ -142,6 +158,7 @@ void ESP32BLETracker::loop() {
this->client_state_counts_.discovered, this->client_state_counts_.disconnecting);
}
// Scanner failure: reached when set_scanner_state_(FAILED) or scan_set_param_failed_ set
if (this->scanner_state_ == ScannerState::FAILED ||
(this->scan_set_param_failed_ && this->scanner_state_ == ScannerState::RUNNING)) {
this->handle_scanner_failure_();
@@ -160,6 +177,8 @@ void ESP32BLETracker::loop() {
*/
// Start scan: reached when scanner_state_ becomes IDLE (via set_scanner_state_()) and
// all clients are idle (their state changes increment version when they finish)
if (this->scanner_state_ == ScannerState::IDLE && !counts.connecting && !counts.disconnecting && !counts.discovered) {
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
this->update_coex_preference_(false);
@@ -168,8 +187,9 @@ void ESP32BLETracker::loop() {
this->start_scan_(false); // first = false
}
}
// If there is a discovered client and no connecting
// clients, then promote the discovered client to ready to connect.
// Promote discovered clients: reached when a client's state becomes DISCOVERED (via set_state()),
// or when a blocking condition clears (connecting client finishes, scanner reaches RUNNING/IDLE).
// All these trigger state_version_ increment, so we'll process and check promotion eligibility.
// We check both RUNNING and IDLE states because:
// - RUNNING: gap_scan_event_handler initiates stop_scan_() but promotion can happen immediately
// - IDLE: Scanner has already stopped (naturally or by gap_scan_event_handler)
@@ -236,6 +256,7 @@ void ESP32BLETracker::start_scan_(bool first) {
// Start timeout monitoring in loop() instead of using scheduler
// This prevents false reboots when the loop is blocked
this->scan_start_time_ = App.get_loop_component_start_time();
this->scan_timeout_ms_ = this->scan_duration_ * 2000;
this->scan_timeout_state_ = ScanTimeoutState::MONITORING;
esp_err_t err = esp_ble_gap_set_scan_params(&this->scan_params_);
@@ -253,6 +274,10 @@ void ESP32BLETracker::start_scan_(bool first) {
void ESP32BLETracker::register_client(ESPBTClient *client) {
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
client->app_id = ++this->app_id_;
// Give client a pointer to our state_version_ so it can notify us of state changes.
// This enables loop() fast-path optimization - we skip expensive work when no state changed.
// Safe because ESP32BLETracker (singleton) outlives all registered clients.
client->set_tracker_state_version(&this->state_version_);
this->clients_.push_back(client);
this->recalculate_advertisement_parser_types();
#endif
@@ -382,6 +407,7 @@ void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_i
void ESP32BLETracker::set_scanner_state_(ScannerState state) {
this->scanner_state_ = state;
this->state_version_++;
for (auto *listener : this->scanner_state_listeners_) {
listener->on_scanner_state(state);
}

View File

@@ -216,6 +216,19 @@ enum class ConnectionType : uint8_t {
V3_WITHOUT_CACHE
};
/// Base class for BLE GATT clients that connect to remote devices.
///
/// State Change Tracking Design:
/// -----------------------------
/// ESP32BLETracker::loop() needs to know when client states change to avoid
/// expensive polling. Rather than checking all clients every iteration (~7000/min),
/// we use a version counter owned by ESP32BLETracker that clients increment on
/// state changes. The tracker compares versions to skip work when nothing changed.
///
/// Ownership: ESP32BLETracker owns state_version_. Clients hold a non-owning
/// pointer (tracker_state_version_) set during register_client(). Clients
/// increment the counter through this pointer when their state changes.
/// The pointer may be null if the client is not registered with a tracker.
class ESPBTClient : public ESPBTDeviceListener {
public:
virtual bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
@@ -225,26 +238,49 @@ class ESPBTClient : public ESPBTDeviceListener {
virtual void disconnect() = 0;
bool disconnect_pending() const { return this->want_disconnect_; }
void cancel_pending_disconnect() { this->want_disconnect_ = false; }
/// Set the client state with IDLE handling (clears want_disconnect_).
/// Notifies the tracker of state change for loop optimization.
virtual void set_state(ClientState st) {
this->state_ = st;
this->set_state_internal_(st);
if (st == ClientState::IDLE) {
this->want_disconnect_ = false;
}
}
ClientState state() const { return state_; }
ClientState state() const { return this->state_; }
/// Called by ESP32BLETracker::register_client() to enable state change notifications.
/// The pointer must remain valid for the lifetime of the client (guaranteed since
/// ESP32BLETracker is a singleton that outlives all clients).
void set_tracker_state_version(uint8_t *version) { this->tracker_state_version_ = version; }
// Memory optimized layout
uint8_t app_id; // App IDs are small integers assigned sequentially
protected:
// Group 1: 1-byte types
ClientState state_{ClientState::INIT};
/// Set state without IDLE handling - use for direct state transitions.
/// Increments the tracker's state version counter to signal that loop()
/// should do full processing on the next iteration.
void set_state_internal_(ClientState st) {
this->state_ = st;
// Notify tracker that state changed (tracker_state_version_ is owned by ESP32BLETracker)
if (this->tracker_state_version_ != nullptr) {
(*this->tracker_state_version_)++;
}
}
// want_disconnect_ is set to true when a disconnect is requested
// while the client is connecting. This is used to disconnect the
// client as soon as we get the connection id (conn_id_) from the
// ESP_GATTC_OPEN_EVT event.
bool want_disconnect_{false};
// 2 bytes used, 2 bytes padding
private:
ClientState state_{ClientState::INIT};
/// Non-owning pointer to ESP32BLETracker::state_version_. When this client's
/// state changes, we increment the tracker's counter to signal that loop()
/// should perform full processing. Null if client not registered with tracker.
uint8_t *tracker_state_version_{nullptr};
};
class ESP32BLETracker : public Component,
@@ -380,6 +416,16 @@ class ESP32BLETracker : public Component,
// Group 4: 1-byte types (enums, uint8_t, bool)
uint8_t app_id_{0};
uint8_t scan_start_fail_count_{0};
/// Version counter for loop() fast-path optimization. Incremented when:
/// - Scanner state changes (via set_scanner_state_())
/// - Any registered client's state changes (clients hold pointer to this counter)
/// Owned by this class; clients receive non-owning pointer via register_client().
/// When loop() sees state_version_ == last_processed_version_, it skips expensive
/// client state counting and takes the fast path (just timeout check + return).
uint8_t state_version_{0};
/// Last state_version_ value when loop() did full processing. Compared against
/// state_version_ to detect if any state changed since last iteration.
uint8_t last_processed_version_{0};
ScannerState scanner_state_{ScannerState::IDLE};
bool scan_continuous_;
bool scan_active_;
@@ -396,6 +442,8 @@ class ESP32BLETracker : public Component,
EXCEEDED_WAIT, // Timeout exceeded, waiting one loop before reboot
};
uint32_t scan_start_time_{0};
/// Precomputed timeout value: scan_duration_ * 2000
uint32_t scan_timeout_ms_{0};
ScanTimeoutState scan_timeout_state_{ScanTimeoutState::INACTIVE};
};

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
@@ -184,15 +185,23 @@ 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);
if (result == http_request::HttpReadLoopResult::RETRY)
continue;
if (result != http_request::HttpReadLoopResult::DATA)
break; // ERROR or TIMEOUT
json_str.append(reinterpret_cast<char *>(buf), read_or_error);
}
container->end();
@@ -297,32 +306,38 @@ bool Esp32HostedUpdate::stream_firmware_to_coprocessor_() {
}
// Stream firmware to coprocessor while computing SHA256
// 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;
auto result = http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout);
if (result == http_request::HttpReadLoopResult::RETRY)
continue;
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);
}
// read == 0: no more data available, exit loop
break;
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

View File

@@ -6,6 +6,7 @@ from esphome.const import (
CONF_BASELINE,
CONF_CO2,
CONF_ID,
CONF_WARMUP_TIME,
DEVICE_CLASS_CARBON_DIOXIDE,
ICON_MOLECULE_CO2,
STATE_CLASS_MEASUREMENT,
@@ -14,8 +15,6 @@ from esphome.const import (
DEPENDENCIES = ["uart"]
CONF_WARMUP_TIME = "warmup_time"
hc8_ns = cg.esphome_ns.namespace("hc8")
HC8Component = hc8_ns.class_("HC8Component", cg.PollingComponent, uart.UARTDevice)
HC8CalibrateAction = hc8_ns.class_("HC8CalibrateAction", automation.Action)

View File

@@ -107,7 +107,7 @@ CONFIG_SCHEMA = cv.All(
cv.Required(CONF_MAX_TEMPERATURE): cv.temperature,
}
),
cv.only_with_arduino,
cv.Any(cv.only_with_arduino, cv.only_on_esp32),
)
@@ -126,6 +126,6 @@ async def to_code(config):
cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE]))
cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE]))
cg.add_library("tonia/HeatpumpIR", "1.0.37")
cg.add_library("tonia/HeatpumpIR", "1.0.40")
if CORE.is_libretiny or CORE.is_esp32:
CORE.add_platformio_option("lib_ignore", ["IRremoteESP8266"])

View File

@@ -1,6 +1,6 @@
#include "heatpumpir.h"
#ifdef USE_ARDUINO
#if defined(USE_ARDUINO) || defined(USE_ESP32)
#include <map>
#include "ir_sender_esphome.h"

View File

@@ -1,6 +1,6 @@
#pragma once
#ifdef USE_ARDUINO
#if defined(USE_ARDUINO) || defined(USE_ESP32)
#include "esphome/components/climate_ir/climate_ir.h"

View File

@@ -1,6 +1,6 @@
#include "ir_sender_esphome.h"
#ifdef USE_ARDUINO
#if defined(USE_ARDUINO) || defined(USE_ESP32)
namespace esphome {
namespace heatpumpir {

View File

@@ -1,6 +1,6 @@
#pragma once
#ifdef USE_ARDUINO
#if defined(USE_ARDUINO) || defined(USE_ESP32)
#include "esphome/components/remote_base/remote_base.h"
#include <IRSender.h> // arduino-heatpump library

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

@@ -79,6 +79,80 @@ 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 (non-blocking, caller should RETRY)
* < 0: error or connection closed (caller should EXIT)
* HTTP_ERROR_CONNECTION_CLOSED (-1) = connection closed prematurely
* other negative values = platform-specific errors
*
* This non-blocking design allows consistent behavior across:
* - ESP-IDF (async mode with EAGAIN handling)
* - Arduino (available() + connected() checks)
*
* 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
RETRY, ///< No data yet, already delayed, caller should continue loop
ERROR, ///< Read error, caller should exit loop
TIMEOUT, ///< Timeout waiting for data, caller should exit loop
};
/// Process a read result with timeout tracking and delay handling
/// @param bytes_read_or_error Return value from read() - positive for bytes read, negative for error
/// @param last_data_time Time of last successful read, updated when data received
/// @param timeout_ms Maximum time to wait for data
/// @return DATA if data received, RETRY if should continue loop, ERROR/TIMEOUT if should exit
inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_t &last_data_time,
uint32_t timeout_ms) {
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: no data available yet
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> {
@@ -88,6 +162,28 @@ class HttpContainer : public Parented<HttpRequestComponent> {
int status_code;
uint32_t duration_ms;
/**
* @brief Read data from the HTTP response body (non-blocking).
*
* WARNING: These semantics differ from BSD sockets!
* BSD sockets: 0 = EOF (connection closed)
* This method: 0 = no data yet (retry), 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 (NOT EOF!), caller should retry
* - HTTP_ERROR_CONNECTION_CLOSED (-1): Connection closed prematurely
* - < -1: Other error (platform-specific error code)
*
* 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;
@@ -110,6 +206,38 @@ class HttpContainer : public Parented<HttpRequestComponent> {
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
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);
if (result == HttpReadLoopResult::RETRY)
continue;
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 +252,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; }
@@ -249,15 +378,21 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
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);
if (result == HttpReadLoopResult::RETRY)
continue;
if (result != HttpReadLoopResult::DATA)
break; // ERROR or TIMEOUT
read_index += read_or_error;
}
response_body.reserve(read_index);
response_body.assign((char *) buf, read_index);

View File

@@ -139,6 +139,23 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
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,7 +163,7 @@ 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();
@@ -154,7 +171,15 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
if (bufsize == 0) {
this->duration_ms += (millis() - start);
return 0;
// Check if we've read all expected content
if (this->bytes_read_ >= this->content_length) {
return 0; // All content read successfully
}
// No data available - check if connection is still open
if (!stream_ptr->connected()) {
return HTTP_ERROR_CONNECTION_CLOSED; // Connection closed prematurely
}
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
@@ -100,6 +100,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
config.buffer_size = this->buffer_size_rx_;
config.buffer_size_tx = this->buffer_size_tx_;
config.is_async = true; // Enable non-blocking mode
const uint32_t start = millis();
watchdog::WatchdogManager wdm(this->get_watchdog_timeout());
@@ -209,26 +210,67 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
return container;
}
// ESP-IDF HTTP read implementation
//
// WARNING: Return values differ from BSD sockets! See http_request.h for full documentation.
//
// Uses non-blocking mode (config.is_async = true) for consistent behavior with Arduino.
// esp_http_client_read() in async mode returns:
// > 0: bytes read
// 0: connection closed (end of stream) <-- BSD socket EOF semantics
// -ESP_ERR_HTTP_EAGAIN: no data available yet (would block)
// other negative: error
//
// 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 HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
const uint32_t start = millis();
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
this->feed_wdt();
int read_len = esp_http_client_read(this->client_, (char *) buf, max_len);
this->feed_wdt();
if (read_len > 0) {
this->bytes_read_ += read_len;
// Check if we've already read all expected content
if (this->bytes_read_ >= this->content_length) {
return 0; // All content read successfully
}
this->feed_wdt();
int read_len_or_error = esp_http_client_read(this->client_, (char *) buf, max_len);
this->feed_wdt();
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;
}
// No data available yet in non-blocking mode
// ESP_ERR_HTTP_EAGAIN is returned as a negative error code
if (read_len_or_error == -ESP_ERR_HTTP_EAGAIN) {
return 0; // No data yet, caller should retry
}
// Connection closed by server
if (read_len_or_error == 0) {
// We haven't read all content yet (early check handles success case)
// Return error so caller exits immediately instead of waiting for timeout
return HTTP_ERROR_CONNECTION_CLOSED;
}
// Other negative value - real 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,47 @@ 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;
auto result = http_read_loop_result(bufsize_or_error, last_data_time, read_timeout);
if (result == HttpReadLoopResult::RETRY)
continue;
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);
}
// bufsize == 0: no more data available, exit loop
break;
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 +252,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,6 +1,6 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.core import CoroPriority, coroutine_with_priority
from esphome.core import CORE, CoroPriority, coroutine_with_priority
CODEOWNERS = ["@esphome/core"]
json_ns = cg.esphome_ns.namespace("json")
@@ -12,6 +12,11 @@ CONFIG_SCHEMA = cv.All(
@coroutine_with_priority(CoroPriority.BUS)
async def to_code(config):
cg.add_library("bblanchon/ArduinoJson", "7.4.2")
if CORE.is_esp32:
from esphome.components.esp32 import add_idf_component
add_idf_component(name="bblanchon/arduinojson", ref="7.4.2")
else:
cg.add_library("bblanchon/ArduinoJson", "7.4.2")
cg.add_define("USE_JSON")
cg.add_global(json_ns.using)

View File

@@ -382,4 +382,11 @@ async def component_to_code(config):
"custom_options.sys_config#h", _BK7231N_SYS_CONFIG_OPTIONS
)
# Disable LWIP statistics to save RAM - not needed in production
# Must explicitly disable all sub-stats to avoid redefinition warnings
cg.add_platformio_option(
"custom_options.lwip",
["LWIP_STATS=0", "MEM_STATS=0", "MEMP_STATS=0"],
)
await cg.register_component(var, config)

View File

@@ -166,8 +166,8 @@ class LibreTinyPreferences : public ESPPreferences {
return true;
}
// Allocate buffer on heap to avoid stack allocation for large data
auto stored_data = std::make_unique<uint8_t[]>(kv.value_len);
// Most preferences are small, use stack buffer with heap fallback for large ones
SmallBufferWithHeapFallback<256> stored_data(kv.value_len);
fdb_blob_make(&this->blob, stored_data.get(), kv.value_len);
size_t actual_len = fdb_kv_get_blob(db, key_str, &this->blob);
if (actual_len != kv.value_len) {

View File

@@ -1,8 +1,5 @@
#include "logger.h"
#include <cinttypes>
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
#include <memory> // For unique_ptr
#endif
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
@@ -199,7 +196,8 @@ inline uint8_t Logger::level_for(const char *tag) {
Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate), tx_buffer_size_(tx_buffer_size) {
// add 1 to buffer size for null terminator
this->tx_buffer_ = new char[this->tx_buffer_size_ + 1]; // NOLINT
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed
this->tx_buffer_ = new char[this->tx_buffer_size_ + 1];
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
this->main_task_ = xTaskGetCurrentTaskHandle();
#elif defined(USE_ZEPHYR)
@@ -212,11 +210,14 @@ Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate
void Logger::init_log_buffer(size_t total_buffer_size) {
#ifdef USE_HOST
// Host uses slot count instead of byte size
this->log_buffer_ = esphome::make_unique<logger::TaskLogBufferHost>(total_buffer_size);
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed
this->log_buffer_ = new logger::TaskLogBufferHost(total_buffer_size);
#elif defined(USE_ESP32)
this->log_buffer_ = esphome::make_unique<logger::TaskLogBuffer>(total_buffer_size);
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed
this->log_buffer_ = new logger::TaskLogBuffer(total_buffer_size);
#elif defined(USE_LIBRETINY)
this->log_buffer_ = esphome::make_unique<logger::TaskLogBufferLibreTiny>(total_buffer_size);
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed
this->log_buffer_ = new logger::TaskLogBufferLibreTiny(total_buffer_size);
#endif
#if defined(USE_ESP32) || defined(USE_LIBRETINY)

View File

@@ -412,11 +412,11 @@ class Logger : public Component {
#endif
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
#ifdef USE_HOST
std::unique_ptr<logger::TaskLogBufferHost> log_buffer_; // Will be initialized with init_log_buffer
logger::TaskLogBufferHost *log_buffer_{nullptr}; // Allocated once, never freed
#elif defined(USE_ESP32)
std::unique_ptr<logger::TaskLogBuffer> log_buffer_; // Will be initialized with init_log_buffer
logger::TaskLogBuffer *log_buffer_{nullptr}; // Allocated once, never freed
#elif defined(USE_LIBRETINY)
std::unique_ptr<logger::TaskLogBufferLibreTiny> log_buffer_; // Will be initialized with init_log_buffer
logger::TaskLogBufferLibreTiny *log_buffer_{nullptr}; // Allocated once, never freed
#endif
#endif

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

@@ -24,13 +24,14 @@ static void register_esp32(MDNSComponent *comp, StaticVector<MDNSService, MDNS_S
mdns_instance_name_set(hostname);
for (const auto &service : services) {
auto txt_records = std::make_unique<mdns_txt_item_t[]>(service.txt_records.size());
// Stack buffer for up to 16 txt records, heap fallback for more
SmallBufferWithHeapFallback<16, mdns_txt_item_t> txt_records(service.txt_records.size());
for (size_t i = 0; i < service.txt_records.size(); i++) {
const auto &record = service.txt_records[i];
// key and value are either compile-time string literals in flash or pointers to dynamic_txt_values_
// Both remain valid for the lifetime of this function, and ESP-IDF makes internal copies
txt_records[i].key = MDNS_STR_ARG(record.key);
txt_records[i].value = MDNS_STR_ARG(record.value);
txt_records.get()[i].key = MDNS_STR_ARG(record.key);
txt_records.get()[i].value = MDNS_STR_ARG(record.value);
}
uint16_t port = const_cast<TemplatableValue<uint16_t> &>(service.port).value();
err = mdns_service_add(nullptr, MDNS_STR_ARG(service.service_type), MDNS_STR_ARG(service.proto), port,

View File

@@ -7,6 +7,7 @@ from esphome.const import (
CONF_CO2,
CONF_ID,
CONF_TEMPERATURE,
CONF_WARMUP_TIME,
DEVICE_CLASS_CARBON_DIOXIDE,
DEVICE_CLASS_TEMPERATURE,
ICON_MOLECULE_CO2,
@@ -18,7 +19,6 @@ from esphome.const import (
DEPENDENCIES = ["uart"]
CONF_AUTOMATIC_BASELINE_CALIBRATION = "automatic_baseline_calibration"
CONF_WARMUP_TIME = "warmup_time"
CONF_DETECTION_RANGE = "detection_range"
mhz19_ns = cg.esphome_ns.namespace("mhz19")

View File

@@ -9,6 +9,7 @@ from esphome.const import (
CONF_ABOVE,
CONF_ACCURACY_DECIMALS,
CONF_ALPHA,
CONF_BASELINE,
CONF_BELOW,
CONF_CALIBRATION,
CONF_DEVICE_CLASS,
@@ -38,7 +39,6 @@ from esphome.const import (
CONF_TIMEOUT,
CONF_TO,
CONF_TRIGGER_ID,
CONF_TYPE,
CONF_UNIT_OF_MEASUREMENT,
CONF_VALUE,
CONF_WEB_SERVER,
@@ -107,7 +107,7 @@ from esphome.const import (
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass
from esphome.cpp_generator import MockObj, MockObjClass
from esphome.util import Registry
CODEOWNERS = ["@esphome/core"]
@@ -574,38 +574,56 @@ async def lambda_filter_to_code(config, filter_id):
return automation.new_lambda_pvariable(filter_id, lambda_, StatelessLambdaFilter)
DELTA_SCHEMA = cv.Schema(
{
cv.Required(CONF_VALUE): cv.positive_float,
cv.Optional(CONF_TYPE, default="absolute"): cv.one_of(
"absolute", "percentage", lower=True
),
}
def validate_delta_value(value):
if isinstance(value, str) and value.endswith("%"):
# Check it's a well-formed percentage, but return the string as-is
try:
cv.positive_float(value[:-1])
return value
except cv.Invalid as exc:
raise cv.Invalid("Malformed delta % value") from exc
return cv.positive_float(value)
# This ideally would be done with `cv.maybe_simple_value` but it doesn't seem to respect the default for min_value.
DELTA_SCHEMA = cv.Any(
cv.All(
{
# Ideally this would be 'default=float("inf")' but it doesn't translate well to C++
cv.Optional(CONF_MAX_VALUE): validate_delta_value,
cv.Optional(CONF_MIN_VALUE, default="0.0"): validate_delta_value,
cv.Optional(CONF_BASELINE): cv.templatable(cv.float_),
},
cv.has_at_least_one_key(CONF_MAX_VALUE, CONF_MIN_VALUE),
),
validate_delta_value,
)
def validate_delta(config):
try:
value = cv.positive_float(config)
return DELTA_SCHEMA({CONF_VALUE: value, CONF_TYPE: "absolute"})
except cv.Invalid:
pass
try:
value = cv.percentage(config)
return DELTA_SCHEMA({CONF_VALUE: value, CONF_TYPE: "percentage"})
except cv.Invalid:
pass
raise cv.Invalid("Delta filter requires a positive number or percentage value.")
def _get_delta(value):
if isinstance(value, str):
assert value.endswith("%")
return 0.0, float(value[:-1])
return value, 0.0
@FILTER_REGISTRY.register("delta", DeltaFilter, cv.Any(DELTA_SCHEMA, validate_delta))
@FILTER_REGISTRY.register("delta", DeltaFilter, DELTA_SCHEMA)
async def delta_filter_to_code(config, filter_id):
percentage = config[CONF_TYPE] == "percentage"
return cg.new_Pvariable(
filter_id,
config[CONF_VALUE],
percentage,
)
# The config could be just the min_value, or it could be a dict.
max = MockObj("std::numeric_limits<float>::infinity()"), 0
if isinstance(config, dict):
min = _get_delta(config[CONF_MIN_VALUE])
if CONF_MAX_VALUE in config:
max = _get_delta(config[CONF_MAX_VALUE])
else:
min = _get_delta(config)
var = cg.new_Pvariable(filter_id, *min, *max)
if isinstance(config, dict) and (baseline_lambda := config.get(CONF_BASELINE)):
baseline = await cg.process_lambda(
baseline_lambda, [(float, "x")], return_type=float
)
cg.add(var.set_baseline(baseline))
return var
@FILTER_REGISTRY.register("or", OrFilter, validate_filters)

View File

@@ -291,22 +291,27 @@ optional<float> ThrottleWithPriorityFilter::new_value(float value) {
}
// DeltaFilter
DeltaFilter::DeltaFilter(float delta, bool percentage_mode)
: delta_(delta), current_delta_(delta), last_value_(NAN), percentage_mode_(percentage_mode) {}
DeltaFilter::DeltaFilter(float min_a0, float min_a1, float max_a0, float max_a1)
: min_a0_(min_a0), min_a1_(min_a1), max_a0_(max_a0), max_a1_(max_a1) {}
void DeltaFilter::set_baseline(float (*fn)(float)) { this->baseline_ = fn; }
optional<float> DeltaFilter::new_value(float value) {
if (std::isnan(value)) {
if (std::isnan(this->last_value_)) {
return {};
} else {
return this->last_value_ = value;
}
// Always yield the first value.
if (std::isnan(this->last_value_)) {
this->last_value_ = value;
return value;
}
float diff = fabsf(value - this->last_value_);
if (std::isnan(this->last_value_) || (diff > 0.0f && diff >= this->current_delta_)) {
if (this->percentage_mode_) {
this->current_delta_ = fabsf(value * this->delta_);
}
return this->last_value_ = value;
// calculate min and max using the linear equation
float ref = this->baseline_(this->last_value_);
float min = fabsf(this->min_a0_ + ref * this->min_a1_);
float max = fabsf(this->max_a0_ + ref * this->max_a1_);
float delta = fabsf(value - ref);
// if there is no reference, e.g. for the first value, just accept this one,
// otherwise accept only if within range.
if (delta > min && delta <= max) {
this->last_value_ = value;
return value;
}
return {};
}

View File

@@ -452,15 +452,21 @@ class HeartbeatFilter : public Filter, public Component {
class DeltaFilter : public Filter {
public:
explicit DeltaFilter(float delta, bool percentage_mode);
explicit DeltaFilter(float min_a0, float min_a1, float max_a0, float max_a1);
void set_baseline(float (*fn)(float));
optional<float> new_value(float value) override;
protected:
float delta_;
float current_delta_;
// These values represent linear equations for the min and max values but in practice only one of a0 and a1 will be
// non-zero Each limit is calculated as fabs(a0 + value * a1)
float min_a0_, min_a1_, max_a0_, max_a1_;
// default baseline is the previous value
float (*baseline_)(float) = [](float last_value) { return last_value; };
float last_value_{NAN};
bool percentage_mode_;
};
class OrFilter : public Filter {

View File

@@ -46,15 +46,15 @@ static inline const char *esphome_inet_ntop6(const void *addr, char *buf, size_t
#endif
// Format sockaddr into caller-provided buffer, returns length written (excluding null)
static size_t format_sockaddr_to(const struct sockaddr_storage &storage, std::span<char, SOCKADDR_STR_LEN> buf) {
if (storage.ss_family == AF_INET) {
const auto *addr = reinterpret_cast<const struct sockaddr_in *>(&storage);
size_t format_sockaddr_to(const struct sockaddr *addr_ptr, socklen_t len, std::span<char, SOCKADDR_STR_LEN> buf) {
if (addr_ptr->sa_family == AF_INET && len >= sizeof(const struct sockaddr_in)) {
const auto *addr = reinterpret_cast<const struct sockaddr_in *>(addr_ptr);
if (esphome_inet_ntop4(&addr->sin_addr, buf.data(), buf.size()) != nullptr)
return strlen(buf.data());
}
#if USE_NETWORK_IPV6
else if (storage.ss_family == AF_INET6) {
const auto *addr = reinterpret_cast<const struct sockaddr_in6 *>(&storage);
else if (addr_ptr->sa_family == AF_INET6 && len >= sizeof(sockaddr_in6)) {
const auto *addr = reinterpret_cast<const struct sockaddr_in6 *>(addr_ptr);
#ifndef USE_SOCKET_IMPL_LWIP_TCP
// Format IPv4-mapped IPv6 addresses as regular IPv4 (not supported on ESP8266 raw TCP)
if (addr->sin6_addr.un.u32_addr[0] == 0 && addr->sin6_addr.un.u32_addr[1] == 0 &&
@@ -78,7 +78,7 @@ size_t Socket::getpeername_to(std::span<char, SOCKADDR_STR_LEN> buf) {
buf[0] = '\0';
return 0;
}
return format_sockaddr_to(storage, buf);
return format_sockaddr_to(reinterpret_cast<struct sockaddr *>(&storage), len, buf);
}
size_t Socket::getsockname_to(std::span<char, SOCKADDR_STR_LEN> buf) {
@@ -88,7 +88,7 @@ size_t Socket::getsockname_to(std::span<char, SOCKADDR_STR_LEN> buf) {
buf[0] = '\0';
return 0;
}
return format_sockaddr_to(storage, buf);
return format_sockaddr_to(reinterpret_cast<struct sockaddr *>(&storage), len, buf);
}
std::unique_ptr<Socket> socket_ip(int type, int protocol) {

View File

@@ -102,6 +102,9 @@ inline socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const st
/// Set a sockaddr to the any address and specified port for the IP version used by socket_ip().
socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t port);
/// Format sockaddr into caller-provided buffer, returns length written (excluding null)
size_t format_sockaddr_to(const struct sockaddr *addr_ptr, socklen_t len, std::span<char, SOCKADDR_STR_LEN> buf);
#if defined(USE_ESP8266) && defined(USE_SOCKET_IMPL_LWIP_TCP)
/// Delay that can be woken early by socket activity.
/// On ESP8266, lwip callbacks set a flag and call esp_schedule() to wake the delay.

View File

@@ -487,7 +487,7 @@ void AsyncEventSource::deferrable_send_state(void *source, const char *event_typ
AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest *request,
esphome::web_server_idf::AsyncEventSource *server,
esphome::web_server::WebServer *ws)
: server_(server), web_server_(ws), entities_iterator_(new esphome::web_server::ListEntitiesIterator(ws, server)) {
: server_(server), web_server_(ws), entities_iterator_(ws, server) {
httpd_req_t *req = *request;
httpd_resp_set_status(req, HTTPD_200);
@@ -531,12 +531,12 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest *
}
#endif
this->entities_iterator_->begin(ws->include_internal_);
this->entities_iterator_.begin(ws->include_internal_);
// just dump them all up-front and take advantage of the deferred queue
// on second thought that takes too long, but leaving the commented code here for debug purposes
// while(!this->entities_iterator_->completed()) {
// this->entities_iterator_->advance();
// while(!this->entities_iterator_.completed()) {
// this->entities_iterator_.advance();
//}
}
@@ -634,8 +634,8 @@ void AsyncEventSourceResponse::process_buffer_() {
void AsyncEventSourceResponse::loop() {
process_buffer_();
process_deferred_queue_();
if (!this->entities_iterator_->completed())
this->entities_iterator_->advance();
if (!this->entities_iterator_.completed())
this->entities_iterator_.advance();
}
bool AsyncEventSourceResponse::try_send_nodefer(const char *message, const char *event, uint32_t id,
@@ -781,7 +781,7 @@ void AsyncEventSourceResponse::deferrable_send_state(void *source, const char *e
message_generator_t *message_generator) {
// allow all json "details_all" to go through before publishing bare state events, this avoids unnamed entries showing
// up in the web GUI and reduces event load during initial connect
if (!entities_iterator_->completed() && 0 != strcmp(event_type, "state_detail_all"))
if (!this->entities_iterator_.completed() && 0 != strcmp(event_type, "state_detail_all"))
return;
if (source == nullptr)

View File

@@ -13,11 +13,14 @@
#include <utility>
#include <vector>
#ifdef USE_WEBSERVER
#include "esphome/components/web_server/list_entities.h"
#endif
namespace esphome {
#ifdef USE_WEBSERVER
namespace web_server {
class WebServer;
class ListEntitiesIterator;
}; // namespace web_server
#endif
namespace web_server_idf {
@@ -284,7 +287,7 @@ class AsyncEventSourceResponse {
std::atomic<int> fd_{};
std::vector<DeferredEvent> deferred_queue_;
esphome::web_server::WebServer *web_server_;
std::unique_ptr<esphome::web_server::ListEntitiesIterator> entities_iterator_;
esphome::web_server::ListEntitiesIterator entities_iterator_;
std::string event_buffer_{""};
size_t event_bytes_sent_;
uint16_t consecutive_send_failures_{0};

View File

@@ -920,7 +920,16 @@ bssid_t WiFiComponent::wifi_bssid() {
}
return bssid;
}
std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); }
std::string WiFiComponent::wifi_ssid() {
struct station_config conf {};
if (!wifi_station_get_config(&conf)) {
return "";
}
// conf.ssid is uint8[32], not null-terminated if full
auto *ssid_s = reinterpret_cast<const char *>(conf.ssid);
size_t len = strnlen(ssid_s, sizeof(conf.ssid));
return {ssid_s, len};
}
const char *WiFiComponent::wifi_ssid_to(std::span<char, SSID_BUFFER_SIZE> buffer) {
struct station_config conf {};
if (!wifi_station_get_config(&conf)) {
@@ -934,16 +943,24 @@ const char *WiFiComponent::wifi_ssid_to(std::span<char, SSID_BUFFER_SIZE> buffer
return buffer.data();
}
int8_t WiFiComponent::wifi_rssi() {
if (WiFi.status() != WL_CONNECTED)
if (wifi_station_get_connect_status() != STATION_GOT_IP)
return WIFI_RSSI_DISCONNECTED;
int8_t rssi = WiFi.RSSI();
sint8 rssi = wifi_station_get_rssi();
// Values >= 31 are error codes per NONOS SDK API, not valid RSSI readings
return rssi >= 31 ? WIFI_RSSI_DISCONNECTED : rssi;
}
int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); }
network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {(const ip_addr_t *) WiFi.subnetMask()}; }
network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {(const ip_addr_t *) WiFi.gatewayIP()}; }
network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return {(const ip_addr_t *) WiFi.dnsIP(num)}; }
int32_t WiFiComponent::get_wifi_channel() { return wifi_get_channel(); }
network::IPAddress WiFiComponent::wifi_subnet_mask_() {
struct ip_info ip {};
wifi_get_ip_info(STATION_IF, &ip);
return network::IPAddress(&ip.netmask);
}
network::IPAddress WiFiComponent::wifi_gateway_ip_() {
struct ip_info ip {};
wifi_get_ip_info(STATION_IF, &ip);
return network::IPAddress(&ip.gw);
}
network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return network::IPAddress(dns_getserver(num)); }
void WiFiComponent::wifi_loop_() {}
} // namespace esphome::wifi

View File

@@ -827,16 +827,17 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
}
uint16_t number = it.number;
auto records = std::make_unique<wifi_ap_record_t[]>(number);
err = esp_wifi_scan_get_ap_records(&number, records.get());
if (err != ESP_OK) {
ESP_LOGW(TAG, "esp_wifi_scan_get_ap_records failed: %s", esp_err_to_name(err));
return;
}
scan_result_.init(number);
for (int i = 0; i < number; i++) {
auto &record = records[i];
// Process one record at a time to avoid large buffer allocation
wifi_ap_record_t record;
for (uint16_t i = 0; i < number; i++) {
err = esp_wifi_scan_get_ap_record(&record);
if (err != ESP_OK) {
ESP_LOGW(TAG, "esp_wifi_scan_get_ap_record failed: %s", esp_err_to_name(err));
esp_wifi_clear_ap_list(); // Free remaining records not yet retrieved
break;
}
bssid_t bssid;
std::copy(record.bssid, record.bssid + 6, bssid.begin());
std::string ssid(reinterpret_cast<const char *>(record.ssid));

View File

@@ -79,13 +79,17 @@ async def setup_conf(config, key):
async def to_code(config):
# Request specific WiFi listeners based on which sensors are configured
# Each sensor needs its own listener slot - call request for EACH sensor
# SSID and BSSID use WiFiConnectStateListener
if CONF_SSID in config or CONF_BSSID in config:
wifi.request_wifi_connect_state_listener()
for key in (CONF_SSID, CONF_BSSID):
if key in config:
wifi.request_wifi_connect_state_listener()
# IP address and DNS use WiFiIPStateListener
if CONF_IP_ADDRESS in config or CONF_DNS_ADDRESS in config:
wifi.request_wifi_ip_state_listener()
for key in (CONF_IP_ADDRESS, CONF_DNS_ADDRESS):
if key in config:
wifi.request_wifi_ip_state_listener()
# Scan results use WiFiScanResultsListener
if CONF_SCAN_RESULTS in config:

View File

@@ -6,7 +6,7 @@ namespace x9c {
static const char *const TAG = "x9c.output";
void X9cOutput::trim_value(int change_amount) {
void X9cOutput::trim_value(int32_t change_amount) {
if (change_amount == 0) {
return;
}
@@ -47,17 +47,17 @@ void X9cOutput::setup() {
if (this->initial_value_ <= 0.50) {
this->trim_value(-101); // Set min value (beyond 0)
this->trim_value(static_cast<uint32_t>(roundf(this->initial_value_ * 100)));
this->trim_value(lroundf(this->initial_value_ * 100));
} else {
this->trim_value(101); // Set max value (beyond 100)
this->trim_value(static_cast<uint32_t>(roundf(this->initial_value_ * 100) - 100));
this->trim_value(lroundf(this->initial_value_ * 100) - 100);
}
this->pot_value_ = this->initial_value_;
this->write_state(this->initial_value_);
}
void X9cOutput::write_state(float state) {
this->trim_value(static_cast<uint32_t>(roundf((state - this->pot_value_) * 100)));
this->trim_value(lroundf((state - this->pot_value_) * 100));
this->pot_value_ = state;
}

View File

@@ -18,7 +18,7 @@ class X9cOutput : public output::FloatOutput, public Component {
void setup() override;
void dump_config() override;
void trim_value(int change_amount);
void trim_value(int32_t change_amount);
protected:
void write_state(float state) override;

View File

@@ -2,7 +2,7 @@
#ifdef USE_ZEPHYR
#include "esphome/core/hal.h"
struct device;
#include <zephyr/device.h>
namespace esphome {
namespace zephyr {

View File

@@ -1086,6 +1086,7 @@ CONF_WAKEUP_PIN = "wakeup_pin"
CONF_WAND_ID = "wand_id"
CONF_WARM_WHITE = "warm_white"
CONF_WARM_WHITE_COLOR_TEMPERATURE = "warm_white_color_temperature"
CONF_WARMUP_TIME = "warmup_time"
CONF_WATCHDOG_THRESHOLD = "watchdog_threshold"
CONF_WATCHDOG_TIMEOUT = "watchdog_timeout"
CONF_WATER_HEATER = "water_heater"
@@ -1379,6 +1380,7 @@ KEY_FRAMEWORK_VERSION = "framework_version"
KEY_NAME = "name"
KEY_VARIANT = "variant"
KEY_PAST_SAFE_MODE = "past_safe_mode"
KEY_NATIVE_IDF = "native_idf"
# Entity categories
ENTITY_CATEGORY_NONE = ""

View File

@@ -17,6 +17,7 @@ from esphome.const import (
CONF_WEB_SERVER,
CONF_WIFI,
KEY_CORE,
KEY_NATIVE_IDF,
KEY_TARGET_FRAMEWORK,
KEY_TARGET_PLATFORM,
PLATFORM_BK72XX,
@@ -763,6 +764,9 @@ class EsphomeCore:
@property
def firmware_bin(self) -> Path:
# Check if using native ESP-IDF build (--native-idf)
if self.data.get(KEY_NATIVE_IDF, False):
return self.relative_build_path("build", f"{self.name}.bin")
if self.is_libretiny:
return self.relative_pioenvs_path(self.name, "firmware.uf2")
return self.relative_pioenvs_path(self.name, "firmware.bin")

View File

@@ -47,18 +47,21 @@ struct ComponentPriorityOverride {
};
// Error messages for failed components
// Using raw pointer instead of unique_ptr to avoid global constructor/destructor overhead
// This is never freed as error messages persist for the lifetime of the device
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
std::unique_ptr<std::vector<ComponentErrorMessage>> component_error_messages;
std::vector<ComponentErrorMessage> *component_error_messages = nullptr;
// Setup priority overrides - freed after setup completes
// Using raw pointer instead of unique_ptr to avoid global constructor/destructor overhead
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
std::unique_ptr<std::vector<ComponentPriorityOverride>> setup_priority_overrides;
std::vector<ComponentPriorityOverride> *setup_priority_overrides = nullptr;
// Helper to store error messages - reduces duplication between deprecated and new API
// Remove before 2026.6.0 when deprecated const char* API is removed
void store_component_error_message(const Component *component, const char *message, bool is_flash_ptr) {
// Lazy allocate the error messages vector if needed
if (!component_error_messages) {
component_error_messages = std::make_unique<std::vector<ComponentErrorMessage>>();
component_error_messages = new std::vector<ComponentErrorMessage>();
}
// Check if this component already has an error message
for (auto &entry : *component_error_messages) {
@@ -467,7 +470,7 @@ float Component::get_actual_setup_priority() const {
void Component::set_setup_priority(float priority) {
// Lazy allocate the vector if needed
if (!setup_priority_overrides) {
setup_priority_overrides = std::make_unique<std::vector<ComponentPriorityOverride>>();
setup_priority_overrides = new std::vector<ComponentPriorityOverride>();
// Reserve some space to avoid reallocations (most configs have < 10 overrides)
setup_priority_overrides->reserve(10);
}
@@ -553,7 +556,8 @@ WarnIfComponentBlockingGuard::~WarnIfComponentBlockingGuard() {}
void clear_setup_priority_overrides() {
// Free the setup priority map completely
setup_priority_overrides.reset();
delete setup_priority_overrides;
setup_priority_overrides = nullptr;
}
} // namespace esphome

View File

@@ -17,6 +17,8 @@
#include <vector>
#include <concepts>
#include <strings.h>
#include "esphome/core/optional.h"
#ifdef USE_ESP8266
@@ -348,6 +350,8 @@ template<typename T> class FixedVector {
size_t size() const { return size_; }
bool empty() const { return size_ == 0; }
size_t capacity() const { return capacity_; }
bool full() const { return size_ == capacity_; }
/// Access element without bounds checking (matches std::vector behavior)
/// Caller must ensure index is valid (i < size())
@@ -369,13 +373,15 @@ template<typename T> class FixedVector {
/// @brief Helper class for efficient buffer allocation - uses stack for small sizes, heap for large
/// This is useful when most operations need a small buffer but occasionally need larger ones.
/// The stack buffer avoids heap allocation in the common case, while heap fallback handles edge cases.
template<size_t STACK_SIZE> class SmallBufferWithHeapFallback {
/// @tparam STACK_SIZE Number of elements in the stack buffer
/// @tparam T Element type (default: uint8_t)
template<size_t STACK_SIZE, typename T = uint8_t> class SmallBufferWithHeapFallback {
public:
explicit SmallBufferWithHeapFallback(size_t size) {
if (size <= STACK_SIZE) {
this->buffer_ = this->stack_buffer_;
} else {
this->heap_buffer_ = new uint8_t[size];
this->heap_buffer_ = new T[size];
this->buffer_ = this->heap_buffer_;
}
}
@@ -387,12 +393,12 @@ template<size_t STACK_SIZE> class SmallBufferWithHeapFallback {
SmallBufferWithHeapFallback(SmallBufferWithHeapFallback &&) = delete;
SmallBufferWithHeapFallback &operator=(SmallBufferWithHeapFallback &&) = delete;
uint8_t *get() { return this->buffer_; }
T *get() { return this->buffer_; }
private:
uint8_t stack_buffer_[STACK_SIZE];
uint8_t *heap_buffer_{nullptr};
uint8_t *buffer_;
T stack_buffer_[STACK_SIZE];
T *heap_buffer_{nullptr};
T *buffer_;
};
///@}
@@ -568,6 +574,10 @@ template<typename T> constexpr T convert_little_endian(T val) {
bool str_equals_case_insensitive(const std::string &a, const std::string &b);
/// Compare StringRefs for equality in case-insensitive manner.
bool str_equals_case_insensitive(StringRef a, StringRef b);
/// Compare C strings for equality in case-insensitive manner (no heap allocation).
inline bool str_equals_case_insensitive(const char *a, const char *b) { return strcasecmp(a, b) == 0; }
inline bool str_equals_case_insensitive(const std::string &a, const char *b) { return strcasecmp(a.c_str(), b) == 0; }
inline bool str_equals_case_insensitive(const char *a, const std::string &b) { return strcasecmp(a, b.c_str()) == 0; }
/// Check whether a string starts with a value.
bool str_startswith(const std::string &str, const std::string &start);
@@ -1343,16 +1353,30 @@ template<typename... X> class LazyCallbackManager;
*
* Memory overhead comparison (32-bit systems):
* - CallbackManager: 12 bytes (empty std::vector)
* - LazyCallbackManager: 4 bytes (nullptr unique_ptr)
* - LazyCallbackManager: 4 bytes (nullptr pointer)
*
* Uses plain pointer instead of unique_ptr to avoid template instantiation overhead.
* The class is explicitly non-copyable/non-movable for Rule of Five compliance.
*
* @tparam Ts The arguments for the callbacks, wrapped in void().
*/
template<typename... Ts> class LazyCallbackManager<void(Ts...)> {
public:
LazyCallbackManager() = default;
/// Destructor - clean up allocated CallbackManager if any.
/// In practice this never runs (entities live for device lifetime) but included for correctness.
~LazyCallbackManager() { delete this->callbacks_; }
// Non-copyable and non-movable (entities are never copied or moved)
LazyCallbackManager(const LazyCallbackManager &) = delete;
LazyCallbackManager &operator=(const LazyCallbackManager &) = delete;
LazyCallbackManager(LazyCallbackManager &&) = delete;
LazyCallbackManager &operator=(LazyCallbackManager &&) = delete;
/// Add a callback to the list. Allocates the underlying CallbackManager on first use.
void add(std::function<void(Ts...)> &&callback) {
if (!this->callbacks_) {
this->callbacks_ = make_unique<CallbackManager<void(Ts...)>>();
this->callbacks_ = new CallbackManager<void(Ts...)>();
}
this->callbacks_->add(std::move(callback));
}
@@ -1374,7 +1398,7 @@ template<typename... Ts> class LazyCallbackManager<void(Ts...)> {
void operator()(Ts... args) { this->call(args...); }
protected:
std::unique_ptr<CallbackManager<void(Ts...)>> callbacks_;
CallbackManager<void(Ts...)> *callbacks_{nullptr};
};
/// Helper class to deduplicate items in a series of values.

View File

@@ -67,7 +67,7 @@ std::string ESPTime::strftime(const char *format) {
std::string ESPTime::strftime(const std::string &format) { return this->strftime(format.c_str()); }
bool ESPTime::strptime(const std::string &time_to_parse, ESPTime &esp_time) {
bool ESPTime::strptime(const char *time_to_parse, size_t len, ESPTime &esp_time) {
uint16_t year;
uint8_t month;
uint8_t day;
@@ -75,40 +75,41 @@ bool ESPTime::strptime(const std::string &time_to_parse, ESPTime &esp_time) {
uint8_t minute;
uint8_t second;
int num;
const int ilen = static_cast<int>(len);
if (sscanf(time_to_parse.c_str(), "%04hu-%02hhu-%02hhu %02hhu:%02hhu:%02hhu %n", &year, &month, &day, // NOLINT
&hour, // NOLINT
&minute, // NOLINT
&second, &num) == 6 && // NOLINT
num == static_cast<int>(time_to_parse.size())) {
if (sscanf(time_to_parse, "%04hu-%02hhu-%02hhu %02hhu:%02hhu:%02hhu %n", &year, &month, &day, // NOLINT
&hour, // NOLINT
&minute, // NOLINT
&second, &num) == 6 && // NOLINT
num == ilen) {
esp_time.year = year;
esp_time.month = month;
esp_time.day_of_month = day;
esp_time.hour = hour;
esp_time.minute = minute;
esp_time.second = second;
} else if (sscanf(time_to_parse.c_str(), "%04hu-%02hhu-%02hhu %02hhu:%02hhu %n", &year, &month, &day, // NOLINT
&hour, // NOLINT
&minute, &num) == 5 && // NOLINT
num == static_cast<int>(time_to_parse.size())) {
} else if (sscanf(time_to_parse, "%04hu-%02hhu-%02hhu %02hhu:%02hhu %n", &year, &month, &day, // NOLINT
&hour, // NOLINT
&minute, &num) == 5 && // NOLINT
num == ilen) {
esp_time.year = year;
esp_time.month = month;
esp_time.day_of_month = day;
esp_time.hour = hour;
esp_time.minute = minute;
esp_time.second = 0;
} else if (sscanf(time_to_parse.c_str(), "%02hhu:%02hhu:%02hhu %n", &hour, &minute, &second, &num) == 3 && // NOLINT
num == static_cast<int>(time_to_parse.size())) {
} else if (sscanf(time_to_parse, "%02hhu:%02hhu:%02hhu %n", &hour, &minute, &second, &num) == 3 && // NOLINT
num == ilen) {
esp_time.hour = hour;
esp_time.minute = minute;
esp_time.second = second;
} else if (sscanf(time_to_parse.c_str(), "%02hhu:%02hhu %n", &hour, &minute, &num) == 2 && // NOLINT
num == static_cast<int>(time_to_parse.size())) {
} else if (sscanf(time_to_parse, "%02hhu:%02hhu %n", &hour, &minute, &num) == 2 && // NOLINT
num == ilen) {
esp_time.hour = hour;
esp_time.minute = minute;
esp_time.second = 0;
} else if (sscanf(time_to_parse.c_str(), "%04hu-%02hhu-%02hhu %n", &year, &month, &day, &num) == 3 && // NOLINT
num == static_cast<int>(time_to_parse.size())) {
} else if (sscanf(time_to_parse, "%04hu-%02hhu-%02hhu %n", &year, &month, &day, &num) == 3 && // NOLINT
num == ilen) {
esp_time.year = year;
esp_time.month = month;
esp_time.day_of_month = day;

View File

@@ -2,6 +2,7 @@
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <span>
#include <string>
@@ -80,11 +81,20 @@ struct ESPTime {
}
/** Convert a string to ESPTime struct as specified by the format argument.
* @param time_to_parse null-terminated c string formatet like this: 2020-08-25 05:30:00.
* @param time_to_parse c string formatted like this: 2020-08-25 05:30:00.
* @param len length of the string (not including null terminator if present)
* @param esp_time an instance of a ESPTime struct
* @return the success sate of the parsing
* @return the success state of the parsing
*/
static bool strptime(const std::string &time_to_parse, ESPTime &esp_time);
static bool strptime(const char *time_to_parse, size_t len, ESPTime &esp_time);
/// @copydoc strptime(const char *, size_t, ESPTime &)
static bool strptime(const char *time_to_parse, ESPTime &esp_time) {
return strptime(time_to_parse, strlen(time_to_parse), esp_time);
}
/// @copydoc strptime(const char *, size_t, ESPTime &)
static bool strptime(const std::string &time_to_parse, ESPTime &esp_time) {
return strptime(time_to_parse.c_str(), time_to_parse.size(), esp_time);
}
/// Convert a C tm struct instance with a C unix epoch timestamp to an ESPTime instance.
static ESPTime from_c_tm(struct tm *c_tm, time_t c_time);

229
esphome/espidf_api.py Normal file
View File

@@ -0,0 +1,229 @@
"""ESP-IDF direct build API for ESPHome."""
import json
import logging
import os
from pathlib import Path
import shutil
import subprocess
from esphome.components.esp32.const import KEY_ESP32, KEY_FLASH_SIZE
from esphome.const import CONF_COMPILE_PROCESS_LIMIT, CONF_ESPHOME
from esphome.core import CORE, EsphomeError
_LOGGER = logging.getLogger(__name__)
def _get_idf_path() -> Path | None:
"""Get IDF_PATH from environment or common locations."""
# Check environment variable first
if "IDF_PATH" in os.environ:
path = Path(os.environ["IDF_PATH"])
if path.is_dir():
return path
# Check common installation locations
common_paths = [
Path.home() / "esp" / "esp-idf",
Path.home() / ".espressif" / "esp-idf",
Path("/opt/esp-idf"),
]
for path in common_paths:
if path.is_dir() and (path / "tools" / "idf.py").is_file():
return path
return None
def _get_idf_env() -> dict[str, str]:
"""Get environment variables needed for ESP-IDF build.
Requires the user to have sourced export.sh before running esphome.
"""
env = os.environ.copy()
idf_path = _get_idf_path()
if idf_path is None:
raise EsphomeError(
"ESP-IDF not found. Please install ESP-IDF and source export.sh:\n"
" git clone -b v5.3.2 --recursive https://github.com/espressif/esp-idf.git ~/esp-idf\n"
" cd ~/esp-idf && ./install.sh\n"
" source ~/esp-idf/export.sh\n"
"See: https://docs.espressif.com/projects/esp-idf/en/latest/esp32/get-started/"
)
env["IDF_PATH"] = str(idf_path)
return env
def run_idf_py(
*args, cwd: Path | None = None, capture_output: bool = False
) -> int | str:
"""Run idf.py with the given arguments."""
idf_path = _get_idf_path()
if idf_path is None:
raise EsphomeError("ESP-IDF not found")
env = _get_idf_env()
idf_py = idf_path / "tools" / "idf.py"
cmd = ["python", str(idf_py)] + list(args)
if cwd is None:
cwd = CORE.build_path
_LOGGER.debug("Running: %s", " ".join(cmd))
_LOGGER.debug(" in directory: %s", cwd)
if capture_output:
result = subprocess.run(
cmd,
cwd=cwd,
env=env,
capture_output=True,
text=True,
check=False,
)
if result.returncode != 0:
_LOGGER.error("idf.py failed:\n%s", result.stderr)
return result.stdout
result = subprocess.run(
cmd,
cwd=cwd,
env=env,
check=False,
)
return result.returncode
def run_reconfigure() -> int:
"""Run cmake reconfigure only (no build)."""
return run_idf_py("reconfigure")
def run_compile(config, verbose: bool) -> int:
"""Compile the ESP-IDF project.
Uses two-phase configure to auto-discover available components:
1. If no previous build, configure with minimal REQUIRES to discover components
2. Regenerate CMakeLists.txt with discovered components
3. Run full build
"""
from esphome.build_gen.espidf import has_discovered_components, write_project
# Check if we need to do discovery phase
if not has_discovered_components():
_LOGGER.info("Discovering available ESP-IDF components...")
write_project(minimal=True)
rc = run_reconfigure()
if rc != 0:
_LOGGER.error("Component discovery failed")
return rc
_LOGGER.info("Regenerating CMakeLists.txt with discovered components...")
write_project(minimal=False)
# Build
args = ["build"]
if verbose:
args.append("-v")
# Add parallel job limit if configured
if CONF_COMPILE_PROCESS_LIMIT in config.get(CONF_ESPHOME, {}):
limit = config[CONF_ESPHOME][CONF_COMPILE_PROCESS_LIMIT]
args.extend(["-j", str(limit)])
# Set the sdkconfig file
sdkconfig_path = CORE.relative_build_path(f"sdkconfig.{CORE.name}")
if sdkconfig_path.is_file():
args.extend(["-D", f"SDKCONFIG={sdkconfig_path}"])
return run_idf_py(*args)
def get_firmware_path() -> Path:
"""Get the path to the compiled firmware binary."""
build_dir = CORE.relative_build_path("build")
return build_dir / f"{CORE.name}.bin"
def get_factory_firmware_path() -> Path:
"""Get the path to the factory firmware (with bootloader)."""
build_dir = CORE.relative_build_path("build")
return build_dir / f"{CORE.name}.factory.bin"
def create_factory_bin() -> bool:
"""Create factory.bin by merging bootloader, partition table, and app."""
build_dir = CORE.relative_build_path("build")
flasher_args_path = build_dir / "flasher_args.json"
if not flasher_args_path.is_file():
_LOGGER.warning("flasher_args.json not found, cannot create factory.bin")
return False
try:
with open(flasher_args_path, encoding="utf-8") as f:
flash_data = json.load(f)
except (json.JSONDecodeError, OSError) as e:
_LOGGER.error("Failed to read flasher_args.json: %s", e)
return False
# Get flash size from config
flash_size = CORE.data[KEY_ESP32][KEY_FLASH_SIZE]
# Build esptool merge command
sections = []
for addr, fname in sorted(
flash_data.get("flash_files", {}).items(), key=lambda kv: int(kv[0], 16)
):
file_path = build_dir / fname
if file_path.is_file():
sections.extend([addr, str(file_path)])
else:
_LOGGER.warning("Flash file not found: %s", file_path)
if not sections:
_LOGGER.warning("No flash sections found")
return False
output_path = get_factory_firmware_path()
chip = flash_data.get("extra_esptool_args", {}).get("chip", "esp32")
cmd = [
"python",
"-m",
"esptool",
"--chip",
chip,
"merge_bin",
"--flash_size",
flash_size,
"--output",
str(output_path),
] + sections
_LOGGER.info("Creating factory.bin...")
result = subprocess.run(cmd, capture_output=True, text=True, check=False)
if result.returncode != 0:
_LOGGER.error("Failed to create factory.bin: %s", result.stderr)
return False
_LOGGER.info("Created: %s", output_path)
return True
def create_ota_bin() -> bool:
"""Copy the firmware to .ota.bin for ESPHome OTA compatibility."""
firmware_path = get_firmware_path()
ota_path = firmware_path.with_suffix(".ota.bin")
if not firmware_path.is_file():
_LOGGER.warning("Firmware not found: %s", firmware_path)
return False
shutil.copy(firmware_path, ota_path)
_LOGGER.info("Created: %s", ota_path)
return True

View File

@@ -1,4 +1,8 @@
dependencies:
bblanchon/arduinojson:
version: "7.4.2"
esphome/esp-audio-libs:
version: 2.0.3
espressif/esp-tflite-micro:
version: 1.3.3~1
espressif/esp32-camera:

View File

@@ -34,7 +34,6 @@ build_flags =
[common]
; Base dependencies for all environments
lib_deps_base =
bblanchon/ArduinoJson@7.4.2 ; json
wjtje/qr-code-generator-library@1.7.0 ; qr_code
functionpointer/arduino-MLX90393@1.0.2 ; mlx90393
pavlodn/HaierProtocol@0.9.31 ; haier
@@ -85,7 +84,7 @@ lib_deps =
fastled/FastLED@3.9.16 ; fastled_base
freekode/TM1651@1.0.1 ; tm1651
dudanov/MideaUART@1.1.9 ; midea
tonia/HeatpumpIR@1.0.37 ; heatpumpir
tonia/HeatpumpIR@1.0.40 ; heatpumpir
build_flags =
${common.build_flags}
-DUSE_ARDUINO
@@ -111,6 +110,7 @@ platform_packages =
framework = arduino
lib_deps =
${common:arduino.lib_deps}
bblanchon/ArduinoJson@7.4.2 ; json
ESP8266WiFi ; wifi (Arduino built-in)
Update ; ota (Arduino built-in)
ESP32Async/ESPAsyncTCP@2.0.0 ; async_tcp
@@ -154,7 +154,6 @@ lib_deps =
makuna/NeoPixelBus@2.8.0 ; neopixelbus
esphome/ESP32-audioI2S@2.3.0 ; i2s_audio
droscy/esp_wireguard@0.4.2 ; wireguard
esphome/esp-audio-libs@2.0.1 ; audio
kahrendt/ESPMicroSpeechFeatures@1.1.0 ; micro_wake_word
build_flags =
@@ -178,7 +177,7 @@ lib_deps =
${common:idf.lib_deps}
droscy/esp_wireguard@0.4.2 ; wireguard
kahrendt/ESPMicroSpeechFeatures@1.1.0 ; micro_wake_word
esphome/esp-audio-libs@2.0.1 ; audio
tonia/HeatpumpIR@1.0.40 ; heatpumpir
build_flags =
${common:idf.build_flags}
-Wno-nonnull-compare
@@ -201,6 +200,7 @@ platform_packages =
framework = arduino
lib_deps =
${common:arduino.lib_deps}
bblanchon/ArduinoJson@7.4.2 ; json
ESP32Async/ESPAsyncWebServer@3.7.8 ; web_server_base
build_flags =
${common:arduino.build_flags}
@@ -216,6 +216,7 @@ platform = libretiny@1.9.2
framework = arduino
lib_compat_mode = soft
lib_deps =
bblanchon/ArduinoJson@7.4.2 ; json
ESP32Async/ESPAsyncWebServer@3.7.8 ; web_server_base
droscy/esp_wireguard@0.4.2 ; wireguard
build_flags =
@@ -239,6 +240,7 @@ build_flags =
-DUSE_NRF52
lib_deps =
${common.lib_deps_base}
bblanchon/ArduinoJson@7.4.2 ; json
; All the actual environments are defined below.

View File

@@ -1,5 +1,5 @@
[build-system]
requires = ["setuptools==80.9.0", "wheel>=0.43,<0.46"]
requires = ["setuptools==80.10.1", "wheel>=0.43,<0.46"]
build-backend = "setuptools.build_meta"
[project]

View File

@@ -732,6 +732,26 @@ def lint_no_heap_allocating_helpers(fname, match):
)
@lint_re_check(
# Match sprintf/vsprintf but not snprintf/vsnprintf
# [^\w] ensures we don't match the safe variants
r"[^\w](v?sprintf)\s*\(" + CPP_RE_EOL,
include=cpp_include,
)
def lint_no_sprintf(fname, match):
func = match.group(1)
safe_func = func.replace("sprintf", "snprintf")
return (
f"{highlight(func + '()')} is not allowed in ESPHome. It has no buffer size limit "
f"and can cause buffer overflows.\n"
f"Please use one of these alternatives:\n"
f" - {highlight(safe_func + '(buf, sizeof(buf), fmt, ...)')} for general formatting\n"
f" - {highlight('buf_append_printf(buf, sizeof(buf), pos, fmt, ...)')} for "
f"offset-based formatting (also stores format strings in flash on ESP8266)\n"
f"(If strictly necessary, add `// NOLINT` to the end of the line)"
)
@lint_content_find_check(
"ESP_LOG",
include=["*.h", "*.tcc"],

View File

@@ -1 +1,5 @@
<<: !include common.yaml
nrf52:
reg0:
voltage: 2.1V

View File

@@ -0,0 +1,4 @@
packages:
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml
<<: !include common.yaml

View File

@@ -121,6 +121,8 @@ sensor:
min_value: -10.0
- debounce: 0.1s
- delta: 5.0
- delta:
max_value: 2%
- exponential_moving_average:
alpha: 0.1
send_every: 15

View File

@@ -0,0 +1,180 @@
esphome:
name: test-delta-filters
host:
api:
batch_delay: 0ms # Disable batching to receive all state updates
logger:
level: DEBUG
sensor:
- platform: template
name: "Source Sensor 1"
id: source_sensor_1
accuracy_decimals: 1
- platform: template
name: "Source Sensor 2"
id: source_sensor_2
accuracy_decimals: 1
- platform: template
name: "Source Sensor 3"
id: source_sensor_3
accuracy_decimals: 1
- platform: template
name: "Source Sensor 4"
id: source_sensor_4
accuracy_decimals: 1
- platform: copy
source_id: source_sensor_1
name: "Filter Min"
id: filter_min
filters:
- delta:
min_value: 10
- platform: copy
source_id: source_sensor_2
name: "Filter Max"
id: filter_max
filters:
- delta:
max_value: 10
- platform: copy
source_id: source_sensor_3
id: test_3_baseline
filters:
- median:
window_size: 6
send_every: 1
send_first_at: 1
- platform: copy
source_id: source_sensor_3
name: "Filter Baseline Max"
id: filter_baseline_max
filters:
- delta:
max_value: 10
baseline: !lambda return id(test_3_baseline).state;
- platform: copy
source_id: source_sensor_4
name: "Filter Zero Delta"
id: filter_zero_delta
filters:
- delta: 0
script:
- id: test_filter_min
then:
- sensor.template.publish:
id: source_sensor_1
state: 1.0
- delay: 20ms
- sensor.template.publish:
id: source_sensor_1
state: 5.0 # Filtered out
- delay: 20ms
- sensor.template.publish:
id: source_sensor_1
state: 12.0
- delay: 20ms
- sensor.template.publish:
id: source_sensor_1
state: 8.0 # Filtered out
- delay: 20ms
- sensor.template.publish:
id: source_sensor_1
state: -2.0
- id: test_filter_max
then:
- sensor.template.publish:
id: source_sensor_2
state: 1.0
- delay: 20ms
- sensor.template.publish:
id: source_sensor_2
state: 5.0
- delay: 20ms
- sensor.template.publish:
id: source_sensor_2
state: 40.0 # Filtered out
- delay: 20ms
- sensor.template.publish:
id: source_sensor_2
state: 10.0
- delay: 20ms
- sensor.template.publish:
id: source_sensor_2
state: -40.0 # Filtered out
- id: test_filter_baseline_max
then:
- sensor.template.publish:
id: source_sensor_3
state: 1.0
- delay: 20ms
- sensor.template.publish:
id: source_sensor_3
state: 2.0
- delay: 20ms
- sensor.template.publish:
id: source_sensor_3
state: 3.0
- delay: 20ms
- sensor.template.publish:
id: source_sensor_3
state: 40.0 # Filtered out
- delay: 20ms
- sensor.template.publish:
id: source_sensor_3
state: 20.0 # Filtered out
- delay: 20ms
- sensor.template.publish:
id: source_sensor_3
state: 20.0
- id: test_filter_zero_delta
then:
- sensor.template.publish:
id: source_sensor_4
state: 1.0
- delay: 20ms
- sensor.template.publish:
id: source_sensor_4
state: 1.0 # Filtered out
- delay: 20ms
- sensor.template.publish:
id: source_sensor_4
state: 2.0
button:
- platform: template
name: "Test Filter Min"
id: btn_filter_min
on_press:
- script.execute: test_filter_min
- platform: template
name: "Test Filter Max"
id: btn_filter_max
on_press:
- script.execute: test_filter_max
- platform: template
name: "Test Filter Baseline Max"
id: btn_filter_baseline_max
on_press:
- script.execute: test_filter_baseline_max
- platform: template
name: "Test Filter Zero Delta"
id: btn_filter_zero_delta
on_press:
- script.execute: test_filter_zero_delta

View File

@@ -0,0 +1,163 @@
"""Test sensor DeltaFilter functionality."""
from __future__ import annotations
import asyncio
from aioesphomeapi import ButtonInfo, EntityState, SensorState
import pytest
from .state_utils import InitialStateHelper, build_key_to_entity_mapping
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_sensor_filters_delta(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
loop = asyncio.get_running_loop()
sensor_values: dict[str, list[float]] = {
"filter_min": [],
"filter_max": [],
"filter_baseline_max": [],
"filter_zero_delta": [],
}
filter_min_done = loop.create_future()
filter_max_done = loop.create_future()
filter_baseline_max_done = loop.create_future()
filter_zero_delta_done = loop.create_future()
def on_state(state: EntityState) -> None:
if not isinstance(state, SensorState) or state.missing_state:
return
sensor_name = key_to_sensor.get(state.key)
if sensor_name not in sensor_values:
return
sensor_values[sensor_name].append(state.state)
# Check completion conditions
if (
sensor_name == "filter_min"
and len(sensor_values[sensor_name]) == 3
and not filter_min_done.done()
):
filter_min_done.set_result(True)
elif (
sensor_name == "filter_max"
and len(sensor_values[sensor_name]) == 3
and not filter_max_done.done()
):
filter_max_done.set_result(True)
elif (
sensor_name == "filter_baseline_max"
and len(sensor_values[sensor_name]) == 4
and not filter_baseline_max_done.done()
):
filter_baseline_max_done.set_result(True)
elif (
sensor_name == "filter_zero_delta"
and len(sensor_values[sensor_name]) == 2
and not filter_zero_delta_done.done()
):
filter_zero_delta_done.set_result(True)
async with (
run_compiled(yaml_config),
api_client_connected() as client,
):
# Get entities and build key mapping
entities, _ = await client.list_entities_services()
key_to_sensor = build_key_to_entity_mapping(
entities,
{
"filter_min": "Filter Min",
"filter_max": "Filter Max",
"filter_baseline_max": "Filter Baseline Max",
"filter_zero_delta": "Filter Zero Delta",
},
)
# Set up initial state helper with all entities
initial_state_helper = InitialStateHelper(entities)
# Subscribe to state changes with wrapper
client.subscribe_states(initial_state_helper.on_state_wrapper(on_state))
# Wait for initial states
await initial_state_helper.wait_for_initial_states()
# Find all buttons
button_name_map = {
"Test Filter Min": "filter_min",
"Test Filter Max": "filter_max",
"Test Filter Baseline Max": "filter_baseline_max",
"Test Filter Zero Delta": "filter_zero_delta",
}
buttons = {}
for entity in entities:
if isinstance(entity, ButtonInfo) and entity.name in button_name_map:
buttons[button_name_map[entity.name]] = entity.key
assert len(buttons) == 4, f"Expected 3 buttons, found {len(buttons)}"
# Test 1: Min
sensor_values["filter_min"].clear()
client.button_command(buttons["filter_min"])
try:
await asyncio.wait_for(filter_min_done, timeout=2.0)
except TimeoutError:
pytest.fail(f"Test 1 timed out. Values: {sensor_values['filter_min']}")
expected = [1.0, 12.0, -2.0]
assert sensor_values["filter_min"] == pytest.approx(expected), (
f"Test 1 failed: expected {expected}, got {sensor_values['filter_min']}"
)
# Test 2: Max
sensor_values["filter_max"].clear()
client.button_command(buttons["filter_max"])
try:
await asyncio.wait_for(filter_max_done, timeout=2.0)
except TimeoutError:
pytest.fail(f"Test 2 timed out. Values: {sensor_values['filter_max']}")
expected = [1.0, 5.0, 10.0]
assert sensor_values["filter_max"] == pytest.approx(expected), (
f"Test 2 failed: expected {expected}, got {sensor_values['filter_max']}"
)
# Test 3: Baseline Max
sensor_values["filter_baseline_max"].clear()
client.button_command(buttons["filter_baseline_max"])
try:
await asyncio.wait_for(filter_baseline_max_done, timeout=2.0)
except TimeoutError:
pytest.fail(
f"Test 3 timed out. Values: {sensor_values['filter_baseline_max']}"
)
expected = [1.0, 2.0, 3.0, 20.0]
assert sensor_values["filter_baseline_max"] == pytest.approx(expected), (
f"Test 3 failed: expected {expected}, got {sensor_values['filter_baseline_max']}"
)
# Test 4: Zero Delta
sensor_values["filter_zero_delta"].clear()
client.button_command(buttons["filter_zero_delta"])
try:
await asyncio.wait_for(filter_zero_delta_done, timeout=2.0)
except TimeoutError:
pytest.fail(
f"Test 4 timed out. Values: {sensor_values['filter_zero_delta']}"
)
expected = [1.0, 2.0]
assert sensor_values["filter_zero_delta"] == pytest.approx(expected), (
f"Test 4 failed: expected {expected}, got {sensor_values['filter_zero_delta']}"
)