Compare commits

..

159 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
90407aa9f2 Fix: Fetch file content from PR head using GitHub API
The workflow was checking out the base branch but trying to read files
from the filesystem, which meant it couldn't see changes in the PR.
Now using github.rest.repos.getContent with the PR head SHA to fetch
the actual file content from the PR branch.

Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2026-01-26 02:35:36 +00:00
copilot-swe-agent[bot]
a167ea8269 Fix triple-quote regex pattern for multiline strings
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2026-01-26 01:03:21 +00:00
copilot-swe-agent[bot]
354f60a9cf Address code review: add triple-quote support and improve error handling
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2026-01-26 01:01:40 +00:00
copilot-swe-agent[bot]
b8ac92c537 Improve regex and add waveshare_epaper deprecation for testing
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2026-01-26 00:59:35 +00:00
copilot-swe-agent[bot]
71390c075e Add test deprecated component for validation
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2026-01-26 00:58:09 +00:00
copilot-swe-agent[bot]
0ad82b8bbf Add deprecated component detection to auto-label workflow
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2026-01-26 00:57:23 +00:00
copilot-swe-agent[bot]
7863ac8b3a Initial plan 2026-01-26 00:53:55 +00:00
Jonathan Swoboda
011407ea8b Merge branch 'release' into dev 2026-01-25 13:21:39 -05:00
Jonathan Swoboda
1141e83a7c Merge pull request #13529 from esphome/bump-2026.1.2
2026.1.2
2026-01-25 13:21:26 -05:00
Jonathan Swoboda
214ce95cf3 Bump version to 2026.1.2 2026-01-25 12:22:18 -05:00
J. Nick Koston
3a7b83ba93 [wifi] Fix scan flag race condition causing reconnect failure on ESP8266/LibreTiny (#13514) 2026-01-25 12:22:18 -05:00
Clyde Stubbs
cc2f3d85dc [wifi] Fix watchdog timeout on P4 WiFi scan (#13520) 2026-01-25 12:22:18 -05:00
Jonathan Swoboda
723f67d5e2 [i2c] Increase ESP-IDF I2C transaction timeout from 20ms to 100ms (#13483)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 12:22:18 -05:00
Jonathan Swoboda
70e45706d9 [modbus_controller] Fix YAML serialization error with custom_command (#13482)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 12:22:18 -05:00
Jas Strong
56a2a2269f [rd03d] Fix speed and resolution field order (#13495)
Co-authored-by: jas <jas@asspa.in>
2026-01-25 12:22:18 -05:00
Keith Burzinski
d6841ba33a [light] Fix cwww state restore (#13493) 2026-01-25 12:22:18 -05:00
Clyde Stubbs
10cbd0164a [lvgl] Fix setting empty text (#13494) 2026-01-25 12:22:18 -05:00
Big Mike
d285706b41 [sen5x] Fix store baseline functionality (#13469) 2026-01-25 12:22:18 -05:00
J. Nick Koston
ef469c20df [slow_pwm] Fix dump_summary deprecation warning (#13460) 2026-01-25 12:22:18 -05:00
Clyde Stubbs
6870d3dc50 [mipi_rgb] Add software reset command to st7701s init sequence (#13470) 2026-01-25 12:22:18 -05:00
Keith Burzinski
9cc39621a6 [ir_rf_proxy] Remove unnecessary headers, add tests (#13464) 2026-01-25 12:22:18 -05:00
J. Nick Koston
c4f7d09553 [rpi_dpi_rgb] Fix dump_summary deprecation warning (#13461) 2026-01-25 12:22:18 -05:00
J. Nick Koston
ab1661ef22 [mipi_rgb] Fix dump_summary deprecation warning (#13463) 2026-01-25 12:22:18 -05:00
J. Nick Koston
ccbf17d5ab [st7701s] Fix dump_summary deprecation warning (#13462) 2026-01-25 12:22:18 -05:00
J. Nick Koston
bac96086be [wifi] Fix scan flag race condition causing reconnect failure on ESP8266/LibreTiny (#13514) 2026-01-25 12:16:07 -05:00
Clyde Stubbs
c32e4bc65b [wifi] Fix watchdog timeout on P4 WiFi scan (#13520) 2026-01-26 03:52:23 +11:00
Douwe
993765d732 [water_heater] Remove Component inheritance from base class (#13510)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 01:18:13 +00:00
Stephen Cox
8d84fe0113 [sy6970] Support for the sy6970 BMS chip (#13311) 2026-01-25 08:31:26 +11:00
Big Mike
58746b737f [sen5x] Eliminate product name string (#13489) 2026-01-24 11:07:12 -10:00
Big Mike
f93e843972 [sen5x] Fix mangled serial number (#13491) 2026-01-24 09:55:51 -10:00
Peter Meiser
60968d311b [thermostat] make comparisons consistent with documentation (#13499) 2026-01-24 00:20:18 -06:00
J. Nick Koston
30584e2e96 [sensirion_common] Use SmallBufferWithHeapFallback helper (#13496) 2026-01-23 22:53:44 -06:00
Jonathan Swoboda
468ae39a9e [i2c] Increase ESP-IDF I2C transaction timeout from 20ms to 100ms (#13483)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 23:13:03 -05:00
Big Mike
beb9c8d328 [sen5x] Fix missing this-> on class members and member functions (#13497) 2026-01-23 17:04:09 -10:00
Jonathan Swoboda
cdda3fb7cc [modbus_controller] Fix YAML serialization error with custom_command (#13482)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 22:01:40 -05:00
Jas Strong
bba00a3906 [rd03d] Fix speed and resolution field order (#13495)
Co-authored-by: jas <jas@asspa.in>
2026-01-23 22:01:19 -05:00
dependabot[bot]
42e50ca178 Bump github/codeql-action from 4.31.10 to 4.31.11 (#13488)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-23 16:26:11 -10:00
Big Mike
165e362a1b [sensirion_common] Fix incorrect Big Endian conversion (#13492) 2026-01-23 16:19:41 -10:00
dependabot[bot]
e4763f8e71 Bump ruff from 0.14.13 to 0.14.14 (#13487)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-01-23 16:12:17 -10:00
Daniel Kent
9fddd0659e [bmp581] Split into bmp581_base and bmp581_i2c (#12485)
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
2026-01-23 19:28:14 -06:00
Keith Burzinski
faea546a0e [light] Fix cwww state restore (#13493) 2026-01-23 18:53:20 -06:00
Clyde Stubbs
069db2e128 [lvgl] Fix setting empty text (#13494) 2026-01-24 11:44:34 +11:00
Big Mike
5f2203b915 [sen5x] Fix store baseline functionality (#13469) 2026-01-23 18:03:23 -05:00
J. Nick Koston
5c67e04fef [slow_pwm] Fix dump_summary deprecation warning (#13460) 2026-01-23 12:37:06 -10:00
Clyde Stubbs
0cdcacc7fc [mipi_rgb] Add software reset command to st7701s init sequence (#13470) 2026-01-24 09:02:27 +11:00
Keith Burzinski
cfb61bc50a [ir_rf_proxy] Remove unnecessary headers, add tests (#13464) 2026-01-22 20:35:37 -06:00
Jonathan Swoboda
547c985672 Merge branch 'release' into dev 2026-01-22 18:19:32 -05:00
Jonathan Swoboda
44e624d7a7 Merge pull request #13459 from esphome/bump-2026.1.1
2026.1.1
2026-01-22 18:19:18 -05:00
J. Nick Koston
5779e3e6e4 [atm90e32] Fix dump_summary deprecation warning and remove stored cs_summary_ (#13465) 2026-01-22 12:54:01 -10:00
J. Nick Koston
3184717607 [rpi_dpi_rgb] Fix dump_summary deprecation warning (#13461) 2026-01-22 12:53:38 -10:00
J. Nick Koston
e8972c65c8 [mipi_rgb] Fix dump_summary deprecation warning (#13463) 2026-01-22 12:53:15 -10:00
J. Nick Koston
71cda05073 [st7701s] Fix dump_summary deprecation warning (#13462) 2026-01-22 12:42:28 -10:00
Clyde Stubbs
3dbebb728d [sensor] Clamp filter handles non-finite values better (#13457) 2026-01-22 22:34:29 +00:00
Jonathan Swoboda
f938de16af Bump version to 2026.1.1 2026-01-22 16:30:52 -05:00
J. Nick Koston
ec791063b3 [time] Always call time sync callbacks even when time unchanged (#13456) 2026-01-22 16:30:52 -05:00
Jonathan Swoboda
fb984cd052 [aqi] Remove unit_of_measurement to fix Home Assistant warning (#13448)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 16:30:52 -05:00
Jonathan Swoboda
85181779d1 [fingerprint_grow] Use buffer-based dump_summary to fix deprecation warnings (#13447)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 16:30:52 -05:00
J. Nick Koston
95b23702e4 [wifi] Fix stale error_from_callback_ causing immediate connection failures (#13450) 2026-01-22 16:30:52 -05:00
J. Nick Koston
95eebcd74f [api] Limit Nagle batching for log messages to reduce LWIP buffer pressure (#13439) 2026-01-22 16:30:52 -05:00
Rene Guca
3c3d5c2fca [dht] Increase delay for DHT22 and RHT03 (#13446) 2026-01-22 16:30:52 -05:00
J. Nick Koston
811ac81320 [http_request] Fix OTA failures on ESP8266/Arduino by making read semantics consistent (#13435)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-22 16:30:52 -05:00
J. Nick Koston
f01bd68a4b [spi] Fix display init failure by marking displays as write-only for half-duplex mode (#13431) 2026-01-22 16:30:52 -05:00
J. Nick Koston
5433c0f707 [wifi] Fix bk72xx manual_ip preventing API connection (#13426) 2026-01-22 16:30:52 -05:00
Jonathan Swoboda
b06cce9eeb [esp32] Add warning for experimental 400MHz on ESP32-P4 (#13433)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 16:30:52 -05:00
Jonathan Swoboda
65bcfee035 [http_request] Fix verify_ssl: false not working on ESP32 (#13422)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 16:30:52 -05:00
Copilot
9261b9ecaa [lvgl] Validate LVGL dropdown symbols require Unicode codepoint ≥ 0x100 (#13394)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-01-22 16:30:52 -05:00
J. Nick Koston
6725e6c01e [wifi] Process scan results one at a time to avoid heap allocation (#13400) 2026-01-22 16:30:52 -05:00
J. Nick Koston
effbcece49 [time] Always call time sync callbacks even when time unchanged (#13456) 2026-01-22 21:27:04 +00:00
Jonathan Swoboda
98a926f37f [heatpumpir] Fix ambiguous millis() call with HeatpumpIR library (#13458)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 16:22:33 -05:00
dependabot[bot]
110c173eac Update wheel requirement from <0.46,>=0.43 to >=0.43,<0.47 (#13451)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-22 11:16:53 -10:00
dependabot[bot]
6008abae62 Bump actions/setup-python from 6.1.0 to 6.2.0 in /.github/actions/restore-python (#13453)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-22 11:16:40 -10:00
dependabot[bot]
04e102f344 Bump actions/setup-python from 6.1.0 to 6.2.0 (#13454)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-22 11:16:27 -10:00
dependabot[bot]
bb67b1ca1e Bump actions/checkout from 6.0.1 to 6.0.2 (#13452)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-22 11:16:15 -10:00
esphomebot
6d7956a062 Update webserver local assets to 20260122-204614 (#13455)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-01-22 11:15:42 -10:00
Jonathan Swoboda
afbbdd1492 [aqi] Remove unit_of_measurement to fix Home Assistant warning (#13448)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 16:10:55 -05:00
Jonathan Swoboda
b06568c132 [fingerprint_grow] Use buffer-based dump_summary to fix deprecation warnings (#13447)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 16:07:41 -05:00
J. Nick Koston
3c5fc638d5 [wifi] Fix stale error_from_callback_ causing immediate connection failures (#13450) 2026-01-22 10:42:14 -10:00
J. Nick Koston
ddb762f8f5 [api] Limit Nagle batching for log messages to reduce LWIP buffer pressure (#13439) 2026-01-22 08:09:14 -10:00
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
Jonathan Swoboda
15f0986a59 Merge pull request #13406 from esphome/bump-2026.1.0
2026.1.0
2026-01-20 22:43:06 -05:00
Jonathan Swoboda
90edf32acf Bump version to 2026.1.0 2026-01-20 21:15:02 -05:00
polyfloyd
3c0f43db9e Add the max_delta filter (#12605)
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2026-01-21 10:58:47 +11:00
Jonathan Swoboda
6edecd3d45 Merge branch 'beta' into dev 2026-01-20 17:01:47 -05:00
Jonathan Swoboda
055c00f1ac Merge pull request #13396 from esphome/bump-2026.1.0b4
2026.1.0b4
2026-01-20 17:01:36 -05:00
Jonathan Swoboda
7dc40881e2 Bump version to 2026.1.0b4 2026-01-20 15:55:03 -05:00
J. Nick Koston
b04373687e [wifi_info] Fix missing state when both IP+DNS or SSID+BSSID configure (#13385) 2026-01-20 15:55:03 -05:00
Jonathan Swoboda
b89c127f62 [x9c] Fix potentiometer unable to decrement (#13382)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 15:55:03 -05:00
Jonathan Swoboda
47dc5d0a1f [core] Fix state leakage and module caching when processing multiple configurations (#13368)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 15:55:03 -05:00
J. Nick Koston
21886dd3ac [api] Fix truncation of Home Assistant attributes longer than 255 characters (#13348) 2026-01-20 15:55:03 -05:00
J. Nick Koston
85a5a26519 [network] Fix IPAddress::str_to() to lowercase IPv6 hex digits (#13325) 2026-01-20 15:55:03 -05:00
Clyde Stubbs
79ccacd6d6 [helpers] Allow reading capacity of FixedVector (#13391) 2026-01-20 09:24:42 -10:00
J. Nick Koston
e2319ba651 [wifi_info] Fix missing state when both IP+DNS or SSID+BSSID configure (#13385) 2026-01-20 07:55:59 -10:00
Jonathan Swoboda
ed4ebffa74 [x9c] Fix potentiometer unable to decrement (#13382)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 22:57:54 -05:00
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
281 changed files with 13877 additions and 10560 deletions

View File

@@ -1 +1 @@
d272a88e8ca28ae9340a9a03295a566432a52cb696501908f57764475bf7ca65
15dc295268b2dcf75942f42759b3ddec64eba89f75525698eb39c95a7f4b14ce

View File

@@ -17,7 +17,7 @@ runs:
steps:
- name: Set up Python ${{ inputs.python-version }}
id: python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ inputs.python-version }}
- name: Restore Python virtual environment

View File

@@ -22,7 +22,7 @@ jobs:
if: github.event.action != 'labeled' || github.event.sender.type != 'Bot'
steps:
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Generate a token
id: generate-token
@@ -46,6 +46,7 @@ jobs:
const BOT_COMMENT_MARKER = '<!-- auto-label-pr-bot -->';
const CODEOWNERS_MARKER = '<!-- codeowners-request -->';
const TOO_BIG_MARKER = '<!-- too-big-request -->';
const DEPRECATED_COMPONENT_MARKER = '<!-- deprecated-component-request -->';
const MANAGED_LABELS = [
'new-component',
@@ -69,7 +70,8 @@ jobs:
'new-feature',
'breaking-change',
'developer-breaking-change',
'code-quality'
'code-quality',
'deprecated_component'
];
const DOCS_PR_PATTERNS = [
@@ -382,6 +384,75 @@ jobs:
return labels;
}
// Strategy: Deprecated component detection
async function detectDeprecatedComponents() {
const labels = new Set();
const deprecatedInfo = [];
// Compile regex once for better performance
const componentFileRegex = /^esphome\/components\/([^\/]+)\/[^/]*\.py$/;
// Get files that are modified or added in components directory
const componentFiles = changedFiles.filter(file => componentFileRegex.test(file));
if (componentFiles.length === 0) {
return { labels, deprecatedInfo };
}
// Extract unique component names using the same regex
const components = new Set();
for (const file of componentFiles) {
const match = file.match(componentFileRegex);
if (match) {
components.add(match[1]);
}
}
// Get PR head SHA to fetch files from the PR branch
const prHeadSha = context.payload.pull_request.head.sha;
// Check each component's __init__.py for DEPRECATED_COMPONENT constant
for (const component of components) {
const initFile = `esphome/components/${component}/__init__.py`;
try {
// Fetch file content from PR head using GitHub API
const { data: fileData } = await github.rest.repos.getContent({
owner,
repo,
path: initFile,
ref: prHeadSha
});
// Decode base64 content
const content = Buffer.from(fileData.content, 'base64').toString('utf8');
// Look for DEPRECATED_COMPONENT = "message" or DEPRECATED_COMPONENT = 'message'
// Support single quotes, double quotes, and triple quotes (for multiline)
const doubleQuoteMatch = content.match(/DEPRECATED_COMPONENT\s*=\s*"""([\s\S]*?)"""/s) ||
content.match(/DEPRECATED_COMPONENT\s*=\s*"((?:[^"\\]|\\.)*)"/);
const singleQuoteMatch = content.match(/DEPRECATED_COMPONENT\s*=\s*'''([\s\S]*?)'''/s) ||
content.match(/DEPRECATED_COMPONENT\s*=\s*'((?:[^'\\]|\\.)*)'/);
const deprecatedMatch = doubleQuoteMatch || singleQuoteMatch;
if (deprecatedMatch) {
labels.add('deprecated_component');
deprecatedInfo.push({
component: component,
message: deprecatedMatch[1]
});
console.log(`Found deprecated component: ${component}`);
}
} catch (error) {
// Only log if it's not a simple "file not found" error (404)
if (error.status !== 404) {
console.log(`Error reading ${initFile}:`, error.message);
}
}
}
return { labels, deprecatedInfo };
}
// Strategy: Requirements detection
async function detectRequirements(allLabels) {
const labels = new Set();
@@ -418,10 +489,26 @@ jobs:
}
// Generate review messages
function generateReviewMessages(finalLabels, originalLabelCount) {
function generateReviewMessages(finalLabels, originalLabelCount, deprecatedInfo) {
const messages = [];
const prAuthor = context.payload.pull_request.user.login;
// Deprecated component message
if (finalLabels.includes('deprecated_component') && deprecatedInfo && deprecatedInfo.length > 0) {
let message = `${DEPRECATED_COMPONENT_MARKER}\n### ⚠️ Deprecated Component\n\n`;
message += `Hey there @${prAuthor},\n`;
message += `This PR modifies one or more deprecated components. Please be aware:\n\n`;
for (const info of deprecatedInfo) {
message += `#### Component: \`${info.component}\`\n`;
message += `${info.message}\n\n`;
}
message += `Consider migrating to the recommended alternative if applicable.`;
messages.push(message);
}
// Too big message
if (finalLabels.includes('too-big')) {
const testAdditions = prFiles
@@ -468,10 +555,10 @@ jobs:
}
// Handle reviews
async function handleReviews(finalLabels, originalLabelCount) {
const reviewMessages = generateReviewMessages(finalLabels, originalLabelCount);
async function handleReviews(finalLabels, originalLabelCount, deprecatedInfo) {
const reviewMessages = generateReviewMessages(finalLabels, originalLabelCount, deprecatedInfo);
const hasReviewableLabels = finalLabels.some(label =>
['too-big', 'needs-codeowners'].includes(label)
['too-big', 'needs-codeowners', 'deprecated_component'].includes(label)
);
const { data: reviews } = await github.rest.pulls.listReviews({
@@ -580,7 +667,8 @@ jobs:
actionsLabels,
codeOwnerLabels,
testLabels,
checkboxLabels
checkboxLabels,
deprecatedResult
] = await Promise.all([
detectMergeBranch(),
detectComponentPlatforms(apiData),
@@ -592,9 +680,14 @@ jobs:
detectGitHubActionsChanges(),
detectCodeOwner(),
detectTests(),
detectPRTemplateCheckboxes()
detectPRTemplateCheckboxes(),
detectDeprecatedComponents()
]);
// Extract deprecated component info
const deprecatedLabels = deprecatedResult.labels;
const deprecatedInfo = deprecatedResult.deprecatedInfo;
// Combine all labels
const allLabels = new Set([
...branchLabels,
@@ -607,7 +700,8 @@ jobs:
...actionsLabels,
...codeOwnerLabels,
...testLabels,
...checkboxLabels
...checkboxLabels,
...deprecatedLabels
]);
// Detect requirements based on all other labels
@@ -638,7 +732,7 @@ jobs:
console.log('Computed labels:', finalLabels.join(', '));
// Handle reviews
await handleReviews(finalLabels, originalLabelCount);
await handleReviews(finalLabels, originalLabelCount, deprecatedInfo);
// Apply labels
if (finalLabels.length > 0) {

View File

@@ -21,9 +21,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.11"

View File

@@ -21,10 +21,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.11"

View File

@@ -43,9 +43,9 @@ jobs:
- "docker"
# - "lint"
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.11"
- name: Set up Docker Buildx

View File

@@ -49,7 +49,7 @@ jobs:
- name: Check out code from base repository
if: steps.pr.outputs.skip != 'true'
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Always check out from the base repository (esphome/esphome), never from forks
# Use the PR's target branch to ensure we run trusted code from the main repo

View File

@@ -36,13 +36,13 @@ jobs:
cache-key: ${{ steps.cache-key.outputs.key }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Generate cache-key
id: cache-key
run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment
@@ -70,7 +70,7 @@ jobs:
if: needs.determine-jobs.outputs.python-linters == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -91,7 +91,7 @@ jobs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -132,7 +132,7 @@ jobs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Restore Python
id: restore-python
uses: ./.github/actions/restore-python
@@ -183,7 +183,7 @@ jobs:
component-test-batches: ${{ steps.determine.outputs.component-test-batches }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Fetch enough history to find the merge base
fetch-depth: 2
@@ -237,10 +237,10 @@ jobs:
if: needs.determine-jobs.outputs.integration-tests == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Python 3.13
id: python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.13"
- name: Restore Python virtual environment
@@ -273,7 +273,7 @@ jobs:
if: github.event_name == 'pull_request' && (needs.determine-jobs.outputs.cpp-unit-tests-run-all == 'true' || needs.determine-jobs.outputs.cpp-unit-tests-components != '[]')
steps:
- name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Restore Python
uses: ./.github/actions/restore-python
@@ -321,7 +321,7 @@ jobs:
steps:
- name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
@@ -400,7 +400,7 @@ jobs:
GH_TOKEN: ${{ github.token }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
@@ -489,7 +489,7 @@ jobs:
steps:
- name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
@@ -577,7 +577,7 @@ jobs:
version: 1.0
- name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -662,7 +662,7 @@ jobs:
if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release')
steps:
- name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -688,7 +688,7 @@ jobs:
skip: ${{ steps.check-script.outputs.skip }}
steps:
- name: Check out target branch
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.base_ref }}
@@ -840,7 +840,7 @@ jobs:
flash_usage: ${{ steps.extract.outputs.flash_usage }}
steps:
- name: Check out PR branch
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -908,7 +908,7 @@ jobs:
GH_TOKEN: ${{ github.token }}
steps:
- name: Check out code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:

View File

@@ -54,11 +54,11 @@ jobs:
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
uses: github/codeql-action/init@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -86,6 +86,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
uses: github/codeql-action/analyze@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11
with:
category: "/language:${{matrix.language}}"

View File

@@ -20,7 +20,7 @@ jobs:
branch_build: ${{ steps.tag.outputs.branch_build }}
deploy_env: ${{ steps.tag.outputs.deploy_env }}
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Get tag
id: tag
# yamllint disable rule:line-length
@@ -60,9 +60,9 @@ jobs:
contents: read
id-token: write
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.x"
- name: Build
@@ -92,9 +92,9 @@ jobs:
os: "ubuntu-24.04-arm"
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.11"
@@ -168,7 +168,7 @@ jobs:
- ghcr
- dockerhub
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Download digests
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0

View File

@@ -13,16 +13,16 @@ jobs:
if: github.repository == 'esphome/esphome'
steps:
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Checkout Home Assistant
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: home-assistant/core
path: lib/home-assistant
- name: Setup Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: 3.13
@@ -41,7 +41,7 @@ jobs:
python script/run-in-env.py pre-commit run --all-files
- name: Commit changes
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
commit-message: "Synchronise Device Classes from Home Assistant"
committer: esphomebot <esphome@openhomefoundation.org>

View File

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

View File

@@ -88,7 +88,8 @@ esphome/components/bmp3xx/* @latonita
esphome/components/bmp3xx_base/* @latonita @martgras
esphome/components/bmp3xx_i2c/* @latonita
esphome/components/bmp3xx_spi/* @latonita
esphome/components/bmp581/* @kahrendt
esphome/components/bmp581_base/* @danielkent-net @kahrendt
esphome/components/bmp581_i2c/* @danielkent-net @kahrendt
esphome/components/bp1658cj/* @Cossid
esphome/components/bp5758d/* @Cossid
esphome/components/bthome_mithermometer/* @nagyrobi
@@ -481,6 +482,7 @@ esphome/components/switch/* @esphome/core
esphome/components/switch/binary_sensor/* @ssieb
esphome/components/sx126x/* @swoboda1337
esphome/components/sx127x/* @swoboda1337
esphome/components/sy6970/* @linkedupbits
esphome/components/syslog/* @clydebarrow
esphome/components/t6615/* @tylermenezes
esphome/components/tc74/* @sethgirvan

View File

@@ -1,5 +1,6 @@
# PYTHON_ARGCOMPLETE_OK
import argparse
from collections.abc import Callable
from datetime import datetime
import functools
import getpass
@@ -42,6 +43,7 @@ from esphome.const import (
CONF_SUBSTITUTIONS,
CONF_TOPIC,
ENV_NOGITIGNORE,
KEY_NATIVE_IDF,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_RP2040,
@@ -115,6 +117,7 @@ class ArgsProtocol(Protocol):
configuration: str
name: str
upload_speed: str | None
native_idf: bool
def choose_prompt(options, purpose: str = None):
@@ -499,12 +502,15 @@ def wrap_to_code(name, comp):
return wrapped
def write_cpp(config: ConfigType) -> int:
def write_cpp(config: ConfigType, native_idf: bool = False) -> int:
if not get_bool_env(ENV_NOGITIGNORE):
writer.write_gitignore()
# Store native_idf flag so esp32 component can check it
CORE.data[KEY_NATIVE_IDF] = native_idf
generate_cpp_contents(config)
return write_cpp_file()
return write_cpp_file(native_idf=native_idf)
def generate_cpp_contents(config: ConfigType) -> None:
@@ -518,32 +524,54 @@ def generate_cpp_contents(config: ConfigType) -> None:
CORE.flush_tasks()
def write_cpp_file() -> int:
def write_cpp_file(native_idf: bool = False) -> int:
code_s = indent(CORE.cpp_main_section)
writer.write_cpp(code_s)
from esphome.build_gen import platformio
if native_idf and CORE.is_esp32 and CORE.target_framework == "esp-idf":
from esphome.build_gen import espidf
platformio.write_project()
espidf.write_project()
else:
from esphome.build_gen import platformio
platformio.write_project()
return 0
def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
from esphome import platformio_api
native_idf = getattr(args, "native_idf", False)
# NOTE: "Build path:" format is parsed by script/ci_memory_impact_extract.py
# If you change this format, update the regex in that script as well
_LOGGER.info("Compiling app... Build path: %s", CORE.build_path)
rc = platformio_api.run_compile(config, CORE.verbose)
if rc != 0:
return rc
if native_idf and CORE.is_esp32 and CORE.target_framework == "esp-idf":
from esphome import espidf_api
rc = espidf_api.run_compile(config, CORE.verbose)
if rc != 0:
return rc
# Create factory.bin and ota.bin
espidf_api.create_factory_bin()
espidf_api.create_ota_bin()
else:
from esphome import platformio_api
rc = platformio_api.run_compile(config, CORE.verbose)
if rc != 0:
return rc
idedata = platformio_api.get_idedata(config)
if idedata is None:
return 1
# Check if firmware was rebuilt and emit build_info + create manifest
_check_and_emit_build_info()
idedata = platformio_api.get_idedata(config)
return 0 if idedata is not None else 1
return 0
def _check_and_emit_build_info() -> None:
@@ -800,7 +828,8 @@ def command_vscode(args: ArgsProtocol) -> int | None:
def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
exit_code = write_cpp(config)
native_idf = getattr(args, "native_idf", False)
exit_code = write_cpp(config, native_idf=native_idf)
if exit_code != 0:
return exit_code
if args.only_generate:
@@ -855,7 +884,8 @@ def command_logs(args: ArgsProtocol, config: ConfigType) -> int | None:
def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
exit_code = write_cpp(config)
native_idf = getattr(args, "native_idf", False)
exit_code = write_cpp(config, native_idf=native_idf)
if exit_code != 0:
return exit_code
exit_code = compile_program(args, config)
@@ -936,11 +966,21 @@ def command_dashboard(args: ArgsProtocol) -> int | None:
return dashboard.start_dashboard(args)
def command_update_all(args: ArgsProtocol) -> int | None:
def run_multiple_configs(
files: list, command_builder: Callable[[str], list[str]]
) -> int:
"""Run a command for each configuration file in a subprocess.
Args:
files: List of configuration files to process.
command_builder: Callable that takes a file path and returns a command list.
Returns:
Number of failed files.
"""
import click
success = {}
files = list_yaml_files(args.configuration)
twidth = 60
def print_bar(middle_text):
@@ -950,17 +990,19 @@ def command_update_all(args: ArgsProtocol) -> int | None:
safe_print(f"{half_line}{middle_text}{half_line}")
for f in files:
safe_print(f"Updating {color(AnsiFore.CYAN, str(f))}")
f_path = Path(f) if not isinstance(f, Path) else f
if any(f_path.name == x for x in SECRETS_FILES):
_LOGGER.warning("Skipping secrets file %s", f_path)
continue
safe_print(f"Processing {color(AnsiFore.CYAN, str(f))}")
safe_print("-" * twidth)
safe_print()
if CORE.dashboard:
rc = run_external_process(
"esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA"
)
else:
rc = run_external_process(
"esphome", "run", f, "--no-logs", "--device", "OTA"
)
cmd = command_builder(f)
rc = run_external_process(*cmd)
if rc == 0:
print_bar(f"[{color(AnsiFore.BOLD_GREEN, 'SUCCESS')}] {str(f)}")
success[f] = True
@@ -975,6 +1017,8 @@ def command_update_all(args: ArgsProtocol) -> int | None:
print_bar(f"[{color(AnsiFore.BOLD_WHITE, 'SUMMARY')}]")
failed = 0
for f in files:
if f not in success:
continue # Skipped file
if success[f]:
safe_print(f" - {str(f)}: {color(AnsiFore.GREEN, 'SUCCESS')}")
else:
@@ -983,6 +1027,17 @@ def command_update_all(args: ArgsProtocol) -> int | None:
return failed
def command_update_all(args: ArgsProtocol) -> int | None:
files = list_yaml_files(args.configuration)
def build_command(f):
if CORE.dashboard:
return ["esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA"]
return ["esphome", "run", f, "--no-logs", "--device", "OTA"]
return run_multiple_configs(files, build_command)
def command_idedata(args: ArgsProtocol, config: ConfigType) -> int:
import json
@@ -1284,6 +1339,11 @@ def parse_args(argv):
help="Only generate source code, do not compile.",
action="store_true",
)
parser_compile.add_argument(
"--native-idf",
help="Build with native ESP-IDF instead of PlatformIO (ESP32 esp-idf framework only).",
action="store_true",
)
parser_upload = subparsers.add_parser(
"upload",
@@ -1365,6 +1425,11 @@ def parse_args(argv):
help="Reset the device before starting serial logs.",
default=os.getenv("ESPHOME_SERIAL_LOGGING_RESET"),
)
parser_run.add_argument(
"--native-idf",
help="Build with native ESP-IDF instead of PlatformIO (ESP32 esp-idf framework only).",
action="store_true",
)
parser_clean = subparsers.add_parser(
"clean-mqtt",
@@ -1533,38 +1598,48 @@ def run_esphome(argv):
_LOGGER.info("ESPHome %s", const.__version__)
for conf_path in args.configuration:
conf_path = Path(conf_path)
if any(conf_path.name == x for x in SECRETS_FILES):
_LOGGER.warning("Skipping secrets file %s", conf_path)
continue
# Multiple configurations: use subprocesses to avoid state leakage
# between compilations (e.g., LVGL touchscreen state in module globals)
if len(args.configuration) > 1:
# Build command by reusing argv, replacing all configs with single file
# argv[0] is the program path, skip it since we prefix with "esphome"
def build_command(f):
return (
["esphome"]
+ [arg for arg in argv[1:] if arg not in args.configuration]
+ [str(f)]
)
CORE.config_path = conf_path
CORE.dashboard = args.dashboard
return run_multiple_configs(args.configuration, build_command)
# For logs command, skip updating external components
skip_external = args.command == "logs"
config = read_config(
dict(args.substitution) if args.substitution else {},
skip_external_update=skip_external,
)
if config is None:
return 2
CORE.config = config
# Single configuration
conf_path = Path(args.configuration[0])
if any(conf_path.name == x for x in SECRETS_FILES):
_LOGGER.warning("Skipping secrets file %s", conf_path)
return 0
if args.command not in POST_CONFIG_ACTIONS:
safe_print(f"Unknown command {args.command}")
CORE.config_path = conf_path
CORE.dashboard = args.dashboard
try:
rc = POST_CONFIG_ACTIONS[args.command](args, config)
except EsphomeError as e:
_LOGGER.error(e, exc_info=args.verbose)
return 1
if rc != 0:
return rc
# For logs command, skip updating external components
skip_external = args.command == "logs"
config = read_config(
dict(args.substitution) if args.substitution else {},
skip_external_update=skip_external,
)
if config is None:
return 2
CORE.config = config
CORE.reset()
return 0
if args.command not in POST_CONFIG_ACTIONS:
safe_print(f"Unknown command {args.command}")
return 1
try:
return POST_CONFIG_ACTIONS[args.command](args, config)
except EsphomeError as e:
_LOGGER.error(e, exc_info=args.verbose)
return 1
def main():

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,
Parented,
PollingComponent,
StringRef,
arduino_json_ns,
bool_,
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_overlay(
f"""
&adc {{
#address-cells = <1>;
#size-cells = <0>;
&adc {{
#address-cells = <1>;
#size-cells = <0>;
channel@{channel_id} {{
reg = <{channel_id}>;
zephyr,gain = "{gain}";
zephyr,reference = "ADC_REF_INTERNAL";
zephyr,acquisition-time = <ADC_ACQ_TIME_DEFAULT>;
zephyr,input-positive = <NRF_SAADC_{pin_number}>;
zephyr,resolution = <14>;
zephyr,oversampling = <8>;
}};
}};
"""
channel@{channel_id} {{
reg = <{channel_id}>;
zephyr,gain = "{gain}";
zephyr,reference = "ADC_REF_INTERNAL";
zephyr,acquisition-time = <ADC_ACQ_TIME_DEFAULT>;
zephyr,input-positive = <NRF_SAADC_{pin_number}>;
zephyr,resolution = <14>;
zephyr,oversampling = <8>;
}};
}};
"""
)

View File

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

View File

@@ -76,37 +76,53 @@ class AlarmControlPanel : public EntityBase {
*
* @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
*
* @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
*
* @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
*
* @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
*
* @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
*
* @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
*
@@ -118,6 +134,8 @@ class AlarmControlPanel : public EntityBase {
protected:
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
ESPPreferenceObject pref_;
// current state

View File

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

View File

@@ -14,7 +14,8 @@ class AlarmControlPanelCall {
public:
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_home();
AlarmControlPanelCall &arm_night();

View File

@@ -66,15 +66,7 @@ template<typename... Ts> class ArmAwayAction : public Action<Ts...> {
TEMPLATABLE_VALUE(std::string, code)
void play(const Ts &...x) override {
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();
}
void play(const Ts &...x) override { this->alarm_control_panel_->arm_away(this->code_.optional_value(x...)); }
protected:
AlarmControlPanel *alarm_control_panel_;
@@ -86,15 +78,7 @@ template<typename... Ts> class ArmHomeAction : public Action<Ts...> {
TEMPLATABLE_VALUE(std::string, code)
void play(const Ts &...x) override {
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();
}
void play(const Ts &...x) override { this->alarm_control_panel_->arm_home(this->code_.optional_value(x...)); }
protected:
AlarmControlPanel *alarm_control_panel_;
@@ -106,15 +90,7 @@ template<typename... Ts> class ArmNightAction : public Action<Ts...> {
TEMPLATABLE_VALUE(std::string, code)
void play(const Ts &...x) override {
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();
}
void play(const Ts &...x) override { this->alarm_control_panel_->arm_night(this->code_.optional_value(x...)); }
protected:
AlarmControlPanel *alarm_control_panel_;

View File

@@ -1,21 +1,12 @@
#include "am43_base.h"
#include "esphome/core/helpers.h"
#include <cstring>
#include <cstdio>
namespace esphome {
namespace am43 {
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() {
uint8_t data = 0x1;
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);
this->packet_.length = length + 7;
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_;
}
@@ -88,7 +81,8 @@ void Am43Decoder::decode(const uint8_t *data, uint16_t length) {
this->has_set_state_response_ = false;
this->has_position_ = 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)
return;

View File

@@ -18,31 +18,31 @@ AnovaPacket *AnovaCodec::clean_packet_() {
AnovaPacket *AnovaCodec::get_read_device_status_request() {
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_();
}
AnovaPacket *AnovaCodec::get_read_target_temp_request() {
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_();
}
AnovaPacket *AnovaCodec::get_read_current_temp_request() {
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_();
}
AnovaPacket *AnovaCodec::get_read_unit_request() {
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_();
}
AnovaPacket *AnovaCodec::get_read_data_request() {
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_();
}
@@ -50,25 +50,25 @@ AnovaPacket *AnovaCodec::get_set_target_temp_request(float temperature) {
this->current_query_ = SET_TARGET_TEMPERATURE;
if (this->fahrenheit_)
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_();
}
AnovaPacket *AnovaCodec::get_set_unit_request(char 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_();
}
AnovaPacket *AnovaCodec::get_start_request() {
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_();
}
AnovaPacket *AnovaCodec::get_stop_request() {
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_();
}

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
// Use stack buffer for common case (states), heap fallback for large attributes
size_t state_len = msg.state.size();
SmallBufferWithHeapFallback<256> state_buf_alloc(state_len + 1);
SmallBufferWithHeapFallback<MAX_STATE_LEN + 1> state_buf_alloc(state_len + 1);
char *state_buf = reinterpret_cast<char *>(state_buf_alloc.get());
if (state_len > 0) {
memcpy(state_buf, msg.state.c_str(), state_len);
@@ -1844,23 +1844,8 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
return false;
}
// Toggle Nagle's algorithm based on message type to prevent log messages from
// filling the TCP send buffer and crowding out important state updates.
//
// This honors the `no_delay` proto option - SubscribeLogsResponse is the only
// message with `option (no_delay) = false;` in api.proto, indicating it should
// allow Nagle coalescing. This option existed since 2019 but was never implemented.
//
// - Log messages: Enable Nagle (NODELAY=false) so small log packets coalesce
// into fewer, larger packets. They flush naturally via TCP delayed ACK timer
// (~200ms), buffer filling, or when a state update triggers a flush.
//
// - All other messages (state updates, responses): Disable Nagle (NODELAY=true)
// for immediate delivery. These are time-sensitive and should not be delayed.
//
// This must be done proactively BEFORE the buffer fills up - checking buffer
// state here would be too late since we'd already be in a degraded state.
this->helper_->set_nodelay(!is_log_message);
// Set TCP_NODELAY based on message type - see set_nodelay_for_message() for details
this->helper_->set_nodelay_for_message(is_log_message);
APIError err = this->helper_->write_protobuf_packet(message_type, buffer);
if (err == APIError::WOULD_BLOCK)

View File

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

View File

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

View File

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

View File

@@ -108,10 +108,14 @@ void ATM90E32Component::update() {
#endif
}
void ATM90E32Component::get_cs_summary_(std::span<char, GPIO_SUMMARY_MAX_LEN> buffer) {
this->cs_->dump_summary(buffer.data(), buffer.size());
}
void ATM90E32Component::setup() {
this->spi_setup();
this->cs_summary_ = this->cs_->dump_summary();
const char *cs = this->cs_summary_.c_str();
char cs[GPIO_SUMMARY_MAX_LEN];
this->get_cs_summary_(cs);
uint16_t mmode0 = 0x87; // 3P4W 50Hz
uint16_t high_thresh = 0;
@@ -159,13 +163,13 @@ void ATM90E32Component::setup() {
if (this->enable_offset_calibration_) {
// Initialize flash storage for offset calibrations
uint32_t o_hash = fnv1_hash("_offset_calibration_");
o_hash = fnv1_hash_extend(o_hash, this->cs_summary_);
o_hash = fnv1_hash_extend(o_hash, cs);
this->offset_pref_ = global_preferences->make_preference<OffsetCalibration[3]>(o_hash, true);
this->restore_offset_calibrations_();
// Initialize flash storage for power offset calibrations
uint32_t po_hash = fnv1_hash("_power_offset_calibration_");
po_hash = fnv1_hash_extend(po_hash, this->cs_summary_);
po_hash = fnv1_hash_extend(po_hash, cs);
this->power_offset_pref_ = global_preferences->make_preference<PowerOffsetCalibration[3]>(po_hash, true);
this->restore_power_offset_calibrations_();
} else {
@@ -186,7 +190,7 @@ void ATM90E32Component::setup() {
if (this->enable_gain_calibration_) {
// Initialize flash storage for gain calibration
uint32_t g_hash = fnv1_hash("_gain_calibration_");
g_hash = fnv1_hash_extend(g_hash, this->cs_summary_);
g_hash = fnv1_hash_extend(g_hash, cs);
this->gain_calibration_pref_ = global_preferences->make_preference<GainCalibration[3]>(g_hash, true);
this->restore_gain_calibrations_();
@@ -217,7 +221,8 @@ void ATM90E32Component::setup() {
}
void ATM90E32Component::log_calibration_status_() {
const char *cs = this->cs_summary_.c_str();
char cs[GPIO_SUMMARY_MAX_LEN];
this->get_cs_summary_(cs);
bool offset_mismatch = false;
bool power_mismatch = false;
@@ -568,7 +573,8 @@ float ATM90E32Component::get_chip_temperature_() {
}
void ATM90E32Component::run_gain_calibrations() {
const char *cs = this->cs_summary_.c_str();
char cs[GPIO_SUMMARY_MAX_LEN];
this->get_cs_summary_(cs);
if (!this->enable_gain_calibration_) {
ESP_LOGW(TAG, "[CALIBRATION][%s] Gain calibration is disabled! Enable it first with enable_gain_calibration: true",
cs);
@@ -668,7 +674,8 @@ void ATM90E32Component::run_gain_calibrations() {
}
void ATM90E32Component::save_gain_calibration_to_memory_() {
const char *cs = this->cs_summary_.c_str();
char cs[GPIO_SUMMARY_MAX_LEN];
this->get_cs_summary_(cs);
bool success = this->gain_calibration_pref_.save(&this->gain_phase_);
global_preferences->sync();
if (success) {
@@ -681,7 +688,8 @@ void ATM90E32Component::save_gain_calibration_to_memory_() {
}
void ATM90E32Component::save_offset_calibration_to_memory_() {
const char *cs = this->cs_summary_.c_str();
char cs[GPIO_SUMMARY_MAX_LEN];
this->get_cs_summary_(cs);
bool success = this->offset_pref_.save(&this->offset_phase_);
global_preferences->sync();
if (success) {
@@ -697,7 +705,8 @@ void ATM90E32Component::save_offset_calibration_to_memory_() {
}
void ATM90E32Component::save_power_offset_calibration_to_memory_() {
const char *cs = this->cs_summary_.c_str();
char cs[GPIO_SUMMARY_MAX_LEN];
this->get_cs_summary_(cs);
bool success = this->power_offset_pref_.save(&this->power_offset_phase_);
global_preferences->sync();
if (success) {
@@ -713,7 +722,8 @@ void ATM90E32Component::save_power_offset_calibration_to_memory_() {
}
void ATM90E32Component::run_offset_calibrations() {
const char *cs = this->cs_summary_.c_str();
char cs[GPIO_SUMMARY_MAX_LEN];
this->get_cs_summary_(cs);
if (!this->enable_offset_calibration_) {
ESP_LOGW(TAG,
"[CALIBRATION][%s] Offset calibration is disabled! Enable it first with enable_offset_calibration: true",
@@ -743,7 +753,8 @@ void ATM90E32Component::run_offset_calibrations() {
}
void ATM90E32Component::run_power_offset_calibrations() {
const char *cs = this->cs_summary_.c_str();
char cs[GPIO_SUMMARY_MAX_LEN];
this->get_cs_summary_(cs);
if (!this->enable_offset_calibration_) {
ESP_LOGW(
TAG,
@@ -816,7 +827,8 @@ void ATM90E32Component::write_power_offsets_to_registers_(uint8_t phase, int16_t
}
void ATM90E32Component::restore_gain_calibrations_() {
const char *cs = this->cs_summary_.c_str();
char cs[GPIO_SUMMARY_MAX_LEN];
this->get_cs_summary_(cs);
for (uint8_t i = 0; i < 3; ++i) {
this->config_gain_phase_[i].voltage_gain = this->phase_[i].voltage_gain_;
this->config_gain_phase_[i].current_gain = this->phase_[i].ct_gain_;
@@ -870,7 +882,8 @@ void ATM90E32Component::restore_gain_calibrations_() {
}
void ATM90E32Component::restore_offset_calibrations_() {
const char *cs = this->cs_summary_.c_str();
char cs[GPIO_SUMMARY_MAX_LEN];
this->get_cs_summary_(cs);
for (uint8_t i = 0; i < 3; ++i)
this->config_offset_phase_[i] = this->offset_phase_[i];
@@ -912,7 +925,8 @@ void ATM90E32Component::restore_offset_calibrations_() {
}
void ATM90E32Component::restore_power_offset_calibrations_() {
const char *cs = this->cs_summary_.c_str();
char cs[GPIO_SUMMARY_MAX_LEN];
this->get_cs_summary_(cs);
for (uint8_t i = 0; i < 3; ++i)
this->config_power_offset_phase_[i] = this->power_offset_phase_[i];
@@ -954,7 +968,8 @@ void ATM90E32Component::restore_power_offset_calibrations_() {
}
void ATM90E32Component::clear_gain_calibrations() {
const char *cs = this->cs_summary_.c_str();
char cs[GPIO_SUMMARY_MAX_LEN];
this->get_cs_summary_(cs);
if (!this->using_saved_calibrations_) {
ESP_LOGI(TAG, "[CALIBRATION][%s] No stored gain calibrations to clear. Current values:", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] ----------------------------------------------------------", cs);
@@ -1003,7 +1018,8 @@ void ATM90E32Component::clear_gain_calibrations() {
}
void ATM90E32Component::clear_offset_calibrations() {
const char *cs = this->cs_summary_.c_str();
char cs[GPIO_SUMMARY_MAX_LEN];
this->get_cs_summary_(cs);
if (!this->restored_offset_calibration_) {
ESP_LOGI(TAG, "[CALIBRATION][%s] No stored offset calibrations to clear. Current values:", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs);
@@ -1045,7 +1061,8 @@ void ATM90E32Component::clear_offset_calibrations() {
}
void ATM90E32Component::clear_power_offset_calibrations() {
const char *cs = this->cs_summary_.c_str();
char cs[GPIO_SUMMARY_MAX_LEN];
this->get_cs_summary_(cs);
if (!this->restored_power_offset_calibration_) {
ESP_LOGI(TAG, "[CALIBRATION][%s] No stored power offsets to clear. Current values:", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
@@ -1120,7 +1137,8 @@ int16_t ATM90E32Component::calibrate_power_offset(uint8_t phase, bool reactive)
}
bool ATM90E32Component::verify_gain_writes_() {
const char *cs = this->cs_summary_.c_str();
char cs[GPIO_SUMMARY_MAX_LEN];
this->get_cs_summary_(cs);
bool success = true;
for (uint8_t phase = 0; phase < 3; phase++) {
uint16_t read_voltage = this->read16_(voltage_gain_registers[phase]);

View File

@@ -1,11 +1,13 @@
#pragma once
#include <span>
#include <unordered_map>
#include "atm90e32_reg.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/spi/spi.h"
#include "esphome/core/application.h"
#include "esphome/core/component.h"
#include "esphome/core/gpio.h"
#include "esphome/core/helpers.h"
#include "esphome/core/preferences.h"
@@ -182,6 +184,7 @@ class ATM90E32Component : public PollingComponent,
bool verify_gain_writes_();
bool validate_spi_read_(uint16_t expected, const char *context = nullptr);
void log_calibration_status_();
void get_cs_summary_(std::span<char, GPIO_SUMMARY_MAX_LEN> buffer);
struct ATM90E32Phase {
uint16_t voltage_gain_{0};
@@ -247,7 +250,6 @@ class ATM90E32Component : public PollingComponent,
ESPPreferenceObject offset_pref_;
ESPPreferenceObject power_offset_pref_;
ESPPreferenceObject gain_calibration_pref_;
std::string cs_summary_;
sensor::Sensor *freq_sensor_{nullptr};
#ifdef USE_TEXT_SENSOR

View File

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

View File

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

View File

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

View File

@@ -1,164 +1,5 @@
import math
import esphome.codegen as cg
from esphome.components import i2c, sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_IIR_FILTER,
CONF_OVERSAMPLING,
CONF_PRESSURE,
CONF_TEMPERATURE,
DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_PASCAL,
CONFIG_SCHEMA = cv.invalid(
"The bmp581 sensor component has been renamed to bmp581_i2c."
)
CODEOWNERS = ["@kahrendt"]
DEPENDENCIES = ["i2c"]
bmp581_ns = cg.esphome_ns.namespace("bmp581")
Oversampling = bmp581_ns.enum("Oversampling")
OVERSAMPLING_OPTIONS = {
"NONE": Oversampling.OVERSAMPLING_NONE,
"2X": Oversampling.OVERSAMPLING_X2,
"4X": Oversampling.OVERSAMPLING_X4,
"8X": Oversampling.OVERSAMPLING_X8,
"16X": Oversampling.OVERSAMPLING_X16,
"32X": Oversampling.OVERSAMPLING_X32,
"64X": Oversampling.OVERSAMPLING_X64,
"128X": Oversampling.OVERSAMPLING_X128,
}
IIRFilter = bmp581_ns.enum("IIRFilter")
IIR_FILTER_OPTIONS = {
"OFF": IIRFilter.IIR_FILTER_OFF,
"2X": IIRFilter.IIR_FILTER_2,
"4X": IIRFilter.IIR_FILTER_4,
"8X": IIRFilter.IIR_FILTER_8,
"16X": IIRFilter.IIR_FILTER_16,
"32X": IIRFilter.IIR_FILTER_32,
"64X": IIRFilter.IIR_FILTER_64,
"128X": IIRFilter.IIR_FILTER_128,
}
BMP581Component = bmp581_ns.class_(
"BMP581Component", cg.PollingComponent, i2c.I2CDevice
)
def compute_measurement_conversion_time(config):
# - adds up sensor conversion time based on temperature and pressure oversampling rates given in datasheet
# - returns a rounded up time in ms
# Page 12 of datasheet
PRESSURE_OVERSAMPLING_CONVERSION_TIMES = {
"NONE": 1.0,
"2X": 1.7,
"4X": 2.9,
"8X": 5.4,
"16X": 10.4,
"32X": 20.4,
"64X": 40.4,
"128X": 80.4,
}
# Page 12 of datasheet
TEMPERATURE_OVERSAMPLING_CONVERSION_TIMES = {
"NONE": 1.0,
"2X": 1.1,
"4X": 1.5,
"8X": 2.1,
"16X": 3.3,
"32X": 5.8,
"64X": 10.8,
"128X": 20.8,
}
pressure_conversion_time = (
0.0 # No conversion time necessary without a pressure sensor
)
if pressure_config := config.get(CONF_PRESSURE):
pressure_conversion_time = PRESSURE_OVERSAMPLING_CONVERSION_TIMES[
pressure_config.get(CONF_OVERSAMPLING)
]
temperature_conversion_time = (
1.0 # BMP581 always samples the temperature even if only reading pressure
)
if temperature_config := config.get(CONF_TEMPERATURE):
temperature_conversion_time = TEMPERATURE_OVERSAMPLING_CONVERSION_TIMES[
temperature_config.get(CONF_OVERSAMPLING)
]
# Datasheet indicates a 5% possible error in each conversion time listed
return math.ceil(1.05 * (pressure_conversion_time + temperature_conversion_time))
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(BMP581Component),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_OVERSAMPLING, default="NONE"): cv.enum(
OVERSAMPLING_OPTIONS, upper=True
),
cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum(
IIR_FILTER_OPTIONS, upper=True
),
}
),
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
unit_of_measurement=UNIT_PASCAL,
accuracy_decimals=0,
device_class=DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
OVERSAMPLING_OPTIONS, upper=True
),
cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum(
IIR_FILTER_OPTIONS, upper=True
),
}
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x46))
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature_config)
cg.add(var.set_temperature_sensor(sens))
cg.add(
var.set_temperature_oversampling_config(
temperature_config[CONF_OVERSAMPLING]
)
)
cg.add(
var.set_temperature_iir_filter_config(temperature_config[CONF_IIR_FILTER])
)
if pressure_config := config.get(CONF_PRESSURE):
sens = await sensor.new_sensor(pressure_config)
cg.add(var.set_pressure_sensor(sens))
cg.add(var.set_pressure_oversampling_config(pressure_config[CONF_OVERSAMPLING]))
cg.add(var.set_pressure_iir_filter_config(pressure_config[CONF_IIR_FILTER]))
cg.add(var.set_conversion_time(compute_measurement_conversion_time(config)))

View File

@@ -0,0 +1,157 @@
import math
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_IIR_FILTER,
CONF_OVERSAMPLING,
CONF_PRESSURE,
CONF_TEMPERATURE,
DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_PASCAL,
)
CODEOWNERS = ["@kahrendt", "@danielkent-net"]
bmp581_ns = cg.esphome_ns.namespace("bmp581_base")
Oversampling = bmp581_ns.enum("Oversampling")
OVERSAMPLING_OPTIONS = {
"NONE": Oversampling.OVERSAMPLING_NONE,
"2X": Oversampling.OVERSAMPLING_X2,
"4X": Oversampling.OVERSAMPLING_X4,
"8X": Oversampling.OVERSAMPLING_X8,
"16X": Oversampling.OVERSAMPLING_X16,
"32X": Oversampling.OVERSAMPLING_X32,
"64X": Oversampling.OVERSAMPLING_X64,
"128X": Oversampling.OVERSAMPLING_X128,
}
IIRFilter = bmp581_ns.enum("IIRFilter")
IIR_FILTER_OPTIONS = {
"OFF": IIRFilter.IIR_FILTER_OFF,
"2X": IIRFilter.IIR_FILTER_2,
"4X": IIRFilter.IIR_FILTER_4,
"8X": IIRFilter.IIR_FILTER_8,
"16X": IIRFilter.IIR_FILTER_16,
"32X": IIRFilter.IIR_FILTER_32,
"64X": IIRFilter.IIR_FILTER_64,
"128X": IIRFilter.IIR_FILTER_128,
}
BMP581Component = bmp581_ns.class_("BMP581Component", cg.PollingComponent)
def compute_measurement_conversion_time(config):
# - adds up sensor conversion time based on temperature and pressure oversampling rates given in datasheet
# - returns a rounded up time in ms
# Page 12 of datasheet
PRESSURE_OVERSAMPLING_CONVERSION_TIMES = {
"NONE": 1.0,
"2X": 1.7,
"4X": 2.9,
"8X": 5.4,
"16X": 10.4,
"32X": 20.4,
"64X": 40.4,
"128X": 80.4,
}
# Page 12 of datasheet
TEMPERATURE_OVERSAMPLING_CONVERSION_TIMES = {
"NONE": 1.0,
"2X": 1.1,
"4X": 1.5,
"8X": 2.1,
"16X": 3.3,
"32X": 5.8,
"64X": 10.8,
"128X": 20.8,
}
pressure_conversion_time = (
0.0 # No conversion time necessary without a pressure sensor
)
if pressure_config := config.get(CONF_PRESSURE):
pressure_conversion_time = PRESSURE_OVERSAMPLING_CONVERSION_TIMES[
pressure_config.get(CONF_OVERSAMPLING)
]
temperature_conversion_time = (
1.0 # BMP581 always samples the temperature even if only reading pressure
)
if temperature_config := config.get(CONF_TEMPERATURE):
temperature_conversion_time = TEMPERATURE_OVERSAMPLING_CONVERSION_TIMES[
temperature_config.get(CONF_OVERSAMPLING)
]
# Datasheet indicates a 5% possible error in each conversion time listed
return math.ceil(1.05 * (pressure_conversion_time + temperature_conversion_time))
CONFIG_SCHEMA_BASE = cv.Schema(
{
cv.GenerateID(): cv.declare_id(BMP581Component),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_OVERSAMPLING, default="NONE"): cv.enum(
OVERSAMPLING_OPTIONS, upper=True
),
cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum(
IIR_FILTER_OPTIONS, upper=True
),
}
),
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
unit_of_measurement=UNIT_PASCAL,
accuracy_decimals=0,
device_class=DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
OVERSAMPLING_OPTIONS, upper=True
),
cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum(
IIR_FILTER_OPTIONS, upper=True
),
}
),
}
).extend(cv.polling_component_schema("60s"))
async def to_code_base(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
if temperature_config := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature_config)
cg.add(var.set_temperature_sensor(sens))
cg.add(
var.set_temperature_oversampling_config(
temperature_config[CONF_OVERSAMPLING]
)
)
cg.add(
var.set_temperature_iir_filter_config(temperature_config[CONF_IIR_FILTER])
)
if pressure_config := config.get(CONF_PRESSURE):
sens = await sensor.new_sensor(pressure_config)
cg.add(var.set_pressure_sensor(sens))
cg.add(var.set_pressure_oversampling_config(pressure_config[CONF_OVERSAMPLING]))
cg.add(var.set_pressure_iir_filter_config(pressure_config[CONF_IIR_FILTER]))
cg.add(var.set_conversion_time(compute_measurement_conversion_time(config)))
return var

View File

@@ -10,12 +10,11 @@
* - All datasheet page references refer to Bosch Document Number BST-BMP581-DS004-04 (revision number 1.4)
*/
#include "bmp581.h"
#include "bmp581_base.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
namespace esphome {
namespace bmp581 {
namespace esphome::bmp581_base {
static const char *const TAG = "bmp581";
@@ -91,7 +90,6 @@ void BMP581Component::dump_config() {
break;
}
LOG_I2C_DEVICE(this);
LOG_UPDATE_INTERVAL(this);
ESP_LOGCONFIG(TAG, " Measurement conversion time: %ums", this->conversion_time_);
@@ -149,7 +147,7 @@ void BMP581Component::setup() {
uint8_t chip_id;
// read chip id from sensor
if (!this->read_byte(BMP581_CHIP_ID, &chip_id)) {
if (!this->bmp_read_byte(BMP581_CHIP_ID, &chip_id)) {
ESP_LOGE(TAG, "Read chip ID failed");
this->error_code_ = ERROR_COMMUNICATION_FAILED;
@@ -172,7 +170,7 @@ void BMP581Component::setup() {
// 3) Verify sensor status (check if NVM is okay) //
////////////////////////////////////////////////////
if (!this->read_byte(BMP581_STATUS, &this->status_.reg)) {
if (!this->bmp_read_byte(BMP581_STATUS, &this->status_.reg)) {
ESP_LOGE(TAG, "Failed to read status register");
this->error_code_ = ERROR_COMMUNICATION_FAILED;
@@ -359,7 +357,7 @@ bool BMP581Component::check_data_readiness_() {
uint8_t status;
if (!this->read_byte(BMP581_INT_STATUS, &status)) {
if (!this->bmp_read_byte(BMP581_INT_STATUS, &status)) {
ESP_LOGE(TAG, "Failed to read interrupt status register");
return false;
}
@@ -400,7 +398,7 @@ bool BMP581Component::prime_iir_filter_() {
// flush the IIR filter with forced measurements (we will only flush once)
this->dsp_config_.bit.iir_flush_forced_en = true;
if (!this->write_byte(BMP581_DSP, this->dsp_config_.reg)) {
if (!this->bmp_write_byte(BMP581_DSP, this->dsp_config_.reg)) {
ESP_LOGE(TAG, "Failed to write IIR source register");
return false;
@@ -430,7 +428,7 @@ bool BMP581Component::prime_iir_filter_() {
// disable IIR filter flushings on future forced measurements
this->dsp_config_.bit.iir_flush_forced_en = false;
if (!this->write_byte(BMP581_DSP, this->dsp_config_.reg)) {
if (!this->bmp_write_byte(BMP581_DSP, this->dsp_config_.reg)) {
ESP_LOGE(TAG, "Failed to write IIR source register");
return false;
@@ -454,7 +452,7 @@ bool BMP581Component::read_temperature_(float &temperature) {
}
uint8_t data[3];
if (!this->read_bytes(BMP581_MEASUREMENT_DATA, &data[0], 3)) {
if (!this->bmp_read_bytes(BMP581_MEASUREMENT_DATA, &data[0], 3)) {
ESP_LOGW(TAG, "Failed to read measurement");
this->status_set_warning();
@@ -483,7 +481,7 @@ bool BMP581Component::read_temperature_and_pressure_(float &temperature, float &
}
uint8_t data[6];
if (!this->read_bytes(BMP581_MEASUREMENT_DATA, &data[0], 6)) {
if (!this->bmp_read_bytes(BMP581_MEASUREMENT_DATA, &data[0], 6)) {
ESP_LOGW(TAG, "Failed to read measurement");
this->status_set_warning();
@@ -507,7 +505,7 @@ bool BMP581Component::reset_() {
// - returns the Power-On-Reboot interrupt status, which is asserted if successful
// writes reset command to BMP's command register
if (!this->write_byte(BMP581_COMMAND, RESET_COMMAND)) {
if (!this->bmp_write_byte(BMP581_COMMAND, RESET_COMMAND)) {
ESP_LOGE(TAG, "Failed to write reset command");
return false;
@@ -518,7 +516,7 @@ bool BMP581Component::reset_() {
delay(3);
// read interrupt status register
if (!this->read_byte(BMP581_INT_STATUS, &this->int_status_.reg)) {
if (!this->bmp_read_byte(BMP581_INT_STATUS, &this->int_status_.reg)) {
ESP_LOGE(TAG, "Failed to read interrupt status register");
return false;
@@ -562,7 +560,7 @@ bool BMP581Component::write_iir_settings_(IIRFilter temperature_iir, IIRFilter p
// BMP581_DSP register and BMP581_DSP_IIR registers are successive
// - allows us to write the IIR configuration with one command to both registers
uint8_t register_data[2] = {this->dsp_config_.reg, this->iir_config_.reg};
return this->write_bytes(BMP581_DSP, register_data, sizeof(register_data));
return this->bmp_write_bytes(BMP581_DSP, register_data, sizeof(register_data));
}
bool BMP581Component::write_interrupt_source_settings_(bool data_ready_enable) {
@@ -572,7 +570,7 @@ bool BMP581Component::write_interrupt_source_settings_(bool data_ready_enable) {
this->int_source_.bit.drdy_data_reg_en = data_ready_enable;
// write interrupt source register
return this->write_byte(BMP581_INT_SOURCE, this->int_source_.reg);
return this->bmp_write_byte(BMP581_INT_SOURCE, this->int_source_.reg);
}
bool BMP581Component::write_oversampling_settings_(Oversampling temperature_oversampling,
@@ -583,7 +581,7 @@ bool BMP581Component::write_oversampling_settings_(Oversampling temperature_over
this->osr_config_.bit.osr_t = temperature_oversampling;
this->osr_config_.bit.osr_p = pressure_oversampling;
return this->write_byte(BMP581_OSR, this->osr_config_.reg);
return this->bmp_write_byte(BMP581_OSR, this->osr_config_.reg);
}
bool BMP581Component::write_power_mode_(OperationMode mode) {
@@ -593,8 +591,7 @@ bool BMP581Component::write_power_mode_(OperationMode mode) {
this->odr_config_.bit.pwr_mode = mode;
// write odr register
return this->write_byte(BMP581_ODR, this->odr_config_.reg);
return this->bmp_write_byte(BMP581_ODR, this->odr_config_.reg);
}
} // namespace bmp581
} // namespace esphome
} // namespace esphome::bmp581_base

View File

@@ -3,11 +3,9 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/sensor/sensor.h"
namespace esphome {
namespace bmp581 {
namespace esphome::bmp581_base {
static const uint8_t BMP581_ASIC_ID = 0x50; // BMP581's ASIC chip ID (page 51 of datasheet)
static const uint8_t RESET_COMMAND = 0xB6; // Soft reset command
@@ -59,7 +57,7 @@ enum IIRFilter {
IIR_FILTER_128 = 0x7
};
class BMP581Component : public PollingComponent, public i2c::I2CDevice {
class BMP581Component : public PollingComponent {
public:
void dump_config() override;
@@ -84,6 +82,11 @@ class BMP581Component : public PollingComponent, public i2c::I2CDevice {
void set_conversion_time(uint8_t conversion_time) { this->conversion_time_ = conversion_time; }
protected:
virtual bool bmp_read_byte(uint8_t a_register, uint8_t *data) = 0;
virtual bool bmp_write_byte(uint8_t a_register, uint8_t data) = 0;
virtual bool bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0;
virtual bool bmp_write_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0;
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *pressure_sensor_{nullptr};
@@ -216,5 +219,4 @@ class BMP581Component : public PollingComponent, public i2c::I2CDevice {
} odr_config_ = {.reg = 0};
};
} // namespace bmp581
} // namespace esphome
} // namespace esphome::bmp581_base

View File

@@ -0,0 +1,12 @@
#include "bmp581_i2c.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome::bmp581_i2c {
void BMP581I2CComponent::dump_config() {
LOG_I2C_DEVICE(this);
BMP581Component::dump_config();
}
} // namespace esphome::bmp581_i2c

View File

@@ -0,0 +1,24 @@
#pragma once
#include "esphome/components/bmp581_base/bmp581_base.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome::bmp581_i2c {
static const char *const TAG = "bmp581_i2c.sensor";
/// This class implements support for the BMP581 Temperature+Pressure i2c sensor.
class BMP581I2CComponent : public esphome::bmp581_base::BMP581Component, public i2c::I2CDevice {
public:
bool bmp_read_byte(uint8_t a_register, uint8_t *data) override { return read_byte(a_register, data); }
bool bmp_write_byte(uint8_t a_register, uint8_t data) override { return write_byte(a_register, data); }
bool bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) override {
return read_bytes(a_register, data, len);
}
bool bmp_write_bytes(uint8_t a_register, uint8_t *data, size_t len) override {
return write_bytes(a_register, data, len);
}
void dump_config() override;
};
} // namespace esphome::bmp581_i2c

View File

@@ -0,0 +1,23 @@
import esphome.codegen as cg
from esphome.components import i2c
import esphome.config_validation as cv
from ..bmp581_base import CONFIG_SCHEMA_BASE, to_code_base
AUTO_LOAD = ["bmp581_base"]
CODEOWNERS = ["@kahrendt", "@danielkent-net"]
DEPENDENCIES = ["i2c"]
bmp581_ns = cg.esphome_ns.namespace("bmp581_i2c")
BMP581I2CComponent = bmp581_ns.class_(
"BMP581I2CComponent", cg.PollingComponent, i2c.I2CDevice
)
CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend(
i2c.i2c_device_schema(default_address=0x46)
).extend({cv.GenerateID(): cv.declare_id(BMP581I2CComponent)})
async def to_code(config):
var = await to_code_base(config)
await i2c.register_i2c_device(var, config)

View File

@@ -1,7 +1,8 @@
import esphome.codegen as cg
from esphome.components import esp32_ble_tracker
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"]
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.Required(CONF_MAC_ADDRESS): cv.mac_address,
cv.Optional(CONF_BINDKEY): cv.bind_key,
}
)
.extend(BLE_DEVICE_SCHEMA)
@@ -34,3 +36,9 @@ async def setup_bthome_mithermometer(var, config):
await cg.register_component(var, config)
await esp32_ble_tracker.register_ble_device(var, config)
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/log.h"
#include <algorithm>
#include <array>
#include <cstring>
#include <span>
#ifdef USE_ESP32
#include "mbedtls/ccm.h"
namespace esphome {
namespace 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) {
std::array<uint8_t, MAC_ADDRESS_SIZE> mac{};
@@ -130,6 +138,10 @@ void BTHomeMiThermometer::dump_config() {
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
ESP_LOGCONFIG(TAG, "BTHome MiThermometer");
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(" ", "Humidity", this->humidity_);
LOG_SENSOR(" ", "Battery Level", this->battery_level_);
@@ -150,6 +162,60 @@ bool BTHomeMiThermometer::parse_device(const esp32_ble_tracker::ESPBTDevice &dev
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,
const esp32_ble_tracker::ESPBTDevice &device) {
if (!service_data.uuid.contains(0xD2, 0xFC)) {
@@ -173,51 +239,88 @@ bool BTHomeMiThermometer::handle_service_data_(const esp32_ble_tracker::ServiceD
return false;
}
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
if (is_encrypted) {
ESP_LOGV(TAG, "Ignoring encrypted BTHome frame from %s", device.address_str_to(addr_buf));
uint64_t source_address = device.address_uint64();
bool address_matches = source_address == this->address_;
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;
}
size_t payload_index = 1;
uint64_t source_address = device.address_uint64();
if (!is_encrypted && this->has_bindkey_) {
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 (data.size() < 7) {
if (payload_size < 6) {
ESP_LOGVV(TAG, "BTHome payload missing MAC address");
return false;
}
source_address = 0;
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_) {
ESP_LOGVV(TAG, "BTHome frame from unexpected device %s", format_mac_address(addr_buf, source_address));
return false;
}
if (payload_index >= data.size()) {
if (payload_size == 0) {
ESP_LOGVV(TAG, "BTHome payload empty after header");
return false;
}
bool reported = false;
size_t offset = payload_index;
size_t offset = 0;
uint8_t last_type = 0;
while (offset < data.size()) {
const uint8_t obj_type = data[offset++];
while (offset < payload_size) {
const uint8_t obj_type = payload[offset++];
size_t value_length = 0;
bool has_length_byte = obj_type == 0x53; // text objects include explicit length
if (has_length_byte) {
if (offset >= data.size()) {
if (offset >= payload_size) {
break;
}
value_length = data[offset++];
value_length = payload[offset++];
} else {
if (!get_bthome_value_length(obj_type, value_length)) {
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;
}
if (offset + value_length > data.size()) {
if (offset + value_length > payload_size) {
ESP_LOGVV(TAG, "BTHome object length exceeds payload");
break;
}
const uint8_t *value = &data[offset];
const uint8_t *value = &payload[offset];
offset += value_length;
if (obj_type < last_type) {

View File

@@ -5,6 +5,8 @@
#include "esphome/core/component.h"
#include <cstdint>
#include <initializer_list>
#include <vector>
#ifdef USE_ESP32
@@ -14,6 +16,7 @@ namespace bthome_mithermometer {
class BTHomeMiThermometer : public esp32_ble_tracker::ESPBTDeviceListener, public Component {
public:
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_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; }
@@ -27,9 +30,13 @@ class BTHomeMiThermometer : public esp32_ble_tracker::ESPBTDeviceListener, publi
protected:
bool handle_service_data_(const esp32_ble_tracker::ServiceData &service_data,
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};
optional<uint8_t> last_packet_id_{};
bool has_bindkey_{false};
uint8_t bindkey_[16];
sensor::Sensor *temperature_{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() {
if (this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO) || this->gdo0_pin_ == nullptr ||
!this->gdo0_pin_->digital_read()) {
@@ -198,7 +205,7 @@ void CC1101Component::loop() {
bool crc_ok = (this->state_.LQI & STATUS_CRC_OK_MASK) != 0;
uint8_t lqi = this->state_.LQI & STATUS_LQI_MASK;
if (this->state_.CRC_EN == 0 || crc_ok) {
this->packet_trigger_->trigger(this->packet_, freq_offset, rssi, lqi);
this->call_listeners_(this->packet_, freq_offset, rssi, lqi);
}
// Return to rx

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -30,7 +30,7 @@ void DebugComponent::dump_config() {
char device_info_buffer[DEVICE_INFO_BUFFER_SIZE];
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_();
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/macros.h"
#include <span>
#include <cstdarg>
#include <cstdio>
#include <algorithm>
#ifdef USE_ESP8266
#include <pgmspace.h>
#endif
#ifdef USE_SENSOR
#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 RESET_REASON_BUFFER_SIZE = 128;
#ifdef USE_ESP8266
// 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
// buf_append_printf is now provided by esphome/core/helpers.h
class DebugComponent : public PollingComponent {
public:

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_speed = ESP.getFlashChipSpeed() / 1000000; // NOLINT
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,
flash_mode);
pos = buf_append_printf(buf, size, pos, "|Flash: %" PRIu32 "kB Speed:%" PRIu32 "MHz Mode:%s", flash_size, flash_speed,
flash_mode);
#endif
esp_chip_info_t info;
@@ -182,52 +182,52 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
const char *model = ESPHOME_VARIANT;
// 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;
for (const auto &feature : CHIP_FEATURES) {
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;
info.features &= ~feature.bit;
}
}
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);
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;
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
#ifdef USE_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)
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
ESP_LOGW(TAG, "Framework: UNKNOWN");
pos = buf_append(buf, size, pos, "|Framework: UNKNOWN");
pos = buf_append_printf(buf, size, pos, "|Framework: UNKNOWN");
#endif
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];
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]);
pos = buf_append(buf, size, pos, "|EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4],
mac[5]);
pos = buf_append_printf(buf, size, pos, "|EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3],
mac[4], mac[5]);
char reason_buffer[RESET_REASON_BUFFER_SIZE];
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));
pos = buf_append(buf, size, pos, "|Wakeup: %s", wakeup_cause);
pos = buf_append_printf(buf, size, pos, "|Wakeup: %s", wakeup_cause);
return pos;
}

View File

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

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

View File

@@ -20,9 +20,9 @@ static size_t append_reset_reason(char *buf, size_t size, size_t pos, bool set,
return pos;
}
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) {
@@ -132,6 +132,26 @@ void DebugComponent::log_partition_info_() {
flash_area_foreach(fa_cb, nullptr);
}
static const char *regout0_to_str(uint32_t value) {
switch (value) {
case (UICR_REGOUT0_VOUT_DEFAULT):
return "1.8V (default)";
case (UICR_REGOUT0_VOUT_1V8):
return "1.8V";
case (UICR_REGOUT0_VOUT_2V1):
return "2.1V";
case (UICR_REGOUT0_VOUT_2V4):
return "2.4V";
case (UICR_REGOUT0_VOUT_2V7):
return "2.7V";
case (UICR_REGOUT0_VOUT_3V0):
return "3.0V";
case (UICR_REGOUT0_VOUT_3V3):
return "3.3V";
}
return "???V";
}
size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE> buffer, size_t pos) {
constexpr size_t size = DEVICE_INFO_BUFFER_SIZE;
char *buf = buffer.data();
@@ -140,48 +160,28 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
const char *supply_status =
(nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_NORMAL) ? "Normal voltage." : "High voltage.";
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
if (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_HIGH) {
const char *reg0_type = nrf_power_dcdcen_vddh_get(NRF_POWER) ? "DC/DC" : "LDO";
const char *reg0_voltage;
switch (NRF_UICR->REGOUT0 & UICR_REGOUT0_VOUT_Msk) {
case (UICR_REGOUT0_VOUT_DEFAULT << UICR_REGOUT0_VOUT_Pos):
reg0_voltage = "1.8V (default)";
break;
case (UICR_REGOUT0_VOUT_1V8 << UICR_REGOUT0_VOUT_Pos):
reg0_voltage = "1.8V";
break;
case (UICR_REGOUT0_VOUT_2V1 << UICR_REGOUT0_VOUT_Pos):
reg0_voltage = "2.1V";
break;
case (UICR_REGOUT0_VOUT_2V4 << UICR_REGOUT0_VOUT_Pos):
reg0_voltage = "2.4V";
break;
case (UICR_REGOUT0_VOUT_2V7 << UICR_REGOUT0_VOUT_Pos):
reg0_voltage = "2.7V";
break;
case (UICR_REGOUT0_VOUT_3V0 << UICR_REGOUT0_VOUT_Pos):
reg0_voltage = "3.0V";
break;
case (UICR_REGOUT0_VOUT_3V3 << UICR_REGOUT0_VOUT_Pos):
reg0_voltage = "3.3V";
break;
default:
reg0_voltage = "???V";
}
const char *reg0_voltage = regout0_to_str((NRF_UICR->REGOUT0 & UICR_REGOUT0_VOUT_Msk) >> UICR_REGOUT0_VOUT_Pos);
ESP_LOGD(TAG, "Regulator stage 0: %s, %s", reg0_type, reg0_voltage);
pos = buf_append(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 {
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
const char *reg1_type = nrf_power_dcdcen_get(NRF_POWER) ? "DC/DC" : "LDO";
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
const char *usb_state;
@@ -195,7 +195,7 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
usb_state = "disconnected";
}
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
bool enabled;
@@ -300,14 +300,14 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
break;
}
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 {
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 {
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) {

View File

@@ -75,8 +75,8 @@ class SetLatencyCommand : public Command {
class SensorCfgStartCommand : public Command {
public:
SensorCfgStartCommand(bool startup_mode) : startup_mode_(startup_mode) {
char tmp_cmd[20] = {0};
sprintf(tmp_cmd, "sensorCfgStart %d", startup_mode);
char tmp_cmd[20]; // "sensorCfgStart " (15) + "0/1" (1) + null = 17
buf_append_printf(tmp_cmd, sizeof(tmp_cmd), 0, "sensorCfgStart %d", startup_mode);
cmd_ = std::string(tmp_cmd);
}
uint8_t on_message(std::string &message) override;
@@ -142,8 +142,8 @@ class SensitivityCommand : public Command {
SensitivityCommand(uint8_t sensitivity) : sensitivity_(sensitivity) {
if (sensitivity > 9)
sensitivity_ = sensitivity = 9;
char tmp_cmd[20] = {0};
sprintf(tmp_cmd, "setSensitivity %d", sensitivity);
char tmp_cmd[20]; // "setSensitivity " (15) + "0-9" (1) + null = 17
buf_append_printf(tmp_cmd, sizeof(tmp_cmd), 0, "setSensitivity %d", sensitivity);
cmd_ = std::string(tmp_cmd);
};
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);
} else if (this->model_ == DHT_MODEL_DHT22_TYPE2) {
delayMicroseconds(2000);
} else if (this->model_ == DHT_MODEL_AM2120 || this->model_ == DHT_MODEL_AM2302) {
delayMicroseconds(1000);
} else {
delayMicroseconds(800);
delayMicroseconds(1000);
}
#ifdef USE_ESP32

View File

@@ -25,29 +25,13 @@ dsmr_ns = cg.esphome_ns.namespace("esphome::dsmr")
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(
cv.Schema(
{
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_GAS_MBUS_ID, default=1): cv.int_,
cv.Optional(CONF_WATER_MBUS_ID, default=2): cv.int_,

View File

@@ -1,4 +1,5 @@
#include "dsmr.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <AES.h>
@@ -294,8 +295,8 @@ void Dsmr::dump_config() {
DSMR_TEXT_SENSOR_LIST(DSMR_LOG_TEXT_SENSOR, )
}
void Dsmr::set_decryption_key(const std::string &decryption_key) {
if (decryption_key.empty()) {
void Dsmr::set_decryption_key(const char *decryption_key) {
if (decryption_key == nullptr || decryption_key[0] == '\0') {
ESP_LOGI(TAG, "Disabling decryption");
this->decryption_key_.clear();
if (this->crypt_telegram_ != nullptr) {
@@ -305,21 +306,15 @@ void Dsmr::set_decryption_key(const std::string &decryption_key) {
return;
}
if (decryption_key.length() != 32) {
ESP_LOGE(TAG, "Error, decryption key must be 32 character long");
if (!parse_hex(decryption_key, this->decryption_key_, 16)) {
ESP_LOGE(TAG, "Error, decryption key must be 32 hex characters");
this->decryption_key_.clear();
return;
}
this->decryption_key_.clear();
ESP_LOGI(TAG, "Decryption key is set");
// Verbose level prints decryption key
ESP_LOGV(TAG, "Using decryption key: %s", decryption_key.c_str());
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));
}
ESP_LOGV(TAG, "Using decryption key: %s", decryption_key);
if (this->crypt_telegram_ == nullptr) {
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 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_request_pin(GPIOPin *request_pin) { this->request_pin_ = request_pin; }
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
display_config = {k: v for k, v in config.items() if k != CONF_ROTATION}
await display.register_display(var, display_config)
await spi.register_spi_device(var, config)
await spi.register_spi_device(var, config, write_only=True)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
cg.add(var.set_dc_pin(dc))

View File

@@ -34,6 +34,7 @@ from esphome.const import (
KEY_CORE,
KEY_FRAMEWORK_VERSION,
KEY_NAME,
KEY_NATIVE_IDF,
KEY_TARGET_FRAMEWORK,
KEY_TARGET_PLATFORM,
PLATFORM_ESP32,
@@ -53,6 +54,7 @@ from .const import ( # noqa
KEY_COMPONENTS,
KEY_ESP32,
KEY_EXTRA_BUILD_FILES,
KEY_FLASH_SIZE,
KEY_PATH,
KEY_REF,
KEY_REPO,
@@ -180,6 +182,12 @@ def set_core_data(config):
path=[CONF_CPU_FREQUENCY],
)
if variant == VARIANT_ESP32P4 and cpu_frequency == "400MHZ":
_LOGGER.warning(
"400MHz on ESP32-P4 is experimental and may not boot. "
"Consider using 360MHz instead. See https://github.com/esphome/esphome/issues/13425"
)
CORE.data[KEY_ESP32] = {}
CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_ESP32
conf = config[CONF_FRAMEWORK]
@@ -199,6 +207,7 @@ def set_core_data(config):
)
CORE.data[KEY_ESP32][KEY_BOARD] = config[CONF_BOARD]
CORE.data[KEY_ESP32][KEY_FLASH_SIZE] = config[CONF_FLASH_SIZE]
CORE.data[KEY_ESP32][KEY_VARIANT] = variant
CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES] = {}
@@ -339,7 +348,12 @@ def add_extra_build_file(filename: str, path: Path) -> bool:
def _format_framework_arduino_version(ver: cv.Version) -> str:
# format the given arduino (https://github.com/espressif/arduino-esp32/releases) version to
# 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:
@@ -374,11 +388,12 @@ def _is_framework_url(source: str) -> bool:
# The default/recommended arduino framework version
# - https://github.com/espressif/arduino-esp32/releases
ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
"recommended": cv.Version(3, 3, 5),
"latest": cv.Version(3, 3, 5),
"dev": cv.Version(3, 3, 5),
"recommended": cv.Version(3, 3, 6),
"latest": cv.Version(3, 3, 6),
"dev": cv.Version(3, 3, 6),
}
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, 4): 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
# See: https://github.com/pioarduino/esp-idf/releases
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, 4): 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),
}
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, 0): cv.Version(55, 3, 31, "2"),
cv.Version(5, 4, 3): cv.Version(55, 3, 32),
@@ -435,9 +451,9 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = {
# The platform-espressif32 version
# - https://github.com/pioarduino/platform-espressif32/releases
PLATFORM_VERSION_LOOKUP = {
"recommended": cv.Version(55, 3, 35),
"latest": cv.Version(55, 3, 35),
"dev": cv.Version(55, 3, 35),
"recommended": cv.Version(55, 3, 36),
"latest": cv.Version(55, 3, 36),
"dev": cv.Version(55, 3, 36),
}
@@ -962,12 +978,54 @@ async def _add_yaml_idf_components(components: list[ConfigType]):
async def to_code(config):
cg.add_platformio_option("board", config[CONF_BOARD])
cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE])
cg.add_platformio_option(
"board_upload.maximum_size",
int(config[CONF_FLASH_SIZE].removesuffix("MB")) * 1024 * 1024,
)
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
conf = config[CONF_FRAMEWORK]
# Check if using native ESP-IDF build (--native-idf)
use_platformio = not CORE.data.get(KEY_NATIVE_IDF, False)
if use_platformio:
# Clear IDF environment variables to avoid conflicts with PlatformIO's ESP-IDF
# but keep them when using --native-idf for native ESP-IDF builds
for clean_var in ("IDF_PATH", "IDF_TOOLS_PATH"):
os.environ.pop(clean_var, None)
cg.add_platformio_option("lib_ldf_mode", "off")
cg.add_platformio_option("lib_compat_mode", "strict")
cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION])
cg.add_platformio_option("board", config[CONF_BOARD])
cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE])
cg.add_platformio_option(
"board_upload.maximum_size",
int(config[CONF_FLASH_SIZE].removesuffix("MB")) * 1024 * 1024,
)
if CONF_SOURCE in conf:
cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]])
add_extra_script(
"pre",
"pre_build.py",
Path(__file__).parent / "pre_build.py.script",
)
add_extra_script(
"post",
"post_build.py",
Path(__file__).parent / "post_build.py.script",
)
# In testing mode, add IRAM fix script to allow linking grouped component tests
# Similar to ESP8266's approach but for ESP-IDF
if CORE.testing_mode:
cg.add_build_flag("-DESPHOME_TESTING_MODE")
add_extra_script(
"pre",
"iram_fix.py",
Path(__file__).parent / "iram_fix.py.script",
)
else:
cg.add_build_flag("-Wno-error=format")
cg.set_cpp_standard("gnu++20")
cg.add_build_flag("-DUSE_ESP32")
cg.add_build_flag("-Wl,-z,noexecstack")
@@ -977,79 +1035,49 @@ async def to_code(config):
cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[variant])
cg.add_define(ThreadModel.MULTI_ATOMICS)
cg.add_platformio_option("lib_ldf_mode", "off")
cg.add_platformio_option("lib_compat_mode", "strict")
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
conf = config[CONF_FRAMEWORK]
cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION])
if CONF_SOURCE in conf:
cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]])
if conf[CONF_ADVANCED][CONF_IGNORE_EFUSE_CUSTOM_MAC]:
cg.add_define("USE_ESP32_IGNORE_EFUSE_CUSTOM_MAC")
for clean_var in ("IDF_PATH", "IDF_TOOLS_PATH"):
os.environ.pop(clean_var, None)
# Set the location of the IDF component manager cache
os.environ["IDF_COMPONENT_CACHE_PATH"] = str(
CORE.relative_internal_path(".espressif")
)
add_extra_script(
"pre",
"pre_build.py",
Path(__file__).parent / "pre_build.py.script",
)
add_extra_script(
"post",
"post_build.py",
Path(__file__).parent / "post_build.py.script",
)
# In testing mode, add IRAM fix script to allow linking grouped component tests
# Similar to ESP8266's approach but for ESP-IDF
if CORE.testing_mode:
cg.add_build_flag("-DESPHOME_TESTING_MODE")
add_extra_script(
"pre",
"iram_fix.py",
Path(__file__).parent / "iram_fix.py.script",
)
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
cg.add_platformio_option("framework", "espidf")
cg.add_build_flag("-DUSE_ESP_IDF")
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF")
if use_platformio:
cg.add_platformio_option("framework", "espidf")
else:
cg.add_platformio_option("framework", "arduino, espidf")
cg.add_build_flag("-DUSE_ARDUINO")
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO")
if use_platformio:
cg.add_platformio_option("framework", "arduino, espidf")
# Add IDF framework source for Arduino builds to ensure it uses the same version as
# the ESP-IDF framework
if (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is not None:
cg.add_platformio_option(
"platform_packages",
[_format_framework_espidf_version(idf_ver, None)],
)
# ESP32-S2 Arduino: Disable USB Serial on boot to avoid TinyUSB dependency
if get_esp32_variant() == VARIANT_ESP32S2:
cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=1")
cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=0")
cg.add_build_flag("-DARDUINO_USB_CDC_ON_BOOT=0")
cg.add_define(
"USE_ARDUINO_VERSION_CODE",
cg.RawExpression(
f"VERSION_CODE({framework_ver.major}, {framework_ver.minor}, {framework_ver.patch})"
),
)
add_idf_sdkconfig_option("CONFIG_MBEDTLS_PSK_MODES", True)
add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True)
# Add IDF framework source for Arduino builds to ensure it uses the same version as
# the ESP-IDF framework
if (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is not None:
cg.add_platformio_option(
"platform_packages", [_format_framework_espidf_version(idf_ver, None)]
)
# ESP32-S2 Arduino: Disable USB Serial on boot to avoid TinyUSB dependency
if get_esp32_variant() == VARIANT_ESP32S2:
cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=1")
cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=0")
cg.add_build_flag("-DARDUINO_USB_CDC_ON_BOOT=0")
cg.add_build_flag("-Wno-nonnull-compare")
add_idf_sdkconfig_option(f"CONFIG_IDF_TARGET_{variant}", True)
@@ -1196,7 +1224,8 @@ async def to_code(config):
"CONFIG_VFS_SUPPORT_DIR", not advanced[CONF_DISABLE_VFS_SUPPORT_DIR]
)
cg.add_platformio_option("board_build.partitions", "partitions.csv")
if use_platformio:
cg.add_platformio_option("board_build.partitions", "partitions.csv")
if CONF_PARTITIONS in config:
add_extra_build_file(
"partitions.csv", CORE.relative_config_path(config[CONF_PARTITIONS])
@@ -1361,19 +1390,16 @@ def copy_files():
_write_idf_component_yml()
if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]:
flash_size = CORE.data[KEY_ESP32][KEY_FLASH_SIZE]
if CORE.using_arduino:
write_file_if_changed(
CORE.relative_build_path("partitions.csv"),
get_arduino_partition_csv(
CORE.platformio_options.get("board_upload.flash_size")
),
get_arduino_partition_csv(flash_size),
)
else:
write_file_if_changed(
CORE.relative_build_path("partitions.csv"),
get_idf_partition_csv(
CORE.platformio_options.get("board_upload.flash_size")
),
get_idf_partition_csv(flash_size),
)
# IDF build scripts look for version string to put in the build.
# However, if the build path does not have an initialized git repo,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,6 +11,7 @@
#include <esp_ota_ops.h>
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
#include "esphome/components/http_request/http_request.h"
#include "esphome/components/json/json_util.h"
#include "esphome/components/network/util.h"
#endif
@@ -184,15 +185,23 @@ bool Esp32HostedUpdate::fetch_manifest_() {
}
// Read manifest JSON into string (manifest is small, ~1KB max)
// NOTE: HttpContainer::read() has non-BSD socket semantics - see http_request.h
// Use http_read_loop_result() helper instead of checking return values directly
std::string json_str;
json_str.reserve(container->content_length);
uint8_t buf[256];
uint32_t last_data_time = millis();
const uint32_t read_timeout = this->http_request_parent_->get_timeout();
while (container->get_bytes_read() < container->content_length) {
int read = container->read(buf, sizeof(buf));
if (read > 0) {
json_str.append(reinterpret_cast<char *>(buf), read);
}
int read_or_error = container->read(buf, sizeof(buf));
App.feed_wdt();
yield();
auto result = http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout);
if (result == http_request::HttpReadLoopResult::RETRY)
continue;
if (result != http_request::HttpReadLoopResult::DATA)
break; // ERROR or TIMEOUT
json_str.append(reinterpret_cast<char *>(buf), read_or_error);
}
container->end();
@@ -297,32 +306,38 @@ bool Esp32HostedUpdate::stream_firmware_to_coprocessor_() {
}
// Stream firmware to coprocessor while computing SHA256
// NOTE: HttpContainer::read() has non-BSD socket semantics - see http_request.h
// Use http_read_loop_result() helper instead of checking return values directly
sha256::SHA256 hasher;
hasher.init();
uint8_t buffer[CHUNK_SIZE];
uint32_t last_data_time = millis();
const uint32_t read_timeout = this->http_request_parent_->get_timeout();
while (container->get_bytes_read() < total_size) {
int read = container->read(buffer, sizeof(buffer));
int read_or_error = container->read(buffer, sizeof(buffer));
// Feed watchdog and give other tasks a chance to run
App.feed_wdt();
yield();
// Exit loop if no data available (stream closed or end of data)
if (read <= 0) {
if (read < 0) {
ESP_LOGE(TAG, "Stream closed with error");
esp_hosted_slave_ota_end(); // NOLINT
container->end();
this->status_set_error(LOG_STR("Download failed"));
return false;
auto result = http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout);
if (result == http_request::HttpReadLoopResult::RETRY)
continue;
if (result != http_request::HttpReadLoopResult::DATA) {
if (result == http_request::HttpReadLoopResult::TIMEOUT) {
ESP_LOGE(TAG, "Timeout reading firmware data");
} else {
ESP_LOGE(TAG, "Error reading firmware data: %d", read_or_error);
}
// read == 0: no more data available, exit loop
break;
esp_hosted_slave_ota_end(); // NOLINT
container->end();
this->status_set_error(LOG_STR("Download failed"));
return false;
}
hasher.add(buffer, read);
err = esp_hosted_slave_ota_write(buffer, read); // NOLINT
hasher.add(buffer, read_or_error);
err = esp_hosted_slave_ota_write(buffer, read_or_error); // NOLINT
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to write OTA data: %s", esp_err_to_name(err));
esp_hosted_slave_ota_end(); // NOLINT

View File

@@ -12,7 +12,6 @@ extern "C" {
#include "preferences.h"
#include <cstring>
#include <memory>
namespace esphome::esp8266 {
@@ -143,16 +142,8 @@ class ESP8266PreferenceBackend : public ESPPreferenceBackend {
return false;
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
uint32_t stack_buffer[PREF_BUFFER_WORDS];
std::unique_ptr<uint32_t[]> heap_buffer;
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();
}
SmallBufferWithHeapFallback<PREF_BUFFER_WORDS, uint32_t> buffer_alloc(buffer_size);
uint32_t *buffer = buffer_alloc.get();
memset(buffer, 0, buffer_size * sizeof(uint32_t));
memcpy(buffer, data, len);
@@ -167,16 +158,8 @@ class ESP8266PreferenceBackend : public ESPPreferenceBackend {
return false;
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
uint32_t stack_buffer[PREF_BUFFER_WORDS];
std::unique_ptr<uint32_t[]> heap_buffer;
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();
}
SmallBufferWithHeapFallback<PREF_BUFFER_WORDS, uint32_t> buffer_alloc(buffer_size);
uint32_t *buffer = buffer_alloc.get();
bool ret = this->in_flash ? load_from_flash(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, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger, [(cg.std_string, "event_type")], conf
)
await automation.build_automation(trigger, [(cg.StringRef, "event_type")], conf)
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...)); }
};
class EventTrigger : public Trigger<std::string> {
class EventTrigger : public Trigger<StringRef> {
public:
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;
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)
ControllerRegistry::notify_event(this);
#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
}
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));
}

View File

@@ -70,10 +70,10 @@ class Event : public EntityBase, public EntityBase_DeviceClass {
/// Check if an event has been triggered.
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:
LazyCallbackManager<void(const std::string &event_type)> event_callback_;
LazyCallbackManager<void(StringRef event_type)> event_callback_;
FixedVector<const char *> types_;
private:

View File

@@ -318,90 +318,93 @@ void EzoPMP::send_next_command_() {
switch (this->next_command_) {
// Read Commands
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;
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;
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;
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;
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;
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;
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;
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;
// Non-Read Commands
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
break;
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;
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;
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;
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;
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;
// Non-Read commands with parameters
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;
case EZO_PMP_COMMAND_DOSE_VOLUME_OVER_TIME: // Dose over time (page 56)
command_buffer_length =
sprintf((char *) command_buffer, "D,%0.1f,%i", this->next_command_volume_, this->next_command_duration_);
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "D,%0.1f,%i",
this->next_command_volume_, this->next_command_duration_);
break;
case EZO_PMP_COMMAND_DOSE_WITH_CONSTANT_FLOW_RATE: // Constant Flow Rate (page 57)
command_buffer_length =
sprintf((char *) command_buffer, "DC,%0.1f,%i", this->next_command_volume_, this->next_command_duration_);
command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "DC,%0.1f,%i",
this->next_command_volume_, this->next_command_duration_);
break;
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;
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;
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);
break;

View File

@@ -77,7 +77,7 @@ FanSpeedSetTrigger = fan_ns.class_(
"FanSpeedSetTrigger", automation.Trigger.template(cg.int_)
)
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())
@@ -287,7 +287,7 @@ async def setup_fan_core_(var, config):
await automation.build_automation(trigger, [(cg.int_, "x")], conf)
for conf in config.get(CONF_ON_PRESET_SET, []):
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):

View File

@@ -208,7 +208,7 @@ class FanSpeedSetTrigger : public Trigger<int> {
int last_speed_;
};
class FanPresetSetTrigger : public Trigger<std::string> {
class FanPresetSetTrigger : public Trigger<StringRef> {
public:
FanPresetSetTrigger(Fan *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_;
this->last_preset_mode_ = preset_mode;
if (should_trigger) {
this->trigger(std::string(preset_mode));
this->trigger(preset_mode);
}
});
this->last_preset_mode_ = state->get_preset_mode();

View File

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

View File

@@ -9,55 +9,29 @@ from esphome.const import (
CONF_VALUE,
)
from esphome.core import CoroPriority, coroutine_with_priority
from esphome.types import ConfigType
CODEOWNERS = ["@esphome/core"]
globals_ns = cg.esphome_ns.namespace("globals")
GlobalsComponent = globals_ns.class_("GlobalsComponent", cg.Component)
RestoringGlobalsComponent = globals_ns.class_(
"RestoringGlobalsComponent", cg.PollingComponent
)
RestoringGlobalsComponent = globals_ns.class_("RestoringGlobalsComponent", cg.Component)
RestoringGlobalStringComponent = globals_ns.class_(
"RestoringGlobalStringComponent", cg.PollingComponent
"RestoringGlobalStringComponent", cg.Component
)
GlobalVarSetAction = globals_ns.class_("GlobalVarSetAction", automation.Action)
CONF_MAX_RESTORE_DATA_LENGTH = "max_restore_data_length"
# Base schema fields shared by both variants
_BASE_SCHEMA = {
cv.Required(CONF_ID): cv.declare_id(GlobalsComponent),
cv.Required(CONF_TYPE): cv.string_strict,
cv.Optional(CONF_INITIAL_VALUE): cv.string_strict,
cv.Optional(CONF_MAX_RESTORE_DATA_LENGTH): cv.int_range(0, 254),
}
# Non-restoring globals: regular Component (no polling needed)
_NON_RESTORING_SCHEMA = cv.Schema(
{
**_BASE_SCHEMA,
cv.Optional(CONF_RESTORE_VALUE, default=False): cv.boolean,
}
).extend(cv.COMPONENT_SCHEMA)
# Restoring globals: PollingComponent with configurable update_interval
_RESTORING_SCHEMA = cv.Schema(
{
**_BASE_SCHEMA,
cv.Optional(CONF_RESTORE_VALUE, default=True): cv.boolean,
}
).extend(cv.polling_component_schema("1s"))
def _globals_schema(config: ConfigType) -> ConfigType:
"""Select schema based on restore_value setting."""
if config.get(CONF_RESTORE_VALUE, False):
return _RESTORING_SCHEMA(config)
return _NON_RESTORING_SCHEMA(config)
MULTI_CONF = True
CONFIG_SCHEMA = _globals_schema
CONFIG_SCHEMA = cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(GlobalsComponent),
cv.Required(CONF_TYPE): cv.string_strict,
cv.Optional(CONF_INITIAL_VALUE): cv.string_strict,
cv.Optional(CONF_RESTORE_VALUE, default=False): cv.boolean,
cv.Optional(CONF_MAX_RESTORE_DATA_LENGTH): cv.int_range(0, 254),
}
).extend(cv.COMPONENT_SCHEMA)
# Run with low priority so that namespaces are registered first

View File

@@ -5,7 +5,8 @@
#include "esphome/core/helpers.h"
#include <cstring>
namespace esphome::globals {
namespace esphome {
namespace globals {
template<typename T> class GlobalsComponent : public Component {
public:
@@ -23,14 +24,13 @@ template<typename T> class GlobalsComponent : public Component {
T value_{};
};
template<typename T> class RestoringGlobalsComponent : public PollingComponent {
template<typename T> class RestoringGlobalsComponent : public Component {
public:
using value_type = T;
explicit RestoringGlobalsComponent() : PollingComponent(1000) {}
explicit RestoringGlobalsComponent(T initial_value) : PollingComponent(1000), value_(initial_value) {}
explicit RestoringGlobalsComponent() = default;
explicit RestoringGlobalsComponent(T initial_value) : value_(initial_value) {}
explicit RestoringGlobalsComponent(
std::array<typename std::remove_extent<T>::type, std::extent<T>::value> initial_value)
: PollingComponent(1000) {
std::array<typename std::remove_extent<T>::type, std::extent<T>::value> initial_value) {
memcpy(this->value_, initial_value.data(), sizeof(T));
}
@@ -44,7 +44,7 @@ template<typename T> class RestoringGlobalsComponent : public PollingComponent {
float get_setup_priority() const override { return setup_priority::HARDWARE; }
void update() override { store_value_(); }
void loop() override { store_value_(); }
void on_shutdown() override { store_value_(); }
@@ -66,14 +66,13 @@ template<typename T> class RestoringGlobalsComponent : public PollingComponent {
};
// Use with string or subclasses of strings
template<typename T, uint8_t SZ> class RestoringGlobalStringComponent : public PollingComponent {
template<typename T, uint8_t SZ> class RestoringGlobalStringComponent : public Component {
public:
using value_type = T;
explicit RestoringGlobalStringComponent() : PollingComponent(1000) {}
explicit RestoringGlobalStringComponent(T initial_value) : PollingComponent(1000) { this->value_ = initial_value; }
explicit RestoringGlobalStringComponent() = default;
explicit RestoringGlobalStringComponent(T initial_value) { this->value_ = initial_value; }
explicit RestoringGlobalStringComponent(
std::array<typename std::remove_extent<T>::type, std::extent<T>::value> initial_value)
: PollingComponent(1000) {
std::array<typename std::remove_extent<T>::type, std::extent<T>::value> initial_value) {
memcpy(this->value_, initial_value.data(), sizeof(T));
}
@@ -91,7 +90,7 @@ template<typename T, uint8_t SZ> class RestoringGlobalStringComponent : public P
float get_setup_priority() const override { return setup_priority::HARDWARE; }
void update() override { store_value_(); }
void loop() override { store_value_(); }
void on_shutdown() override { store_value_(); }
@@ -145,4 +144,5 @@ template<typename T> T &id(GlobalsComponent<T> *value) { return value->value();
template<typename T> T &id(RestoringGlobalsComponent<T> *value) { return value->value(); }
template<typename T, uint8_t SZ> T &id(RestoringGlobalStringComponent<T, SZ> *value) { return value->value(); }
} // namespace esphome::globals
} // namespace globals
} // namespace esphome

View File

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

View File

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

View File

@@ -1,15 +1,46 @@
#include "heatpumpir.h"
#ifdef USE_ARDUINO
#if defined(USE_ARDUINO) || defined(USE_ESP32)
#include <map>
#include "ir_sender_esphome.h"
#include "HeatpumpIRFactory.h"
#include <IRSender.h>
#include <HeatpumpIRFactory.h>
#include "esphome/components/remote_base/remote_base.h"
#include "esphome/core/log.h"
namespace esphome {
namespace heatpumpir {
// IRSenderESPHome - bridge between ESPHome's remote_transmitter and HeatpumpIR library
// Defined here (not in a header) to isolate HeatpumpIR's headers from the rest of ESPHome,
// as they define conflicting symbols like millis() in the global namespace.
class IRSenderESPHome : public IRSender {
public:
IRSenderESPHome(remote_base::RemoteTransmitterBase *transmitter) : IRSender(0), transmit_(transmitter->transmit()) {}
void setFrequency(int frequency) override { // NOLINT(readability-identifier-naming)
auto *data = this->transmit_.get_data();
data->set_carrier_frequency(1000 * frequency);
}
void space(int space_length) override {
if (space_length) {
auto *data = this->transmit_.get_data();
data->space(space_length);
} else {
this->transmit_.perform();
}
}
void mark(int mark_length) override {
auto *data = this->transmit_.get_data();
data->mark(mark_length);
}
protected:
remote_base::RemoteTransmitterBase::TransmitCall transmit_;
};
static const char *const TAG = "heatpumpir.climate";
const std::map<Protocol, std::function<HeatpumpIR *()>> PROTOCOL_CONSTRUCTOR_MAP = {

View File

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

View File

@@ -1,32 +0,0 @@
#include "ir_sender_esphome.h"
#ifdef USE_ARDUINO
namespace esphome {
namespace heatpumpir {
void IRSenderESPHome::setFrequency(int frequency) { // NOLINT(readability-identifier-naming)
auto *data = transmit_.get_data();
data->set_carrier_frequency(1000 * frequency);
}
// Send an IR 'mark' symbol, i.e. transmitter ON
void IRSenderESPHome::mark(int mark_length) {
auto *data = transmit_.get_data();
data->mark(mark_length);
}
// Send an IR 'space' symbol, i.e. transmitter OFF
void IRSenderESPHome::space(int space_length) {
if (space_length) {
auto *data = transmit_.get_data();
data->space(space_length);
} else {
transmit_.perform();
}
}
} // namespace heatpumpir
} // namespace esphome
#endif

View File

@@ -1,25 +0,0 @@
#pragma once
#ifdef USE_ARDUINO
#include "esphome/components/remote_base/remote_base.h"
#include <IRSender.h> // arduino-heatpump library
namespace esphome {
namespace heatpumpir {
class IRSenderESPHome : public IRSender {
public:
IRSenderESPHome(remote_base::RemoteTransmitterBase *transmitter) : IRSender(0), transmit_(transmitter->transmit()){};
void setFrequency(int frequency) override; // NOLINT(readability-identifier-naming)
void space(int space_length) override;
void mark(int mark_length) override;
protected:
remote_base::RemoteTransmitterBase::TransmitCall transmit_;
};
} // namespace heatpumpir
} // namespace esphome
#endif

View File

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

View File

@@ -79,6 +79,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; }
/*
* 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 HttpContainer : public Parented<HttpRequestComponent> {
@@ -88,6 +163,33 @@ class HttpContainer : public Parented<HttpRequestComponent> {
int status_code;
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 void end() = 0;
@@ -110,6 +212,38 @@ class HttpContainer : public Parented<HttpRequestComponent> {
std::map<std::string, std::list<std::string>> response_headers_{};
};
/// Read data from HTTP container into buffer with timeout handling
/// Handles feed_wdt, yield, and timeout checking internally
/// @param container The HTTP container to read from
/// @param buffer Buffer to read into
/// @param total_size Total bytes to read
/// @param chunk_size Maximum bytes per read call
/// @param timeout_ms Read timeout in milliseconds
/// @return HttpReadResult with status and error_code on failure
inline HttpReadResult http_read_fully(HttpContainer *container, uint8_t *buffer, size_t total_size, size_t chunk_size,
uint32_t timeout_ms) {
size_t read_index = 0;
uint32_t last_data_time = millis();
while (read_index < total_size) {
int read_bytes_or_error = container->read(buffer + read_index, std::min(chunk_size, total_size - read_index));
App.feed_wdt();
yield();
auto result = http_read_loop_result(read_bytes_or_error, last_data_time, timeout_ms);
if (result == HttpReadLoopResult::RETRY)
continue;
if (result == HttpReadLoopResult::ERROR)
return {HttpReadStatus::ERROR, read_bytes_or_error};
if (result == HttpReadLoopResult::TIMEOUT)
return {HttpReadStatus::TIMEOUT, 0};
read_index += read_bytes_or_error;
}
return {HttpReadStatus::OK, 0};
}
class HttpRequestResponseTrigger : public Trigger<std::shared_ptr<HttpContainer>, std::string &> {
public:
void process(const std::shared_ptr<HttpContainer> &container, std::string &response_body) {
@@ -124,6 +258,7 @@ class HttpRequestComponent : public Component {
void set_useragent(const char *useragent) { this->useragent_ = useragent; }
void set_timeout(uint32_t timeout) { this->timeout_ = timeout; }
uint32_t get_timeout() const { return this->timeout_; }
void set_watchdog_timeout(uint32_t watchdog_timeout) { this->watchdog_timeout_ = watchdog_timeout; }
uint32_t get_watchdog_timeout() const { return this->watchdog_timeout_; }
void set_follow_redirects(bool follow_redirects) { this->follow_redirects_ = follow_redirects; }
@@ -249,15 +384,21 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
RAMAllocator<uint8_t> allocator;
uint8_t *buf = allocator.allocate(max_length);
if (buf != nullptr) {
// NOTE: HttpContainer::read() has non-BSD socket semantics - see top of this file
// Use http_read_loop_result() helper instead of checking return values directly
size_t read_index = 0;
uint32_t last_data_time = millis();
const uint32_t read_timeout = this->parent_->get_timeout();
while (container->get_bytes_read() < max_length) {
int read = container->read(buf + read_index, std::min<size_t>(max_length - read_index, 512));
if (read <= 0) {
break;
}
int read_or_error = container->read(buf + read_index, std::min<size_t>(max_length - read_index, 512));
App.feed_wdt();
yield();
read_index += read;
auto result = http_read_loop_result(read_or_error, last_data_time, read_timeout);
if (result == HttpReadLoopResult::RETRY)
continue;
if (result != HttpReadLoopResult::DATA)
break; // ERROR or TIMEOUT
read_index += read_or_error;
}
response_body.reserve(read_index);
response_body.assign((char *) buf, read_index);

View File

@@ -139,6 +139,23 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
return container;
}
// Arduino HTTP read implementation
//
// WARNING: Return values differ from BSD sockets! See http_request.h for full documentation.
//
// Arduino's WiFiClient is inherently non-blocking - available() returns 0 when
// no data is ready. We use connected() to distinguish "no data yet" from
// "connection closed".
//
// WiFiClient behavior:
// available() > 0: data ready to read
// available() == 0 && connected(): no data yet, still connected
// available() == 0 && !connected(): connection closed
//
// We normalize to HttpContainer::read() contract (NOT BSD socket semantics!):
// > 0: bytes read
// 0: no data yet, retry <-- NOTE: 0 means retry, NOT EOF!
// < 0: error/connection closed <-- connection closed returns -1, not 0
int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
const uint32_t start = millis();
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
@@ -146,7 +163,7 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
WiFiClient *stream_ptr = this->client_.getStreamPtr();
if (stream_ptr == nullptr) {
ESP_LOGE(TAG, "Stream pointer vanished!");
return -1;
return HTTP_ERROR_CONNECTION_CLOSED;
}
int available_data = stream_ptr->available();
@@ -154,7 +171,15 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
if (bufsize == 0) {
this->duration_ms += (millis() - start);
return 0;
// Check if we've read all expected content
if (this->bytes_read_ >= this->content_length) {
return 0; // All content read successfully
}
// No data available - check if connection is still open
if (!stream_ptr->connected()) {
return HTTP_ERROR_CONNECTION_CLOSED; // Connection closed prematurely
}
return 0; // No data yet, caller should retry
}
App.feed_wdt();

View File

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

View File

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

View File

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

View File

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

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