Compare commits

..

161 Commits
release ... dev

Author SHA1 Message Date
H. Árkosi Róbert
4ac7fe84b4 [bthome_mithermometer] add encrypted beacon support (#13428) 2026-01-23 03:14:14 +11:00
Sven Kocksch
d6a41ed51e [mipi_dsi] Add M5Stack Tab5 (Rev2/V2) DriverChip (#12074)
Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
2026-01-23 02:31:38 +11:00
Rene Guca
8d1379a275 [dht] Increase delay for DHT22 and RHT03 (#13446) 2026-01-22 07:54:10 -05:00
J. Nick Koston
5bbf9153ca [http_request] Fix OTA failures on ESP8266/Arduino by making read semantics consistent (#13435)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-21 19:48:32 -10:00
J. Nick Koston
a1c4d56268 [alarm_control_panel] Reduce heap allocations in arm/disarm methods (#13358) 2026-01-21 18:37:13 -10:00
J. Nick Koston
a9ce3df04c [esp8266] Use SmallBufferWithHeapFallback in preferences (#13397) 2026-01-21 18:36:12 -10:00
J. Nick Koston
99aa83564e [mqtt] Reduce heap allocations in hot paths (#13362)
Co-authored-by: Copilot <175728472+Copilot@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 18:35:59 -10:00
J. Nick Koston
aa5092bdc2 [mqtt] Use stack buffers for discovery message formatting (#13216)
Co-authored-by: Copilot <175728472+Copilot@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 18:35:43 -10:00
Edward Firmo
645832a070 [nextion] Add configurable startup and queue timeout constants (#11098)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
2026-01-21 20:10:12 -06:00
Jonathan Swoboda
19c1d3aee7 [esp32] Bump Arduino to 3.3.6, platform to 55.03.36 (#13438)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 20:41:59 -05:00
J. Nick Koston
ce5ec7a78f [spi] Fix display init failure by marking displays as write-only for half-duplex mode (#13431) 2026-01-21 14:04:07 -10:00
J. Nick Koston
ebf589560d [wifi] Fix bk72xx manual_ip preventing API connection (#13426) 2026-01-21 14:03:49 -10:00
Jonathan Swoboda
8dd1aec606 [esp32] Add warning for experimental 400MHz on ESP32-P4 (#13433)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 17:17:11 -05: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
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
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
J. Nick Koston
c213de4861 [mapping] Use stack buffers for numeric key error logging (#13299) 2026-01-19 17:42:08 -10:00
J. Nick Koston
6cf320fd60 [mqtt] Eliminate per-entity loop overhead and heap churn (#13356) 2026-01-19 17:41:55 -10:00
J. Nick Koston
aeea340bc6 [cs5460a] Remove unnecessary empty loop override (#13357) 2026-01-19 17:41:03 -10:00
J. Nick Koston
d0e50ed030 [lock] Extract set_state_ helper to reduce code duplication (#13359) 2026-01-19 17:40:51 -10:00
J. Nick Koston
280d460025 [statsd] Use direct appends and stack buffer instead of str_sprintf (#13223)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-19 17:40:20 -10:00
J. Nick Koston
ea70faf642 [debug] Use shared buf_append_printf helper from core (#13260) 2026-01-19 17:38:56 -10:00
J. Nick Koston
5d7b38b261 [ezo_pmp] Replace sprintf with bounds-checked snprintf (#13304) 2026-01-19 17:38:22 -10:00
J. Nick Koston
e88093ca60 [am43][lightwaverf][rf_bridge][spi_led_strip] Replace sprintf with safe alternatives (#13302) 2026-01-19 17:38:08 -10:00
J. Nick Koston
b48d4ab785 [mqtt] Reduce heap allocations in publish path (#13372) 2026-01-19 17:37:54 -10:00
J. Nick Koston
8ade9dfc10 [shtcx] Use LogString for type to_string to save RAM on ESP8266 (#13370) 2026-01-19 17:37:33 -10:00
J. Nick Koston
4e0e7796de [mqtt] Remove unnecessary defer in ESP8266 on_message callback (#13373) 2026-01-19 17:37:19 -10:00
J. Nick Koston
62b6c9bf7c [esp32_ble] Deprecate ESPBTUUID::to_string() in favor of heap-free to_str() (#13376) 2026-01-19 17:37:03 -10:00
J. Nick Koston
b5fe271d6b [sprinkler] Disable loops when idle to reduce CPU overhead (#13381) 2026-01-19 17:36:47 -10:00
J. Nick Koston
5d787e2512 [sprinkler] Eliminate std::string heap allocations (#13379) 2026-01-19 17:35:58 -10:00
J. Nick Koston
8998ef0bc3 [network] Deprecate IPAddress::str() in favor of heap-free str_to() (#13378) 2026-01-19 17:35:32 -10:00
J. Nick Koston
8ec31dd769 [voice_assistant] Deprecate Timer::to_string() in favor of heap-free to_str() (#13377) 2026-01-19 17:35:19 -10:00
J. Nick Koston
0193464f92 [dsmr] Avoid std::string allocation for decryption key (#13375) 2026-01-19 17:34:49 -10:00
Jonathan Swoboda
1996bc425f [core] Fix state leakage and module caching when processing multiple configurations (#13368)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 14:46:24 -05:00
Clyde Stubbs
a0d3d54d69 [mipi_spi] Add variants of ESP32-2432S028 displays (#13340) 2026-01-20 05:13:36 +11:00
J. Nick Koston
ee264d0fd4 [anova] Replace sprintf with bounds-checked alternatives (#13303) 2026-01-18 23:57:42 -10:00
J. Nick Koston
892e9b006f [api] Use MAX_STATE_LEN constant for Home Assistant state buffer (#13278) 2026-01-18 23:57:27 -10:00
J. Nick Koston
f8bd4ef57d [template][event] Use StringRef for set_action and on_event triggers (#13328) 2026-01-18 22:22:57 -10:00
J. Nick Koston
bfcc0e26a3 [dfrobot_sen0395][pipsolar][sim800l][wl_134] Replace sprintf with snprintf/buf_append_printf (#13301)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-18 22:22:44 -10:00
J. Nick Koston
86a1b4cf69 [select][fan] Use StringRef for on_value/on_preset_set triggers to avoid heap allocation (#13324) 2026-01-18 19:51:11 -10:00
J. Nick Koston
d8a28f6fba [scheduler] Replace resize() with erase() to save ~ 436 bytes flash (#13214) 2026-01-18 18:54:30 -10:00
J. Nick Koston
e80a940222 [gdk101] Use stack buffer to eliminate heap allocation for firmware version (#13224) 2026-01-18 18:52:49 -10:00
J. Nick Koston
e99dbe05f7 [toshiba] Replace to_string with stack buffer in debug logging (#13296) 2026-01-18 18:52:34 -10:00
J. Nick Koston
f453a8d9a1 [dfrobot_sen0395] Reduce heap allocations in command building (#13219) 2026-01-18 18:44:56 -10:00
J. Nick Koston
126190d26a [ezo] Replace str_sprintf with stack-based formatting (#13218)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-18 18:44:41 -10:00
J. Nick Koston
e40201a98d [cse7766] Use stack buffer for verbose debug logging (#13217) 2026-01-18 18:44:27 -10:00
J. Nick Koston
8142f5db44 [zephyr] Avoid heap allocation in preferences key formatting (#13215) 2026-01-18 18:43:50 -10:00
J. Nick Koston
98ccab87a7 [tormatic] Use stack buffers instead of str_sprintf in debug methods (#13225) 2026-01-18 18:43:36 -10:00
J. Nick Koston
b9e72a8774 [daikin_arc] Fix undefined behavior in sprintf calls (#13279)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-18 18:43:19 -10:00
J. Nick Koston
d9fc625c6a [web_server] Simplify datetime formatting with buf_append_printf (#13281) 2026-01-18 18:43:05 -10:00
J. Nick Koston
dfbf79d6d6 [homeassistant] Use buf_append_printf for ESP8266 flash optimization (#13284) 2026-01-18 18:42:19 -10:00
J. Nick Koston
ea0fac96cb [core][mqtt] Add str_sanitize_to(), soft-deprecate str_sanitize() (#13233) 2026-01-18 18:42:04 -10:00
J. Nick Koston
3182222d60 [esp32_hosted] Use stack buffer instead of str_sprintf for version string (#13226) 2026-01-18 18:41:47 -10:00
J. Nick Koston
d8849b16f2 [gpio] Use buf_append_printf in dump_summary for ESP8266 flash optimization (#13283) 2026-01-18 18:41:34 -10:00
J. Nick Koston
635983f163 [uptime] Use buf_append_printf for ESP8266 flash optimization (#13282) 2026-01-18 18:41:19 -10:00
J. Nick Koston
6cbe672004 [tuya] Use buf_append_printf for ESP8266 flash optimization (#13287) 2026-01-18 18:41:07 -10:00
J. Nick Koston
226867b05c [esp8266] Use direct SDK calls instead of Arduino ESP class wrappers (#13353) 2026-01-18 18:40:53 -10:00
J. Nick Koston
67871a1683 [ccs811] Use buf_append_printf for buffer safety and ESP8266 flash optimization (#13300) 2026-01-18 18:40:14 -10:00
J. Nick Koston
f60c03e350 [syslog] Use buf_append_printf for ESP8266 flash optimization (#13286) 2026-01-18 18:39:53 -10:00
J. Nick Koston
eb66429144 [sml] Use stack buffers instead of str_sprintf (#13222)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-18 18:39:23 -10:00
J. Nick Koston
0f3bac5dd6 [nextion] Replace to_string with stack buffer and fix unsafe sprintf (#13295) 2026-01-18 18:37:29 -10:00
J. Nick Koston
5b92d0b89e [wiegand] Replace heap-allocating to_string with stack buffers (#13294) 2026-01-18 18:37:14 -10:00
J. Nick Koston
052b05df56 [tuya] Replace unsafe sprintf with snprintf in light color formatting (#13292) 2026-01-18 18:37:02 -10:00
J. Nick Koston
7b0db659d1 [rc522_spi] Replace unsafe sprintf with buf_append_printf (#13291) 2026-01-18 18:36:46 -10:00
J. Nick Koston
2f7270cf8f [uart] Replace unsafe sprintf with buf_append_printf in debugger (#13288) 2026-01-18 18:36:32 -10:00
J. Nick Koston
b44727aee6 [socket] Eliminate heap allocations in set_sockaddr() (#13228) 2026-01-18 18:29:31 -10:00
J. Nick Koston
1a55254258 [status] Convert to PollingComponent to reduce CPU usage (#13342) 2026-01-18 18:28:24 -10:00
J. Nick Koston
baf2b0e3c9 [api] Fix truncation of Home Assistant attributes longer than 255 characters (#13348) 2026-01-18 18:23:11 -10:00
J. Nick Koston
680e92a226 [core] Add str_endswith_ignore_case to avoid heap allocation in audio file type detection (#13313) 2026-01-18 08:36:56 -10:00
J. Nick Koston
db0b32bfc9 [network] Fix IPAddress::str_to() to lowercase IPv6 hex digits (#13325) 2026-01-17 18:06:54 -10:00
J. Nick Koston
21794e28e5 [modbus_controller] Use stack buffers instead of heap-allocating string helpers (#13221)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-01-17 17:26:51 -10:00
J. Nick Koston
728236270c [weikai] Replace bitset to_string with format_bin_to (#13297) 2026-01-17 15:53:01 -10:00
J. Nick Koston
01cdc4ed58 [core] Add fnv1_hash_extend() string overloads, use in atm90e32 (#13326) 2026-01-17 15:52:19 -10:00
J. Nick Koston
d6a0c8ffbb [template] Store alarm control panel codes in flash instead of heap (#13329) 2026-01-17 15:52:06 -10:00
J. Nick Koston
4cc0f874f7 [wireguard] Store configuration strings in flash instead of heap (#13331) 2026-01-17 15:51:26 -10:00
J. Nick Koston
ed58b9372f [template] Store text initial_value in flash and avoid heap allocation in setup (#13332) 2026-01-17 15:51:12 -10:00
J. Nick Koston
ee2a81923b [sun] Store text sensor format string in flash (#13335) 2026-01-17 15:51:01 -10:00
J. Nick Koston
0a1e7ee50b [pipsolar] Store command strings in flash (#13336) 2026-01-17 15:50:42 -10:00
J. Nick Koston
4d4283bcfa [udp] Store addresses in flash instead of heap (#13330) 2026-01-17 15:50:23 -10:00
J. Nick Koston
e4fb6988ff [web_server] Use ESPHOME_F for canHandle domain checks to reduce ESP8266 RAM (#13315)
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
2026-01-17 22:29:29 +00:00
J. Nick Koston
d31b733dce [light] Store color mode JSON strings in flash on ESP8266 (#13314) 2026-01-17 16:06:25 -06:00
Keith Burzinski
b25a2f8d8e [infrared][web_server] Implement initial web_server support (#13202)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-01-17 16:01:13 -06:00
J. Nick Koston
3f892711c7 [core][opentherm] Add format_bin_to(), soft-deprecate format_bin() (#13232) 2026-01-17 11:09:42 -10:00
Jonathan Swoboda
798d3bd956 Merge branch 'beta' into dev 2026-01-16 23:45:36 -05:00
Jonathan Swoboda
d830787c71 Merge branch 'release' into dev 2026-01-16 22:49:39 -05:00
Mike Ford
1f4221abfa [http_request] Unable to handle chunked responses (#7884)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-01-16 22:18:48 -05:00
Stuart Parmenter
92808a09c7 [hub75] Bump esp-hub75 version to 0.3.0 (#13243) 2026-01-16 22:17:36 -05:00
J. Nick Koston
e54d5ee898 [hmac_sha256] Replace unsafe sprintf with format_hex_to (#13290) 2026-01-16 22:16:38 -05:00
J. Nick Koston
bbe1155518 [web_server] Skip defer on ESP8266 where callbacks already run in main loop (#13261) 2026-01-16 20:08:04 -06:00
J. Nick Koston
69d7b6e921 [api] Use subtraction for protobuf bounds checking (#13306) 2026-01-16 15:46:15 -10:00
Keith Burzinski
510c874061 [helpers] Remove base85 functions (#13266) 2026-01-17 01:23:41 +00:00
Keith Burzinski
f7ad324d81 [infrared, remote_base] Replace base85 with base64url for web server infrared transmissions (#13265) 2026-01-16 18:15:27 -06:00
Keith Burzinski
58a9e30017 [helpers] Add base64_decode_int32_vector function (#13289)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-01-16 23:05:19 +00:00
J. Nick Koston
52ac9e1861 [remote_base] Replace unsafe sprintf with buf_append_printf; fix buffer overflow (#13257)
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-16 16:56:47 -06:00
Clyde Stubbs
c5e4a60884 [select] Add condition for testing select option (#13267)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2026-01-17 08:35:40 +11:00
dependabot[bot]
a680884138 Bump ruff from 0.14.12 to 0.14.13 (#13275)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-01-16 20:29:02 +00:00
Jonathan Swoboda
6832efbacc Add Claude Code PR workflow skill (#13271)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 10:24:28 -10:00
dependabot[bot]
3057a0484f Bump actions/cache from 5.0.1 to 5.0.2 in /.github/actions/restore-python (#13277)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-16 09:36:42 -10:00
dependabot[bot]
bc78f80f77 Bump actions/cache from 5.0.1 to 5.0.2 (#13276)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-16 09:36:29 -10:00
J. Nick Koston
916b028fb2 [mqtt] Replace sprintf with snprintf for friendly name hash (#13262) 2026-01-16 08:30:22 -10:00
mrtoy-me
16adae7359 [ntc, resistance] change log level to verbose (#13268) 2026-01-16 10:19:09 -05:00
Remco van Essen
4906f87751 [mipi_dsi] add JC8012P4A1 (#13241) 2026-01-16 21:17:32 +11:00
Keith Burzinski
5b37d2fb27 [helpers] Support base64url encoding (#13264) 2026-01-16 08:55:24 +00:00
J. Nick Koston
68affe0b9c [core] Add --device hint when DNS resolution fails (#13240) 2026-01-15 18:55:32 -10:00
J. Nick Koston
8263a8273f [debug] Add min_free heap sensor for ESP32 and LibreTiny, add fragmentation for ESP32 (#13231) 2026-01-15 18:08:26 -10:00
Keith Burzinski
14b7539094 [infrared, remote_base] Optimize IR transmit path for web_server base85 data (#13238) 2026-01-15 22:04:21 -06:00
J. Nick Koston
b37cb812a7 [core] Add buf_append_printf helper for safe buffer formatting (#13258) 2026-01-15 22:03:11 -06:00
J. Nick Koston
42491569c8 [analyze_memory] Add nRF52/Zephyr platform support for memory analysis (#13249) 2026-01-15 17:53:53 -10:00
J. Nick Koston
b1230ec6bb [esp32_ble_client] Reduce GATT data event logging to prevent firmware update failures (#13252) 2026-01-15 16:49:19 -10:00
J. Nick Koston
4eda9e965f [api] Fix clock conflicts when multiple clients connected to homeassistant time (#13253) 2026-01-15 16:49:01 -10:00
J. Nick Koston
d2528af649 [dallas_temp] Use const char* for set_timeout to fix deprecation warning and heap churn (#13250) 2026-01-15 16:48:44 -10:00
Keith Burzinski
2eabc1b96b [helpers] Add base85 support (#13254)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-01-16 02:22:05 +00:00
J. Nick Koston
535c3eb2a2 [sprinkler] Fix scheduler deprecation warnings and heap churn with FixedVector (#13251) 2026-01-15 11:32:02 -10:00
Jonathan Swoboda
20f937692e Merge branch 'beta' into dev 2026-01-15 16:24:19 -05:00
J. Nick Koston
00cc9e44b6 [analyze_memory] Fix ELF section mapping for RTL87xx and LN882X platforms (#13213) 2026-01-15 10:38:24 -10:00
dependabot[bot]
0427350101 Bump ruff from 0.14.11 to 0.14.12 (#13244)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-15 09:59:40 -10:00
J. Nick Koston
41dceb76ec [web_server][captive_portal] Change default compression from Brotli to gzip (#13246) 2026-01-15 19:56:35 +00:00
John Stenger
6380458d78 [qr_code] Allocate and free memory for QR code buffer (#13161)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-01-15 14:18:08 -05:00
Jonathan Swoboda
0dc5a7c9a4 [safe_mode] Detect bootloader rollback support at runtime (#13230)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 14:17:00 -05:00
J. Nick Koston
9003844eda [core] Fix ESP32-S2/S3 hardware SHA crash by aligning HashBase digest buffer (#13234)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-01-15 18:29:11 +00:00
J. Nick Koston
22a4ec69c2 [core] Fix platform subcomponents not filtering source files (#13208) 2026-01-15 07:38:44 -10:00
J. Nick Koston
9d42bfd161 [api] Fix state updates being sent to clients that did not subscribe (#13237) 2026-01-15 07:38:18 -10:00
J. Nick Koston
49c881d067 [core] Optimize and normalize entity state publishing logs with >> format (#13236) 2026-01-15 10:13:05 +00:00
J. Nick Koston
78aee4f498 [web_server] Remove unused button_state_json_generator (#13235) 2026-01-14 23:48:55 -06:00
Clyde Stubbs
9da2c08f36 [image] Correctly handle dimensions in physical units (#13209) 2026-01-15 03:27:26 +00:00
J. Nick Koston
03f3deff41 [lvgl] Use stack buffer for event code formatting, document justified str_sprintf usage (#13220)
Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
2026-01-15 01:24:42 +00:00
dependabot[bot]
f1e5d3a39a Bump resvg-py from 0.2.5 to 0.2.6 (#13211)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-14 10:40:26 -10:00
Jonathan Swoboda
2f6863230d Merge branch 'beta' into dev 2026-01-14 10:52:28 -05:00
Jonathan Swoboda
f44036310c Bump version to 2026.2.0-dev 2026-01-14 09:19:45 -05:00
296 changed files with 5112 additions and 1607 deletions

View File

@@ -1 +1 @@
d272a88e8ca28ae9340a9a03295a566432a52cb696501908f57764475bf7ca65 15dc295268b2dcf75942f42759b3ddec64eba89f75525698eb39c95a7f4b14ce

View File

@@ -0,0 +1,96 @@
---
name: pr-workflow
description: Create pull requests for esphome. Use when creating PRs, submitting changes, or preparing contributions.
allowed-tools: Read, Bash, Glob, Grep
---
# ESPHome PR Workflow
When creating a pull request for esphome, follow these steps:
## 1. Create Branch from Upstream
Always base your branch on **upstream** (not origin/fork) to ensure you have the latest code:
```bash
git fetch upstream
git checkout -b <branch-name> upstream/dev
```
## 2. Read the PR Template
Before creating a PR, read `.github/PULL_REQUEST_TEMPLATE.md` to understand required fields.
## 3. Create the PR
Use `gh pr create` with the **full template** filled in. Never skip or abbreviate sections.
Required fields:
- **What does this implement/fix?**: Brief description of changes
- **Types of changes**: Check ONE appropriate box (Bugfix, New feature, Breaking change, etc.)
- **Related issue**: Use `fixes <link>` syntax if applicable
- **Pull request in esphome-docs**: Link if docs are needed
- **Test Environment**: Check platforms you tested on
- **Example config.yaml**: Include working example YAML
- **Checklist**: Verify code is tested and tests added
## 4. Example PR Body
```markdown
# What does this implement/fix?
<describe your changes here>
## Types of changes
- [ ] Bugfix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Developer breaking change (an API change that could break external components)
- [ ] Code quality improvements to existing code or addition of tests
- [ ] Other
**Related issue or feature (if applicable):**
- fixes https://github.com/esphome/esphome/issues/XXX
**Pull request in [esphome-docs](https://github.com/esphome/esphome-docs) with documentation (if applicable):**
- esphome/esphome-docs#XXX
## Test Environment
- [x] ESP32
- [x] ESP32 IDF
- [ ] ESP8266
- [ ] RP2040
- [ ] BK72xx
- [ ] RTL87xx
- [ ] LN882x
- [ ] nRF52840
## Example entry for `config.yaml`:
```yaml
# Example config.yaml
component_name:
id: my_component
option: value
```
## Checklist:
- [x] The code change is tested and works locally.
- [x] Tests have been added to verify that the new code works (under `tests/` folder).
If user exposed functionality or configuration variables are added/changed:
- [ ] Documentation added/updated in [esphome-docs](https://github.com/esphome/esphome-docs).
```
## 5. Push and Create PR
```bash
git push -u origin <branch-name>
gh pr create --repo esphome/esphome --base dev --title "[component] Brief description"
```
Title should be prefixed with the component name in brackets, e.g. `[safe_mode] Add feature`.

View File

@@ -22,7 +22,7 @@ runs:
python-version: ${{ inputs.python-version }} python-version: ${{ inputs.python-version }}
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with: with:
path: venv path: venv
# yamllint disable-line rule:line-length # yamllint disable-line rule:line-length

View File

@@ -47,7 +47,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with: with:
path: venv path: venv
# yamllint disable-line rule:line-length # yamllint disable-line rule:line-length
@@ -157,7 +157,7 @@ jobs:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
- name: Save Python virtual environment cache - name: Save Python virtual environment cache
if: github.ref == 'refs/heads/dev' if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache/save@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with: with:
path: venv path: venv
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }} key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
@@ -193,7 +193,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }} cache-key: ${{ needs.common.outputs.cache-key }}
- name: Restore components graph cache - name: Restore components graph cache
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with: with:
path: .temp/components_graph.json path: .temp/components_graph.json
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }} key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
@@ -223,7 +223,7 @@ jobs:
echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT
- name: Save components graph cache - name: Save components graph cache
if: github.ref == 'refs/heads/dev' if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache/save@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with: with:
path: .temp/components_graph.json path: .temp/components_graph.json
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }} key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
@@ -245,7 +245,7 @@ jobs:
python-version: "3.13" python-version: "3.13"
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with: with:
path: venv path: venv
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }} key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
@@ -334,14 +334,14 @@ jobs:
- name: Cache platformio - name: Cache platformio
if: github.ref == 'refs/heads/dev' if: github.ref == 'refs/heads/dev'
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }} key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
- name: Cache platformio - name: Cache platformio
if: github.ref != 'refs/heads/dev' if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }} key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
@@ -413,14 +413,14 @@ jobs:
- name: Cache platformio - name: Cache platformio
if: github.ref == 'refs/heads/dev' if: github.ref == 'refs/heads/dev'
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
- name: Cache platformio - name: Cache platformio
if: github.ref != 'refs/heads/dev' if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
@@ -502,14 +502,14 @@ jobs:
- name: Cache platformio - name: Cache platformio
if: github.ref == 'refs/heads/dev' if: github.ref == 'refs/heads/dev'
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
- name: Cache platformio - name: Cache platformio
if: github.ref != 'refs/heads/dev' if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
@@ -735,7 +735,7 @@ jobs:
- name: Restore cached memory analysis - name: Restore cached memory analysis
id: cache-memory-analysis id: cache-memory-analysis
if: steps.check-script.outputs.skip != 'true' if: steps.check-script.outputs.skip != 'true'
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with: with:
path: memory-analysis-target.json path: memory-analysis-target.json
key: ${{ steps.cache-key.outputs.cache-key }} key: ${{ steps.cache-key.outputs.cache-key }}
@@ -759,7 +759,7 @@ jobs:
- name: Cache platformio - name: Cache platformio
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }} key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
@@ -800,7 +800,7 @@ jobs:
- name: Save memory analysis to cache - name: Save memory analysis to cache
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success' if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success'
uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache/save@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with: with:
path: memory-analysis-target.json path: memory-analysis-target.json
key: ${{ steps.cache-key.outputs.cache-key }} key: ${{ steps.cache-key.outputs.cache-key }}
@@ -847,7 +847,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }} cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache platformio - name: Cache platformio
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }} key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}

View File

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

View File

@@ -11,7 +11,7 @@ ci:
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: v0.14.11 rev: v0.14.13
hooks: hooks:
# Run the linter. # Run the linter.
- id: ruff - id: ruff

View File

@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version # could be handy for archiving the generated documentation or if some version
# control system is used. # control system is used.
PROJECT_NUMBER = 2026.1.0 PROJECT_NUMBER = 2026.2.0-dev
# Using the PROJECT_BRIEF tag one can provide an optional one line description # Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a # for a project that appears at the top of each page and should give viewer a

View File

@@ -43,6 +43,7 @@ from esphome.const import (
CONF_SUBSTITUTIONS, CONF_SUBSTITUTIONS,
CONF_TOPIC, CONF_TOPIC,
ENV_NOGITIGNORE, ENV_NOGITIGNORE,
KEY_NATIVE_IDF,
PLATFORM_ESP32, PLATFORM_ESP32,
PLATFORM_ESP8266, PLATFORM_ESP8266,
PLATFORM_RP2040, PLATFORM_RP2040,
@@ -116,6 +117,7 @@ class ArgsProtocol(Protocol):
configuration: str configuration: str
name: str name: str
upload_speed: str | None upload_speed: str | None
native_idf: bool
def choose_prompt(options, purpose: str = None): def choose_prompt(options, purpose: str = None):
@@ -223,8 +225,13 @@ def choose_upload_log_host(
else: else:
resolved.append(device) resolved.append(device)
if not resolved: if not resolved:
if CORE.dashboard:
hint = "If you know the IP, set 'use_address' in your network config."
else:
hint = "If you know the IP, try --device <IP>"
raise EsphomeError( raise EsphomeError(
f"All specified devices {defaults} could not be resolved. Is the device connected to the network?" f"All specified devices {defaults} could not be resolved. "
f"Is the device connected to the network? {hint}"
) )
return resolved return resolved
@@ -495,12 +502,15 @@ def wrap_to_code(name, comp):
return wrapped 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): if not get_bool_env(ENV_NOGITIGNORE):
writer.write_gitignore() writer.write_gitignore()
# Store native_idf flag so esp32 component can check it
CORE.data[KEY_NATIVE_IDF] = native_idf
generate_cpp_contents(config) generate_cpp_contents(config)
return write_cpp_file() return write_cpp_file(native_idf=native_idf)
def generate_cpp_contents(config: ConfigType) -> None: def generate_cpp_contents(config: ConfigType) -> None:
@@ -514,32 +524,54 @@ def generate_cpp_contents(config: ConfigType) -> None:
CORE.flush_tasks() CORE.flush_tasks()
def write_cpp_file() -> int: def write_cpp_file(native_idf: bool = False) -> int:
code_s = indent(CORE.cpp_main_section) code_s = indent(CORE.cpp_main_section)
writer.write_cpp(code_s) 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 return 0
def compile_program(args: ArgsProtocol, config: ConfigType) -> int: 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 # 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 # If you change this format, update the regex in that script as well
_LOGGER.info("Compiling app... Build path: %s", CORE.build_path) _LOGGER.info("Compiling app... Build path: %s", CORE.build_path)
rc = platformio_api.run_compile(config, CORE.verbose)
if rc != 0: if native_idf and CORE.is_esp32 and CORE.target_framework == "esp-idf":
return rc 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 if firmware was rebuilt and emit build_info + create manifest
_check_and_emit_build_info() _check_and_emit_build_info()
idedata = platformio_api.get_idedata(config) return 0
return 0 if idedata is not None else 1
def _check_and_emit_build_info() -> None: def _check_and_emit_build_info() -> None:
@@ -796,7 +828,8 @@ def command_vscode(args: ArgsProtocol) -> int | None:
def command_compile(args: ArgsProtocol, config: ConfigType) -> 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: if exit_code != 0:
return exit_code return exit_code
if args.only_generate: if args.only_generate:
@@ -851,7 +884,8 @@ def command_logs(args: ArgsProtocol, config: ConfigType) -> int | None:
def command_run(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: if exit_code != 0:
return exit_code return exit_code
exit_code = compile_program(args, config) exit_code = compile_program(args, config)
@@ -1305,6 +1339,11 @@ def parse_args(argv):
help="Only generate source code, do not compile.", help="Only generate source code, do not compile.",
action="store_true", 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( parser_upload = subparsers.add_parser(
"upload", "upload",
@@ -1386,6 +1425,11 @@ def parse_args(argv):
help="Reset the device before starting serial logs.", help="Reset the device before starting serial logs.",
default=os.getenv("ESPHOME_SERIAL_LOGGING_RESET"), 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( parser_clean = subparsers.add_parser(
"clean-mqtt", "clean-mqtt",

View File

@@ -22,7 +22,7 @@ from .helpers import (
map_section_name, map_section_name,
parse_symbol_line, parse_symbol_line,
) )
from .toolchain import find_tool, run_tool from .toolchain import find_tool, resolve_tool_path, run_tool
if TYPE_CHECKING: if TYPE_CHECKING:
from esphome.platformio_api import IDEData from esphome.platformio_api import IDEData
@@ -132,6 +132,12 @@ class MemoryAnalyzer:
readelf_path = readelf_path or idedata.readelf_path readelf_path = readelf_path or idedata.readelf_path
_LOGGER.debug("Using toolchain paths from PlatformIO idedata") _LOGGER.debug("Using toolchain paths from PlatformIO idedata")
# Validate paths exist, fall back to find_tool if they don't
# This handles cases like Zephyr where cc_path doesn't include full path
# and the toolchain prefix may differ (e.g., arm-zephyr-eabi- vs arm-none-eabi-)
objdump_path = resolve_tool_path("objdump", objdump_path, objdump_path)
readelf_path = resolve_tool_path("readelf", readelf_path, objdump_path)
self.objdump_path = objdump_path or "objdump" self.objdump_path = objdump_path or "objdump"
self.readelf_path = readelf_path or "readelf" self.readelf_path = readelf_path or "readelf"
self.external_components = external_components or set() self.external_components = external_components or set()

View File

@@ -9,11 +9,61 @@ ESPHOME_COMPONENT_PATTERN = re.compile(r"esphome::([a-zA-Z0-9_]+)::")
# Maps standard section names to their various platform-specific variants # Maps standard section names to their various platform-specific variants
# Note: Order matters! More specific patterns (.bss) must come before general ones (.dram) # Note: Order matters! More specific patterns (.bss) must come before general ones (.dram)
# because ESP-IDF uses names like ".dram0.bss" which would match ".dram" otherwise # because ESP-IDF uses names like ".dram0.bss" which would match ".dram" otherwise
#
# Platform-specific sections:
# - ESP8266/ESP32: .iram*, .dram*
# - LibreTiny RTL87xx: .xip.code_* (flash), .ram.code_* (RAM)
# - LibreTiny BK7231: .itcm.code (fast RAM), .vectors (interrupt vectors)
# - LibreTiny LN882X: .flash_text, .flash_copy* (flash code)
# - Zephyr/nRF52: text, rodata, datas, bss (no leading dots)
SECTION_MAPPING = { SECTION_MAPPING = {
".text": frozenset([".text", ".iram"]), ".text": frozenset(
".rodata": frozenset([".rodata"]), [
".bss": frozenset([".bss"]), # Must be before .data to catch ".dram0.bss" ".text",
".data": frozenset([".data", ".dram"]), ".iram",
# LibreTiny RTL87xx XIP (eXecute In Place) flash code
".xip.code",
# LibreTiny RTL87xx RAM code
".ram.code_text",
# LibreTiny BK7231 fast RAM code and vectors
".itcm.code",
".vectors",
# LibreTiny LN882X flash code
".flash_text",
".flash_copy",
# Zephyr/nRF52 sections (no leading dots)
"text",
"rom_start",
]
),
".rodata": frozenset(
[
".rodata",
# LibreTiny RTL87xx read-only data in RAM
".ram.code_rodata",
# Zephyr/nRF52 sections (no leading dots)
"rodata",
]
),
# .bss patterns - must be before .data to catch ".dram0.bss"
".bss": frozenset(
[
".bss",
# LibreTiny LN882X BSS
".bss_ram",
# Zephyr/nRF52 sections (no leading dots)
"bss",
"noinit",
]
),
".data": frozenset(
[
".data",
".dram",
# Zephyr/nRF52 sections (no leading dots)
"datas",
]
),
} }
# Section to ComponentMemory attribute mapping # Section to ComponentMemory attribute mapping

View File

@@ -94,13 +94,13 @@ def parse_symbol_line(line: str) -> tuple[str, str, int, str] | None:
return None return None
# Find section, size, and name # Find section, size, and name
# Try each part as a potential section name
for i, part in enumerate(parts): for i, part in enumerate(parts):
if not part.startswith("."): # Skip parts that are clearly flags, addresses, or other metadata
continue # Sections start with '.' (standard ELF) or are known section names (Zephyr)
section = map_section_name(part) section = map_section_name(part)
if not section: if not section:
break continue
# Need at least size field after section # Need at least size field after section
if i + 1 >= len(parts): if i + 1 >= len(parts):

View File

@@ -3,6 +3,7 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
import os
from pathlib import Path from pathlib import Path
import subprocess import subprocess
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@@ -17,10 +18,82 @@ TOOLCHAIN_PREFIXES = [
"xtensa-lx106-elf-", # ESP8266 "xtensa-lx106-elf-", # ESP8266
"xtensa-esp32-elf-", # ESP32 "xtensa-esp32-elf-", # ESP32
"xtensa-esp-elf-", # ESP32 (newer IDF) "xtensa-esp-elf-", # ESP32 (newer IDF)
"arm-zephyr-eabi-", # nRF52/Zephyr SDK
"arm-none-eabi-", # Generic ARM (RP2040, etc.)
"", # System default (no prefix) "", # System default (no prefix)
] ]
def _find_in_platformio_packages(tool_name: str) -> str | None:
"""Search for a tool in PlatformIO package directories.
This handles cases like Zephyr SDK where tools are installed in nested
directories that aren't in PATH.
Args:
tool_name: Name of the tool (e.g., "readelf", "objdump")
Returns:
Full path to the tool or None if not found
"""
# Get PlatformIO packages directory
platformio_home = Path(os.path.expanduser("~/.platformio/packages"))
if not platformio_home.exists():
return None
# Search patterns for toolchains that might contain the tool
# Order matters - more specific patterns first
search_patterns = [
# Zephyr SDK deeply nested structure (4 levels)
# e.g., toolchain-gccarmnoneeabi/zephyr-sdk-0.17.4/arm-zephyr-eabi/bin/arm-zephyr-eabi-objdump
f"toolchain-*/*/*/bin/*-{tool_name}",
# Zephyr SDK nested structure (3 levels)
f"toolchain-*/*/bin/*-{tool_name}",
f"toolchain-*/bin/*-{tool_name}",
# Standard PlatformIO toolchain structure
f"toolchain-*/bin/*{tool_name}",
]
for pattern in search_patterns:
matches = list(platformio_home.glob(pattern))
if matches:
# Sort to get consistent results, prefer arm-zephyr-eabi over arm-none-eabi
matches.sort(key=lambda p: ("zephyr" not in str(p), str(p)))
tool_path = str(matches[0])
_LOGGER.debug("Found %s in PlatformIO packages: %s", tool_name, tool_path)
return tool_path
return None
def resolve_tool_path(
tool_name: str,
derived_path: str | None,
objdump_path: str | None = None,
) -> str | None:
"""Resolve a tool path, falling back to find_tool if derived path doesn't exist.
Args:
tool_name: Name of the tool (e.g., "objdump", "readelf")
derived_path: Path derived from idedata (may not exist for some platforms)
objdump_path: Path to objdump binary to derive other tool paths from
Returns:
Resolved path to the tool, or the original derived_path if it exists
"""
if derived_path and not Path(derived_path).exists():
found = find_tool(tool_name, objdump_path)
if found:
_LOGGER.debug(
"Derived %s path %s not found, using %s",
tool_name,
derived_path,
found,
)
return found
return derived_path
def find_tool( def find_tool(
tool_name: str, tool_name: str,
objdump_path: str | None = None, objdump_path: str | None = None,
@@ -28,7 +101,8 @@ def find_tool(
"""Find a toolchain tool by name. """Find a toolchain tool by name.
First tries to derive the tool path from objdump_path (if provided), First tries to derive the tool path from objdump_path (if provided),
then falls back to searching for platform-specific tools. then searches PlatformIO package directories (for cross-compile toolchains),
and finally falls back to searching for platform-specific tools in PATH.
Args: Args:
tool_name: Name of the tool (e.g., "objdump", "nm", "c++filt") tool_name: Name of the tool (e.g., "objdump", "nm", "c++filt")
@@ -47,7 +121,13 @@ def find_tool(
_LOGGER.debug("Found %s at: %s", tool_name, potential_path) _LOGGER.debug("Found %s at: %s", tool_name, potential_path)
return potential_path return potential_path
# Try platform-specific tools # Search in PlatformIO packages directory first (handles Zephyr SDK, etc.)
# This must come before PATH search because system tools (e.g., /usr/bin/objdump)
# are for the host architecture, not the target (ARM, Xtensa, etc.)
if found := _find_in_platformio_packages(tool_name):
return found
# Try platform-specific tools in PATH (fallback for when tools are installed globally)
for prefix in TOOLCHAIN_PREFIXES: for prefix in TOOLCHAIN_PREFIXES:
cmd = f"{prefix}{tool_name}" cmd = f"{prefix}{tool_name}"
try: try:

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

@@ -69,6 +69,7 @@ from esphome.cpp_types import ( # noqa: F401
JsonObjectConst, JsonObjectConst,
Parented, Parented,
PollingComponent, PollingComponent,
StringRef,
arduino_json_ns, arduino_json_ns,
bool_, bool_,
const_char_ptr, const_char_ptr,

View File

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

View File

@@ -67,52 +67,29 @@ void AlarmControlPanel::add_on_ready_callback(std::function<void()> &&callback)
this->ready_callback_.add(std::move(callback)); this->ready_callback_.add(std::move(callback));
} }
void AlarmControlPanel::arm_away(optional<std::string> code) { void AlarmControlPanel::arm_with_code_(AlarmControlPanelCall &(AlarmControlPanelCall::*arm_method)(),
const char *code) {
auto call = this->make_call(); auto call = this->make_call();
call.arm_away(); (call.*arm_method)();
if (code.has_value()) if (code != nullptr)
call.set_code(code.value()); call.set_code(code);
call.perform(); call.perform();
} }
void AlarmControlPanel::arm_home(optional<std::string> code) { void AlarmControlPanel::arm_away(const char *code) { this->arm_with_code_(&AlarmControlPanelCall::arm_away, code); }
auto call = this->make_call();
call.arm_home(); void AlarmControlPanel::arm_home(const char *code) { this->arm_with_code_(&AlarmControlPanelCall::arm_home, code); }
if (code.has_value())
call.set_code(code.value()); void AlarmControlPanel::arm_night(const char *code) { this->arm_with_code_(&AlarmControlPanelCall::arm_night, code); }
call.perform();
void AlarmControlPanel::arm_vacation(const char *code) {
this->arm_with_code_(&AlarmControlPanelCall::arm_vacation, code);
} }
void AlarmControlPanel::arm_night(optional<std::string> code) { void AlarmControlPanel::arm_custom_bypass(const char *code) {
auto call = this->make_call(); this->arm_with_code_(&AlarmControlPanelCall::arm_custom_bypass, code);
call.arm_night();
if (code.has_value())
call.set_code(code.value());
call.perform();
} }
void AlarmControlPanel::arm_vacation(optional<std::string> code) { void AlarmControlPanel::disarm(const char *code) { this->arm_with_code_(&AlarmControlPanelCall::disarm, code); }
auto call = this->make_call();
call.arm_vacation();
if (code.has_value())
call.set_code(code.value());
call.perform();
}
void AlarmControlPanel::arm_custom_bypass(optional<std::string> code) {
auto call = this->make_call();
call.arm_custom_bypass();
if (code.has_value())
call.set_code(code.value());
call.perform();
}
void AlarmControlPanel::disarm(optional<std::string> code) {
auto call = this->make_call();
call.disarm();
if (code.has_value())
call.set_code(code.value());
call.perform();
}
} // namespace esphome::alarm_control_panel } // namespace esphome::alarm_control_panel

View File

@@ -76,37 +76,53 @@ class AlarmControlPanel : public EntityBase {
* *
* @param code The code * @param code The code
*/ */
void arm_away(optional<std::string> code = nullopt); void arm_away(const char *code = nullptr);
void arm_away(const optional<std::string> &code) {
this->arm_away(code.has_value() ? code.value().c_str() : nullptr);
}
/** arm the alarm in home mode /** arm the alarm in home mode
* *
* @param code The code * @param code The code
*/ */
void arm_home(optional<std::string> code = nullopt); void arm_home(const char *code = nullptr);
void arm_home(const optional<std::string> &code) {
this->arm_home(code.has_value() ? code.value().c_str() : nullptr);
}
/** arm the alarm in night mode /** arm the alarm in night mode
* *
* @param code The code * @param code The code
*/ */
void arm_night(optional<std::string> code = nullopt); void arm_night(const char *code = nullptr);
void arm_night(const optional<std::string> &code) {
this->arm_night(code.has_value() ? code.value().c_str() : nullptr);
}
/** arm the alarm in vacation mode /** arm the alarm in vacation mode
* *
* @param code The code * @param code The code
*/ */
void arm_vacation(optional<std::string> code = nullopt); void arm_vacation(const char *code = nullptr);
void arm_vacation(const optional<std::string> &code) {
this->arm_vacation(code.has_value() ? code.value().c_str() : nullptr);
}
/** arm the alarm in custom bypass mode /** arm the alarm in custom bypass mode
* *
* @param code The code * @param code The code
*/ */
void arm_custom_bypass(optional<std::string> code = nullopt); void arm_custom_bypass(const char *code = nullptr);
void arm_custom_bypass(const optional<std::string> &code) {
this->arm_custom_bypass(code.has_value() ? code.value().c_str() : nullptr);
}
/** disarm the alarm /** disarm the alarm
* *
* @param code The code * @param code The code
*/ */
void disarm(optional<std::string> code = nullopt); void disarm(const char *code = nullptr);
void disarm(const optional<std::string> &code) { this->disarm(code.has_value() ? code.value().c_str() : nullptr); }
/** Get the state /** Get the state
* *
@@ -118,6 +134,8 @@ class AlarmControlPanel : public EntityBase {
protected: protected:
friend AlarmControlPanelCall; friend AlarmControlPanelCall;
// Helper to reduce code duplication for arm/disarm methods
void arm_with_code_(AlarmControlPanelCall &(AlarmControlPanelCall::*arm_method)(), const char *code);
// in order to store last panel state in flash // in order to store last panel state in flash
ESPPreferenceObject pref_; ESPPreferenceObject pref_;
// current state // current state

View File

@@ -10,8 +10,10 @@ static const char *const TAG = "alarm_control_panel";
AlarmControlPanelCall::AlarmControlPanelCall(AlarmControlPanel *parent) : parent_(parent) {} AlarmControlPanelCall::AlarmControlPanelCall(AlarmControlPanel *parent) : parent_(parent) {}
AlarmControlPanelCall &AlarmControlPanelCall::set_code(const std::string &code) { AlarmControlPanelCall &AlarmControlPanelCall::set_code(const char *code) {
this->code_ = code; if (code != nullptr) {
this->code_ = std::string(code);
}
return *this; return *this;
} }

View File

@@ -14,7 +14,8 @@ class AlarmControlPanelCall {
public: public:
AlarmControlPanelCall(AlarmControlPanel *parent); AlarmControlPanelCall(AlarmControlPanel *parent);
AlarmControlPanelCall &set_code(const std::string &code); AlarmControlPanelCall &set_code(const char *code);
AlarmControlPanelCall &set_code(const std::string &code) { return this->set_code(code.c_str()); }
AlarmControlPanelCall &arm_away(); AlarmControlPanelCall &arm_away();
AlarmControlPanelCall &arm_home(); AlarmControlPanelCall &arm_home();
AlarmControlPanelCall &arm_night(); AlarmControlPanelCall &arm_night();

View File

@@ -66,15 +66,7 @@ template<typename... Ts> class ArmAwayAction : public Action<Ts...> {
TEMPLATABLE_VALUE(std::string, code) TEMPLATABLE_VALUE(std::string, code)
void play(const Ts &...x) override { void play(const Ts &...x) override { this->alarm_control_panel_->arm_away(this->code_.optional_value(x...)); }
auto call = this->alarm_control_panel_->make_call();
auto code = this->code_.optional_value(x...);
if (code.has_value()) {
call.set_code(code.value());
}
call.arm_away();
call.perform();
}
protected: protected:
AlarmControlPanel *alarm_control_panel_; AlarmControlPanel *alarm_control_panel_;
@@ -86,15 +78,7 @@ template<typename... Ts> class ArmHomeAction : public Action<Ts...> {
TEMPLATABLE_VALUE(std::string, code) TEMPLATABLE_VALUE(std::string, code)
void play(const Ts &...x) override { void play(const Ts &...x) override { this->alarm_control_panel_->arm_home(this->code_.optional_value(x...)); }
auto call = this->alarm_control_panel_->make_call();
auto code = this->code_.optional_value(x...);
if (code.has_value()) {
call.set_code(code.value());
}
call.arm_home();
call.perform();
}
protected: protected:
AlarmControlPanel *alarm_control_panel_; AlarmControlPanel *alarm_control_panel_;
@@ -106,15 +90,7 @@ template<typename... Ts> class ArmNightAction : public Action<Ts...> {
TEMPLATABLE_VALUE(std::string, code) TEMPLATABLE_VALUE(std::string, code)
void play(const Ts &...x) override { void play(const Ts &...x) override { this->alarm_control_panel_->arm_night(this->code_.optional_value(x...)); }
auto call = this->alarm_control_panel_->make_call();
auto code = this->code_.optional_value(x...);
if (code.has_value()) {
call.set_code(code.value());
}
call.arm_night();
call.perform();
}
protected: protected:
AlarmControlPanel *alarm_control_panel_; AlarmControlPanel *alarm_control_panel_;

View File

@@ -1,21 +1,12 @@
#include "am43_base.h" #include "am43_base.h"
#include "esphome/core/helpers.h"
#include <cstring> #include <cstring>
#include <cstdio>
namespace esphome { namespace esphome {
namespace am43 { namespace am43 {
const uint8_t START_PACKET[5] = {0x00, 0xff, 0x00, 0x00, 0x9a}; const uint8_t START_PACKET[5] = {0x00, 0xff, 0x00, 0x00, 0x9a};
std::string pkt_to_hex(const uint8_t *data, uint16_t len) {
char buf[64];
memset(buf, 0, 64);
for (int i = 0; i < len; i++)
sprintf(&buf[i * 2], "%02x", data[i]);
std::string ret = buf;
return ret;
}
Am43Packet *Am43Encoder::get_battery_level_request() { Am43Packet *Am43Encoder::get_battery_level_request() {
uint8_t data = 0x1; uint8_t data = 0x1;
return this->encode_(0xA2, &data, 1); return this->encode_(0xA2, &data, 1);
@@ -73,7 +64,9 @@ Am43Packet *Am43Encoder::encode_(uint8_t command, uint8_t *data, uint8_t length)
memcpy(&this->packet_.data[7], data, length); memcpy(&this->packet_.data[7], data, length);
this->packet_.length = length + 7; this->packet_.length = length + 7;
this->checksum_(); this->checksum_();
ESP_LOGV("am43", "ENC(%d): 0x%s", packet_.length, pkt_to_hex(packet_.data, packet_.length).c_str()); char hex_buf[format_hex_size(sizeof(this->packet_.data))];
ESP_LOGV("am43", "ENC(%d): 0x%s", this->packet_.length,
format_hex_to(hex_buf, this->packet_.data, this->packet_.length));
return &this->packet_; return &this->packet_;
} }
@@ -88,7 +81,8 @@ void Am43Decoder::decode(const uint8_t *data, uint16_t length) {
this->has_set_state_response_ = false; this->has_set_state_response_ = false;
this->has_position_ = false; this->has_position_ = false;
this->has_pin_response_ = false; this->has_pin_response_ = false;
ESP_LOGV("am43", "DEC(%d): 0x%s", length, pkt_to_hex(data, length).c_str()); char hex_buf[format_hex_size(24)]; // Max expected packet size
ESP_LOGV("am43", "DEC(%d): 0x%s", length, format_hex_to(hex_buf, data, length));
if (length < 2 || data[0] != 0x9a) if (length < 2 || data[0] != 0x9a)
return; return;

View File

@@ -18,31 +18,31 @@ AnovaPacket *AnovaCodec::clean_packet_() {
AnovaPacket *AnovaCodec::get_read_device_status_request() { AnovaPacket *AnovaCodec::get_read_device_status_request() {
this->current_query_ = READ_DEVICE_STATUS; this->current_query_ = READ_DEVICE_STATUS;
sprintf((char *) this->packet_.data, "%s", CMD_READ_DEVICE_STATUS); snprintf((char *) this->packet_.data, sizeof(this->packet_.data), "%s", CMD_READ_DEVICE_STATUS);
return this->clean_packet_(); return this->clean_packet_();
} }
AnovaPacket *AnovaCodec::get_read_target_temp_request() { AnovaPacket *AnovaCodec::get_read_target_temp_request() {
this->current_query_ = READ_TARGET_TEMPERATURE; this->current_query_ = READ_TARGET_TEMPERATURE;
sprintf((char *) this->packet_.data, "%s", CMD_READ_TARGET_TEMP); snprintf((char *) this->packet_.data, sizeof(this->packet_.data), "%s", CMD_READ_TARGET_TEMP);
return this->clean_packet_(); return this->clean_packet_();
} }
AnovaPacket *AnovaCodec::get_read_current_temp_request() { AnovaPacket *AnovaCodec::get_read_current_temp_request() {
this->current_query_ = READ_CURRENT_TEMPERATURE; this->current_query_ = READ_CURRENT_TEMPERATURE;
sprintf((char *) this->packet_.data, "%s", CMD_READ_CURRENT_TEMP); snprintf((char *) this->packet_.data, sizeof(this->packet_.data), "%s", CMD_READ_CURRENT_TEMP);
return this->clean_packet_(); return this->clean_packet_();
} }
AnovaPacket *AnovaCodec::get_read_unit_request() { AnovaPacket *AnovaCodec::get_read_unit_request() {
this->current_query_ = READ_UNIT; this->current_query_ = READ_UNIT;
sprintf((char *) this->packet_.data, "%s", CMD_READ_UNIT); snprintf((char *) this->packet_.data, sizeof(this->packet_.data), "%s", CMD_READ_UNIT);
return this->clean_packet_(); return this->clean_packet_();
} }
AnovaPacket *AnovaCodec::get_read_data_request() { AnovaPacket *AnovaCodec::get_read_data_request() {
this->current_query_ = READ_DATA; this->current_query_ = READ_DATA;
sprintf((char *) this->packet_.data, "%s", CMD_READ_DATA); snprintf((char *) this->packet_.data, sizeof(this->packet_.data), "%s", CMD_READ_DATA);
return this->clean_packet_(); return this->clean_packet_();
} }
@@ -50,25 +50,25 @@ AnovaPacket *AnovaCodec::get_set_target_temp_request(float temperature) {
this->current_query_ = SET_TARGET_TEMPERATURE; this->current_query_ = SET_TARGET_TEMPERATURE;
if (this->fahrenheit_) if (this->fahrenheit_)
temperature = ctof(temperature); temperature = ctof(temperature);
sprintf((char *) this->packet_.data, CMD_SET_TARGET_TEMP, temperature); snprintf((char *) this->packet_.data, sizeof(this->packet_.data), CMD_SET_TARGET_TEMP, temperature);
return this->clean_packet_(); return this->clean_packet_();
} }
AnovaPacket *AnovaCodec::get_set_unit_request(char unit) { AnovaPacket *AnovaCodec::get_set_unit_request(char unit) {
this->current_query_ = SET_UNIT; this->current_query_ = SET_UNIT;
sprintf((char *) this->packet_.data, CMD_SET_TEMP_UNIT, unit); snprintf((char *) this->packet_.data, sizeof(this->packet_.data), CMD_SET_TEMP_UNIT, unit);
return this->clean_packet_(); return this->clean_packet_();
} }
AnovaPacket *AnovaCodec::get_start_request() { AnovaPacket *AnovaCodec::get_start_request() {
this->current_query_ = START; this->current_query_ = START;
sprintf((char *) this->packet_.data, CMD_START); snprintf((char *) this->packet_.data, sizeof(this->packet_.data), "%s", CMD_START);
return this->clean_packet_(); return this->clean_packet_();
} }
AnovaPacket *AnovaCodec::get_stop_request() { AnovaPacket *AnovaCodec::get_stop_request() {
this->current_query_ = STOP; this->current_query_ = STOP;
sprintf((char *) this->packet_.data, CMD_STOP); snprintf((char *) this->packet_.data, sizeof(this->packet_.data), "%s", CMD_STOP);
return this->clean_packet_(); return this->clean_packet_();
} }

View File

@@ -1715,7 +1715,7 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes
// HA state max length is 255 characters, but attributes can be much longer // HA state max length is 255 characters, but attributes can be much longer
// Use stack buffer for common case (states), heap fallback for large attributes // Use stack buffer for common case (states), heap fallback for large attributes
size_t state_len = msg.state.size(); size_t state_len = msg.state.size();
SmallBufferWithHeapFallback<256> state_buf_alloc(state_len + 1); SmallBufferWithHeapFallback<MAX_STATE_LEN + 1> state_buf_alloc(state_len + 1);
char *state_buf = reinterpret_cast<char *>(state_buf_alloc.get()); char *state_buf = reinterpret_cast<char *>(state_buf_alloc.get());
if (state_len > 0) { if (state_len > 0) {
memcpy(state_buf, msg.state.c_str(), state_len); memcpy(state_buf, msg.state.c_str(), state_len);

View File

@@ -3,6 +3,7 @@
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
#include "api_connection.h" // For ClientInfo struct #include "api_connection.h" // For ClientInfo struct
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
@@ -256,28 +257,30 @@ APIError APINoiseFrameHelper::state_action_() {
} }
if (state_ == State::SERVER_HELLO) { if (state_ == State::SERVER_HELLO) {
// send server hello // send server hello
constexpr size_t mac_len = 13; // 12 hex chars + null terminator
const std::string &name = App.get_name(); const std::string &name = App.get_name();
char mac[mac_len]; char mac[MAC_ADDRESS_BUFFER_SIZE];
get_mac_address_into_buffer(mac); get_mac_address_into_buffer(mac);
// Calculate positions and sizes // Calculate positions and sizes
size_t name_len = name.size() + 1; // including null terminator size_t name_len = name.size() + 1; // including null terminator
size_t name_offset = 1; size_t name_offset = 1;
size_t mac_offset = name_offset + name_len; 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 // chosen proto
msg[0] = 0x01; msg[0] = 0x01;
// node name, terminated by null byte // 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 // 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) if (aerr != APIError::OK)
return aerr; return aerr;
@@ -353,35 +356,32 @@ APIError APINoiseFrameHelper::state_action_() {
return APIError::OK; return APIError::OK;
} }
void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reason) { 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 #ifdef USE_STORE_LOG_STR_IN_FLASH
// On ESP8266 with flash strings, we need to use PROGMEM-aware functions // 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 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) { 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 #else
// Normal memory access // Normal memory access
const char *reason_str = LOG_STR_ARG(reason); const char *reason_str = LOG_STR_ARG(reason);
size_t reason_len = strlen(reason_str); 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) { 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 #endif
size_t data_size = reason_len + 1;
// temporarily remove failed state // temporarily remove failed state
auto orig_state = state_; auto orig_state = state_;
state_ = State::EXPLICIT_REJECT; state_ = State::EXPLICIT_REJECT;
write_frame_(data.get(), data_size); write_frame_(data, data_size);
state_ = orig_state; state_ = orig_state;
} }
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {

View File

@@ -158,12 +158,14 @@ void ATM90E32Component::setup() {
if (this->enable_offset_calibration_) { if (this->enable_offset_calibration_) {
// Initialize flash storage for offset calibrations // Initialize flash storage for offset calibrations
uint32_t o_hash = fnv1_hash(std::string("_offset_calibration_") + this->cs_summary_); uint32_t o_hash = fnv1_hash("_offset_calibration_");
o_hash = fnv1_hash_extend(o_hash, this->cs_summary_);
this->offset_pref_ = global_preferences->make_preference<OffsetCalibration[3]>(o_hash, true); this->offset_pref_ = global_preferences->make_preference<OffsetCalibration[3]>(o_hash, true);
this->restore_offset_calibrations_(); this->restore_offset_calibrations_();
// Initialize flash storage for power offset calibrations // Initialize flash storage for power offset calibrations
uint32_t po_hash = fnv1_hash(std::string("_power_offset_calibration_") + this->cs_summary_); uint32_t po_hash = fnv1_hash("_power_offset_calibration_");
po_hash = fnv1_hash_extend(po_hash, this->cs_summary_);
this->power_offset_pref_ = global_preferences->make_preference<PowerOffsetCalibration[3]>(po_hash, true); this->power_offset_pref_ = global_preferences->make_preference<PowerOffsetCalibration[3]>(po_hash, true);
this->restore_power_offset_calibrations_(); this->restore_power_offset_calibrations_();
} else { } else {
@@ -183,7 +185,8 @@ void ATM90E32Component::setup() {
if (this->enable_gain_calibration_) { if (this->enable_gain_calibration_) {
// Initialize flash storage for gain calibration // Initialize flash storage for gain calibration
uint32_t g_hash = fnv1_hash(std::string("_gain_calibration_") + this->cs_summary_); uint32_t g_hash = fnv1_hash("_gain_calibration_");
g_hash = fnv1_hash_extend(g_hash, this->cs_summary_);
this->gain_calibration_pref_ = global_preferences->make_preference<GainCalibration[3]>(g_hash, true); this->gain_calibration_pref_ = global_preferences->make_preference<GainCalibration[3]>(g_hash, true);
this->restore_gain_calibrations_(); this->restore_gain_calibrations_();

View File

@@ -1,4 +1,5 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components.esp32 import add_idf_component
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_BITS_PER_SAMPLE, CONF_NUM_CHANNELS, CONF_SAMPLE_RATE from esphome.const import CONF_BITS_PER_SAMPLE, CONF_NUM_CHANNELS, CONF_SAMPLE_RATE
import esphome.final_validate as fv import esphome.final_validate as fv
@@ -165,4 +166,7 @@ def final_validate_audio_schema(
async def to_code(config): 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 // Advance read pointer to match the offset for the syncword
this->input_transfer_buffer_->decrease_buffer_length(offset); 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(); buffer_length = (int) this->input_transfer_buffer_->available();
int err = esp_audio_libs::helix_decoder::MP3Decode(this->mp3_decoder_, &buffer_start, &buffer_length, int err = esp_audio_libs::helix_decoder::MP3Decode(this->mp3_decoder_, &buffer_start, &buffer_length,

View File

@@ -185,18 +185,16 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) {
return err; return err;
} }
std::string url_string = str_lower_case(url); if (str_endswith_ignore_case(url, ".wav")) {
if (str_endswith(url_string, ".wav")) {
file_type = AudioFileType::WAV; file_type = AudioFileType::WAV;
} }
#ifdef USE_AUDIO_MP3_SUPPORT #ifdef USE_AUDIO_MP3_SUPPORT
else if (str_endswith(url_string, ".mp3")) { else if (str_endswith_ignore_case(url, ".mp3")) {
file_type = AudioFileType::MP3; file_type = AudioFileType::MP3;
} }
#endif #endif
#ifdef USE_AUDIO_FLAC_SUPPORT #ifdef USE_AUDIO_FLAC_SUPPORT
else if (str_endswith(url_string, ".flac")) { else if (str_endswith_ignore_case(url, ".flac")) {
file_type = AudioFileType::FLAC; file_type = AudioFileType::FLAC;
} }
#endif #endif

View File

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

View File

@@ -1,7 +1,8 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import esp32_ble_tracker from esphome.components import esp32_ble_tracker
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_MAC_ADDRESS from esphome.const import CONF_BINDKEY, CONF_ID, CONF_MAC_ADDRESS
from esphome.core import HexInt
CODEOWNERS = ["@nagyrobi"] CODEOWNERS = ["@nagyrobi"]
DEPENDENCIES = ["esp32_ble_tracker"] DEPENDENCIES = ["esp32_ble_tracker"]
@@ -22,6 +23,7 @@ def bthome_mithermometer_base_schema(extra_schema=None):
{ {
cv.GenerateID(CONF_ID): cv.declare_id(BTHomeMiThermometer), cv.GenerateID(CONF_ID): cv.declare_id(BTHomeMiThermometer),
cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
cv.Optional(CONF_BINDKEY): cv.bind_key,
} }
) )
.extend(BLE_DEVICE_SCHEMA) .extend(BLE_DEVICE_SCHEMA)
@@ -34,3 +36,9 @@ async def setup_bthome_mithermometer(var, config):
await cg.register_component(var, config) await cg.register_component(var, config)
await esp32_ble_tracker.register_ble_device(var, config) await esp32_ble_tracker.register_ble_device(var, config)
cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
if bindkey := config.get(CONF_BINDKEY):
bindkey_bytes = [
HexInt(int(bindkey[index : index + 2], 16))
for index in range(0, len(bindkey), 2)
]
cg.add(var.set_bindkey(cg.ArrayInitializer(*bindkey_bytes)))

View File

@@ -3,15 +3,23 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <algorithm>
#include <array> #include <array>
#include <cstring>
#include <span> #include <span>
#ifdef USE_ESP32 #ifdef USE_ESP32
#include "mbedtls/ccm.h"
namespace esphome { namespace esphome {
namespace bthome_mithermometer { namespace bthome_mithermometer {
static const char *const TAG = "bthome_mithermometer"; static const char *const TAG = "bthome_mithermometer";
static constexpr size_t BTHOME_BINDKEY_SIZE = 16;
static constexpr size_t BTHOME_NONCE_SIZE = 13;
static constexpr size_t BTHOME_MIC_SIZE = 4;
static constexpr size_t BTHOME_COUNTER_SIZE = 4;
static const char *format_mac_address(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buffer, uint64_t address) { static const char *format_mac_address(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buffer, uint64_t address) {
std::array<uint8_t, MAC_ADDRESS_SIZE> mac{}; std::array<uint8_t, MAC_ADDRESS_SIZE> mac{};
@@ -130,6 +138,10 @@ void BTHomeMiThermometer::dump_config() {
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
ESP_LOGCONFIG(TAG, "BTHome MiThermometer"); ESP_LOGCONFIG(TAG, "BTHome MiThermometer");
ESP_LOGCONFIG(TAG, " MAC Address: %s", format_mac_address(addr_buf, this->address_)); ESP_LOGCONFIG(TAG, " MAC Address: %s", format_mac_address(addr_buf, this->address_));
if (this->has_bindkey_) {
char bindkey_hex[format_hex_pretty_size(BTHOME_BINDKEY_SIZE)];
ESP_LOGCONFIG(TAG, " Bindkey: %s", format_hex_pretty_to(bindkey_hex, this->bindkey_, BTHOME_BINDKEY_SIZE, '.'));
}
LOG_SENSOR(" ", "Temperature", this->temperature_); LOG_SENSOR(" ", "Temperature", this->temperature_);
LOG_SENSOR(" ", "Humidity", this->humidity_); LOG_SENSOR(" ", "Humidity", this->humidity_);
LOG_SENSOR(" ", "Battery Level", this->battery_level_); LOG_SENSOR(" ", "Battery Level", this->battery_level_);
@@ -150,6 +162,60 @@ bool BTHomeMiThermometer::parse_device(const esp32_ble_tracker::ESPBTDevice &dev
return matched; return matched;
} }
void BTHomeMiThermometer::set_bindkey(std::initializer_list<uint8_t> bindkey) {
if (bindkey.size() != sizeof(this->bindkey_)) {
ESP_LOGW(TAG, "BTHome bindkey size mismatch: %zu", bindkey.size());
return;
}
std::copy(bindkey.begin(), bindkey.end(), this->bindkey_);
this->has_bindkey_ = true;
}
bool BTHomeMiThermometer::decrypt_bthome_payload_(const std::vector<uint8_t> &data, uint64_t source_address,
std::vector<uint8_t> &payload) const {
if (data.size() <= 1 + BTHOME_COUNTER_SIZE + BTHOME_MIC_SIZE) {
ESP_LOGVV(TAG, "Encrypted BTHome payload too short: %zu", data.size());
return false;
}
const size_t ciphertext_size = data.size() - 1 - BTHOME_COUNTER_SIZE - BTHOME_MIC_SIZE;
payload.resize(ciphertext_size);
std::array<uint8_t, MAC_ADDRESS_SIZE> mac{};
for (size_t i = 0; i < MAC_ADDRESS_SIZE; i++) {
mac[i] = (source_address >> ((MAC_ADDRESS_SIZE - 1 - i) * 8)) & 0xFF;
}
std::array<uint8_t, BTHOME_NONCE_SIZE> nonce{};
memcpy(nonce.data(), mac.data(), mac.size());
nonce[6] = 0xD2;
nonce[7] = 0xFC;
nonce[8] = data[0];
memcpy(nonce.data() + 9, &data[data.size() - BTHOME_COUNTER_SIZE - BTHOME_MIC_SIZE], BTHOME_COUNTER_SIZE);
const uint8_t *ciphertext = data.data() + 1;
const uint8_t *mic = data.data() + data.size() - BTHOME_MIC_SIZE;
mbedtls_ccm_context ctx;
mbedtls_ccm_init(&ctx);
int ret = mbedtls_ccm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, this->bindkey_, BTHOME_BINDKEY_SIZE * 8);
if (ret) {
ESP_LOGVV(TAG, "mbedtls_ccm_setkey() failed.");
mbedtls_ccm_free(&ctx);
return false;
}
ret = mbedtls_ccm_auth_decrypt(&ctx, ciphertext_size, nonce.data(), nonce.size(), nullptr, 0, ciphertext,
payload.data(), mic, BTHOME_MIC_SIZE);
mbedtls_ccm_free(&ctx);
if (ret) {
ESP_LOGVV(TAG, "BTHome decryption failed (ret=%d).", ret);
return false;
}
return true;
}
bool BTHomeMiThermometer::handle_service_data_(const esp32_ble_tracker::ServiceData &service_data, bool BTHomeMiThermometer::handle_service_data_(const esp32_ble_tracker::ServiceData &service_data,
const esp32_ble_tracker::ESPBTDevice &device) { const esp32_ble_tracker::ESPBTDevice &device) {
if (!service_data.uuid.contains(0xD2, 0xFC)) { if (!service_data.uuid.contains(0xD2, 0xFC)) {
@@ -173,51 +239,88 @@ bool BTHomeMiThermometer::handle_service_data_(const esp32_ble_tracker::ServiceD
return false; return false;
} }
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; uint64_t source_address = device.address_uint64();
if (is_encrypted) { bool address_matches = source_address == this->address_;
ESP_LOGV(TAG, "Ignoring encrypted BTHome frame from %s", device.address_str_to(addr_buf)); if (!is_encrypted && mac_included && data.size() >= 7) {
uint64_t advertised_address = 0;
for (int i = 5; i >= 0; i--) {
advertised_address = (advertised_address << 8) | data[1 + i];
}
address_matches = address_matches || advertised_address == this->address_;
}
if (is_encrypted && !this->has_bindkey_) {
if (address_matches) {
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
ESP_LOGE(TAG, "Encrypted BTHome frame received but no bindkey configured for %s",
device.address_str_to(addr_buf));
}
return false; return false;
} }
size_t payload_index = 1; if (!is_encrypted && this->has_bindkey_) {
uint64_t source_address = device.address_uint64(); if (address_matches) {
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
ESP_LOGE(TAG, "Unencrypted BTHome frame received with bindkey configured for %s",
device.address_str_to(addr_buf));
}
return false;
}
std::vector<uint8_t> decrypted_payload;
const uint8_t *payload = nullptr;
size_t payload_size = 0;
if (is_encrypted) {
if (!this->decrypt_bthome_payload_(data, source_address, decrypted_payload)) {
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
ESP_LOGVV(TAG, "Failed to decrypt BTHome frame from %s", device.address_str_to(addr_buf));
return false;
}
payload = decrypted_payload.data();
payload_size = decrypted_payload.size();
} else {
payload = data.data() + 1;
payload_size = data.size() - 1;
}
if (mac_included) { if (mac_included) {
if (data.size() < 7) { if (payload_size < 6) {
ESP_LOGVV(TAG, "BTHome payload missing MAC address"); ESP_LOGVV(TAG, "BTHome payload missing MAC address");
return false; return false;
} }
source_address = 0; source_address = 0;
for (int i = 5; i >= 0; i--) { for (int i = 5; i >= 0; i--) {
source_address = (source_address << 8) | data[1 + i]; source_address = (source_address << 8) | payload[i];
} }
payload_index = 7; payload += 6;
payload_size -= 6;
} }
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
if (source_address != this->address_) { if (source_address != this->address_) {
ESP_LOGVV(TAG, "BTHome frame from unexpected device %s", format_mac_address(addr_buf, source_address)); ESP_LOGVV(TAG, "BTHome frame from unexpected device %s", format_mac_address(addr_buf, source_address));
return false; return false;
} }
if (payload_index >= data.size()) { if (payload_size == 0) {
ESP_LOGVV(TAG, "BTHome payload empty after header"); ESP_LOGVV(TAG, "BTHome payload empty after header");
return false; return false;
} }
bool reported = false; bool reported = false;
size_t offset = payload_index; size_t offset = 0;
uint8_t last_type = 0; uint8_t last_type = 0;
while (offset < data.size()) { while (offset < payload_size) {
const uint8_t obj_type = data[offset++]; const uint8_t obj_type = payload[offset++];
size_t value_length = 0; size_t value_length = 0;
bool has_length_byte = obj_type == 0x53; // text objects include explicit length bool has_length_byte = obj_type == 0x53; // text objects include explicit length
if (has_length_byte) { if (has_length_byte) {
if (offset >= data.size()) { if (offset >= payload_size) {
break; break;
} }
value_length = data[offset++]; value_length = payload[offset++];
} else { } else {
if (!get_bthome_value_length(obj_type, value_length)) { if (!get_bthome_value_length(obj_type, value_length)) {
ESP_LOGVV(TAG, "Unknown BTHome object 0x%02X", obj_type); ESP_LOGVV(TAG, "Unknown BTHome object 0x%02X", obj_type);
@@ -229,12 +332,12 @@ bool BTHomeMiThermometer::handle_service_data_(const esp32_ble_tracker::ServiceD
break; break;
} }
if (offset + value_length > data.size()) { if (offset + value_length > payload_size) {
ESP_LOGVV(TAG, "BTHome object length exceeds payload"); ESP_LOGVV(TAG, "BTHome object length exceeds payload");
break; break;
} }
const uint8_t *value = &data[offset]; const uint8_t *value = &payload[offset];
offset += value_length; offset += value_length;
if (obj_type < last_type) { if (obj_type < last_type) {

View File

@@ -5,6 +5,8 @@
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include <cstdint> #include <cstdint>
#include <initializer_list>
#include <vector>
#ifdef USE_ESP32 #ifdef USE_ESP32
@@ -14,6 +16,7 @@ namespace bthome_mithermometer {
class BTHomeMiThermometer : public esp32_ble_tracker::ESPBTDeviceListener, public Component { class BTHomeMiThermometer : public esp32_ble_tracker::ESPBTDeviceListener, public Component {
public: public:
void set_address(uint64_t address) { this->address_ = address; } void set_address(uint64_t address) { this->address_ = address; }
void set_bindkey(std::initializer_list<uint8_t> bindkey);
void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; } void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; }
void set_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; } void set_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; }
@@ -27,9 +30,13 @@ class BTHomeMiThermometer : public esp32_ble_tracker::ESPBTDeviceListener, publi
protected: protected:
bool handle_service_data_(const esp32_ble_tracker::ServiceData &service_data, bool handle_service_data_(const esp32_ble_tracker::ServiceData &service_data,
const esp32_ble_tracker::ESPBTDevice &device); const esp32_ble_tracker::ESPBTDevice &device);
bool decrypt_bthome_payload_(const std::vector<uint8_t> &data, uint64_t source_address,
std::vector<uint8_t> &payload) const;
uint64_t address_{0}; uint64_t address_{0};
optional<uint8_t> last_packet_id_{}; optional<uint8_t> last_packet_id_{};
bool has_bindkey_{false};
uint8_t bindkey_[16];
sensor::Sensor *temperature_{nullptr}; sensor::Sensor *temperature_{nullptr};
sensor::Sensor *humidity_{nullptr}; sensor::Sensor *humidity_{nullptr};

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() { void CC1101Component::loop() {
if (this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO) || this->gdo0_pin_ == nullptr || if (this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO) || this->gdo0_pin_ == nullptr ||
!this->gdo0_pin_->digital_read()) { !this->gdo0_pin_->digital_read()) {
@@ -198,7 +205,7 @@ void CC1101Component::loop() {
bool crc_ok = (this->state_.LQI & STATUS_CRC_OK_MASK) != 0; bool crc_ok = (this->state_.LQI & STATUS_CRC_OK_MASK) != 0;
uint8_t lqi = this->state_.LQI & STATUS_LQI_MASK; uint8_t lqi = this->state_.LQI & STATUS_LQI_MASK;
if (this->state_.CRC_EN == 0 || crc_ok) { 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 // 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 }; 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, class CC1101Component : public Component,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_1MHZ> { spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_1MHZ> {
@@ -73,6 +78,7 @@ class CC1101Component : public Component,
// Packet mode operations // Packet mode operations
CC1101Error transmit_packet(const std::vector<uint8_t> &packet); 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_; } Trigger<std::vector<uint8_t>, float, float, uint8_t> *get_packet_trigger() const { return this->packet_trigger_; }
protected: protected:
@@ -89,9 +95,11 @@ class CC1101Component : public Component,
InternalGPIOPin *gdo0_pin_{nullptr}; InternalGPIOPin *gdo0_pin_{nullptr};
// Packet handling // 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_{ Trigger<std::vector<uint8_t>, float, float, uint8_t> *packet_trigger_{
new Trigger<std::vector<uint8_t>, float, float, uint8_t>()}; new Trigger<std::vector<uint8_t>, float, float, uint8_t>()};
std::vector<uint8_t> packet_; std::vector<uint8_t> packet_;
std::vector<CC1101Listener *> listeners_;
// Low-level Helpers // Low-level Helpers
uint8_t strobe_(Command cmd); uint8_t strobe_(Command cmd);

View File

@@ -81,8 +81,8 @@ void CCS811Component::setup() {
bootloader_version, application_version); bootloader_version, application_version);
if (this->version_ != nullptr) { if (this->version_ != nullptr) {
char version[20]; // "15.15.15 (0xffff)" is 17 chars, plus NUL, plus wiggle room char version[20]; // "15.15.15 (0xffff)" is 17 chars, plus NUL, plus wiggle room
sprintf(version, "%d.%d.%d (0x%02x)", (application_version >> 12 & 15), (application_version >> 8 & 15), buf_append_printf(version, sizeof(version), 0, "%d.%d.%d (0x%02x)", (application_version >> 12 & 15),
(application_version >> 4 & 15), application_version); (application_version >> 8 & 15), (application_version >> 4 & 15), application_version);
ESP_LOGD(TAG, "publishing version state: %s", version); ESP_LOGD(TAG, "publishing version state: %s", version);
this->version_->publish_state(version); this->version_->publish_state(version);
} }

View File

@@ -133,7 +133,7 @@ bool CH422GGPIOPin::digital_read() { return this->parent_->digital_read(this->pi
void CH422GGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value ^ this->inverted_); } void CH422GGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value ^ this->inverted_); }
size_t CH422GGPIOPin::dump_summary(char *buffer, size_t len) const { size_t CH422GGPIOPin::dump_summary(char *buffer, size_t len) const {
return snprintf(buffer, len, "EXIO%u via CH422G", this->pin_); return buf_append_printf(buffer, len, 0, "EXIO%u via CH422G", this->pin_);
} }
void CH422GGPIOPin::set_flags(gpio::Flags flags) { void CH422GGPIOPin::set_flags(gpio::Flags flags) {
flags_ = flags; flags_ = flags;

View File

@@ -76,7 +76,6 @@ class CS5460AComponent : public Component,
void restart() { restart_(); } void restart() { restart_(); }
void setup() override; void setup() override;
void loop() override {}
void dump_config() override; void dump_config() override;
protected: protected:

View File

@@ -207,20 +207,24 @@ void CSE7766Component::parse_data_() {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
{ {
std::string buf = "Parsed:"; // Buffer: 7 + 15 + 33 + 15 + 25 = 95 chars max + null, rounded to 128 for safety margin.
// Float sizes with %.4f can be up to 11 chars for large values (e.g., 999999.9999).
char buf[128];
size_t pos = buf_append_printf(buf, sizeof(buf), 0, "Parsed:");
if (have_voltage) { if (have_voltage) {
buf += str_sprintf(" V=%fV", voltage); pos = buf_append_printf(buf, sizeof(buf), pos, " V=%.4fV", voltage);
} }
if (have_current) { if (have_current) {
buf += str_sprintf(" I=%fmA (~%fmA)", current * 1000.0f, calculated_current * 1000.0f); pos = buf_append_printf(buf, sizeof(buf), pos, " I=%.4fmA (~%.4fmA)", current * 1000.0f,
calculated_current * 1000.0f);
} }
if (have_power) { if (have_power) {
buf += str_sprintf(" P=%fW", power); pos = buf_append_printf(buf, sizeof(buf), pos, " P=%.4fW", power);
} }
if (energy != 0.0f) { if (energy != 0.0f) {
buf += str_sprintf(" E=%fkWh (%u)", energy, cf_pulses); buf_append_printf(buf, sizeof(buf), pos, " E=%.4fkWh (%u)", energy, cf_pulses);
} }
ESP_LOGVV(TAG, "%s", buf.c_str()); ESP_LOGVV(TAG, "%s", buf);
} }
#endif #endif
} }

View File

@@ -258,8 +258,9 @@ bool DaikinArcClimate::parse_state_frame_(const uint8_t frame[]) {
} }
char buf[DAIKIN_STATE_FRAME_SIZE * 3 + 1] = {0}; char buf[DAIKIN_STATE_FRAME_SIZE * 3 + 1] = {0};
size_t pos = 0;
for (size_t i = 0; i < DAIKIN_STATE_FRAME_SIZE; i++) { for (size_t i = 0; i < DAIKIN_STATE_FRAME_SIZE; i++) {
sprintf(buf, "%s%02x ", buf, frame[i]); pos = buf_append_printf(buf, sizeof(buf), pos, "%02x ", frame[i]);
} }
ESP_LOGD(TAG, "FRAME %s", buf); ESP_LOGD(TAG, "FRAME %s", buf);
@@ -349,8 +350,9 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) {
if (data.expect_item(DAIKIN_HEADER_MARK, DAIKIN_HEADER_SPACE)) { if (data.expect_item(DAIKIN_HEADER_MARK, DAIKIN_HEADER_SPACE)) {
valid_daikin_frame = true; valid_daikin_frame = true;
size_t bytes_count = data.size() / 2 / 8; size_t bytes_count = data.size() / 2 / 8;
std::unique_ptr<char[]> buf(new char[bytes_count * 3 + 1]); size_t buf_size = bytes_count * 3 + 1;
buf[0] = '\0'; std::unique_ptr<char[]> buf(new char[buf_size]()); // value-initialize (zero-fill)
size_t buf_pos = 0;
for (size_t i = 0; i < bytes_count; i++) { for (size_t i = 0; i < bytes_count; i++) {
uint8_t byte = 0; uint8_t byte = 0;
for (int8_t bit = 0; bit < 8; bit++) { for (int8_t bit = 0; bit < 8; bit++) {
@@ -361,19 +363,19 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) {
break; break;
} }
} }
sprintf(buf.get(), "%s%02x ", buf.get(), byte); buf_pos = buf_append_printf(buf.get(), buf_size, buf_pos, "%02x ", byte);
} }
ESP_LOGD(TAG, "WHOLE FRAME %s size: %d", buf.get(), data.size()); ESP_LOGD(TAG, "WHOLE FRAME %s size: %d", buf.get(), data.size());
} }
if (!valid_daikin_frame) { if (!valid_daikin_frame) {
char sbuf[16 * 10 + 1]; char sbuf[16 * 10 + 1] = {0};
sbuf[0] = '\0'; size_t sbuf_pos = 0;
for (size_t j = 0; j < static_cast<size_t>(data.size()); j++) { for (size_t j = 0; j < static_cast<size_t>(data.size()); j++) {
if ((j - 2) % 16 == 0) { if ((j - 2) % 16 == 0) {
if (j > 0) { if (j > 0) {
ESP_LOGD(TAG, "DATA %04x: %s", (j - 16 > 0xffff ? 0 : j - 16), sbuf); ESP_LOGD(TAG, "DATA %04x: %s", (j - 16 > 0xffff ? 0 : j - 16), sbuf);
} }
sbuf[0] = '\0'; sbuf_pos = 0;
} }
char type_ch = ' '; char type_ch = ' ';
// debug_tolerance = 25% // debug_tolerance = 25%
@@ -401,9 +403,10 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) {
type_ch = '0'; type_ch = '0';
if (abs(data[j]) > 100000) { if (abs(data[j]) > 100000) {
sprintf(sbuf, "%s%-5d[%c] ", sbuf, data[j] > 0 ? 99999 : -99999, type_ch); sbuf_pos = buf_append_printf(sbuf, sizeof(sbuf), sbuf_pos, "%-5d[%c] ", data[j] > 0 ? 99999 : -99999, type_ch);
} else { } else {
sprintf(sbuf, "%s%-5d[%c] ", sbuf, (int) (round(data[j] / 10.) * 10), type_ch); sbuf_pos =
buf_append_printf(sbuf, sizeof(sbuf), sbuf_pos, "%-5d[%c] ", (int) (round(data[j] / 10.) * 10), type_ch);
} }
if (j + 1 == static_cast<size_t>(data.size())) { if (j + 1 == static_cast<size_t>(data.size())) {
ESP_LOGD(TAG, "DATA %04x: %s", (j - 8 > 0xffff ? 0 : j - 8), sbuf); ESP_LOGD(TAG, "DATA %04x: %s", (j - 8 > 0xffff ? 0 : j - 8), sbuf);

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(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{}; 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"); ESP_LOGE(TAG, "Could not convert the date string to an ESPTime object");
return *this; return *this;
} }

View File

@@ -67,7 +67,9 @@ class DateCall {
void perform(); void perform();
DateCall &set_date(uint16_t year, uint8_t month, uint8_t day); DateCall &set_date(uint16_t year, uint8_t month, uint8_t day);
DateCall &set_date(ESPTime time); 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) { DateCall &set_year(uint16_t year) {
this->year_ = year; this->year_ = year;

View File

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

View File

@@ -71,7 +71,11 @@ class DateTimeCall {
void perform(); 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(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(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_datetime(time_t epoch_seconds);
DateTimeCall &set_year(uint16_t year) { 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(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{}; 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"); ESP_LOGE(TAG, "Could not convert the time string to an ESPTime object");
return *this; return *this;
} }

View File

@@ -69,7 +69,9 @@ class TimeCall {
void perform(); void perform();
TimeCall &set_time(uint8_t hour, uint8_t minute, uint8_t second); TimeCall &set_time(uint8_t hour, uint8_t minute, uint8_t second);
TimeCall &set_time(ESPTime time); 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) { TimeCall &set_hour(uint8_t hour) {
this->hour_ = hour; this->hour_ = hour;

View File

@@ -30,7 +30,7 @@ void DebugComponent::dump_config() {
char device_info_buffer[DEVICE_INFO_BUFFER_SIZE]; char device_info_buffer[DEVICE_INFO_BUFFER_SIZE];
ESP_LOGD(TAG, "ESPHome version %s", ESPHOME_VERSION); ESP_LOGD(TAG, "ESPHome version %s", ESPHOME_VERSION);
size_t pos = buf_append(device_info_buffer, DEVICE_INFO_BUFFER_SIZE, 0, "%s", ESPHOME_VERSION); size_t pos = buf_append_printf(device_info_buffer, DEVICE_INFO_BUFFER_SIZE, 0, "%s", ESPHOME_VERSION);
this->free_heap_ = get_free_heap_(); this->free_heap_ = get_free_heap_();
ESP_LOGD(TAG, "Free Heap Size: %" PRIu32 " bytes", this->free_heap_); ESP_LOGD(TAG, "Free Heap Size: %" PRIu32 " bytes", this->free_heap_);

View File

@@ -5,12 +5,6 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/macros.h" #include "esphome/core/macros.h"
#include <span> #include <span>
#include <cstdarg>
#include <cstdio>
#include <algorithm>
#ifdef USE_ESP8266
#include <pgmspace.h>
#endif
#ifdef USE_SENSOR #ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h" #include "esphome/components/sensor/sensor.h"
@@ -25,40 +19,7 @@ namespace debug {
static constexpr size_t DEVICE_INFO_BUFFER_SIZE = 256; static constexpr size_t DEVICE_INFO_BUFFER_SIZE = 256;
static constexpr size_t RESET_REASON_BUFFER_SIZE = 128; static constexpr size_t RESET_REASON_BUFFER_SIZE = 128;
#ifdef USE_ESP8266 // buf_append_printf is now provided by esphome/core/helpers.h
// ESP8266: Use vsnprintf_P to keep format strings in flash (PROGMEM)
// Format strings must be wrapped with PSTR() macro
inline size_t buf_append_p(char *buf, size_t size, size_t pos, PGM_P fmt, ...) {
if (pos >= size) {
return size;
}
va_list args;
va_start(args, fmt);
int written = vsnprintf_P(buf + pos, size - pos, fmt, args);
va_end(args);
if (written < 0) {
return pos; // encoding error
}
return std::min(pos + static_cast<size_t>(written), size);
}
#define buf_append(buf, size, pos, fmt, ...) buf_append_p(buf, size, pos, PSTR(fmt), ##__VA_ARGS__)
#else
/// Safely append formatted string to buffer, returning new position (capped at size)
__attribute__((format(printf, 4, 5))) inline size_t buf_append(char *buf, size_t size, size_t pos, const char *fmt,
...) {
if (pos >= size) {
return size;
}
va_list args;
va_start(args, fmt);
int written = vsnprintf(buf + pos, size - pos, fmt, args);
va_end(args);
if (written < 0) {
return pos; // encoding error
}
return std::min(pos + static_cast<size_t>(written), size);
}
#endif
class DebugComponent : public PollingComponent { class DebugComponent : public PollingComponent {
public: public:
@@ -74,8 +35,11 @@ class DebugComponent : public PollingComponent {
#ifdef USE_SENSOR #ifdef USE_SENSOR
void set_free_sensor(sensor::Sensor *free_sensor) { free_sensor_ = free_sensor; } void set_free_sensor(sensor::Sensor *free_sensor) { free_sensor_ = free_sensor; }
void set_block_sensor(sensor::Sensor *block_sensor) { block_sensor_ = block_sensor; } void set_block_sensor(sensor::Sensor *block_sensor) { block_sensor_ = block_sensor; }
#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) #if (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)) || defined(USE_ESP32)
void set_fragmentation_sensor(sensor::Sensor *fragmentation_sensor) { fragmentation_sensor_ = fragmentation_sensor; } void set_fragmentation_sensor(sensor::Sensor *fragmentation_sensor) { fragmentation_sensor_ = fragmentation_sensor; }
#endif
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
void set_min_free_sensor(sensor::Sensor *min_free_sensor) { min_free_sensor_ = min_free_sensor; }
#endif #endif
void set_loop_time_sensor(sensor::Sensor *loop_time_sensor) { loop_time_sensor_ = loop_time_sensor; } void set_loop_time_sensor(sensor::Sensor *loop_time_sensor) { loop_time_sensor_ = loop_time_sensor; }
#ifdef USE_ESP32 #ifdef USE_ESP32
@@ -97,8 +61,11 @@ class DebugComponent : public PollingComponent {
sensor::Sensor *free_sensor_{nullptr}; sensor::Sensor *free_sensor_{nullptr};
sensor::Sensor *block_sensor_{nullptr}; sensor::Sensor *block_sensor_{nullptr};
#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) #if (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)) || defined(USE_ESP32)
sensor::Sensor *fragmentation_sensor_{nullptr}; sensor::Sensor *fragmentation_sensor_{nullptr};
#endif
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
sensor::Sensor *min_free_sensor_{nullptr};
#endif #endif
sensor::Sensor *loop_time_sensor_{nullptr}; sensor::Sensor *loop_time_sensor_{nullptr};
#ifdef USE_ESP32 #ifdef USE_ESP32

View File

@@ -173,8 +173,8 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT
uint32_t flash_speed = ESP.getFlashChipSpeed() / 1000000; // 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); ESP_LOGD(TAG, "Flash Chip: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed, flash_mode);
pos = buf_append(buf, size, pos, "|Flash: %" PRIu32 "kB Speed:%" PRIu32 "MHz Mode:%s", flash_size, flash_speed, pos = buf_append_printf(buf, size, pos, "|Flash: %" PRIu32 "kB Speed:%" PRIu32 "MHz Mode:%s", flash_size, flash_speed,
flash_mode); flash_mode);
#endif #endif
esp_chip_info_t info; esp_chip_info_t info;
@@ -182,60 +182,71 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
const char *model = ESPHOME_VARIANT; const char *model = ESPHOME_VARIANT;
// Build features string // Build features string
pos = buf_append(buf, size, pos, "|Chip: %s Features:", model); pos = buf_append_printf(buf, size, pos, "|Chip: %s Features:", model);
bool first_feature = true; bool first_feature = true;
for (const auto &feature : CHIP_FEATURES) { for (const auto &feature : CHIP_FEATURES) {
if (info.features & feature.bit) { if (info.features & feature.bit) {
pos = buf_append(buf, size, pos, "%s%s", first_feature ? "" : ", ", feature.name); pos = buf_append_printf(buf, size, pos, "%s%s", first_feature ? "" : ", ", feature.name);
first_feature = false; first_feature = false;
info.features &= ~feature.bit; info.features &= ~feature.bit;
} }
} }
if (info.features != 0) { if (info.features != 0) {
pos = buf_append(buf, size, pos, "%sOther:0x%" PRIx32, first_feature ? "" : ", ", info.features); pos = buf_append_printf(buf, size, pos, "%sOther:0x%" PRIx32, first_feature ? "" : ", ", info.features);
} }
ESP_LOGD(TAG, "Chip: Model=%s, Cores=%u, Revision=%u", model, info.cores, info.revision); ESP_LOGD(TAG, "Chip: Model=%s, Cores=%u, Revision=%u", model, info.cores, info.revision);
pos = buf_append(buf, size, pos, " Cores:%u Revision:%u", info.cores, info.revision); pos = buf_append_printf(buf, size, pos, " Cores:%u Revision:%u", info.cores, info.revision);
uint32_t cpu_freq_mhz = arch_get_cpu_freq_hz() / 1000000; uint32_t cpu_freq_mhz = arch_get_cpu_freq_hz() / 1000000;
ESP_LOGD(TAG, "CPU Frequency: %" PRIu32 " MHz", cpu_freq_mhz); ESP_LOGD(TAG, "CPU Frequency: %" PRIu32 " MHz", cpu_freq_mhz);
pos = buf_append(buf, size, pos, "|CPU Frequency: %" PRIu32 " MHz", cpu_freq_mhz); pos = buf_append_printf(buf, size, pos, "|CPU Frequency: %" PRIu32 " MHz", cpu_freq_mhz);
// Framework detection // Framework detection
#ifdef USE_ARDUINO #ifdef USE_ARDUINO
ESP_LOGD(TAG, "Framework: Arduino"); ESP_LOGD(TAG, "Framework: Arduino");
pos = buf_append(buf, size, pos, "|Framework: Arduino"); pos = buf_append_printf(buf, size, pos, "|Framework: Arduino");
#elif defined(USE_ESP32) #elif defined(USE_ESP32)
ESP_LOGD(TAG, "Framework: ESP-IDF"); ESP_LOGD(TAG, "Framework: ESP-IDF");
pos = buf_append(buf, size, pos, "|Framework: ESP-IDF"); pos = buf_append_printf(buf, size, pos, "|Framework: ESP-IDF");
#else #else
ESP_LOGW(TAG, "Framework: UNKNOWN"); ESP_LOGW(TAG, "Framework: UNKNOWN");
pos = buf_append(buf, size, pos, "|Framework: UNKNOWN"); pos = buf_append_printf(buf, size, pos, "|Framework: UNKNOWN");
#endif #endif
ESP_LOGD(TAG, "ESP-IDF Version: %s", esp_get_idf_version()); ESP_LOGD(TAG, "ESP-IDF Version: %s", esp_get_idf_version());
pos = buf_append(buf, size, pos, "|ESP-IDF: %s", esp_get_idf_version()); pos = buf_append_printf(buf, size, pos, "|ESP-IDF: %s", esp_get_idf_version());
uint8_t mac[6]; uint8_t mac[6];
get_mac_address_raw(mac); get_mac_address_raw(mac);
ESP_LOGD(TAG, "EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); ESP_LOGD(TAG, "EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
pos = buf_append(buf, size, pos, "|EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], pos = buf_append_printf(buf, size, pos, "|EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3],
mac[5]); mac[4], mac[5]);
char reason_buffer[RESET_REASON_BUFFER_SIZE]; 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_(std::span<char, RESET_REASON_BUFFER_SIZE>(reason_buffer));
pos = buf_append(buf, size, pos, "|Reset: %s", reset_reason); pos = buf_append_printf(buf, size, pos, "|Reset: %s", reset_reason);
const char *wakeup_cause = get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE>(reason_buffer)); const char *wakeup_cause = get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE>(reason_buffer));
pos = buf_append(buf, size, pos, "|Wakeup: %s", wakeup_cause); pos = buf_append_printf(buf, size, pos, "|Wakeup: %s", wakeup_cause);
return pos; return pos;
} }
void DebugComponent::update_platform_() { void DebugComponent::update_platform_() {
#ifdef USE_SENSOR #ifdef USE_SENSOR
uint32_t max_alloc = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL);
if (this->block_sensor_ != nullptr) { if (this->block_sensor_ != nullptr) {
this->block_sensor_->publish_state(heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL)); this->block_sensor_->publish_state(max_alloc);
}
if (this->min_free_sensor_ != nullptr) {
this->min_free_sensor_->publish_state(heap_caps_get_minimum_free_size(MALLOC_CAP_INTERNAL));
}
if (this->fragmentation_sensor_ != nullptr) {
uint32_t free_heap = heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
if (free_heap > 0) {
float fragmentation = 100.0f - (100.0f * max_alloc / free_heap);
this->fragmentation_sensor_->publish_state(fragmentation);
}
} }
if (this->psram_sensor_ != nullptr) { if (this->psram_sensor_ != nullptr) {
this->psram_sensor_->publish_state(heap_caps_get_free_size(MALLOC_CAP_SPIRAM)); this->psram_sensor_->publish_state(heap_caps_get_free_size(MALLOC_CAP_SPIRAM));

View File

@@ -3,21 +3,80 @@
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <Esp.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 esphome {
namespace debug { namespace debug {
static const char *const TAG = "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) { const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) {
char *buf = buffer.data(); // Copy from flash to provided buffer
#if !defined(CLANG_TIDY) strncpy_P(buffer.data(), (PGM_P) get_reset_reason_str(resetInfo.reason), RESET_REASON_BUFFER_SIZE - 1);
String reason = ESP.getResetReason(); // NOLINT buffer[RESET_REASON_BUFFER_SIZE - 1] = '\0';
snprintf_P(buf, RESET_REASON_BUFFER_SIZE, PSTR("%s"), reason.c_str()); return buffer.data();
return buf;
#else
buf[0] = '\0';
return buf;
#endif
} }
const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { 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; constexpr size_t size = DEVICE_INFO_BUFFER_SIZE;
char *buf = buffer.data(); char *buf = buffer.data();
const char *flash_mode; const LogString *flash_mode;
switch (ESP.getFlashChipMode()) { // NOLINT(readability-static-accessed-through-instance) switch (ESP.getFlashChipMode()) { // NOLINT(readability-static-accessed-through-instance)
case FM_QIO: case FM_QIO:
flash_mode = "QIO"; flash_mode = LOG_STR("QIO");
break; break;
case FM_QOUT: case FM_QOUT:
flash_mode = "QOUT"; flash_mode = LOG_STR("QOUT");
break; break;
case FM_DIO: case FM_DIO:
flash_mode = "DIO"; flash_mode = LOG_STR("DIO");
break; break;
case FM_DOUT: case FM_DOUT:
flash_mode = "DOUT"; flash_mode = LOG_STR("DOUT");
break; break;
default: default:
flash_mode = "UNKNOWN"; flash_mode = LOG_STR("UNKNOWN");
} }
uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT(readability-static-accessed-through-instance)
uint32_t flash_speed = ESP.getFlashChipSpeed() / 1000000; // NOLINT 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, flash_mode); ESP_LOGD(TAG, "Flash Chip: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed,
pos = buf_append(buf, size, pos, "|Flash: %" PRIu32 "kB Speed:%" PRIu32 "MHz Mode:%s", flash_size, flash_speed, LOG_STR_ARG(flash_mode));
flash_mode); pos = buf_append_printf(buf, size, pos, "|Flash: %" PRIu32 "kB Speed:%" PRIu32 "MHz Mode:%s", flash_size, flash_speed,
LOG_STR_ARG(flash_mode));
#if !defined(CLANG_TIDY)
char reason_buffer[RESET_REASON_BUFFER_SIZE]; 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(); uint32_t chip_id = ESP.getChipId();
uint8_t boot_version = ESP.getBootVersion(); uint8_t boot_version = ESP.getBootVersion();
uint8_t boot_mode = ESP.getBootMode(); uint8_t boot_mode = ESP.getBootMode();
uint8_t cpu_freq = ESP.getCpuFreqMHz(); uint8_t cpu_freq = ESP.getCpuFreqMHz();
uint32_t flash_chip_id = ESP.getFlashChipId(); uint32_t flash_chip_id = ESP.getFlashChipId();
const char *sdk_version = ESP.getSdkVersion();
// NOLINTEND(readability-static-accessed-through-instance)
ESP_LOGD(TAG, ESP_LOGD(TAG,
"Chip ID: 0x%08" PRIX32 "\n" "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" "Flash Chip ID=0x%08" PRIX32 "\n"
"Reset Reason: %s\n" "Reset Reason: %s\n"
"Reset Info: %s", "Reset Info: %s",
chip_id, ESP.getSdkVersion(), ESP.getCoreVersion().c_str(), boot_version, boot_mode, cpu_freq, flash_chip_id, chip_id, sdk_version, get_core_version_str(core_version_buffer), boot_version, boot_mode, cpu_freq,
reset_reason, ESP.getResetInfo().c_str()); flash_chip_id, reset_reason, get_reset_info_str(reset_info_buffer, resetInfo.reason));
pos = buf_append(buf, size, pos, "|Chip: 0x%08" PRIX32, chip_id); pos = buf_append_printf(buf, size, pos, "|Chip: 0x%08" PRIX32, chip_id);
pos = buf_append(buf, size, pos, "|SDK: %s", ESP.getSdkVersion()); pos = buf_append_printf(buf, size, pos, "|SDK: %s", sdk_version);
pos = buf_append(buf, size, pos, "|Core: %s", ESP.getCoreVersion().c_str()); pos = buf_append_printf(buf, size, pos, "|Core: %s", get_core_version_str(core_version_buffer));
pos = buf_append(buf, size, pos, "|Boot: %u", boot_version); pos = buf_append_printf(buf, size, pos, "|Boot: %u", boot_version);
pos = buf_append(buf, size, pos, "|Mode: %u", boot_mode); pos = buf_append_printf(buf, size, pos, "|Mode: %u", boot_mode);
pos = buf_append(buf, size, pos, "|CPU: %u", cpu_freq); pos = buf_append_printf(buf, size, pos, "|CPU: %u", cpu_freq);
pos = buf_append(buf, size, pos, "|Flash: 0x%08" PRIX32, flash_chip_id); pos = buf_append_printf(buf, size, pos, "|Flash: 0x%08" PRIX32, flash_chip_id);
pos = buf_append(buf, size, pos, "|Reset: %s", reset_reason); pos = buf_append_printf(buf, size, pos, "|Reset: %s", reset_reason);
pos = buf_append(buf, size, pos, "|%s", ESP.getResetInfo().c_str()); pos = buf_append_printf(buf, size, pos, "|%s", get_reset_info_str(reset_info_buffer, resetInfo.reason));
#endif
return pos; return pos;
} }

View File

@@ -36,12 +36,12 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
lt_get_version(), lt_cpu_get_model_name(), lt_cpu_get_model(), lt_cpu_get_freq_mhz(), mac_id, lt_get_version(), lt_cpu_get_model_name(), lt_cpu_get_model(), lt_cpu_get_freq_mhz(), mac_id,
lt_get_board_code(), flash_kib, ram_kib, reset_reason); lt_get_board_code(), flash_kib, ram_kib, reset_reason);
pos = buf_append(buf, size, pos, "|Version: %s", LT_BANNER_STR + 10); pos = buf_append_printf(buf, size, pos, "|Version: %s", LT_BANNER_STR + 10);
pos = buf_append(buf, size, pos, "|Reset Reason: %s", reset_reason); pos = buf_append_printf(buf, size, pos, "|Reset Reason: %s", reset_reason);
pos = buf_append(buf, size, pos, "|Chip Name: %s", lt_cpu_get_model_name()); pos = buf_append_printf(buf, size, pos, "|Chip Name: %s", lt_cpu_get_model_name());
pos = buf_append(buf, size, pos, "|Chip ID: 0x%06" PRIX32, mac_id); pos = buf_append_printf(buf, size, pos, "|Chip ID: 0x%06" PRIX32, mac_id);
pos = buf_append(buf, size, pos, "|Flash: %" PRIu32 " KiB", flash_kib); pos = buf_append_printf(buf, size, pos, "|Flash: %" PRIu32 " KiB", flash_kib);
pos = buf_append(buf, size, pos, "|RAM: %" PRIu32 " KiB", ram_kib); pos = buf_append_printf(buf, size, pos, "|RAM: %" PRIu32 " KiB", ram_kib);
return pos; return pos;
} }
@@ -51,6 +51,9 @@ void DebugComponent::update_platform_() {
if (this->block_sensor_ != nullptr) { if (this->block_sensor_ != nullptr) {
this->block_sensor_->publish_state(lt_heap_get_max_alloc()); this->block_sensor_->publish_state(lt_heap_get_max_alloc());
} }
if (this->min_free_sensor_ != nullptr) {
this->min_free_sensor_->publish_state(lt_heap_get_min_free());
}
#endif #endif
} }

View File

@@ -19,7 +19,7 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
uint32_t cpu_freq = rp2040.f_cpu(); uint32_t cpu_freq = rp2040.f_cpu();
ESP_LOGD(TAG, "CPU Frequency: %" PRIu32, cpu_freq); ESP_LOGD(TAG, "CPU Frequency: %" PRIu32, cpu_freq);
pos = buf_append(buf, size, pos, "|CPU Frequency: %" PRIu32, cpu_freq); pos = buf_append_printf(buf, size, pos, "|CPU Frequency: %" PRIu32, cpu_freq);
return pos; return pos;
} }

View File

@@ -20,9 +20,9 @@ static size_t append_reset_reason(char *buf, size_t size, size_t pos, bool set,
return pos; return pos;
} }
if (pos > 0) { if (pos > 0) {
pos = buf_append(buf, size, pos, ", "); pos = buf_append_printf(buf, size, pos, ", ");
} }
return buf_append(buf, size, pos, "%s", reason); return buf_append_printf(buf, size, pos, "%s", reason);
} }
static inline uint32_t read_mem_u32(uintptr_t addr) { static inline uint32_t read_mem_u32(uintptr_t addr) {
@@ -132,6 +132,26 @@ void DebugComponent::log_partition_info_() {
flash_area_foreach(fa_cb, nullptr); 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) { 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; constexpr size_t size = DEVICE_INFO_BUFFER_SIZE;
char *buf = buffer.data(); char *buf = buffer.data();
@@ -140,48 +160,28 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
const char *supply_status = const char *supply_status =
(nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_NORMAL) ? "Normal voltage." : "High voltage."; (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_NORMAL) ? "Normal voltage." : "High voltage.";
ESP_LOGD(TAG, "Main supply status: %s", supply_status); ESP_LOGD(TAG, "Main supply status: %s", supply_status);
pos = buf_append(buf, size, pos, "|Main supply status: %s", supply_status); pos = buf_append_printf(buf, size, pos, "|Main supply status: %s", supply_status);
// Regulator stage 0 // Regulator stage 0
if (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_HIGH) { 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_type = nrf_power_dcdcen_vddh_get(NRF_POWER) ? "DC/DC" : "LDO";
const char *reg0_voltage; const char *reg0_voltage = regout0_to_str((NRF_UICR->REGOUT0 & UICR_REGOUT0_VOUT_Msk) >> UICR_REGOUT0_VOUT_Pos);
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";
}
ESP_LOGD(TAG, "Regulator stage 0: %s, %s", reg0_type, reg0_voltage); ESP_LOGD(TAG, "Regulator stage 0: %s, %s", reg0_type, reg0_voltage);
pos = buf_append(buf, size, pos, "|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 { } else {
ESP_LOGD(TAG, "Regulator stage 0: disabled"); ESP_LOGD(TAG, "Regulator stage 0: disabled");
pos = buf_append(buf, size, pos, "|Regulator stage 0: disabled"); pos = buf_append_printf(buf, size, pos, "|Regulator stage 0: disabled");
} }
// Regulator stage 1 // Regulator stage 1
const char *reg1_type = nrf_power_dcdcen_get(NRF_POWER) ? "DC/DC" : "LDO"; const char *reg1_type = nrf_power_dcdcen_get(NRF_POWER) ? "DC/DC" : "LDO";
ESP_LOGD(TAG, "Regulator stage 1: %s", reg1_type); ESP_LOGD(TAG, "Regulator stage 1: %s", reg1_type);
pos = buf_append(buf, size, pos, "|Regulator stage 1: %s", reg1_type); pos = buf_append_printf(buf, size, pos, "|Regulator stage 1: %s", reg1_type);
// USB power state // USB power state
const char *usb_state; const char *usb_state;
@@ -195,7 +195,7 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
usb_state = "disconnected"; usb_state = "disconnected";
} }
ESP_LOGD(TAG, "USB power state: %s", usb_state); ESP_LOGD(TAG, "USB power state: %s", usb_state);
pos = buf_append(buf, size, pos, "|USB power state: %s", usb_state); pos = buf_append_printf(buf, size, pos, "|USB power state: %s", usb_state);
// Power-fail comparator // Power-fail comparator
bool enabled; bool enabled;
@@ -300,14 +300,14 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
break; break;
} }
ESP_LOGD(TAG, "Power-fail comparator: %s, VDDH: %s", pof_voltage, vddh_voltage); ESP_LOGD(TAG, "Power-fail comparator: %s, VDDH: %s", pof_voltage, vddh_voltage);
pos = buf_append(buf, size, pos, "|Power-fail comparator: %s, VDDH: %s", pof_voltage, vddh_voltage); pos = buf_append_printf(buf, size, pos, "|Power-fail comparator: %s, VDDH: %s", pof_voltage, vddh_voltage);
} else { } else {
ESP_LOGD(TAG, "Power-fail comparator: %s", pof_voltage); ESP_LOGD(TAG, "Power-fail comparator: %s", pof_voltage);
pos = buf_append(buf, size, pos, "|Power-fail comparator: %s", pof_voltage); pos = buf_append_printf(buf, size, pos, "|Power-fail comparator: %s", pof_voltage);
} }
} else { } else {
ESP_LOGD(TAG, "Power-fail comparator: disabled"); ESP_LOGD(TAG, "Power-fail comparator: disabled");
pos = buf_append(buf, size, pos, "|Power-fail comparator: disabled"); pos = buf_append_printf(buf, size, pos, "|Power-fail comparator: disabled");
} }
auto package = [](uint32_t value) { auto package = [](uint32_t value) {

View File

@@ -11,6 +11,9 @@ from esphome.const import (
ENTITY_CATEGORY_DIAGNOSTIC, ENTITY_CATEGORY_DIAGNOSTIC,
ICON_COUNTER, ICON_COUNTER,
ICON_TIMER, ICON_TIMER,
PLATFORM_BK72XX,
PLATFORM_LN882X,
PLATFORM_RTL87XX,
UNIT_BYTES, UNIT_BYTES,
UNIT_HERTZ, UNIT_HERTZ,
UNIT_MILLISECOND, UNIT_MILLISECOND,
@@ -25,6 +28,7 @@ from . import ( # noqa: F401 pylint: disable=unused-import
DEPENDENCIES = ["debug"] DEPENDENCIES = ["debug"]
CONF_MIN_FREE = "min_free"
CONF_PSRAM = "psram" CONF_PSRAM = "psram"
CONFIG_SCHEMA = { CONFIG_SCHEMA = {
@@ -42,8 +46,14 @@ CONFIG_SCHEMA = {
entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
), ),
cv.Optional(CONF_FRAGMENTATION): cv.All( cv.Optional(CONF_FRAGMENTATION): cv.All(
cv.only_on_esp8266, cv.Any(
cv.require_framework_version(esp8266_arduino=cv.Version(2, 5, 2)), cv.All(
cv.only_on_esp8266,
cv.require_framework_version(esp8266_arduino=cv.Version(2, 5, 2)),
),
cv.only_on_esp32,
msg="This feature is only available on ESP8266 (Arduino 2.5.2+) and ESP32",
),
sensor.sensor_schema( sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT, unit_of_measurement=UNIT_PERCENT,
icon=ICON_COUNTER, icon=ICON_COUNTER,
@@ -51,6 +61,19 @@ CONFIG_SCHEMA = {
entity_category=ENTITY_CATEGORY_DIAGNOSTIC, entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
), ),
), ),
cv.Optional(CONF_MIN_FREE): cv.All(
cv.Any(
cv.only_on_esp32,
cv.only_on([PLATFORM_BK72XX, PLATFORM_LN882X, PLATFORM_RTL87XX]),
msg="This feature is only available on ESP32 and LibreTiny (BK72xx, LN882x, RTL87xx)",
),
sensor.sensor_schema(
unit_of_measurement=UNIT_BYTES,
icon=ICON_COUNTER,
accuracy_decimals=0,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
),
cv.Optional(CONF_LOOP_TIME): sensor.sensor_schema( cv.Optional(CONF_LOOP_TIME): sensor.sensor_schema(
unit_of_measurement=UNIT_MILLISECOND, unit_of_measurement=UNIT_MILLISECOND,
icon=ICON_TIMER, icon=ICON_TIMER,
@@ -93,6 +116,10 @@ async def to_code(config):
sens = await sensor.new_sensor(fragmentation_conf) sens = await sensor.new_sensor(fragmentation_conf)
cg.add(debug_component.set_fragmentation_sensor(sens)) cg.add(debug_component.set_fragmentation_sensor(sens))
if min_free_conf := config.get(CONF_MIN_FREE):
sens = await sensor.new_sensor(min_free_conf)
cg.add(debug_component.set_min_free_sensor(sens))
if loop_time_conf := config.get(CONF_LOOP_TIME): if loop_time_conf := config.get(CONF_LOOP_TIME):
sens = await sensor.new_sensor(loop_time_conf) sens = await sensor.new_sensor(loop_time_conf)
cg.add(debug_component.set_loop_time_sensor(sens)) cg.add(debug_component.set_loop_time_sensor(sens))

View File

@@ -127,7 +127,9 @@ DetRangeCfgCommand::DetRangeCfgCommand(float min1, float max1, float min2, float
this->min2_ = min2 = this->max2_ = max2 = this->min3_ = min3 = this->max3_ = max3 = this->min4_ = min4 = this->min2_ = min2 = this->max2_ = max2 = this->min3_ = min3 = this->max3_ = max3 = this->min4_ = min4 =
this->max4_ = max4 = -1; this->max4_ = max4 = -1;
this->cmd_ = str_sprintf("detRangeCfg -1 %.0f %.0f", min1 / 0.15, max1 / 0.15); char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f", min1 / 0.15, max1 / 0.15);
this->cmd_ = buf;
} else if (min3 < 0 || max3 < 0) { } else if (min3 < 0 || max3 < 0) {
this->min1_ = min1 = round(min1 / 0.15) * 0.15; this->min1_ = min1 = round(min1 / 0.15) * 0.15;
this->max1_ = max1 = round(max1 / 0.15) * 0.15; this->max1_ = max1 = round(max1 / 0.15) * 0.15;
@@ -135,7 +137,10 @@ DetRangeCfgCommand::DetRangeCfgCommand(float min1, float max1, float min2, float
this->max2_ = max2 = round(max2 / 0.15) * 0.15; this->max2_ = max2 = round(max2 / 0.15) * 0.15;
this->min3_ = min3 = this->max3_ = max3 = this->min4_ = min4 = this->max4_ = max4 = -1; this->min3_ = min3 = this->max3_ = max3 = this->min4_ = min4 = this->max4_ = max4 = -1;
this->cmd_ = str_sprintf("detRangeCfg -1 %.0f %.0f %.0f %.0f", min1 / 0.15, max1 / 0.15, min2 / 0.15, max2 / 0.15); char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null
snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f", min1 / 0.15, max1 / 0.15, min2 / 0.15,
max2 / 0.15);
this->cmd_ = buf;
} else if (min4 < 0 || max4 < 0) { } else if (min4 < 0 || max4 < 0) {
this->min1_ = min1 = round(min1 / 0.15) * 0.15; this->min1_ = min1 = round(min1 / 0.15) * 0.15;
this->max1_ = max1 = round(max1 / 0.15) * 0.15; this->max1_ = max1 = round(max1 / 0.15) * 0.15;
@@ -145,9 +150,10 @@ DetRangeCfgCommand::DetRangeCfgCommand(float min1, float max1, float min2, float
this->max3_ = max3 = round(max3 / 0.15) * 0.15; this->max3_ = max3 = round(max3 / 0.15) * 0.15;
this->min4_ = min4 = this->max4_ = max4 = -1; this->min4_ = min4 = this->max4_ = max4 = -1;
this->cmd_ = str_sprintf("detRangeCfg -1 " char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null
"%.0f %.0f %.0f %.0f %.0f %.0f", snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f %.0f %.0f", min1 / 0.15, max1 / 0.15, min2 / 0.15,
min1 / 0.15, max1 / 0.15, min2 / 0.15, max2 / 0.15, min3 / 0.15, max3 / 0.15); max2 / 0.15, min3 / 0.15, max3 / 0.15);
this->cmd_ = buf;
} else { } else {
this->min1_ = min1 = round(min1 / 0.15) * 0.15; this->min1_ = min1 = round(min1 / 0.15) * 0.15;
this->max1_ = max1 = round(max1 / 0.15) * 0.15; this->max1_ = max1 = round(max1 / 0.15) * 0.15;
@@ -158,10 +164,10 @@ DetRangeCfgCommand::DetRangeCfgCommand(float min1, float max1, float min2, float
this->min4_ = min4 = round(min4 / 0.15) * 0.15; this->min4_ = min4 = round(min4 / 0.15) * 0.15;
this->max4_ = max4 = round(max4 / 0.15) * 0.15; this->max4_ = max4 = round(max4 / 0.15) * 0.15;
this->cmd_ = str_sprintf("detRangeCfg -1 " char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null
"%.0f %.0f %.0f %.0f %.0f %.0f %.0f %.0f", snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f %.0f %.0f %.0f %.0f", min1 / 0.15, max1 / 0.15,
min1 / 0.15, max1 / 0.15, min2 / 0.15, max2 / 0.15, min3 / 0.15, max3 / 0.15, min4 / 0.15, min2 / 0.15, max2 / 0.15, min3 / 0.15, max3 / 0.15, min4 / 0.15, max4 / 0.15);
max4 / 0.15); this->cmd_ = buf;
} }
this->min1_ = min1; this->min1_ = min1;
@@ -203,7 +209,10 @@ SetLatencyCommand::SetLatencyCommand(float delay_after_detection, float delay_af
delay_after_disappear = std::round(delay_after_disappear / 0.025f) * 0.025f; delay_after_disappear = std::round(delay_after_disappear / 0.025f) * 0.025f;
this->delay_after_detection_ = clamp(delay_after_detection, 0.0f, 1638.375f); this->delay_after_detection_ = clamp(delay_after_detection, 0.0f, 1638.375f);
this->delay_after_disappear_ = clamp(delay_after_disappear, 0.0f, 1638.375f); this->delay_after_disappear_ = clamp(delay_after_disappear, 0.0f, 1638.375f);
this->cmd_ = str_sprintf("setLatency %.03f %.03f", this->delay_after_detection_, this->delay_after_disappear_); // max 32: "setLatency "(11) + float(8) + " "(1) + float(8) + null, rounded to 32
char buf[32];
snprintf(buf, sizeof(buf), "setLatency %.03f %.03f", this->delay_after_detection_, this->delay_after_disappear_);
this->cmd_ = buf;
}; };
uint8_t SetLatencyCommand::on_message(std::string &message) { uint8_t SetLatencyCommand::on_message(std::string &message) {

View File

@@ -75,8 +75,8 @@ class SetLatencyCommand : public Command {
class SensorCfgStartCommand : public Command { class SensorCfgStartCommand : public Command {
public: public:
SensorCfgStartCommand(bool startup_mode) : startup_mode_(startup_mode) { SensorCfgStartCommand(bool startup_mode) : startup_mode_(startup_mode) {
char tmp_cmd[20] = {0}; char tmp_cmd[20]; // "sensorCfgStart " (15) + "0/1" (1) + null = 17
sprintf(tmp_cmd, "sensorCfgStart %d", startup_mode); buf_append_printf(tmp_cmd, sizeof(tmp_cmd), 0, "sensorCfgStart %d", startup_mode);
cmd_ = std::string(tmp_cmd); cmd_ = std::string(tmp_cmd);
} }
uint8_t on_message(std::string &message) override; uint8_t on_message(std::string &message) override;
@@ -142,8 +142,8 @@ class SensitivityCommand : public Command {
SensitivityCommand(uint8_t sensitivity) : sensitivity_(sensitivity) { SensitivityCommand(uint8_t sensitivity) : sensitivity_(sensitivity) {
if (sensitivity > 9) if (sensitivity > 9)
sensitivity_ = sensitivity = 9; sensitivity_ = sensitivity = 9;
char tmp_cmd[20] = {0}; char tmp_cmd[20]; // "setSensitivity " (15) + "0-9" (1) + null = 17
sprintf(tmp_cmd, "setSensitivity %d", sensitivity); buf_append_printf(tmp_cmd, sizeof(tmp_cmd), 0, "setSensitivity %d", sensitivity);
cmd_ = std::string(tmp_cmd); cmd_ = std::string(tmp_cmd);
}; };
uint8_t on_message(std::string &message) override; uint8_t on_message(std::string &message) override;

View File

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

View File

@@ -25,29 +25,13 @@ dsmr_ns = cg.esphome_ns.namespace("esphome::dsmr")
Dsmr = dsmr_ns.class_("Dsmr", cg.Component, uart.UARTDevice) Dsmr = dsmr_ns.class_("Dsmr", cg.Component, uart.UARTDevice)
def _validate_key(value):
value = cv.string_strict(value)
parts = [value[i : i + 2] for i in range(0, len(value), 2)]
if len(parts) != 16:
raise cv.Invalid("Decryption key must consist of 16 hexadecimal numbers")
parts_int = []
if any(len(part) != 2 for part in parts):
raise cv.Invalid("Decryption key must be format XX")
for part in parts:
try:
parts_int.append(int(part, 16))
except ValueError:
# pylint: disable=raise-missing-from
raise cv.Invalid("Decryption key must be hex values from 00 to FF")
return "".join(f"{part:02X}" for part in parts_int)
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
cv.Schema( cv.Schema(
{ {
cv.GenerateID(): cv.declare_id(Dsmr), cv.GenerateID(): cv.declare_id(Dsmr),
cv.Optional(CONF_DECRYPTION_KEY): _validate_key, cv.Optional(CONF_DECRYPTION_KEY): lambda value: cv.bind_key(
value, name="Decryption key"
),
cv.Optional(CONF_CRC_CHECK, default=True): cv.boolean, cv.Optional(CONF_CRC_CHECK, default=True): cv.boolean,
cv.Optional(CONF_GAS_MBUS_ID, default=1): cv.int_, cv.Optional(CONF_GAS_MBUS_ID, default=1): cv.int_,
cv.Optional(CONF_WATER_MBUS_ID, default=2): cv.int_, cv.Optional(CONF_WATER_MBUS_ID, default=2): cv.int_,

View File

@@ -1,4 +1,5 @@
#include "dsmr.h" #include "dsmr.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <AES.h> #include <AES.h>
@@ -294,8 +295,8 @@ void Dsmr::dump_config() {
DSMR_TEXT_SENSOR_LIST(DSMR_LOG_TEXT_SENSOR, ) DSMR_TEXT_SENSOR_LIST(DSMR_LOG_TEXT_SENSOR, )
} }
void Dsmr::set_decryption_key(const std::string &decryption_key) { void Dsmr::set_decryption_key(const char *decryption_key) {
if (decryption_key.empty()) { if (decryption_key == nullptr || decryption_key[0] == '\0') {
ESP_LOGI(TAG, "Disabling decryption"); ESP_LOGI(TAG, "Disabling decryption");
this->decryption_key_.clear(); this->decryption_key_.clear();
if (this->crypt_telegram_ != nullptr) { if (this->crypt_telegram_ != nullptr) {
@@ -305,21 +306,15 @@ void Dsmr::set_decryption_key(const std::string &decryption_key) {
return; return;
} }
if (decryption_key.length() != 32) { if (!parse_hex(decryption_key, this->decryption_key_, 16)) {
ESP_LOGE(TAG, "Error, decryption key must be 32 character long"); ESP_LOGE(TAG, "Error, decryption key must be 32 hex characters");
this->decryption_key_.clear();
return; return;
} }
this->decryption_key_.clear();
ESP_LOGI(TAG, "Decryption key is set"); ESP_LOGI(TAG, "Decryption key is set");
// Verbose level prints decryption key // Verbose level prints decryption key
ESP_LOGV(TAG, "Using decryption key: %s", decryption_key.c_str()); ESP_LOGV(TAG, "Using decryption key: %s", decryption_key);
char temp[3] = {0};
for (int i = 0; i < 16; i++) {
strncpy(temp, &(decryption_key.c_str()[i * 2]), 2);
this->decryption_key_.push_back(std::strtoul(temp, nullptr, 16));
}
if (this->crypt_telegram_ == nullptr) { if (this->crypt_telegram_ == nullptr) {
this->crypt_telegram_ = new uint8_t[this->max_telegram_len_]; // NOLINT this->crypt_telegram_ = new uint8_t[this->max_telegram_len_]; // NOLINT

View File

@@ -63,7 +63,7 @@ class Dsmr : public Component, public uart::UARTDevice {
void dump_config() override; void dump_config() override;
void set_decryption_key(const std::string &decryption_key); void set_decryption_key(const char *decryption_key);
void set_max_telegram_length(size_t length) { this->max_telegram_len_ = length; } void set_max_telegram_length(size_t length) { this->max_telegram_len_ = length; }
void set_request_pin(GPIOPin *request_pin) { this->request_pin_ = request_pin; } void set_request_pin(GPIOPin *request_pin) { this->request_pin_ = request_pin; }
void set_request_interval(uint32_t interval) { this->request_interval_ = interval; } void set_request_interval(uint32_t interval) { this->request_interval_ = interval; }

View File

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

View File

@@ -34,6 +34,7 @@ from esphome.const import (
KEY_CORE, KEY_CORE,
KEY_FRAMEWORK_VERSION, KEY_FRAMEWORK_VERSION,
KEY_NAME, KEY_NAME,
KEY_NATIVE_IDF,
KEY_TARGET_FRAMEWORK, KEY_TARGET_FRAMEWORK,
KEY_TARGET_PLATFORM, KEY_TARGET_PLATFORM,
PLATFORM_ESP32, PLATFORM_ESP32,
@@ -53,6 +54,7 @@ from .const import ( # noqa
KEY_COMPONENTS, KEY_COMPONENTS,
KEY_ESP32, KEY_ESP32,
KEY_EXTRA_BUILD_FILES, KEY_EXTRA_BUILD_FILES,
KEY_FLASH_SIZE,
KEY_PATH, KEY_PATH,
KEY_REF, KEY_REF,
KEY_REPO, KEY_REPO,
@@ -180,6 +182,12 @@ def set_core_data(config):
path=[CONF_CPU_FREQUENCY], path=[CONF_CPU_FREQUENCY],
) )
if variant == VARIANT_ESP32P4 and cpu_frequency == "400MHZ":
_LOGGER.warning(
"400MHz on ESP32-P4 is experimental and may not boot. "
"Consider using 360MHz instead. See https://github.com/esphome/esphome/issues/13425"
)
CORE.data[KEY_ESP32] = {} CORE.data[KEY_ESP32] = {}
CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_ESP32 CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_ESP32
conf = config[CONF_FRAMEWORK] conf = config[CONF_FRAMEWORK]
@@ -199,6 +207,7 @@ def set_core_data(config):
) )
CORE.data[KEY_ESP32][KEY_BOARD] = config[CONF_BOARD] 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_VARIANT] = variant
CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES] = {} CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES] = {}
@@ -339,7 +348,12 @@ def add_extra_build_file(filename: str, path: Path) -> bool:
def _format_framework_arduino_version(ver: cv.Version) -> str: def _format_framework_arduino_version(ver: cv.Version) -> str:
# format the given arduino (https://github.com/espressif/arduino-esp32/releases) version to # format the given arduino (https://github.com/espressif/arduino-esp32/releases) version to
# a PIO pioarduino/framework-arduinoespressif32 value # a PIO pioarduino/framework-arduinoespressif32 value
return f"pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/{str(ver)}/esp32-{str(ver)}.zip" # 3.3.6+ changed filename from esp32-{ver}.zip to esp32-core-{ver}.tar.xz
if ver >= cv.Version(3, 3, 6):
filename = f"esp32-core-{ver}.tar.xz"
else:
filename = f"esp32-{ver}.zip"
return f"pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/{ver}/{filename}"
def _format_framework_espidf_version(ver: cv.Version, release: str) -> str: def _format_framework_espidf_version(ver: cv.Version, release: str) -> str:
@@ -374,11 +388,12 @@ def _is_framework_url(source: str) -> bool:
# The default/recommended arduino framework version # The default/recommended arduino framework version
# - https://github.com/espressif/arduino-esp32/releases # - https://github.com/espressif/arduino-esp32/releases
ARDUINO_FRAMEWORK_VERSION_LOOKUP = { ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
"recommended": cv.Version(3, 3, 5), "recommended": cv.Version(3, 3, 6),
"latest": cv.Version(3, 3, 5), "latest": cv.Version(3, 3, 6),
"dev": cv.Version(3, 3, 5), "dev": cv.Version(3, 3, 6),
} }
ARDUINO_PLATFORM_VERSION_LOOKUP = { ARDUINO_PLATFORM_VERSION_LOOKUP = {
cv.Version(3, 3, 6): cv.Version(55, 3, 36),
cv.Version(3, 3, 5): cv.Version(55, 3, 35), cv.Version(3, 3, 5): cv.Version(55, 3, 35),
cv.Version(3, 3, 4): cv.Version(55, 3, 31, "2"), cv.Version(3, 3, 4): cv.Version(55, 3, 31, "2"),
cv.Version(3, 3, 3): cv.Version(55, 3, 31, "2"), cv.Version(3, 3, 3): cv.Version(55, 3, 31, "2"),
@@ -396,6 +411,7 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = {
# These versions correspond to pioarduino/esp-idf releases # These versions correspond to pioarduino/esp-idf releases
# See: https://github.com/pioarduino/esp-idf/releases # See: https://github.com/pioarduino/esp-idf/releases
ARDUINO_IDF_VERSION_LOOKUP = { ARDUINO_IDF_VERSION_LOOKUP = {
cv.Version(3, 3, 6): cv.Version(5, 5, 2),
cv.Version(3, 3, 5): cv.Version(5, 5, 2), cv.Version(3, 3, 5): cv.Version(5, 5, 2),
cv.Version(3, 3, 4): cv.Version(5, 5, 1), cv.Version(3, 3, 4): cv.Version(5, 5, 1),
cv.Version(3, 3, 3): cv.Version(5, 5, 1), cv.Version(3, 3, 3): cv.Version(5, 5, 1),
@@ -418,7 +434,7 @@ ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
"dev": cv.Version(5, 5, 2), "dev": cv.Version(5, 5, 2),
} }
ESP_IDF_PLATFORM_VERSION_LOOKUP = { ESP_IDF_PLATFORM_VERSION_LOOKUP = {
cv.Version(5, 5, 2): cv.Version(55, 3, 35), cv.Version(5, 5, 2): cv.Version(55, 3, 36),
cv.Version(5, 5, 1): cv.Version(55, 3, 31, "2"), cv.Version(5, 5, 1): cv.Version(55, 3, 31, "2"),
cv.Version(5, 5, 0): cv.Version(55, 3, 31, "2"), cv.Version(5, 5, 0): cv.Version(55, 3, 31, "2"),
cv.Version(5, 4, 3): cv.Version(55, 3, 32), cv.Version(5, 4, 3): cv.Version(55, 3, 32),
@@ -435,9 +451,9 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = {
# The platform-espressif32 version # The platform-espressif32 version
# - https://github.com/pioarduino/platform-espressif32/releases # - https://github.com/pioarduino/platform-espressif32/releases
PLATFORM_VERSION_LOOKUP = { PLATFORM_VERSION_LOOKUP = {
"recommended": cv.Version(55, 3, 35), "recommended": cv.Version(55, 3, 36),
"latest": cv.Version(55, 3, 35), "latest": cv.Version(55, 3, 36),
"dev": cv.Version(55, 3, 35), "dev": cv.Version(55, 3, 36),
} }
@@ -962,12 +978,54 @@ async def _add_yaml_idf_components(components: list[ConfigType]):
async def to_code(config): async def to_code(config):
cg.add_platformio_option("board", config[CONF_BOARD]) framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE]) conf = config[CONF_FRAMEWORK]
cg.add_platformio_option(
"board_upload.maximum_size", # Check if using native ESP-IDF build (--native-idf)
int(config[CONF_FLASH_SIZE].removesuffix("MB")) * 1024 * 1024, 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.set_cpp_standard("gnu++20")
cg.add_build_flag("-DUSE_ESP32") cg.add_build_flag("-DUSE_ESP32")
cg.add_build_flag("-Wl,-z,noexecstack") cg.add_build_flag("-Wl,-z,noexecstack")
@@ -977,79 +1035,49 @@ async def to_code(config):
cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[variant]) cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[variant])
cg.add_define(ThreadModel.MULTI_ATOMICS) 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]: if conf[CONF_ADVANCED][CONF_IGNORE_EFUSE_CUSTOM_MAC]:
cg.add_define("USE_ESP32_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 # Set the location of the IDF component manager cache
os.environ["IDF_COMPONENT_CACHE_PATH"] = str( os.environ["IDF_COMPONENT_CACHE_PATH"] = str(
CORE.relative_internal_path(".espressif") 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: 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_ESP_IDF")
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF") cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF")
if use_platformio:
cg.add_platformio_option("framework", "espidf")
else: else:
cg.add_platformio_option("framework", "arduino, espidf")
cg.add_build_flag("-DUSE_ARDUINO") cg.add_build_flag("-DUSE_ARDUINO")
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_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( cg.add_define(
"USE_ARDUINO_VERSION_CODE", "USE_ARDUINO_VERSION_CODE",
cg.RawExpression( cg.RawExpression(
f"VERSION_CODE({framework_ver.major}, {framework_ver.minor}, {framework_ver.patch})" 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_PSK_MODES", True)
add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", 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") cg.add_build_flag("-Wno-nonnull-compare")
add_idf_sdkconfig_option(f"CONFIG_IDF_TARGET_{variant}", True) add_idf_sdkconfig_option(f"CONFIG_IDF_TARGET_{variant}", True)
@@ -1196,7 +1224,8 @@ async def to_code(config):
"CONFIG_VFS_SUPPORT_DIR", not advanced[CONF_DISABLE_VFS_SUPPORT_DIR] "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: if CONF_PARTITIONS in config:
add_extra_build_file( add_extra_build_file(
"partitions.csv", CORE.relative_config_path(config[CONF_PARTITIONS]) "partitions.csv", CORE.relative_config_path(config[CONF_PARTITIONS])
@@ -1361,19 +1390,16 @@ def copy_files():
_write_idf_component_yml() _write_idf_component_yml()
if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]: 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: if CORE.using_arduino:
write_file_if_changed( write_file_if_changed(
CORE.relative_build_path("partitions.csv"), CORE.relative_build_path("partitions.csv"),
get_arduino_partition_csv( get_arduino_partition_csv(flash_size),
CORE.platformio_options.get("board_upload.flash_size")
),
) )
else: else:
write_file_if_changed( write_file_if_changed(
CORE.relative_build_path("partitions.csv"), CORE.relative_build_path("partitions.csv"),
get_idf_partition_csv( get_idf_partition_csv(flash_size),
CORE.platformio_options.get("board_upload.flash_size")
),
) )
# IDF build scripts look for version string to put in the build. # IDF build scripts look for version string to put in the build.
# However, if the build path does not have an initialized git repo, # 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_ESP32 = "esp32"
KEY_BOARD = "board" KEY_BOARD = "board"
KEY_FLASH_SIZE = "flash_size"
KEY_VARIANT = "variant" KEY_VARIANT = "variant"
KEY_SDKCONFIG_OPTIONS = "sdkconfig_options" KEY_SDKCONFIG_OPTIONS = "sdkconfig_options"
KEY_COMPONENTS = "components" KEY_COMPONENTS = "components"

View File

@@ -181,7 +181,8 @@ class ESP32Preferences : public ESPPreferences {
if (actual_len != to_save.len) { if (actual_len != to_save.len) {
return true; 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); err = nvs_get_blob(nvs_handle, key_str, stored_data.get(), &actual_len);
if (err != 0) { if (err != 0) {
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err)); ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err));

View File

@@ -46,6 +46,8 @@ class ESPBTUUID {
esp_bt_uuid_t get_uuid() const; esp_bt_uuid_t get_uuid() const;
// Remove before 2026.8.0
ESPDEPRECATED("Use to_str() instead. Removed in 2026.8.0", "2026.2.0")
std::string to_string() const; std::string to_string() const;
const char *to_str(std::span<char, UUID_STR_LEN> output) const; const char *to_str(std::span<char, UUID_STR_LEN> output) const;

View File

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

View File

@@ -44,7 +44,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
void unconditional_disconnect(); void unconditional_disconnect();
void release_services(); 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; } 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 // 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) { if (this->scanner_state_ == ScannerState::RUNNING) {
switch (this->scan_timeout_state_) { switch (this->scan_timeout_state_) {
case ScanTimeoutState::MONITORING: { 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 // Robust time comparison that handles rollover correctly
// This works because unsigned arithmetic wraps around predictably // 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 // 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 ensures all components have had a chance to process pending events
// This is because esp32_ble may not have run yet and called // 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"); ESP_LOGE(TAG, "Scan never terminated, rebooting");
App.reboot(); App.reboot();
break; break;
case ScanTimeoutState::INACTIVE: case ScanTimeoutState::INACTIVE:
// This case should be unreachable - scanner and timeout states are always synchronized
break; 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_(); ClientStateCounts counts = this->count_client_states_();
if (counts != this->client_state_counts_) { if (counts != this->client_state_counts_) {
this->client_state_counts_ = counts; this->client_state_counts_ = counts;
@@ -142,6 +158,7 @@ void ESP32BLETracker::loop() {
this->client_state_counts_.discovered, this->client_state_counts_.disconnecting); 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 || if (this->scanner_state_ == ScannerState::FAILED ||
(this->scan_set_param_failed_ && this->scanner_state_ == ScannerState::RUNNING)) { (this->scan_set_param_failed_ && this->scanner_state_ == ScannerState::RUNNING)) {
this->handle_scanner_failure_(); 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) { if (this->scanner_state_ == ScannerState::IDLE && !counts.connecting && !counts.disconnecting && !counts.discovered) {
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE #ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
this->update_coex_preference_(false); this->update_coex_preference_(false);
@@ -168,8 +187,9 @@ void ESP32BLETracker::loop() {
this->start_scan_(false); // first = false this->start_scan_(false); // first = false
} }
} }
// If there is a discovered client and no connecting // Promote discovered clients: reached when a client's state becomes DISCOVERED (via set_state()),
// clients, then promote the discovered client to ready to connect. // 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: // We check both RUNNING and IDLE states because:
// - RUNNING: gap_scan_event_handler initiates stop_scan_() but promotion can happen immediately // - 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) // - 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 // Start timeout monitoring in loop() instead of using scheduler
// This prevents false reboots when the loop is blocked // This prevents false reboots when the loop is blocked
this->scan_start_time_ = App.get_loop_component_start_time(); this->scan_start_time_ = App.get_loop_component_start_time();
this->scan_timeout_ms_ = this->scan_duration_ * 2000;
this->scan_timeout_state_ = ScanTimeoutState::MONITORING; this->scan_timeout_state_ = ScanTimeoutState::MONITORING;
esp_err_t err = esp_ble_gap_set_scan_params(&this->scan_params_); 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) { void ESP32BLETracker::register_client(ESPBTClient *client) {
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT #ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
client->app_id = ++this->app_id_; 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->clients_.push_back(client);
this->recalculate_advertisement_parser_types(); this->recalculate_advertisement_parser_types();
#endif #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) { void ESP32BLETracker::set_scanner_state_(ScannerState state) {
this->scanner_state_ = state; this->scanner_state_ = state;
this->state_version_++;
for (auto *listener : this->scanner_state_listeners_) { for (auto *listener : this->scanner_state_listeners_) {
listener->on_scanner_state(state); listener->on_scanner_state(state);
} }

View File

@@ -216,6 +216,19 @@ enum class ConnectionType : uint8_t {
V3_WITHOUT_CACHE 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 { class ESPBTClient : public ESPBTDeviceListener {
public: public:
virtual bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, 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; virtual void disconnect() = 0;
bool disconnect_pending() const { return this->want_disconnect_; } bool disconnect_pending() const { return this->want_disconnect_; }
void cancel_pending_disconnect() { this->want_disconnect_ = false; } 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) { virtual void set_state(ClientState st) {
this->state_ = st; this->set_state_internal_(st);
if (st == ClientState::IDLE) { if (st == ClientState::IDLE) {
this->want_disconnect_ = false; 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 // Memory optimized layout
uint8_t app_id; // App IDs are small integers assigned sequentially uint8_t app_id; // App IDs are small integers assigned sequentially
protected: protected:
// Group 1: 1-byte types /// Set state without IDLE handling - use for direct state transitions.
ClientState state_{ClientState::INIT}; /// 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 // want_disconnect_ is set to true when a disconnect is requested
// while the client is connecting. This is used to disconnect the // while the client is connecting. This is used to disconnect the
// client as soon as we get the connection id (conn_id_) from the // client as soon as we get the connection id (conn_id_) from the
// ESP_GATTC_OPEN_EVT event. // ESP_GATTC_OPEN_EVT event.
bool want_disconnect_{false}; 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, class ESP32BLETracker : public Component,
@@ -380,6 +416,16 @@ class ESP32BLETracker : public Component,
// Group 4: 1-byte types (enums, uint8_t, bool) // Group 4: 1-byte types (enums, uint8_t, bool)
uint8_t app_id_{0}; uint8_t app_id_{0};
uint8_t scan_start_fail_count_{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}; ScannerState scanner_state_{ScannerState::IDLE};
bool scan_continuous_; bool scan_continuous_;
bool scan_active_; bool scan_active_;
@@ -396,6 +442,8 @@ class ESP32BLETracker : public Component,
EXCEEDED_WAIT, // Timeout exceeded, waiting one loop before reboot EXCEEDED_WAIT, // Timeout exceeded, waiting one loop before reboot
}; };
uint32_t scan_start_time_{0}; uint32_t scan_start_time_{0};
/// Precomputed timeout value: scan_duration_ * 2000
uint32_t scan_timeout_ms_{0};
ScanTimeoutState scan_timeout_state_{ScanTimeoutState::INACTIVE}; ScanTimeoutState scan_timeout_state_{ScanTimeoutState::INACTIVE};
}; };

View File

@@ -11,6 +11,7 @@
#include <esp_ota_ops.h> #include <esp_ota_ops.h>
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE #ifdef USE_ESP32_HOSTED_HTTP_UPDATE
#include "esphome/components/http_request/http_request.h"
#include "esphome/components/json/json_util.h" #include "esphome/components/json/json_util.h"
#include "esphome/components/network/util.h" #include "esphome/components/network/util.h"
#endif #endif
@@ -69,7 +70,10 @@ void Esp32HostedUpdate::setup() {
// Get coprocessor version // Get coprocessor version
esp_hosted_coprocessor_fwver_t ver_info; esp_hosted_coprocessor_fwver_t ver_info;
if (esp_hosted_get_coprocessor_fwversion(&ver_info) == ESP_OK) { if (esp_hosted_get_coprocessor_fwversion(&ver_info) == ESP_OK) {
this->update_info_.current_version = str_sprintf("%d.%d.%d", ver_info.major1, ver_info.minor1, ver_info.patch1); // 16 bytes: "255.255.255" (11 chars) + null + safety margin
char buf[16];
snprintf(buf, sizeof(buf), "%d.%d.%d", ver_info.major1, ver_info.minor1, ver_info.patch1);
this->update_info_.current_version = buf;
} else { } else {
this->update_info_.current_version = "unknown"; this->update_info_.current_version = "unknown";
} }
@@ -181,15 +185,23 @@ bool Esp32HostedUpdate::fetch_manifest_() {
} }
// Read manifest JSON into string (manifest is small, ~1KB max) // 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; std::string json_str;
json_str.reserve(container->content_length); json_str.reserve(container->content_length);
uint8_t buf[256]; 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) { while (container->get_bytes_read() < container->content_length) {
int read = container->read(buf, sizeof(buf)); int read_or_error = container->read(buf, sizeof(buf));
if (read > 0) { App.feed_wdt();
json_str.append(reinterpret_cast<char *>(buf), read);
}
yield(); 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(); container->end();
@@ -294,32 +306,38 @@ bool Esp32HostedUpdate::stream_firmware_to_coprocessor_() {
} }
// Stream firmware to coprocessor while computing SHA256 // 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; sha256::SHA256 hasher;
hasher.init(); hasher.init();
uint8_t buffer[CHUNK_SIZE]; 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) { 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 // Feed watchdog and give other tasks a chance to run
App.feed_wdt(); App.feed_wdt();
yield(); yield();
// Exit loop if no data available (stream closed or end of data) auto result = http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout);
if (read <= 0) { if (result == http_request::HttpReadLoopResult::RETRY)
if (read < 0) { continue;
ESP_LOGE(TAG, "Stream closed with error"); if (result != http_request::HttpReadLoopResult::DATA) {
esp_hosted_slave_ota_end(); // NOLINT if (result == http_request::HttpReadLoopResult::TIMEOUT) {
container->end(); ESP_LOGE(TAG, "Timeout reading firmware data");
this->status_set_error(LOG_STR("Download failed")); } else {
return false; ESP_LOGE(TAG, "Error reading firmware data: %d", read_or_error);
} }
// read == 0: no more data available, exit loop esp_hosted_slave_ota_end(); // NOLINT
break; container->end();
this->status_set_error(LOG_STR("Download failed"));
return false;
} }
hasher.add(buffer, read); hasher.add(buffer, read_or_error);
err = esp_hosted_slave_ota_write(buffer, read); // NOLINT err = esp_hosted_slave_ota_write(buffer, read_or_error); // NOLINT
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to write OTA data: %s", esp_err_to_name(err)); ESP_LOGE(TAG, "Failed to write OTA data: %s", esp_err_to_name(err));
esp_hosted_slave_ota_end(); // NOLINT esp_hosted_slave_ota_end(); // NOLINT

View File

@@ -6,7 +6,11 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "preferences.h" #include "preferences.h"
#include <Arduino.h> #include <Arduino.h>
#include <Esp.h> #include <core_esp8266_features.h>
extern "C" {
#include <user_interface.h>
}
namespace esphome { namespace esphome {
@@ -16,23 +20,19 @@ void IRAM_ATTR HOT delay(uint32_t ms) { ::delay(ms); }
uint32_t IRAM_ATTR HOT micros() { return ::micros(); } uint32_t IRAM_ATTR HOT micros() { return ::micros(); }
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); }
void arch_restart() { void arch_restart() {
ESP.restart(); // NOLINT(readability-static-accessed-through-instance) system_restart();
// restart() doesn't always end execution // restart() doesn't always end execution
while (true) { // NOLINT(clang-diagnostic-unreachable-code) while (true) { // NOLINT(clang-diagnostic-unreachable-code)
yield(); yield();
} }
} }
void arch_init() {} void arch_init() {}
void IRAM_ATTR HOT arch_feed_wdt() { void IRAM_ATTR HOT arch_feed_wdt() { system_soft_wdt_feed(); }
ESP.wdtFeed(); // NOLINT(readability-static-accessed-through-instance)
}
uint8_t progmem_read_byte(const uint8_t *addr) { uint8_t progmem_read_byte(const uint8_t *addr) {
return pgm_read_byte(addr); // NOLINT return pgm_read_byte(addr); // NOLINT
} }
uint32_t IRAM_ATTR HOT arch_get_cpu_cycle_count() { uint32_t IRAM_ATTR HOT arch_get_cpu_cycle_count() { return esp_get_cycle_count(); }
return ESP.getCycleCount(); // NOLINT(readability-static-accessed-through-instance)
}
uint32_t arch_get_cpu_freq_hz() { return F_CPU; } uint32_t arch_get_cpu_freq_hz() { return F_CPU; }
void force_link_symbols() { void force_link_symbols() {

View File

@@ -99,7 +99,7 @@ void ESP8266GPIOPin::pin_mode(gpio::Flags flags) {
} }
size_t ESP8266GPIOPin::dump_summary(char *buffer, size_t len) const { size_t ESP8266GPIOPin::dump_summary(char *buffer, size_t len) const {
return snprintf(buffer, len, "GPIO%u", this->pin_); return buf_append_printf(buffer, len, 0, "GPIO%u", this->pin_);
} }
bool ESP8266GPIOPin::digital_read() { bool ESP8266GPIOPin::digital_read() {

View File

@@ -12,7 +12,6 @@ extern "C" {
#include "preferences.h" #include "preferences.h"
#include <cstring> #include <cstring>
#include <memory>
namespace esphome::esp8266 { namespace esphome::esp8266 {
@@ -143,16 +142,8 @@ class ESP8266PreferenceBackend : public ESPPreferenceBackend {
return false; return false;
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1; const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
uint32_t stack_buffer[PREF_BUFFER_WORDS]; SmallBufferWithHeapFallback<PREF_BUFFER_WORDS, uint32_t> buffer_alloc(buffer_size);
std::unique_ptr<uint32_t[]> heap_buffer; uint32_t *buffer = buffer_alloc.get();
uint32_t *buffer;
if (buffer_size <= PREF_BUFFER_WORDS) {
buffer = stack_buffer;
} else {
heap_buffer = make_unique<uint32_t[]>(buffer_size);
buffer = heap_buffer.get();
}
memset(buffer, 0, buffer_size * sizeof(uint32_t)); memset(buffer, 0, buffer_size * sizeof(uint32_t));
memcpy(buffer, data, len); memcpy(buffer, data, len);
@@ -167,16 +158,8 @@ class ESP8266PreferenceBackend : public ESPPreferenceBackend {
return false; return false;
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1; const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
uint32_t stack_buffer[PREF_BUFFER_WORDS]; SmallBufferWithHeapFallback<PREF_BUFFER_WORDS, uint32_t> buffer_alloc(buffer_size);
std::unique_ptr<uint32_t[]> heap_buffer; uint32_t *buffer = buffer_alloc.get();
uint32_t *buffer;
if (buffer_size <= PREF_BUFFER_WORDS) {
buffer = stack_buffer;
} else {
heap_buffer = make_unique<uint32_t[]>(buffer_size);
buffer = heap_buffer.get();
}
bool ret = this->in_flash ? load_from_flash(this->offset, buffer, buffer_size) bool ret = this->in_flash ? load_from_flash(this->offset, buffer, buffer_size)
: load_from_rtc(this->offset, buffer, buffer_size); : load_from_rtc(this->offset, buffer, buffer_size);

View File

@@ -90,9 +90,7 @@ async def setup_event_core_(var, config, *, event_types: list[str]):
for conf in config.get(CONF_ON_EVENT, []): for conf in config.get(CONF_ON_EVENT, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation( await automation.build_automation(trigger, [(cg.StringRef, "event_type")], conf)
trigger, [(cg.std_string, "event_type")], conf
)
cg.add(var.set_event_types(event_types)) cg.add(var.set_event_types(event_types))

View File

@@ -14,10 +14,10 @@ template<typename... Ts> class TriggerEventAction : public Action<Ts...>, public
void play(const Ts &...x) override { this->parent_->trigger(this->event_type_.value(x...)); } void play(const Ts &...x) override { this->parent_->trigger(this->event_type_.value(x...)); }
}; };
class EventTrigger : public Trigger<std::string> { class EventTrigger : public Trigger<StringRef> {
public: public:
EventTrigger(Event *event) { EventTrigger(Event *event) {
event->add_on_event_callback([this](const std::string &event_type) { this->trigger(event_type); }); event->add_on_event_callback([this](StringRef event_type) { this->trigger(event_type); });
} }
}; };

View File

@@ -23,7 +23,7 @@ void Event::trigger(const std::string &event_type) {
} }
this->last_event_type_ = found; this->last_event_type_ = found;
ESP_LOGD(TAG, "'%s' >> '%s'", this->get_name().c_str(), this->last_event_type_); ESP_LOGD(TAG, "'%s' >> '%s'", this->get_name().c_str(), this->last_event_type_);
this->event_callback_.call(event_type); this->event_callback_.call(StringRef(found));
#if defined(USE_EVENT) && defined(USE_CONTROLLER_REGISTRY) #if defined(USE_EVENT) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_event(this); ControllerRegistry::notify_event(this);
#endif #endif
@@ -45,7 +45,7 @@ void Event::set_event_types(const std::vector<const char *> &event_types) {
this->last_event_type_ = nullptr; // Reset when types change this->last_event_type_ = nullptr; // Reset when types change
} }
void Event::add_on_event_callback(std::function<void(const std::string &event_type)> &&callback) { void Event::add_on_event_callback(std::function<void(StringRef event_type)> &&callback) {
this->event_callback_.add(std::move(callback)); this->event_callback_.add(std::move(callback));
} }

View File

@@ -70,10 +70,10 @@ class Event : public EntityBase, public EntityBase_DeviceClass {
/// Check if an event has been triggered. /// Check if an event has been triggered.
bool has_event() const { return this->last_event_type_ != nullptr; } bool has_event() const { return this->last_event_type_ != nullptr; }
void add_on_event_callback(std::function<void(const std::string &event_type)> &&callback); void add_on_event_callback(std::function<void(StringRef event_type)> &&callback);
protected: protected:
LazyCallbackManager<void(const std::string &event_type)> event_callback_; LazyCallbackManager<void(StringRef event_type)> event_callback_;
FixedVector<const char *> types_; FixedVector<const char *> types_;
private: private:

View File

@@ -160,7 +160,7 @@ void EZOSensor::loop() {
this->commands_.pop_front(); this->commands_.pop_front();
} }
void EZOSensor::add_command_(const std::string &command, EzoCommandType command_type, uint16_t delay_ms) { void EZOSensor::add_command_(const char *command, EzoCommandType command_type, uint16_t delay_ms) {
std::unique_ptr<EzoCommand> ezo_command(new EzoCommand); std::unique_ptr<EzoCommand> ezo_command(new EzoCommand);
ezo_command->command = command; ezo_command->command = command;
ezo_command->command_type = command_type; ezo_command->command_type = command_type;
@@ -169,13 +169,17 @@ void EZOSensor::add_command_(const std::string &command, EzoCommandType command_
} }
void EZOSensor::set_calibration_point_(EzoCalibrationType type, float value) { void EZOSensor::set_calibration_point_(EzoCalibrationType type, float value) {
std::string payload = str_sprintf("Cal,%s,%0.2f", EZO_CALIBRATION_TYPE_STRINGS[type], value); // max 21: "Cal,"(4) + type(4) + ","(1) + float(11) + null; use 24 for safety
char payload[24];
snprintf(payload, sizeof(payload), "Cal,%s,%0.2f", EZO_CALIBRATION_TYPE_STRINGS[type], value);
this->add_command_(payload, EzoCommandType::EZO_CALIBRATION, 900); this->add_command_(payload, EzoCommandType::EZO_CALIBRATION, 900);
} }
void EZOSensor::set_address(uint8_t address) { void EZOSensor::set_address(uint8_t address) {
if (address > 0 && address < 128) { if (address > 0 && address < 128) {
std::string payload = str_sprintf("I2C,%u", address); // max 8: "I2C,"(4) + uint8(3) + null
char payload[8];
snprintf(payload, sizeof(payload), "I2C,%u", address);
this->new_address_ = address; this->new_address_ = address;
this->add_command_(payload, EzoCommandType::EZO_I2C); this->add_command_(payload, EzoCommandType::EZO_I2C);
} else { } else {
@@ -194,7 +198,9 @@ void EZOSensor::get_slope() { this->add_command_("Slope,?", EzoCommandType::EZO_
void EZOSensor::get_t() { this->add_command_("T,?", EzoCommandType::EZO_T); } void EZOSensor::get_t() { this->add_command_("T,?", EzoCommandType::EZO_T); }
void EZOSensor::set_t(float value) { void EZOSensor::set_t(float value) {
std::string payload = str_sprintf("T,%0.2f", value); // max 14 bytes: "T,"(2) + float with "%0.2f" (up to 11 chars) + null(1); use 16 for alignment
char payload[16];
snprintf(payload, sizeof(payload), "T,%0.2f", value);
this->add_command_(payload, EzoCommandType::EZO_T); this->add_command_(payload, EzoCommandType::EZO_T);
} }
@@ -215,7 +221,9 @@ void EZOSensor::set_calibration_point_high(float value) {
} }
void EZOSensor::set_calibration_generic(float value) { void EZOSensor::set_calibration_generic(float value) {
std::string payload = str_sprintf("Cal,%0.2f", value); // exact 16 bytes: "Cal," (4) + float with "%0.2f" (up to 11 chars, e.g. "-9999999.99") + null (1) = 16
char payload[16];
snprintf(payload, sizeof(payload), "Cal,%0.2f", value);
this->add_command_(payload, EzoCommandType::EZO_CALIBRATION, 900); this->add_command_(payload, EzoCommandType::EZO_CALIBRATION, 900);
} }
@@ -223,13 +231,11 @@ void EZOSensor::clear_calibration() { this->add_command_("Cal,clear", EzoCommand
void EZOSensor::get_led_state() { this->add_command_("L,?", EzoCommandType::EZO_LED); } void EZOSensor::get_led_state() { this->add_command_("L,?", EzoCommandType::EZO_LED); }
void EZOSensor::set_led_state(bool on) { void EZOSensor::set_led_state(bool on) { this->add_command_(on ? "L,1" : "L,0", EzoCommandType::EZO_LED); }
std::string to_send = "L,";
to_send += on ? "1" : "0";
this->add_command_(to_send, EzoCommandType::EZO_LED);
}
void EZOSensor::send_custom(const std::string &to_send) { this->add_command_(to_send, EzoCommandType::EZO_CUSTOM); } void EZOSensor::send_custom(const std::string &to_send) {
this->add_command_(to_send.c_str(), EzoCommandType::EZO_CUSTOM);
}
} // namespace ezo } // namespace ezo
} // namespace esphome } // namespace esphome

View File

@@ -92,7 +92,7 @@ class EZOSensor : public sensor::Sensor, public PollingComponent, public i2c::I2
std::deque<std::unique_ptr<EzoCommand>> commands_; std::deque<std::unique_ptr<EzoCommand>> commands_;
int new_address_; int new_address_;
void add_command_(const std::string &command, EzoCommandType command_type, uint16_t delay_ms = 300); void add_command_(const char *command, EzoCommandType command_type, uint16_t delay_ms = 300);
void set_calibration_point_(EzoCalibrationType type, float value); void set_calibration_point_(EzoCalibrationType type, float value);

View File

@@ -318,90 +318,93 @@ void EzoPMP::send_next_command_() {
switch (this->next_command_) { switch (this->next_command_) {
// Read Commands // Read Commands
case EZO_PMP_COMMAND_READ_DOSING: // Page 54 case EZO_PMP_COMMAND_READ_DOSING: // Page 54
command_buffer_length = sprintf((char *) command_buffer, "D,?"); command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "D,?");
break; break;
case EZO_PMP_COMMAND_READ_SINGLE_REPORT: // Single Report (page 53) case EZO_PMP_COMMAND_READ_SINGLE_REPORT: // Single Report (page 53)
command_buffer_length = sprintf((char *) command_buffer, "R"); command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "R");
break; break;
case EZO_PMP_COMMAND_READ_MAX_FLOW_RATE: case EZO_PMP_COMMAND_READ_MAX_FLOW_RATE:
command_buffer_length = sprintf((char *) command_buffer, "DC,?"); command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "DC,?");
break; break;
case EZO_PMP_COMMAND_READ_PAUSE_STATUS: case EZO_PMP_COMMAND_READ_PAUSE_STATUS:
command_buffer_length = sprintf((char *) command_buffer, "P,?"); command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "P,?");
break; break;
case EZO_PMP_COMMAND_READ_TOTAL_VOLUME_DOSED: case EZO_PMP_COMMAND_READ_TOTAL_VOLUME_DOSED:
command_buffer_length = sprintf((char *) command_buffer, "TV,?"); command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "TV,?");
break; break;
case EZO_PMP_COMMAND_READ_ABSOLUTE_TOTAL_VOLUME_DOSED: case EZO_PMP_COMMAND_READ_ABSOLUTE_TOTAL_VOLUME_DOSED:
command_buffer_length = sprintf((char *) command_buffer, "ATV,?"); command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "ATV,?");
break; break;
case EZO_PMP_COMMAND_READ_CALIBRATION_STATUS: case EZO_PMP_COMMAND_READ_CALIBRATION_STATUS:
command_buffer_length = sprintf((char *) command_buffer, "Cal,?"); command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "Cal,?");
break; break;
case EZO_PMP_COMMAND_READ_PUMP_VOLTAGE: case EZO_PMP_COMMAND_READ_PUMP_VOLTAGE:
command_buffer_length = sprintf((char *) command_buffer, "PV,?"); command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "PV,?");
break; break;
// Non-Read Commands // Non-Read Commands
case EZO_PMP_COMMAND_FIND: // Find (page 52) case EZO_PMP_COMMAND_FIND: // Find (page 52)
command_buffer_length = sprintf((char *) command_buffer, "Find"); command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "Find");
wait_time_for_command = 60000; // This command will block all updates for a minute wait_time_for_command = 60000; // This command will block all updates for a minute
break; break;
case EZO_PMP_COMMAND_DOSE_CONTINUOUSLY: // Continuous Dispensing (page 54) case EZO_PMP_COMMAND_DOSE_CONTINUOUSLY: // Continuous Dispensing (page 54)
command_buffer_length = sprintf((char *) command_buffer, "D,*"); command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "D,*");
break; break;
case EZO_PMP_COMMAND_CLEAR_TOTAL_VOLUME_DOSED: // Clear Total Volume Dosed (page 64) case EZO_PMP_COMMAND_CLEAR_TOTAL_VOLUME_DOSED: // Clear Total Volume Dosed (page 64)
command_buffer_length = sprintf((char *) command_buffer, "Clear"); command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "Clear");
break; break;
case EZO_PMP_COMMAND_CLEAR_CALIBRATION: // Clear Calibration (page 65) case EZO_PMP_COMMAND_CLEAR_CALIBRATION: // Clear Calibration (page 65)
command_buffer_length = sprintf((char *) command_buffer, "Cal,clear"); command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "Cal,clear");
break; break;
case EZO_PMP_COMMAND_PAUSE_DOSING: // Pause (page 61) case EZO_PMP_COMMAND_PAUSE_DOSING: // Pause (page 61)
command_buffer_length = sprintf((char *) command_buffer, "P"); command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "P");
break; break;
case EZO_PMP_COMMAND_STOP_DOSING: // Stop (page 62) case EZO_PMP_COMMAND_STOP_DOSING: // Stop (page 62)
command_buffer_length = sprintf((char *) command_buffer, "X"); command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "X");
break; break;
// Non-Read commands with parameters // Non-Read commands with parameters
case EZO_PMP_COMMAND_DOSE_VOLUME: // Volume Dispensing (page 55) case EZO_PMP_COMMAND_DOSE_VOLUME: // Volume Dispensing (page 55)
command_buffer_length = sprintf((char *) command_buffer, "D,%0.1f", this->next_command_volume_); command_buffer_length =
snprintf((char *) command_buffer, sizeof(command_buffer), "D,%0.1f", this->next_command_volume_);
break; break;
case EZO_PMP_COMMAND_DOSE_VOLUME_OVER_TIME: // Dose over time (page 56) case EZO_PMP_COMMAND_DOSE_VOLUME_OVER_TIME: // Dose over time (page 56)
command_buffer_length = command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "D,%0.1f,%i",
sprintf((char *) command_buffer, "D,%0.1f,%i", this->next_command_volume_, this->next_command_duration_); this->next_command_volume_, this->next_command_duration_);
break; break;
case EZO_PMP_COMMAND_DOSE_WITH_CONSTANT_FLOW_RATE: // Constant Flow Rate (page 57) case EZO_PMP_COMMAND_DOSE_WITH_CONSTANT_FLOW_RATE: // Constant Flow Rate (page 57)
command_buffer_length = command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "DC,%0.1f,%i",
sprintf((char *) command_buffer, "DC,%0.1f,%i", this->next_command_volume_, this->next_command_duration_); this->next_command_volume_, this->next_command_duration_);
break; break;
case EZO_PMP_COMMAND_SET_CALIBRATION_VOLUME: // Set Calibration Volume (page 65) case EZO_PMP_COMMAND_SET_CALIBRATION_VOLUME: // Set Calibration Volume (page 65)
command_buffer_length = sprintf((char *) command_buffer, "Cal,%0.2f", this->next_command_volume_); command_buffer_length =
snprintf((char *) command_buffer, sizeof(command_buffer), "Cal,%0.2f", this->next_command_volume_);
break; break;
case EZO_PMP_COMMAND_CHANGE_I2C_ADDRESS: // Change I2C Address (page 73) case EZO_PMP_COMMAND_CHANGE_I2C_ADDRESS: // Change I2C Address (page 73)
command_buffer_length = sprintf((char *) command_buffer, "I2C,%i", this->next_command_duration_); command_buffer_length =
snprintf((char *) command_buffer, sizeof(command_buffer), "I2C,%i", this->next_command_duration_);
break; break;
case EZO_PMP_COMMAND_EXEC_ARBITRARY_COMMAND_ADDRESS: // Run an arbitrary command case EZO_PMP_COMMAND_EXEC_ARBITRARY_COMMAND_ADDRESS: // Run an arbitrary command
command_buffer_length = sprintf((char *) command_buffer, this->arbitrary_command_, this->next_command_duration_); command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "%s", this->arbitrary_command_);
ESP_LOGI(TAG, "Sending arbitrary command: %s", (char *) command_buffer); ESP_LOGI(TAG, "Sending arbitrary command: %s", (char *) command_buffer);
break; break;

View File

@@ -77,7 +77,7 @@ FanSpeedSetTrigger = fan_ns.class_(
"FanSpeedSetTrigger", automation.Trigger.template(cg.int_) "FanSpeedSetTrigger", automation.Trigger.template(cg.int_)
) )
FanPresetSetTrigger = fan_ns.class_( FanPresetSetTrigger = fan_ns.class_(
"FanPresetSetTrigger", automation.Trigger.template(cg.std_string) "FanPresetSetTrigger", automation.Trigger.template(cg.StringRef)
) )
FanIsOnCondition = fan_ns.class_("FanIsOnCondition", automation.Condition.template()) FanIsOnCondition = fan_ns.class_("FanIsOnCondition", automation.Condition.template())
@@ -287,7 +287,7 @@ async def setup_fan_core_(var, config):
await automation.build_automation(trigger, [(cg.int_, "x")], conf) await automation.build_automation(trigger, [(cg.int_, "x")], conf)
for conf in config.get(CONF_ON_PRESET_SET, []): for conf in config.get(CONF_ON_PRESET_SET, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(cg.std_string, "x")], conf) await automation.build_automation(trigger, [(cg.StringRef, "x")], conf)
async def register_fan(var, config): async def register_fan(var, config):

View File

@@ -208,7 +208,7 @@ class FanSpeedSetTrigger : public Trigger<int> {
int last_speed_; int last_speed_;
}; };
class FanPresetSetTrigger : public Trigger<std::string> { class FanPresetSetTrigger : public Trigger<StringRef> {
public: public:
FanPresetSetTrigger(Fan *state) { FanPresetSetTrigger(Fan *state) {
state->add_on_state_callback([this, state]() { state->add_on_state_callback([this, state]() {
@@ -216,7 +216,7 @@ class FanPresetSetTrigger : public Trigger<std::string> {
auto should_trigger = preset_mode != this->last_preset_mode_; auto should_trigger = preset_mode != this->last_preset_mode_;
this->last_preset_mode_ = preset_mode; this->last_preset_mode_ = preset_mode;
if (should_trigger) { if (should_trigger) {
this->trigger(std::string(preset_mode)); this->trigger(preset_mode);
} }
}); });
this->last_preset_mode_ = state->get_preset_mode(); this->last_preset_mode_ = state->get_preset_mode();

View File

@@ -163,9 +163,10 @@ bool GDK101Component::read_fw_version_(uint8_t *data) {
return false; return false;
} }
const std::string fw_version_str = str_sprintf("%d.%d", data[0], data[1]); // max 8: "255.255" (7 chars) + null
char buf[8];
this->fw_version_text_sensor_->publish_state(fw_version_str); snprintf(buf, sizeof(buf), "%d.%d", data[0], data[1]);
this->fw_version_text_sensor_->publish_state(buf);
} }
#endif // USE_TEXT_SENSOR #endif // USE_TEXT_SENSOR
return true; return true;

View File

@@ -6,6 +6,7 @@ from esphome.const import (
CONF_BASELINE, CONF_BASELINE,
CONF_CO2, CONF_CO2,
CONF_ID, CONF_ID,
CONF_WARMUP_TIME,
DEVICE_CLASS_CARBON_DIOXIDE, DEVICE_CLASS_CARBON_DIOXIDE,
ICON_MOLECULE_CO2, ICON_MOLECULE_CO2,
STATE_CLASS_MEASUREMENT, STATE_CLASS_MEASUREMENT,
@@ -14,8 +15,6 @@ from esphome.const import (
DEPENDENCIES = ["uart"] DEPENDENCIES = ["uart"]
CONF_WARMUP_TIME = "warmup_time"
hc8_ns = cg.esphome_ns.namespace("hc8") hc8_ns = cg.esphome_ns.namespace("hc8")
HC8Component = hc8_ns.class_("HC8Component", cg.PollingComponent, uart.UARTDevice) HC8Component = hc8_ns.class_("HC8Component", cg.PollingComponent, uart.UARTDevice)
HC8CalibrateAction = hc8_ns.class_("HC8CalibrateAction", automation.Action) 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.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_max_temperature(config[CONF_MAX_TEMPERATURE]))
cg.add(var.set_min_temperature(config[CONF_MIN_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: if CORE.is_libretiny or CORE.is_esp32:
CORE.add_platformio_option("lib_ignore", ["IRremoteESP8266"]) CORE.add_platformio_option("lib_ignore", ["IRremoteESP8266"])

View File

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

View File

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

View File

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

View File

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

View File

@@ -97,7 +97,7 @@ void HomeassistantNumber::control(float value) {
entity_value.key = VALUE_KEY; entity_value.key = VALUE_KEY;
// Stack buffer - no heap allocation; %g produces shortest representation // Stack buffer - no heap allocation; %g produces shortest representation
char value_buf[16]; char value_buf[16];
snprintf(value_buf, sizeof(value_buf), "%g", value); buf_append_printf(value_buf, sizeof(value_buf), 0, "%g", value);
entity_value.value = StringRef(value_buf); entity_value.value = StringRef(value_buf);
api::global_api_server->send_homeassistant_action(resp); api::global_api_server->send_homeassistant_action(resp);

View File

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

View File

@@ -79,6 +79,81 @@ inline bool is_redirect(int const status) {
*/ */
inline bool is_success(int const status) { return status >= HTTP_STATUS_OK && status < HTTP_STATUS_MULTIPLE_CHOICES; } inline bool is_success(int const status) { return status >= HTTP_STATUS_OK && status < HTTP_STATUS_MULTIPLE_CHOICES; }
/*
* HTTP Container Read Semantics
* =============================
*
* IMPORTANT: These semantics differ from standard BSD sockets!
*
* BSD socket read() returns:
* > 0: bytes read
* == 0: connection closed (EOF)
* < 0: error (check errno)
*
* HttpContainer::read() returns:
* > 0: bytes read successfully
* == 0: no data available yet OR all content read
* (caller should check bytes_read vs content_length)
* < 0: error or connection closed (caller should EXIT)
* HTTP_ERROR_CONNECTION_CLOSED (-1) = connection closed prematurely
* other negative values = platform-specific errors
*
* Platform behaviors:
* - ESP-IDF: blocking reads, 0 only returned when all content read
* - Arduino: non-blocking, 0 means "no data yet" or "all content read"
*
* Use the helper functions below instead of checking return values directly:
* - http_read_loop_result(): for manual loops with per-chunk processing
* - http_read_fully(): for simple "read N bytes into buffer" operations
*/
/// Error code returned by HttpContainer::read() when connection closed prematurely
/// NOTE: Unlike BSD sockets where 0 means EOF, here 0 means "no data yet, retry"
static constexpr int HTTP_ERROR_CONNECTION_CLOSED = -1;
/// Status of a read operation
enum class HttpReadStatus : uint8_t {
OK, ///< Read completed successfully
ERROR, ///< Read error occurred
TIMEOUT, ///< Timeout waiting for data
};
/// Result of an HTTP read operation
struct HttpReadResult {
HttpReadStatus status; ///< Status of the read operation
int error_code; ///< Error code from read() on failure, 0 on success
};
/// Result of processing a non-blocking read with timeout (for manual loops)
enum class HttpReadLoopResult : uint8_t {
DATA, ///< Data was read, process it
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 HttpRequestComponent;
class HttpContainer : public Parented<HttpRequestComponent> { class HttpContainer : public Parented<HttpRequestComponent> {
@@ -88,6 +163,33 @@ class HttpContainer : public Parented<HttpRequestComponent> {
int status_code; int status_code;
uint32_t duration_ms; uint32_t duration_ms;
/**
* @brief Read data from the HTTP response body.
*
* WARNING: These semantics differ from BSD sockets!
* BSD sockets: 0 = EOF (connection closed)
* This method: 0 = no data yet OR all content read, negative = error/closed
*
* @param buf Buffer to read data into
* @param max_len Maximum number of bytes to read
* @return
* - > 0: Number of bytes read successfully
* - 0: No data available yet OR all content read
* (check get_bytes_read() >= content_length to distinguish)
* - HTTP_ERROR_CONNECTION_CLOSED (-1): Connection closed prematurely
* - < -1: Other error (platform-specific error code)
*
* Platform notes:
* - ESP-IDF: blocking read, 0 only when all content read
* - Arduino: non-blocking, 0 can mean "no data yet" or "all content read"
*
* Use get_bytes_read() and content_length to track progress.
* When get_bytes_read() >= content_length, all data has been received.
*
* IMPORTANT: Do not use raw return values directly. Use these helpers:
* - http_read_loop_result(): for loops with per-chunk processing
* - http_read_fully(): for simple "read N bytes" operations
*/
virtual int read(uint8_t *buf, size_t max_len) = 0; virtual int read(uint8_t *buf, size_t max_len) = 0;
virtual void end() = 0; virtual void end() = 0;
@@ -110,6 +212,38 @@ class HttpContainer : public Parented<HttpRequestComponent> {
std::map<std::string, std::list<std::string>> response_headers_{}; 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 &> { class HttpRequestResponseTrigger : public Trigger<std::shared_ptr<HttpContainer>, std::string &> {
public: public:
void process(const std::shared_ptr<HttpContainer> &container, std::string &response_body) { void process(const std::shared_ptr<HttpContainer> &container, std::string &response_body) {
@@ -124,6 +258,7 @@ class HttpRequestComponent : public Component {
void set_useragent(const char *useragent) { this->useragent_ = useragent; } void set_useragent(const char *useragent) { this->useragent_ = useragent; }
void set_timeout(uint32_t timeout) { this->timeout_ = timeout; } 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; } void set_watchdog_timeout(uint32_t watchdog_timeout) { this->watchdog_timeout_ = watchdog_timeout; }
uint32_t get_watchdog_timeout() const { return this->watchdog_timeout_; } uint32_t get_watchdog_timeout() const { return this->watchdog_timeout_; }
void set_follow_redirects(bool follow_redirects) { this->follow_redirects_ = follow_redirects; } void set_follow_redirects(bool follow_redirects) { this->follow_redirects_ = follow_redirects; }
@@ -249,15 +384,21 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
RAMAllocator<uint8_t> allocator; RAMAllocator<uint8_t> allocator;
uint8_t *buf = allocator.allocate(max_length); uint8_t *buf = allocator.allocate(max_length);
if (buf != nullptr) { 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; 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) { while (container->get_bytes_read() < max_length) {
int read = container->read(buf + read_index, std::min<size_t>(max_length - read_index, 512)); int read_or_error = container->read(buf + read_index, std::min<size_t>(max_length - read_index, 512));
if (read <= 0) {
break;
}
App.feed_wdt(); App.feed_wdt();
yield(); 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.reserve(read_index);
response_body.assign((char *) buf, 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; 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) { int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
const uint32_t start = millis(); const uint32_t start = millis();
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout()); watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
@@ -146,7 +163,7 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
WiFiClient *stream_ptr = this->client_.getStreamPtr(); WiFiClient *stream_ptr = this->client_.getStreamPtr();
if (stream_ptr == nullptr) { if (stream_ptr == nullptr) {
ESP_LOGE(TAG, "Stream pointer vanished!"); ESP_LOGE(TAG, "Stream pointer vanished!");
return -1; return HTTP_ERROR_CONNECTION_CLOSED;
} }
int available_data = stream_ptr->available(); int available_data = stream_ptr->available();
@@ -154,7 +171,15 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
if (bufsize == 0) { if (bufsize == 0) {
this->duration_ms += (millis() - start); this->duration_ms += (millis() - start);
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(); 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.max_redirection_count = this->redirect_limit_;
config.auth_type = HTTP_AUTH_TYPE_BASIC; config.auth_type = HTTP_AUTH_TYPE_BASIC;
#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE #if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
if (secure) { if (secure && this->verify_ssl_) {
config.crt_bundle_attach = esp_crt_bundle_attach; config.crt_bundle_attach = esp_crt_bundle_attach;
} }
#endif #endif
@@ -209,26 +209,57 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
return container; return container;
} }
// ESP-IDF HTTP read implementation (blocking mode)
//
// WARNING: Return values differ from BSD sockets! See http_request.h for full documentation.
//
// esp_http_client_read() in blocking mode returns:
// > 0: bytes read
// 0: connection closed (end of stream)
// < 0: error
//
// We normalize to HttpContainer::read() contract:
// > 0: bytes read
// 0: no data yet / all content read (caller should check bytes_read vs content_length)
// < 0: error/connection closed
int HttpContainerIDF::read(uint8_t *buf, size_t max_len) { int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
const uint32_t start = millis(); const uint32_t start = millis();
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout()); watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
this->feed_wdt(); // Check if we've already read all expected content
int read_len = esp_http_client_read(this->client_, (char *) buf, max_len); if (this->bytes_read_ >= this->content_length) {
this->feed_wdt(); return 0; // All content read successfully
if (read_len > 0) {
this->bytes_read_ += read_len;
} }
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); 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;
}
// Connection closed by server before all content received
if (read_len_or_error == 0) {
return HTTP_ERROR_CONNECTION_CLOSED;
}
// Negative value - error, return the actual error code for debugging
return read_len_or_error;
} }
void HttpContainerIDF::end() { void HttpContainerIDF::end() {
if (this->client_ == nullptr) {
return; // Already cleaned up
}
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout()); watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
esp_http_client_close(this->client_); esp_http_client_close(this->client_);
esp_http_client_cleanup(this->client_); esp_http_client_cleanup(this->client_);
this->client_ = nullptr;
} }
void HttpContainerIDF::feed_wdt() { 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_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_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: protected:
std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body, 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 // if zero ESP-IDF will use DEFAULT_HTTP_BUF_SIZE
uint16_t buffer_size_rx_{}; uint16_t buffer_size_rx_{};
uint16_t buffer_size_tx_{}; uint16_t buffer_size_tx_{};
bool verify_ssl_{true};
/// @brief Monitors the http client events to gather response headers /// @brief Monitors the http client events to gather response headers
static esp_err_t http_event_handler(esp_http_client_event_t *evt); 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; 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) { while (container->get_bytes_read() < container->content_length) {
// read a maximum of chunk_size bytes into buf. (real read size returned) // read a maximum of chunk_size bytes into buf. (real read size returned, or negative error code)
int bufsize = container->read(buf, OtaHttpRequestComponent::HTTP_RECV_BUFFER); int bufsize_or_error = container->read(buf, OtaHttpRequestComponent::HTTP_RECV_BUFFER);
ESP_LOGVV(TAG, "bytes_read_ = %u, body_length_ = %u, bufsize = %i", container->get_bytes_read(), ESP_LOGVV(TAG, "bytes_read_ = %u, body_length_ = %u, bufsize_or_error = %i", container->get_bytes_read(),
container->content_length, bufsize); container->content_length, bufsize_or_error);
// feed watchdog and give other tasks a chance to run // feed watchdog and give other tasks a chance to run
App.feed_wdt(); App.feed_wdt();
yield(); yield();
// Exit loop if no data available (stream closed or end of data) auto result = http_read_loop_result(bufsize_or_error, last_data_time, read_timeout);
if (bufsize <= 0) { if (result == HttpReadLoopResult::RETRY)
if (bufsize < 0) { continue;
ESP_LOGE(TAG, "Stream closed with error"); if (result != HttpReadLoopResult::DATA) {
this->cleanup_(std::move(backend), container); if (result == HttpReadLoopResult::TIMEOUT) {
return OTA_CONNECTION_ERROR; 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 this->cleanup_(std::move(backend), container);
break; 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 // add read bytes to MD5
md5_receive.add(buf, bufsize); md5_receive.add(buf, bufsize_or_error);
// write bytes to OTA backend // write bytes to OTA backend
this->update_started_ = true; 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) { if (error_code != ota::OTA_RESPONSE_OK) {
// error code explanation available at // error code explanation available at
// https://github.com/esphome/esphome/blob/dev/esphome/components/ota/ota_backend.h // 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, 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); this->cleanup_(std::move(backend), container);
return error_code; return error_code;
} }
@@ -244,19 +252,19 @@ bool OtaHttpRequestComponent::http_get_md5_() {
} }
this->md5_expected_.resize(MD5_SIZE); this->md5_expected_.resize(MD5_SIZE);
int read_len = 0; auto result = http_read_fully(container.get(), (uint8_t *) this->md5_expected_.data(), MD5_SIZE, MD5_SIZE,
while (container->get_bytes_read() < MD5_SIZE) { this->parent_->get_timeout());
read_len = container->read((uint8_t *) this->md5_expected_.data(), MD5_SIZE);
if (read_len <= 0) {
break;
}
App.feed_wdt();
yield();
}
container->end(); container->end();
ESP_LOGV(TAG, "Read len: %u, MD5 expected: %u", read_len, MD5_SIZE); if (result.status != HttpReadStatus::OK) {
return read_len == MD5_SIZE; 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) { 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. // The update function runs in a task only on ESP32s.
#ifdef USE_ESP32 #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 #else
#define UPDATE_RETURN return #define UPDATE_RETURN return
#endif #endif
@@ -70,19 +75,21 @@ void HttpRequestUpdate::update_task(void *params) {
UPDATE_RETURN; UPDATE_RETURN;
} }
size_t read_index = 0; auto read_result = http_read_fully(container.get(), data, container->content_length, MAX_READ_SIZE,
while (container->get_bytes_read() < container->content_length) { this_update->request_parent_->get_timeout());
int read_bytes = container->read(data + read_index, MAX_READ_SIZE); if (read_result.status != HttpReadStatus::OK) {
if (read_result.status == HttpReadStatus::TIMEOUT) {
yield(); ESP_LOGE(TAG, "Timeout reading manifest");
} else {
if (read_bytes <= 0) { ESP_LOGE(TAG, "Error reading manifest: %d", read_result.error_code);
// Network error or connection closed - break to avoid infinite loop
break;
} }
// Defer to main loop to avoid race condition on component_state_ read-modify-write
read_index += read_bytes; 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; bool valid = false;
{ // Ensures the response string falls out of scope and deallocates before the task ends { // Ensures the response string falls out of scope and deallocates before the task ends

View File

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

View File

@@ -18,7 +18,15 @@ InfraredCall &InfraredCall::set_carrier_frequency(uint32_t frequency) {
InfraredCall &InfraredCall::set_raw_timings(const std::vector<int32_t> &timings) { InfraredCall &InfraredCall::set_raw_timings(const std::vector<int32_t> &timings) {
this->raw_timings_ = &timings; this->raw_timings_ = &timings;
this->packed_data_ = nullptr; // Clear packed if vector is set this->packed_data_ = nullptr;
this->base64url_ptr_ = nullptr;
return *this;
}
InfraredCall &InfraredCall::set_raw_timings_base64url(const std::string &base64url) {
this->base64url_ptr_ = &base64url;
this->raw_timings_ = nullptr;
this->packed_data_ = nullptr;
return *this; return *this;
} }
@@ -26,7 +34,8 @@ InfraredCall &InfraredCall::set_raw_timings_packed(const uint8_t *data, uint16_t
this->packed_data_ = data; this->packed_data_ = data;
this->packed_length_ = length; this->packed_length_ = length;
this->packed_count_ = count; this->packed_count_ = count;
this->raw_timings_ = nullptr; // Clear vector if packed is set this->raw_timings_ = nullptr;
this->base64url_ptr_ = nullptr;
return *this; return *this;
} }
@@ -92,6 +101,23 @@ void Infrared::control(const InfraredCall &call) {
call.get_packed_count()); call.get_packed_count());
ESP_LOGD(TAG, "Transmitting packed raw timings: count=%u, repeat=%u", call.get_packed_count(), ESP_LOGD(TAG, "Transmitting packed raw timings: count=%u, repeat=%u", call.get_packed_count(),
call.get_repeat_count()); call.get_repeat_count());
} else if (call.is_base64url()) {
// Decode base64url (URL-safe) into transmit buffer
if (!transmit_data->set_data_from_base64url(call.get_base64url_data())) {
ESP_LOGE(TAG, "Invalid base64url data");
return;
}
// Sanity check: validate timing values are within reasonable bounds
constexpr int32_t max_timing_us = 500000; // 500ms absolute max
for (int32_t timing : transmit_data->get_data()) {
int32_t abs_timing = timing < 0 ? -timing : timing;
if (abs_timing > max_timing_us) {
ESP_LOGE(TAG, "Invalid timing value: %d µs (max %d)", timing, max_timing_us);
return;
}
}
ESP_LOGD(TAG, "Transmitting base64url raw timings: count=%zu, repeat=%u", transmit_data->get_data().size(),
call.get_repeat_count());
} else { } else {
// From vector (lambdas/automations) // From vector (lambdas/automations)
transmit_data->set_data(call.get_raw_timings()); transmit_data->set_data(call.get_raw_timings());

View File

@@ -28,12 +28,29 @@ class InfraredCall {
/// Set the carrier frequency in Hz /// Set the carrier frequency in Hz
InfraredCall &set_carrier_frequency(uint32_t frequency); InfraredCall &set_carrier_frequency(uint32_t frequency);
/// Set the raw timings (positive = mark, negative = space)
/// Note: The timings vector must outlive the InfraredCall (zero-copy reference) // ===== Raw Timings Methods =====
// All set_raw_timings_* methods store pointers/references to external data.
// The referenced data must remain valid until perform() completes.
// Safe pattern: call.set_raw_timings_xxx(data); call.perform(); // synchronous
// Unsafe pattern: call.set_raw_timings_xxx(data); defer([call]() { call.perform(); }); // data may be gone!
/// Set the raw timings from a vector (positive = mark, negative = space)
/// @note Lifetime: Stores a pointer to the vector. The vector must outlive perform().
/// @note Usage: Primarily for lambdas/automations where the vector is in scope.
InfraredCall &set_raw_timings(const std::vector<int32_t> &timings); InfraredCall &set_raw_timings(const std::vector<int32_t> &timings);
/// Set the raw timings from packed protobuf sint32 data (zero-copy from wire)
/// Note: The data must outlive the InfraredCall /// Set the raw timings from base64url-encoded little-endian int32 data
/// @note Lifetime: Stores a pointer to the string. The string must outlive perform().
/// @note Usage: For web_server - base64url is fully URL-safe (uses '-' and '_').
/// @note Decoding happens at perform() time, directly into the transmit buffer.
InfraredCall &set_raw_timings_base64url(const std::string &base64url);
/// Set the raw timings from packed protobuf sint32 data (zigzag + varint encoded)
/// @note Lifetime: Stores a pointer to the buffer. The buffer must outlive perform().
/// @note Usage: For API component where data comes directly from the protobuf message.
InfraredCall &set_raw_timings_packed(const uint8_t *data, uint16_t length, uint16_t count); InfraredCall &set_raw_timings_packed(const uint8_t *data, uint16_t length, uint16_t count);
/// Set the number of times to repeat transmission (1 = transmit once, 2 = transmit twice, etc.) /// Set the number of times to repeat transmission (1 = transmit once, 2 = transmit twice, etc.)
InfraredCall &set_repeat_count(uint32_t count); InfraredCall &set_repeat_count(uint32_t count);
@@ -42,12 +59,18 @@ class InfraredCall {
/// Get the carrier frequency /// Get the carrier frequency
const optional<uint32_t> &get_carrier_frequency() const { return this->carrier_frequency_; } const optional<uint32_t> &get_carrier_frequency() const { return this->carrier_frequency_; }
/// Get the raw timings (only valid if set via set_raw_timings, not packed) /// Get the raw timings (only valid if set via set_raw_timings)
const std::vector<int32_t> &get_raw_timings() const { return *this->raw_timings_; } const std::vector<int32_t> &get_raw_timings() const { return *this->raw_timings_; }
/// Check if raw timings have been set (either vector or packed) /// Check if raw timings have been set (any format)
bool has_raw_timings() const { return this->raw_timings_ != nullptr || this->packed_data_ != nullptr; } bool has_raw_timings() const {
return this->raw_timings_ != nullptr || this->packed_data_ != nullptr || this->base64url_ptr_ != nullptr;
}
/// Check if using packed data format /// Check if using packed data format
bool is_packed() const { return this->packed_data_ != nullptr; } bool is_packed() const { return this->packed_data_ != nullptr; }
/// Check if using base64url data format
bool is_base64url() const { return this->base64url_ptr_ != nullptr; }
/// Get the base64url data string
const std::string &get_base64url_data() const { return *this->base64url_ptr_; }
/// Get packed data (only valid if set via set_raw_timings_packed) /// Get packed data (only valid if set via set_raw_timings_packed)
const uint8_t *get_packed_data() const { return this->packed_data_; } const uint8_t *get_packed_data() const { return this->packed_data_; }
uint16_t get_packed_length() const { return this->packed_length_; } uint16_t get_packed_length() const { return this->packed_length_; }
@@ -59,9 +82,11 @@ class InfraredCall {
uint32_t repeat_count_{1}; uint32_t repeat_count_{1};
Infrared *parent_; Infrared *parent_;
optional<uint32_t> carrier_frequency_; optional<uint32_t> carrier_frequency_;
// Vector-based timings (for lambdas/automations) // Pointer to vector-based timings (caller-owned, must outlive perform())
const std::vector<int32_t> *raw_timings_{nullptr}; const std::vector<int32_t> *raw_timings_{nullptr};
// Packed protobuf timings (for API zero-copy) // Pointer to base64url-encoded string (caller-owned, must outlive perform())
const std::string *base64url_ptr_{nullptr};
// Pointer to packed protobuf buffer (caller-owned, must outlive perform())
const uint8_t *packed_data_{nullptr}; const uint8_t *packed_data_{nullptr};
uint16_t packed_length_{0}; uint16_t packed_length_{0};
uint16_t packed_count_{0}; uint16_t packed_count_{0};

View File

@@ -1,6 +1,6 @@
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv 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"] CODEOWNERS = ["@esphome/core"]
json_ns = cg.esphome_ns.namespace("json") json_ns = cg.esphome_ns.namespace("json")
@@ -12,6 +12,11 @@ CONFIG_SCHEMA = cv.All(
@coroutine_with_priority(CoroPriority.BUS) @coroutine_with_priority(CoroPriority.BUS)
async def to_code(config): 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_define("USE_JSON")
cg.add_global(json_ns.using) cg.add_global(json_ns.using)

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