Compare commits

...

205 Commits

Author SHA1 Message Date
J. Nick Koston
bd3ecad3a1 [core] Add format_hex_pretty_to buffer helper and reduce code duplication (#12687)
Some checks failed
CI / Run C++ unit tests (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 IDF (push) Has been cancelled
CI / Run script/clang-tidy for ESP8266 (push) Has been cancelled
CI / Run script/clang-tidy for ZEPHYR (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 1/4 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 2/4 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 3/4 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 4/4 (push) Has been cancelled
CI / Test components batch (${{ matrix.components }}) (push) Has been cancelled
CI / pre-commit.ci lite (push) Has been cancelled
CI / Build target branch for memory impact (push) Has been cancelled
CI / Build PR branch for memory impact (push) Has been cancelled
CI / Comment memory impact (push) Has been cancelled
CI / CI Status (push) Has been cancelled
Stale / stale (push) Has been cancelled
Lock closed issues and PRs / lock (push) Has been cancelled
Publish Release / Initialize build (push) Has been cancelled
Publish Release / Build and publish to PyPi (push) Has been cancelled
Publish Release / Build ESPHome amd64 (push) Has been cancelled
Publish Release / Build ESPHome arm64 (push) Has been cancelled
Publish Release / Publish ESPHome docker to dockerhub (push) Has been cancelled
Publish Release / Publish ESPHome docker to ghcr (push) Has been cancelled
Publish Release / Publish ESPHome ha-addon to dockerhub (push) Has been cancelled
Publish Release / Publish ESPHome ha-addon to ghcr (push) Has been cancelled
Publish Release / deploy-ha-addon-repo (push) Has been cancelled
Publish Release / deploy-esphome-schema (push) Has been cancelled
Publish Release / version-notifier (push) Has been cancelled
Synchronise Device Classes from Home Assistant / Sync Device Classes (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
2025-12-30 11:51:51 -10:00
J. Nick Koston
dae7ba604a [ethernet_info] Eliminate heap allocations in DNS text sensor (#12756) 2025-12-30 10:25:51 -10:00
Jonathan Swoboda
96c47f3b4d Merge branch 'release' into dev 2025-12-30 09:31:44 -05:00
Jonathan Swoboda
5b5cede5f9 Merge pull request #12752 from esphome/bump-2025.12.3
2025.12.3
2025-12-30 09:31:31 -05:00
Jonathan Swoboda
c737033cc4 Bump version to 2025.12.3 2025-12-30 09:22:03 -05:00
J. Nick Koston
0194bfd9ea [core] Fix incremental build failures when adding components on ESP32-Arduino (#12745) 2025-12-30 09:22:03 -05:00
J. Nick Koston
339399eb70 [lvgl] Fix lambdas in canvas actions called from outside LVGL context (#12671) 2025-12-30 09:22:03 -05:00
Samuel Sieb
a615b28ecf [bme68x_bsec2] add id: to allow extending (#12649) 2025-12-29 23:22:36 -08:00
bakroistvan
468bd7b04f [dallas_temp] higher precision for logged temperature (#12695) 2025-12-29 22:53:28 -08:00
Jonathan Swoboda
4c16afeacb [esp32] Add IDF framework source for Arduino builds (#12731)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2025-12-29 22:25:26 -05:00
J. Nick Koston
d86c05bfe6 [esp32] Breaking Change: Change default framework to ESP-IDF (#12746)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-30 03:23:41 +00:00
J. Nick Koston
63464a13c3 [core] Fix incremental build failures when adding components on ESP32-Arduino (#12745) 2025-12-29 16:57:22 -10:00
Clyde Stubbs
20e43398fa [cli] Report program path on host (#12743) 2025-12-30 13:21:30 +11:00
hsand
2e7cdad532 [pvvx_mithermometer] fix displaying negative numbers (#12735) 2025-12-29 13:58:38 -08:00
dependabot[bot]
636cccc6a3 Bump aioesphomeapi from 43.9.0 to 43.9.1 (#12724)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-29 09:55:26 -10:00
Thomas Rupprecht
93e2a1bd1a [tests] improve mipi_spi variable naming (#12716) 2025-12-29 14:21:07 -05:00
Thomas Rupprecht
dd3beb5841 [tests] fix typo mipi tests (#12715) 2025-12-29 14:20:38 -05:00
Thomas Rupprecht
97af01c5ed [usb_host] sort esp32 variants (#12720) 2025-12-29 14:19:36 -05:00
J. Nick Koston
7e362cdafc [ota] Use precision format specifier for auth logging (#12706)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-29 08:43:54 -10:00
Jonathan Swoboda
890d531cea [esp32] Bump to ESP-IDF 5.5.2, Arduino 3.3.5, platform 55.3.35 (#12681)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-12-29 11:35:54 -05:00
Swaptor
6a6c6b648f [internal_temperature] Add ESP32-C5 support (#12713)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-12-29 11:32:32 -05:00
dependabot[bot]
d0673122a8 Bump aioesphomeapi from 43.8.0 to 43.9.0 (#12702) 2025-12-28 18:15:06 -10:00
dependabot[bot]
5cbef3ef95 Bump aioesphomeapi from 43.7.0 to 43.8.0 (#12701) 2025-12-29 03:15:40 +00:00
dependabot[bot]
a1e0121330 Bump bleak from 2.0.0 to 2.1.0 (#12700)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-28 16:48:20 -10:00
dependabot[bot]
eb050ff13e Bump aioesphomeapi from 43.6.0 to 43.7.0 (#12699)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-28 16:48:08 -10:00
Jonathan Swoboda
45e61f100c [core] Replace USE_ESP_IDF with USE_ESP32 across components (#12673)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-12-27 11:59:55 -10:00
J. Nick Koston
5e99dd14ae [ethernet] Eliminate heap allocations in dump_config logging (#12665) 2025-12-27 08:36:35 -10:00
J. Nick Koston
a6097f4a0f [wifi] Eliminate heap allocations in dump_config logging (#12664)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-27 08:36:19 -10:00
J. Nick Koston
f243e609a5 [wifi] Use StringRef and std::span in WiFiConnectStateListener to avoid allocations (#12672) 2025-12-27 08:35:58 -10:00
J. Nick Koston
be0bf1e5b9 [lvgl] Fix lambdas in canvas actions called from outside LVGL context (#12671) 2025-12-27 08:35:36 -10:00
J. Nick Koston
a275f37135 [udp] Use stack buffer for listen address logging in dump_config (#12667) 2025-12-27 08:35:16 -10:00
J. Nick Koston
e9f2d75aab [core] Add format_hex_to helper for zero-allocation hex formatting (#12670) 2025-12-27 08:34:45 -10:00
J. Nick Koston
34067f8b15 [esp8266] Native OTA backend to reduce Arduino dependencies (#12675)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-27 08:29:15 -10:00
J. Nick Koston
bdc087148a [wifi_info] Reduce heap allocations in text sensor formatting (#12660) 2025-12-26 12:52:41 -10:00
J. Nick Koston
5a2e0612a8 [web_server] Use C++17 nested namespace syntax (#12663) 2025-12-26 08:44:34 -10:00
J. Nick Koston
f1fecd22e3 [web_server] Move HTTP header strings to flash on ESP8266 (#12668) 2025-12-26 08:44:17 -10:00
J. Nick Koston
0919017d49 [wifi] Avoid unnecessary string copy in failed connection logging (#12659) 2025-12-26 08:44:03 -10:00
J. Nick Koston
963f594c9e [text_sensor] Return state by const reference to avoid copies (#12661) 2025-12-26 07:58:46 -10:00
J. Nick Koston
4f70663658 [alarm_control_panel] Use C++17 nested namespace and remove unused include (#12662) 2025-12-26 07:57:33 -10:00
dependabot[bot]
958a35e262 Bump aioesphomeapi from 43.5.0 to 43.6.0 (#12644)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-24 14:17:52 -10:00
J. Nick Koston
0c566c6f00 [core] Deprecate get_object_id() and migrate remaining usages to get_object_id_to() (#12629) 2025-12-23 06:59:07 -10:00
Jonathan Swoboda
ba73289b28 Merge branch 'release' into dev 2025-12-23 11:17:15 -05:00
Jonathan Swoboda
99f7e9aeb7 Merge pull request #12632 from esphome/bump-2025.12.2
2025.12.2
2025-12-23 11:17:01 -05:00
Jonathan Swoboda
ebb6babb3d Fix hash 2025-12-23 09:26:38 -05:00
Jonathan Swoboda
0922f240e0 Bump version to 2025.12.2 2025-12-23 09:23:04 -05:00
Jonathan Swoboda
c8fb694dcb [cc1101] Fix packet mode RSSI/LQI (#12630)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-23 09:23:04 -05:00
J. Nick Koston
6054685dae [esp32_camera] Throttle frame logging to reduce overhead and improve throughput (#12586) 2025-12-23 09:23:04 -05:00
Anna Oake
61ec3508ed [cc1101] Fix option defaults and move them to YAML (#12608) 2025-12-23 09:23:04 -05:00
Leo Bergolth
086ec770ea send NIL ("-") as timestamp if time source is not valid (#12588) 2025-12-23 09:23:04 -05:00
Stuart Parmenter
b055f5b4bf [hub75] Bump esp-hub75 version to 0.1.7 (#12564) 2025-12-23 09:23:00 -05:00
Eduard Llull
726db746c8 [display_menu_base] Call on_value_ after updating the select (#12584) 2025-12-23 09:21:54 -05:00
Keith Burzinski
1922455fa7 [wifi] Fix for wifi_info when static IP is configured (#12576) 2025-12-23 09:21:54 -05:00
Thomas Rupprecht
dc943d7e7a [pca9685,sx126x,sx127x] Use frequency/float_range check (#12490)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-12-23 09:21:54 -05:00
Jonathan Swoboda
ffefa8929e [cc1101] Fix packet mode RSSI/LQI (#12630)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-23 09:05:48 -05:00
J. Nick Koston
7d5342bca5 [logger] Host: Use fwrite() with explicit length and remove platform branching (#12628) 2025-12-22 16:45:22 -10:00
J. Nick Koston
b4c92dd8cb [logger] Zephyr: Use k_str_out() with known length instead of printk() (#12619) 2025-12-22 14:29:47 -10:00
eoasmxd
1b31253287 Add Event Component to UART (#11765)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2025-12-22 12:19:48 -10:00
J. Nick Koston
af0d4d2c2c [web_server] Use stack buffers for value formatting to reduce flash usage (#12575) 2025-12-22 21:56:07 +00:00
J. Nick Koston
f238f93312 [core] Move comment to PROGMEM on ESP8266 (#12554) 2025-12-22 21:37:51 +00:00
J. Nick Koston
bdbe72b7f1 [web_server] Make internal JSON helper methods private (#12624) 2025-12-22 11:14:11 -10:00
J. Nick Koston
c8b531ac06 [safe_mode] Defer preference sync in clean_rtc to avoid blocking event loop (#12625) 2025-12-22 11:13:51 -10:00
Jonathan Swoboda
918bc4b74f [esp32] Remove remaining using_esp_idf checks (#12623)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-22 15:41:14 -05:00
Keith Burzinski
08c0f65f30 [sprinkler] Remove internal latching valve support (#12603) 2025-12-22 14:13:18 -05:00
Keith Burzinski
cd45fe0c3a [thermostat] Optimizations to reduce binary size (#12621) 2025-12-22 14:13:03 -05:00
Jonathan Swoboda
84b5d9b21c [core] Remove deprecated config options from before 2025 (#12622)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-22 14:00:12 -05:00
J. Nick Koston
6383fe4598 [core] Add zero-allocation object_id methods (#12578) 2025-12-22 07:56:33 -10:00
J. Nick Koston
265ad9d264 [esp32_camera] Reduce loop overhead and improve frame latency with wake_loop_threadsafe (#12601) 2025-12-22 07:55:28 -10:00
J. Nick Koston
1bdbc4cb85 [esp32_ble] Avoid string allocation when setting BLE device name (#12579) 2025-12-22 07:54:55 -10:00
J. Nick Koston
1756fc31b0 [api] Use union for iterators to reduce APIConnection size by ~16 bytes (#12563) 2025-12-22 07:54:17 -10:00
J. Nick Koston
74b075d3cf [codegen] Add static storage class to global variables for size optimization (#12616) 2025-12-22 07:03:17 -10:00
Clint Armstrong
52eb08f48f [thermostat] Enhance timer behavior for immediate response to duration changes (#12610) 2025-12-21 23:52:17 -06:00
J. Nick Koston
0d993691d4 [logger] RP2040: Use write() with known length instead of println() (#12615) 2025-12-21 17:59:30 -10:00
Douwe
39926909af [water_heater] (1/4) Implement API/Core/component for new water_heater component (#12498)
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+github@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-12-21 11:36:34 -10:00
J. Nick Koston
637e032528 [esp32_camera] Throttle frame logging to reduce overhead and improve throughput (#12586) 2025-12-21 09:04:43 -10:00
Anna Oake
d89eaf5bf6 [cc1101] Fix option defaults and move them to YAML (#12608) 2025-12-21 13:04:17 -05:00
J. Nick Koston
bf617c3279 [web_server] Replace str_sprintf with stack buffers (#12592) 2025-12-21 07:32:05 -10:00
J. Nick Koston
c70eab931e [api] Add zero-copy support for Home Assistant state response messages (#12585) 2025-12-21 07:31:54 -10:00
J. Nick Koston
a799ac6488 [syslog] Eliminate heap allocations in log path (#12589) 2025-12-21 07:10:27 -10:00
polyfloyd
5a36cea5ec Add nix files to gitignore (#12604) 2025-12-21 09:26:03 -05:00
J. Nick Koston
60756db06d [syslog] Use C++17 nested namespace syntax (#12594) 2025-12-21 02:47:37 -06:00
Keith Burzinski
2113858f89 [sprinkler] Squash a few bugs + minor optimization (#12436) 2025-12-21 02:45:24 -06:00
dependabot[bot]
e89fe9b945 Bump aioesphomeapi from 43.4.0 to 43.5.0 (#12599)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-20 23:59:48 +00:00
dependabot[bot]
f1362cd9fe Bump aioesphomeapi from 43.3.0 to 43.4.0 (#12597)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-20 22:37:10 +00:00
Frédéric Metrich
bf554a58ef [const] Add CONF_ON_DATA and consolidate definitions (#12595)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-20 12:17:09 -10:00
J. Nick Koston
644e806afd [zwave_proxy] Add missing USE_API guards for clang-tidy (#12590) 2025-12-20 10:40:43 -10:00
Leo Bergolth
6c2d255230 send NIL ("-") as timestamp if time source is not valid (#12588) 2025-12-20 10:04:59 -10:00
Stuart Parmenter
6f3bfc2060 [hub75] Bump esp-hub75 version to 0.1.7 (#12564) 2025-12-20 13:18:20 -05:00
J. Nick Koston
40eb898814 [api] Add zero-copy support for noise encryption key requests (#12405) 2025-12-20 06:47:30 -10:00
J. Nick Koston
64269334ce [text_sensor] Avoid string copies in callbacks by passing const ref (#12503)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-12-20 06:46:13 -10:00
Eduard Llull
121375ff39 [display_menu_base] Call on_value_ after updating the select (#12584) 2025-12-20 10:59:14 -05:00
J. Nick Koston
48cdf9e036 [tests] Fix race condition in alarm control panel state transitions test (#12581) 2025-12-20 10:47:29 -05:00
J. Nick Koston
3e313014e1 [core] Migrate entities to use lazy callbacks (#12580) 2025-12-19 19:04:21 -10:00
Martin Ebner
be6c1e4ec0 [sen5x][sgp4x] Move configuration keys from SEN5x and SGP4x to const.py (#12567)
Co-authored-by: Martin Ebner <martinebner@me.com>
2025-12-19 21:29:02 -05:00
Keith Burzinski
730bf206de [wifi] Fix for wifi_info when static IP is configured (#12576) 2025-12-19 21:25:16 -05:00
J. Nick Koston
c9fccdff25 [fan] Add zero-copy support for API preset mode commands (#12404)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-19 22:05:52 +00:00
J. Nick Koston
ada6c42f3f [alarm_control_panel] Remove redundant per-state callbacks (#12171)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-19 11:48:14 -10:00
J. Nick Koston
988b888c63 [ota] Replace std::function callbacks with listener interface (#12167) 2025-12-19 11:19:07 -10:00
J. Nick Koston
940afdbb12 [climate] Add zero-copy support for API custom fan mode and preset commands (#12402) 2025-12-19 11:18:50 -10:00
J. Nick Koston
81e91c2a8f [esp32_ble] Add stack-based UUID formatting to avoid heap allocations (#12510) 2025-12-19 11:18:32 -10:00
J. Nick Koston
ebc3d28ade [wifi] Replace optional with sentinel values to reduce RAM and clarify API (#12446) 2025-12-19 11:18:15 -10:00
Rene Guca
25cebedcfc [dht] Fix "Falling edge for bit 39 failed!" for Sonoff THS01 (#9225)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-12-19 15:42:39 -05:00
dependabot[bot]
98ed679b19 Bump ruff from 0.14.9 to 0.14.10 (#12572)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-12-19 19:22:56 +00:00
dependabot[bot]
59b38d79b4 Bump docker/setup-buildx-action from 3.11.1 to 3.12.0 in the docker-actions group (#12574)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-19 09:18:52 -10:00
dependabot[bot]
26c16f4ca2 Bump voluptuous from 0.15.2 to 0.16.0 (#12573)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-19 09:18:33 -10:00
Jas Strong
940e619481 [aqi, hm3301, pmsx003] Air Quality Index improvements (#12203)
Co-authored-by: jas <jas@asspa.in>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-12-19 13:42:11 -05:00
Jonathan Swoboda
eaca81c3ab Merge branch 'release' into dev 2025-12-19 10:53:18 -05:00
Jonathan Swoboda
93e38f2608 Merge pull request #12569 from esphome/bump-2025.12.1
2025.12.1
2025-12-19 10:53:05 -05:00
Jonathan Swoboda
3a888326d8 Bump version to 2025.12.1 2025-12-19 10:13:35 -05:00
Keith Burzinski
f0d0ea60a7 [esp32_ble, esp32_ble_tracker] Fix crash, error messages when ble.disable called during boot (#12560) 2025-12-19 10:13:35 -05:00
Jonathan Swoboda
7ca11764ab [template.alarm_control_panel] Fix compile without binary_sensor (#12548)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-19 10:13:35 -05:00
Jonathan Swoboda
3e38a5e630 [esp32_camera] Fix I2C driver conflict with other components (#12533)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-12-19 10:13:35 -05:00
Jonathan Swoboda
636be92c97 [bme68x_bsec2_i2c] Add MULTI_CONF support for multiple sensors (#12535)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-19 10:13:35 -05:00
Jack Wilsdon
195b1c6323 [pm1006] Fix "never" update interval detection (#12529) 2025-12-19 10:13:35 -05:00
Anna Oake
7e08092012 [cc1101] Fix default frequencies (#12539) 2025-12-19 10:13:35 -05:00
pixelgrb
f962497db1 [mmc5603] enable AUTO_SR_en to compensate for temperature drift (#12556)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-12-18 23:13:36 -05:00
Keith Burzinski
7ae3a11d6b [esp32_ble, esp32_ble_tracker] Fix crash, error messages when ble.disable called during boot (#12560) 2025-12-18 19:42:47 -06:00
dependabot[bot]
1c50c2b672 Bump ruamel-yaml from 0.18.16 to 0.18.17 (#12555)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-18 11:19:19 -10:00
Jonathan Swoboda
41fd1762e9 [core] Deprecate custom_components folder (#12552)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-18 11:46:16 -05:00
J. Nick Koston
2cf6ed2af7 [socket] Refactor socket implementations for memory efficiency and code quality (#12550) 2025-12-18 09:07:35 -07:00
J. Nick Koston
b47b7d43fd [api] Remove unused force parameter from encode_message (#12551) 2025-12-18 09:06:16 -07:00
Jonathan Swoboda
663a4304e0 [libretiny] Fix millis() ambiguity on BK72XX (#12534)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-18 07:50:31 -05:00
Jonathan Swoboda
ca47bad90a [template.alarm_control_panel] Fix compile without binary_sensor (#12548)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-17 23:34:04 -05:00
J. Nick Koston
4f821a6d76 [wifi] Reduce scan logging to prevent blocking loop during connection (#12544) 2025-12-17 18:21:46 -10:00
J. Nick Koston
426305836d [esp32][libretiny] Avoid duplicate snprintf when syncing preferences (#12542) 2025-12-17 18:16:14 -10:00
dependabot[bot]
1b5af7d21d Bump github/codeql-action from 4.31.8 to 4.31.9 (#12524)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-17 15:22:19 -10:00
David Woodhouse
9de7df7b5b Add build info to image (#12425)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-18 00:06:52 +00:00
Jonathan Swoboda
2b337aa306 [esp32_camera] Fix I2C driver conflict with other components (#12533)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-12-17 17:37:59 -05:00
Jonathan Swoboda
4ddaff4027 [esp32] Dynamically embed managed component server certificates (#12509)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-12-17 17:26:56 -05:00
J. Nick Koston
91c504061b [select] Eliminate string allocation in state callbacks (#12505)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-12-17 12:19:26 -10:00
Jonathan Swoboda
dc8f7abce2 [bme68x_bsec2_i2c] Add MULTI_CONF support for multiple sensors (#12535)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-17 17:07:42 -05:00
Jonathan Swoboda
3d673ac55e [ci] Check changed headers in clang-tidy when using --changed (#12540)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-17 11:13:18 -10:00
Jack Wilsdon
b02696edc0 [pm1006] Fix "never" update interval detection (#12529) 2025-12-18 07:40:31 +11:00
Anna Oake
f9720026d0 [cc1101] Fix default frequencies (#12539) 2025-12-17 14:19:18 -05:00
Jonathan Swoboda
d7b04a3d18 [nextion] Fix clang-tidy error on Zephyr for HTTPClient (#12538)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-17 13:59:49 -05:00
Jonathan Swoboda
0e71fa97a7 [spi] Add SPIInterface stub for clang-tidy on unsupported platforms (#12532)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-17 12:18:25 -05:00
J. Nick Koston
42e061c9ae [text] Avoid string copies in callbacks by passing const ref (#12504) 2025-12-17 12:00:19 -05:00
J. Nick Koston
94763ebdab [libretiny] Store preference keys as uint32_t, convert to string only at FlashDB boundary (#12500) 2025-12-17 11:59:40 -05:00
J. Nick Koston
f32bb618ac [esp32] Store preference keys as uint32_t, convert to string only at NVS boundary (#12494) 2025-12-17 11:59:35 -05:00
Edward Firmo
0707f383a6 [nextion] Use ESP-IDF for ESP32 Arduino (#9429)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-12-17 11:45:17 -05:00
Piotr Szulc
e91c6a79ea [deep_sleep] Deep sleep for BK72xx (#12267)
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: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-12-17 11:45:05 -05:00
J. Nick Koston
63fc8b4e5a [core] Refactor str_snake_case and str_sanitize to use constexpr helpers (#12454) 2025-12-17 11:30:12 -05:00
J. Nick Koston
ab73ed76b8 [esphome] Improve OTA field alignment to save 4 bytes on 32-bit (#12461) 2025-12-17 11:29:58 -05:00
J. Nick Koston
bf6a03d1cf [factory_reset] Optimize memory by storing interval as uint16_t seconds (#12462) 2025-12-17 11:29:51 -05:00
J. Nick Koston
9928ab09cf [time] Convert to C++17 nested namespace syntax (#12463) 2025-12-17 11:29:43 -05:00
Thomas Rupprecht
56c1691d72 [pca9685,sx126x,sx127x] Use frequency/float_range check (#12490)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-12-16 22:52:28 -05:00
Roger Fachini
a065990ab9 [update] Add check action to trigger update checks (#12415) 2025-12-16 22:20:12 -05:00
Stuart Parmenter
084f517a20 [hub75] Add set_brightness action (#12521) 2025-12-16 22:12:33 -05:00
Jonathan Swoboda
1122ec354f [esp32] Add OTA rollback support (#12460)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-16 20:07:57 -05:00
Jonathan Swoboda
431183eebc [ledc,mqtt,resampler] Remove unnecessary ESP-IDF framework restrictions (#12442)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-16 20:07:09 -05:00
Jonathan Swoboda
08beaf8750 [esp32] Remove Arduino-specific code from core.cpp (#12501)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-16 20:06:12 -05:00
Jonathan Swoboda
18814f12dc [http_request] Use ESP-IDF for ESP32 Arduino (#12428)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-16 19:44:14 -05:00
Jonathan Swoboda
9cd888cef6 [spi] Use ESP-IDF driver for ESP32 Arduino (#12420)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-16 19:44:01 -05:00
Thomas Rupprecht
9727c7135c [openthread] channel range, fix typo, use C++17 nested namespace syntax (#12422) 2025-12-16 19:43:18 -05:00
Thomas Rupprecht
93621d85b0 [climate] Improve temperature unit regex (#12032) 2025-12-16 19:43:10 -05:00
Thomas Rupprecht
046ea922e8 [esp32] improve types and variable naming (#12423) 2025-12-16 19:42:52 -05:00
Jeff Zigler
fab4efb469 [esp32] Fix serial logging on h2, c2 & c61 (#12522)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-12-16 19:42:12 -05:00
Jonathan Swoboda
efc5672567 Merge branch 'release' into dev 2025-12-16 18:57:37 -05:00
Jonathan Swoboda
5e630e9255 Merge branch 'beta' into dev 2025-12-16 11:26:08 -05:00
Jonathan Swoboda
1897551b28 [uart] Fix UART on default UART0 pins for ESP-IDF (#12519)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-16 10:17:17 -05:00
J. Nick Koston
ead60bc5c4 [socket] Fix getpeername() returning local address instead of remote in LWIP raw TCP (#12475) 2025-12-16 00:48:30 -06:00
Jonathan Swoboda
7fe8e53f82 Merge branch 'beta' into dev 2025-12-15 19:01:12 -05:00
David Woodhouse
839139df36 Add FNV-1a hash functions (#12502) 2025-12-15 20:23:54 +00:00
dependabot[bot]
24d7e9dd23 Bump tornado from 6.5.3 to 6.5.4 (#12508)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-15 20:08:16 +00:00
dependabot[bot]
1214bb6bad Bump aioesphomeapi from 43.2.1 to 43.3.0 (#12507)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-15 20:07:20 +00:00
Pascal Vizeli
260ffba2a5 [http_request] Fix infinite loop when server doesn't send Content-Length header (#12480)
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-15 12:54:12 -05:00
Jonathan Swoboda
2e899dd010 [esp32] Support all IDF component version operators in shorthand syntax (#12499)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-15 12:07:02 -05:00
David Woodhouse
61cbd07e1d Add hmac-sha256 support (#12437)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2025-12-15 10:55:03 -06:00
Jonathan Swoboda
450962850a [remote_base] Fix crash when ABBWelcome action has no data field (#12493)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-15 09:29:51 -05:00
Jonathan Swoboda
6b088caf5d Merge branch 'beta' into dev 2025-12-14 19:18:10 -05:00
J. Nick Koston
ffce80f96c [wifi] Fix WiFi recovery after failed connection attempts (#12483) 2025-12-14 16:26:34 -06:00
mbohdal
fa5b14fad4 [ethernet] fix used pins validation in configuration of RMII pins (#12486) 2025-12-14 16:40:08 -05:00
guillempages
cee532a1e3 [core] Use Arduino string macros only on ESP8266 (#12471) 2025-12-15 07:15:19 +11:00
J. Nick Koston
8524b894d6 [ota] Match client timeout to device timeout to prevent premature failures (#12484) 2025-12-14 13:47:11 -06:00
J. Nick Koston
3a5e708c13 [web_server_idf] Always enable LRU purge to prevent socket exhaustion (#12481) 2025-12-14 13:31:19 -06:00
J. Nick Koston
96e418a8ca [wifi_signal] Skip publishing disconnected RSSI value (#12482) 2025-12-14 13:31:07 -06:00
J. Nick Koston
780a407b10 [dashboard] Add ESPHOME_TRUSTED_DOMAINS support to events WebSocket (#12479) 2025-12-14 13:30:55 -06:00
Jonathan Swoboda
cfc0d8bdfc [cc1101] Add packet mode support (#12474)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-14 13:22:55 -05:00
Jonathan Swoboda
786d7266f5 [core] Fix polling_component_schema and type consistency (#12478)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-14 12:47:52 -05:00
Clyde Stubbs
ede64a9f47 [packet_transport] Ensure retransmission at update intervals (#12472)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-14 12:47:15 -05:00
J. Nick Koston
e0ce66e011 [core] Fix CORE.raw_config not updated after package merge (#12456) 2025-12-13 07:38:31 -06:00
David Woodhouse
6fce0a6104 Add host platform support to MD5 component (#12458) 2025-12-13 02:50:34 +00:00
David Woodhouse
ff7651875e Add HMAC-MD5 component tests (#12459) 2025-12-12 19:19:31 -06:00
David Woodhouse
1a43a06dd4 Add USE_SHA256 define to sha256 component to enable tests (#12457) 2025-12-12 19:15:50 -06:00
dependabot[bot]
51b187954a Bump ruff from 0.14.8 to 0.14.9 (#12448)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-12-12 19:20:06 +00:00
dependabot[bot]
9126b32c35 Bump actions/cache from 4.3.0 to 5.0.1 in /.github/actions/restore-python (#12453)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-12 13:17:08 -06:00
dependabot[bot]
4993bb2f49 Bump github/codeql-action from 4.31.7 to 4.31.8 (#12451)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-12 13:16:41 -06:00
dependabot[bot]
2b40af3459 Bump actions/cache from 4.3.0 to 5.0.1 (#12450)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-12 13:16:29 -06:00
dependabot[bot]
b3e967a233 Bump actions/download-artifact from 6.0.0 to 7.0.0 (#12449)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-12 13:15:41 -06:00
dependabot[bot]
26a08e3ae3 Bump actions/upload-artifact from 5.0.0 to 6.0.0 (#12452)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-12 13:15:28 -06:00
Jonathan Swoboda
64d650c65c Merge branch 'beta' into dev 2025-12-12 12:15:52 -05:00
Jonathan Swoboda
d30d8156c1 [http_request] Skip update check when network not connected (#12418)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-12 10:31:17 -05:00
dependabot[bot]
8d1e68c4c1 Bump tornado from 6.5.2 to 6.5.3 (#12430)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-11 17:53:12 -06:00
J. Nick Koston
74218bc742 [api] Release prologue memory after noise handshake completes (#12412) 2025-12-10 19:33:22 -06:00
J. Nick Koston
369cc70fdf [climate] Save 48 bytes per entity by conditionally compiling visual overrides (#12406) 2025-12-10 19:10:42 -06:00
dependabot[bot]
1f0a27b181 Bump codecov/codecov-action from 5.5.1 to 5.5.2 (#12408)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-10 22:34:24 +01:00
dependabot[bot]
22918d3bd5 Bump peter-evans/create-pull-request from 7.0.11 to 8.0.0 (#12409)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-10 22:21:29 +01:00
J. Nick Koston
7a9fce90cb [text] Add integration tests for text command API (#12401) 2025-12-10 12:13:40 -05:00
dependabot[bot]
d1d376ebc8 Bump actions/create-github-app-token from 2.2.0 to 2.2.1 (#12370)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-10 13:05:01 +01:00
J. Nick Koston
c124d72ea9 [esp8266] Eliminate up to 16ms socket latency (#12397) 2025-12-10 03:45:27 +00:00
J. Nick Koston
567e82cfec [api] Fix potential buffer overflow in noise PSK base64 decode (#12395) 2025-12-10 04:20:23 +01:00
J. Nick Koston
b1f9100b02 [core] Add constexpr parse_hex_char helper and simplify parse_hex (#12394) 2025-12-10 04:20:08 +01:00
J. Nick Koston
d0fbc82f47 [esp32_ble_client] Use stack-based MAC formatting in auth logging (#12393)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-10 04:19:52 +01:00
J. Nick Koston
03c391bd43 [light] Add zero-copy support for API effect commands (#12384) 2025-12-10 04:19:29 +01:00
Jonathan Swoboda
5601a2b686 Merge branch 'beta' into dev 2025-12-09 21:34:12 -05:00
Jonathan Swoboda
84d5348bd8 Bump version to 2026.1.0-dev 2025-12-09 20:08:35 -05:00
392 changed files with 8439 additions and 2978 deletions

View File

@@ -1 +1 @@
766420905c06eeb6c5f360f68fd965e5ddd9c4a5db6b823263d3ad3accb64a07
94557f94be073390342833aff12ef8676a8b597db5fa770a5a1232e9425cb48f

View File

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

View File

@@ -26,7 +26,7 @@ jobs:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}

View File

@@ -62,7 +62,7 @@ jobs:
run: git diff
- if: failure()
name: Archive artifacts
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: generated-proto-files
path: |

View File

@@ -49,7 +49,7 @@ jobs:
with:
python-version: "3.11"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Set TAG
run: |

View File

@@ -47,7 +47,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: venv
# yamllint disable-line rule:line-length
@@ -152,12 +152,12 @@ jobs:
. venv/bin/activate
pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
with:
token: ${{ secrets.CODECOV_TOKEN }}
- name: Save Python virtual environment cache
if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: venv
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
@@ -193,7 +193,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Restore components graph cache
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: .temp/components_graph.json
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
@@ -223,7 +223,7 @@ jobs:
echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT
- name: Save components graph cache
if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: .temp/components_graph.json
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
@@ -245,7 +245,7 @@ jobs:
python-version: "3.13"
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: venv
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
@@ -334,14 +334,14 @@ jobs:
- name: Cache platformio
if: github.ref == 'refs/heads/dev'
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
@@ -413,14 +413,14 @@ jobs:
- name: Cache platformio
if: github.ref == 'refs/heads/dev'
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
@@ -502,14 +502,14 @@ jobs:
- name: Cache platformio
if: github.ref == 'refs/heads/dev'
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
@@ -735,7 +735,7 @@ jobs:
- name: Restore cached memory analysis
id: cache-memory-analysis
if: steps.check-script.outputs.skip != 'true'
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: memory-analysis-target.json
key: ${{ steps.cache-key.outputs.cache-key }}
@@ -759,7 +759,7 @@ jobs:
- name: Cache platformio
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: ~/.platformio
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
@@ -800,7 +800,7 @@ jobs:
- name: Save memory analysis to cache
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success'
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: memory-analysis-target.json
key: ${{ steps.cache-key.outputs.cache-key }}
@@ -821,7 +821,7 @@ jobs:
fi
- name: Upload memory analysis JSON
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: memory-analysis-target
path: memory-analysis-target.json
@@ -847,7 +847,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache platformio
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: ~/.platformio
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
@@ -885,7 +885,7 @@ jobs:
--platform "$platform"
- name: Upload memory analysis JSON
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: memory-analysis-pr
path: memory-analysis-pr.json
@@ -915,13 +915,13 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Download target analysis JSON
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: memory-analysis-target
path: ./memory-analysis
continue-on-error: true
- name: Download PR analysis JSON
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: memory-analysis-pr
path: ./memory-analysis

View File

@@ -58,7 +58,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
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@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
with:
category: "/language:${{matrix.language}}"

View File

@@ -99,7 +99,7 @@ jobs:
python-version: "3.11"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Log in to docker hub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
@@ -138,7 +138,7 @@ jobs:
# version: ${{ needs.init.outputs.tag }}
- name: Upload digests
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: digests-${{ matrix.platform.arch }}
path: /tmp/digests
@@ -171,14 +171,14 @@ jobs:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Download digests
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
pattern: digests-*
path: /tmp/digests
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Log in to docker hub
if: matrix.registry == 'dockerhub'
@@ -221,7 +221,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
@@ -256,7 +256,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
@@ -287,7 +287,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}

View File

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

4
.gitignore vendored
View File

@@ -91,6 +91,10 @@ venv-*/
# mypy
.mypy_cache/
# nix
/default.nix
/shell.nix
.pioenvs
.piolibdeps
.pio

View File

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

View File

@@ -42,6 +42,7 @@ esphome/components/animation/* @syndlex
esphome/components/anova/* @buxtronix
esphome/components/apds9306/* @aodrenah
esphome/components/api/* @esphome/core
esphome/components/aqi/* @freekode @jasstrong @ximex
esphome/components/as5600/* @ammmze
esphome/components/as5600/sensor/* @ammmze
esphome/components/as7341/* @mrgnr
@@ -215,6 +216,7 @@ esphome/components/hlk_fm22x/* @OnFreund
esphome/components/hlw8032/* @rici4kubicek
esphome/components/hm3301/* @freekode
esphome/components/hmac_md5/* @dwmw2
esphome/components/hmac_sha256/* @dwmw2
esphome/components/homeassistant/* @esphome/core @OttoWinter
esphome/components/homeassistant/number/* @landonr
esphome/components/homeassistant/switch/* @Links2004
@@ -517,6 +519,7 @@ esphome/components/tuya/switch/* @jesserockz
esphome/components/tuya/text_sensor/* @dentra
esphome/components/uart/* @esphome/core
esphome/components/uart/button/* @ssieb
esphome/components/uart/event/* @eoasmxd
esphome/components/uart/packet_transport/* @clydebarrow
esphome/components/udp/* @clydebarrow
esphome/components/ufire_ec/* @pvizeli
@@ -535,6 +538,7 @@ esphome/components/version/* @esphome/core
esphome/components/voice_assistant/* @jesserockz @kahrendt
esphome/components/wake_on_lan/* @clydebarrow @willwill2will54
esphome/components/watchdog/* @oarcher
esphome/components/water_heater/* @dhoeben
esphome/components/waveshare_epaper/* @clydebarrow
esphome/components/web_server/ota/* @esphome/core
esphome/components/web_server_base/* @esphome/core

View File

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

View File

@@ -518,10 +518,49 @@ def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
rc = platformio_api.run_compile(config, CORE.verbose)
if rc != 0:
return rc
# 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
def _check_and_emit_build_info() -> None:
"""Check if firmware was rebuilt and emit build_info."""
import json
firmware_path = CORE.firmware_bin
build_info_json_path = CORE.relative_build_path("build_info.json")
# Check if both files exist
if not firmware_path.exists() or not build_info_json_path.exists():
return
# Check if firmware is newer than build_info (indicating a relink occurred)
if firmware_path.stat().st_mtime <= build_info_json_path.stat().st_mtime:
return
# Read build_info from JSON
try:
with open(build_info_json_path, encoding="utf-8") as f:
build_info = json.load(f)
except (OSError, json.JSONDecodeError) as e:
_LOGGER.debug("Failed to read build_info: %s", e)
return
config_hash = build_info.get("config_hash")
build_time_str = build_info.get("build_time_str")
if config_hash is None or build_time_str is None:
return
# Emit build_info with human-readable time
_LOGGER.info(
"Build Info: config_hash=0x%08x build_time_str=%s", config_hash, build_time_str
)
def upload_using_esptool(
config: ConfigType, port: str, file: str, speed: int
) -> str | int:
@@ -750,6 +789,12 @@ def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
exit_code = compile_program(args, config)
if exit_code != 0:
return exit_code
if CORE.is_host:
from esphome.platformio_api import get_idedata
program_path = str(get_idedata(config).firmware_elf_path)
_LOGGER.info("Successfully compiled program to path '%s'", program_path)
else:
_LOGGER.info("Successfully compiled program.")
return 0
@@ -800,10 +845,8 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
if CORE.is_host:
from esphome.platformio_api import get_idedata
idedata = get_idedata(config)
if idedata is None:
return 1
program_path = idedata.raw["prog_path"]
program_path = str(get_idedata(config).firmware_elf_path)
_LOGGER.info("Running program from path '%s'", program_path)
return run_external_process(program_path)
# Get devices, resolving special identifiers like OTA

View File

@@ -227,7 +227,7 @@ CONFIG_SCHEMA = cv.All(
{
cv.GenerateID(): cv.declare_id(ADE7880),
cv.Optional(CONF_FREQUENCY, default="50Hz"): cv.All(
cv.frequency, cv.Range(min=45.0, max=66.0)
cv.frequency, cv.float_range(min=45.0, max=66.0)
),
cv.Optional(CONF_IRQ0_PIN): pins.internal_gpio_input_pin_schema,
cv.Required(CONF_IRQ1_PIN): pins.internal_gpio_input_pin_schema,

View File

@@ -8,8 +8,7 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
namespace alarm_control_panel {
namespace esphome::alarm_control_panel {
static const char *const TAG = "alarm_control_panel";
@@ -35,26 +34,12 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) {
ESP_LOGD(TAG, "Set state to: %s, previous: %s", LOG_STR_ARG(alarm_control_panel_state_to_string(state)),
LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state)));
this->current_state_ = state;
// Single state callback - triggers check get_state() for specific states
this->state_callback_.call();
#if defined(USE_ALARM_CONTROL_PANEL) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_alarm_control_panel_update(this);
#endif
if (state == ACP_STATE_TRIGGERED) {
this->triggered_callback_.call();
} else if (state == ACP_STATE_ARMING) {
this->arming_callback_.call();
} else if (state == ACP_STATE_PENDING) {
this->pending_callback_.call();
} else if (state == ACP_STATE_ARMED_HOME) {
this->armed_home_callback_.call();
} else if (state == ACP_STATE_ARMED_NIGHT) {
this->armed_night_callback_.call();
} else if (state == ACP_STATE_ARMED_AWAY) {
this->armed_away_callback_.call();
} else if (state == ACP_STATE_DISARMED) {
this->disarmed_callback_.call();
}
// Cleared fires when leaving TRIGGERED state
if (prev_state == ACP_STATE_TRIGGERED) {
this->cleared_callback_.call();
}
@@ -69,34 +54,6 @@ void AlarmControlPanel::add_on_state_callback(std::function<void()> &&callback)
this->state_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_triggered_callback(std::function<void()> &&callback) {
this->triggered_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_arming_callback(std::function<void()> &&callback) {
this->arming_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_armed_home_callback(std::function<void()> &&callback) {
this->armed_home_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_armed_night_callback(std::function<void()> &&callback) {
this->armed_night_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_armed_away_callback(std::function<void()> &&callback) {
this->armed_away_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_pending_callback(std::function<void()> &&callback) {
this->pending_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_disarmed_callback(std::function<void()> &&callback) {
this->disarmed_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_cleared_callback(std::function<void()> &&callback) {
this->cleared_callback_.add(std::move(callback));
}
@@ -157,5 +114,4 @@ void AlarmControlPanel::disarm(optional<std::string> code) {
call.perform();
}
} // namespace alarm_control_panel
} // namespace esphome
} // namespace esphome::alarm_control_panel

View File

@@ -1,7 +1,5 @@
#pragma once
#include <map>
#include "alarm_control_panel_call.h"
#include "alarm_control_panel_state.h"
@@ -9,8 +7,7 @@
#include "esphome/core/entity_base.h"
#include "esphome/core/log.h"
namespace esphome {
namespace alarm_control_panel {
namespace esphome::alarm_control_panel {
enum AlarmControlPanelFeature : uint8_t {
// Matches Home Assistant values
@@ -35,54 +32,13 @@ class AlarmControlPanel : public EntityBase {
*/
void publish_state(AlarmControlPanelState state);
/** Add a callback for when the state of the alarm_control_panel changes
/** Add a callback for when the state of the alarm_control_panel changes.
* Triggers can check get_state() to determine the new state.
*
* @param callback The callback function
*/
void add_on_state_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel chanes to triggered
*
* @param callback The callback function
*/
void add_on_triggered_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel chanes to arming
*
* @param callback The callback function
*/
void add_on_arming_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel changes to pending
*
* @param callback The callback function
*/
void add_on_pending_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel changes to armed_home
*
* @param callback The callback function
*/
void add_on_armed_home_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel changes to armed_night
*
* @param callback The callback function
*/
void add_on_armed_night_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel changes to armed_away
*
* @param callback The callback function
*/
void add_on_armed_away_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel changes to disarmed
*
* @param callback The callback function
*/
void add_on_disarmed_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel clears from triggered
*
* @param callback The callback function
@@ -172,29 +128,14 @@ class AlarmControlPanel : public EntityBase {
uint32_t last_update_;
// the call control function
virtual void control(const AlarmControlPanelCall &call) = 0;
// state callback
CallbackManager<void()> state_callback_{};
// trigger callback
CallbackManager<void()> triggered_callback_{};
// arming callback
CallbackManager<void()> arming_callback_{};
// pending callback
CallbackManager<void()> pending_callback_{};
// armed_home callback
CallbackManager<void()> armed_home_callback_{};
// armed_night callback
CallbackManager<void()> armed_night_callback_{};
// armed_away callback
CallbackManager<void()> armed_away_callback_{};
// disarmed callback
CallbackManager<void()> disarmed_callback_{};
// clear callback
CallbackManager<void()> cleared_callback_{};
// state callback - triggers check get_state() for specific state
LazyCallbackManager<void()> state_callback_{};
// clear callback - fires when leaving TRIGGERED state
LazyCallbackManager<void()> cleared_callback_{};
// chime callback
CallbackManager<void()> chime_callback_{};
LazyCallbackManager<void()> chime_callback_{};
// ready callback
CallbackManager<void()> ready_callback_{};
LazyCallbackManager<void()> ready_callback_{};
};
} // namespace alarm_control_panel
} // namespace esphome
} // namespace esphome::alarm_control_panel

View File

@@ -4,8 +4,7 @@
#include "esphome/core/log.h"
namespace esphome {
namespace alarm_control_panel {
namespace esphome::alarm_control_panel {
static const char *const TAG = "alarm_control_panel";
@@ -99,5 +98,4 @@ void AlarmControlPanelCall::perform() {
}
}
} // namespace alarm_control_panel
} // namespace esphome
} // namespace esphome::alarm_control_panel

View File

@@ -6,8 +6,7 @@
#include "esphome/core/helpers.h"
namespace esphome {
namespace alarm_control_panel {
namespace esphome::alarm_control_panel {
class AlarmControlPanel;
@@ -36,5 +35,4 @@ class AlarmControlPanelCall {
void validate_();
};
} // namespace alarm_control_panel
} // namespace esphome
} // namespace esphome::alarm_control_panel

View File

@@ -1,7 +1,6 @@
#include "alarm_control_panel_state.h"
namespace esphome {
namespace alarm_control_panel {
namespace esphome::alarm_control_panel {
const LogString *alarm_control_panel_state_to_string(AlarmControlPanelState state) {
switch (state) {
@@ -30,5 +29,4 @@ const LogString *alarm_control_panel_state_to_string(AlarmControlPanelState stat
}
}
} // namespace alarm_control_panel
} // namespace esphome
} // namespace esphome::alarm_control_panel

View File

@@ -3,8 +3,7 @@
#include <cstdint>
#include "esphome/core/log.h"
namespace esphome {
namespace alarm_control_panel {
namespace esphome::alarm_control_panel {
enum AlarmControlPanelState : uint8_t {
ACP_STATE_DISARMED = 0,
@@ -25,5 +24,4 @@ enum AlarmControlPanelState : uint8_t {
*/
const LogString *alarm_control_panel_state_to_string(AlarmControlPanelState state);
} // namespace alarm_control_panel
} // namespace esphome
} // namespace esphome::alarm_control_panel

View File

@@ -3,9 +3,9 @@
#include "esphome/core/automation.h"
#include "alarm_control_panel.h"
namespace esphome {
namespace alarm_control_panel {
namespace esphome::alarm_control_panel {
/// Trigger on any state change
class StateTrigger : public Trigger<> {
public:
explicit StateTrigger(AlarmControlPanel *alarm_control_panel) {
@@ -13,55 +13,30 @@ class StateTrigger : public Trigger<> {
}
};
class TriggeredTrigger : public Trigger<> {
/// Template trigger that fires when entering a specific state
template<AlarmControlPanelState State> class StateEnterTrigger : public Trigger<> {
public:
explicit TriggeredTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_triggered_callback([this]() { this->trigger(); });
explicit StateEnterTrigger(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {
alarm_control_panel->add_on_state_callback([this]() {
if (this->alarm_control_panel_->get_state() == State)
this->trigger();
});
}
protected:
AlarmControlPanel *alarm_control_panel_;
};
class ArmingTrigger : public Trigger<> {
public:
explicit ArmingTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_arming_callback([this]() { this->trigger(); });
}
};
class PendingTrigger : public Trigger<> {
public:
explicit PendingTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_pending_callback([this]() { this->trigger(); });
}
};
class ArmedHomeTrigger : public Trigger<> {
public:
explicit ArmedHomeTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_armed_home_callback([this]() { this->trigger(); });
}
};
class ArmedNightTrigger : public Trigger<> {
public:
explicit ArmedNightTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_armed_night_callback([this]() { this->trigger(); });
}
};
class ArmedAwayTrigger : public Trigger<> {
public:
explicit ArmedAwayTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_armed_away_callback([this]() { this->trigger(); });
}
};
class DisarmedTrigger : public Trigger<> {
public:
explicit DisarmedTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_disarmed_callback([this]() { this->trigger(); });
}
};
// Type aliases for state-specific triggers
using TriggeredTrigger = StateEnterTrigger<ACP_STATE_TRIGGERED>;
using ArmingTrigger = StateEnterTrigger<ACP_STATE_ARMING>;
using PendingTrigger = StateEnterTrigger<ACP_STATE_PENDING>;
using ArmedHomeTrigger = StateEnterTrigger<ACP_STATE_ARMED_HOME>;
using ArmedNightTrigger = StateEnterTrigger<ACP_STATE_ARMED_NIGHT>;
using ArmedAwayTrigger = StateEnterTrigger<ACP_STATE_ARMED_AWAY>;
using DisarmedTrigger = StateEnterTrigger<ACP_STATE_DISARMED>;
/// Trigger when leaving TRIGGERED state (alarm cleared)
class ClearedTrigger : public Trigger<> {
public:
explicit ClearedTrigger(AlarmControlPanel *alarm_control_panel) {
@@ -69,6 +44,7 @@ class ClearedTrigger : public Trigger<> {
}
};
/// Trigger on chime event (zone opened while disarmed)
class ChimeTrigger : public Trigger<> {
public:
explicit ChimeTrigger(AlarmControlPanel *alarm_control_panel) {
@@ -76,6 +52,7 @@ class ChimeTrigger : public Trigger<> {
}
};
/// Trigger on ready state change
class ReadyTrigger : public Trigger<> {
public:
explicit ReadyTrigger(AlarmControlPanel *alarm_control_panel) {
@@ -187,5 +164,4 @@ template<typename... Ts> class AlarmControlPanelCondition : public Condition<Ts.
AlarmControlPanel *parent_;
};
} // namespace alarm_control_panel
} // namespace esphome
} // namespace esphome::alarm_control_panel

View File

@@ -477,7 +477,7 @@ message FanCommandRequest {
bool has_speed_level = 10;
int32 speed_level = 11;
bool has_preset_mode = 12;
string preset_mode = 13;
string preset_mode = 13 [(pointer_to_buffer) = true];
uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"];
}
@@ -747,7 +747,7 @@ message NoiseEncryptionSetKeyRequest {
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_API_NOISE";
bytes key = 1;
bytes key = 1 [(pointer_to_buffer) = true];
}
message NoiseEncryptionSetKeyResponse {
@@ -824,9 +824,9 @@ message HomeAssistantStateResponse {
option (no_delay) = true;
option (ifdef) = "USE_API_HOMEASSISTANT_STATES";
string entity_id = 1;
string state = 2;
string attribute = 3;
string entity_id = 1 [(pointer_to_buffer) = true];
string state = 2 [(pointer_to_buffer) = true];
string attribute = 3 [(pointer_to_buffer) = true];
}
// ==================== IMPORT TIME ====================
@@ -1091,16 +1091,95 @@ message ClimateCommandRequest {
bool has_swing_mode = 14;
ClimateSwingMode swing_mode = 15;
bool has_custom_fan_mode = 16;
string custom_fan_mode = 17;
string custom_fan_mode = 17 [(pointer_to_buffer) = true];
bool has_preset = 18;
ClimatePreset preset = 19;
bool has_custom_preset = 20;
string custom_preset = 21;
string custom_preset = 21 [(pointer_to_buffer) = true];
bool has_target_humidity = 22;
float target_humidity = 23;
uint32 device_id = 24 [(field_ifdef) = "USE_DEVICES"];
}
// ==================== WATER_HEATER ====================
enum WaterHeaterMode {
WATER_HEATER_MODE_OFF = 0;
WATER_HEATER_MODE_ECO = 1;
WATER_HEATER_MODE_ELECTRIC = 2;
WATER_HEATER_MODE_PERFORMANCE = 3;
WATER_HEATER_MODE_HIGH_DEMAND = 4;
WATER_HEATER_MODE_HEAT_PUMP = 5;
WATER_HEATER_MODE_GAS = 6;
}
message ListEntitiesWaterHeaterResponse {
option (id) = 132;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_WATER_HEATER";
string object_id = 1;
fixed32 key = 2;
string name = 3;
string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON"];
bool disabled_by_default = 5;
EntityCategory entity_category = 6;
uint32 device_id = 7 [(field_ifdef) = "USE_DEVICES"];
float min_temperature = 8;
float max_temperature = 9;
float target_temperature_step = 10;
repeated WaterHeaterMode supported_modes = 11 [(container_pointer_no_template) = "water_heater::WaterHeaterModeMask"];
// Bitmask of WaterHeaterFeature flags
uint32 supported_features = 12;
}
message WaterHeaterStateResponse {
option (id) = 133;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_WATER_HEATER";
option (no_delay) = true;
fixed32 key = 1;
float current_temperature = 2;
float target_temperature = 3;
WaterHeaterMode mode = 4;
uint32 device_id = 5 [(field_ifdef) = "USE_DEVICES"];
// Bitmask of current state flags (bit 0 = away, bit 1 = on)
uint32 state = 6;
float target_temperature_low = 7;
float target_temperature_high = 8;
}
// Bitmask for WaterHeaterCommandRequest.has_fields
enum WaterHeaterCommandHasField {
WATER_HEATER_COMMAND_HAS_NONE = 0;
WATER_HEATER_COMMAND_HAS_MODE = 1;
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE = 2;
WATER_HEATER_COMMAND_HAS_STATE = 4;
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8;
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH = 16;
}
message WaterHeaterCommandRequest {
option (id) = 134;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_WATER_HEATER";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
// Bitmask of which fields are set (see WaterHeaterCommandHasField)
uint32 has_fields = 2;
WaterHeaterMode mode = 3;
float target_temperature = 4;
uint32 device_id = 5 [(field_ifdef) = "USE_DEVICES"];
// State flags bitmask (bit 0 = away, bit 1 = on)
uint32 state = 6;
float target_temperature_low = 7;
float target_temperature_high = 8;
}
// ==================== NUMBER ====================
enum NumberMode {
NUMBER_MODE_AUTO = 0;

View File

@@ -13,6 +13,7 @@
#include <cinttypes>
#include <functional>
#include <limits>
#include <new>
#include <utility>
#ifdef USE_ESP8266
#include <pgmspace.h>
@@ -42,6 +43,9 @@
#ifdef USE_ZWAVE_PROXY
#include "esphome/components/zwave_proxy/zwave_proxy.h"
#endif
#ifdef USE_WATER_HEATER
#include "esphome/components/water_heater/water_heater.h"
#endif
namespace esphome::api {
@@ -93,8 +97,7 @@ static const int CAMERA_STOP_STREAM = 5000;
return;
#endif // USE_DEVICES
APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent)
: parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) {
APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent) : parent_(parent) {
#if defined(USE_API_PLAINTEXT) && defined(USE_API_NOISE)
auto &noise_ctx = parent->get_noise_ctx();
if (noise_ctx.has_psk()) {
@@ -133,6 +136,7 @@ void APIConnection::start() {
}
APIConnection::~APIConnection() {
this->destroy_active_iterator_();
#ifdef USE_BLUETOOTH_PROXY
if (bluetooth_proxy::global_bluetooth_proxy->get_api_connection() == this) {
bluetooth_proxy::global_bluetooth_proxy->unsubscribe_api_connection(this);
@@ -145,6 +149,32 @@ APIConnection::~APIConnection() {
#endif
}
void APIConnection::destroy_active_iterator_() {
switch (this->active_iterator_) {
case ActiveIterator::LIST_ENTITIES:
this->iterator_storage_.list_entities.~ListEntitiesIterator();
break;
case ActiveIterator::INITIAL_STATE:
this->iterator_storage_.initial_state.~InitialStateIterator();
break;
case ActiveIterator::NONE:
break;
}
this->active_iterator_ = ActiveIterator::NONE;
}
void APIConnection::begin_iterator_(ActiveIterator type) {
this->destroy_active_iterator_();
this->active_iterator_ = type;
if (type == ActiveIterator::LIST_ENTITIES) {
new (&this->iterator_storage_.list_entities) ListEntitiesIterator(this);
this->iterator_storage_.list_entities.begin();
} else {
new (&this->iterator_storage_.initial_state) InitialStateIterator(this);
this->iterator_storage_.initial_state.begin();
}
}
void APIConnection::loop() {
if (this->flags_.next_close) {
// requested a disconnect
@@ -187,13 +217,20 @@ void APIConnection::loop() {
this->process_batch_();
}
if (!this->list_entities_iterator_.completed()) {
this->process_iterator_batch_(this->list_entities_iterator_);
} else if (!this->initial_state_iterator_.completed()) {
this->process_iterator_batch_(this->initial_state_iterator_);
// If we've completed initial states, process any remaining and clear the flag
if (this->initial_state_iterator_.completed()) {
switch (this->active_iterator_) {
case ActiveIterator::LIST_ENTITIES:
if (this->iterator_storage_.list_entities.completed()) {
this->destroy_active_iterator_();
if (this->flags_.state_subscription) {
this->begin_iterator_(ActiveIterator::INITIAL_STATE);
}
} else {
this->process_iterator_batch_(this->iterator_storage_.list_entities);
}
break;
case ActiveIterator::INITIAL_STATE:
if (this->iterator_storage_.initial_state.completed()) {
this->destroy_active_iterator_();
// Process any remaining batched messages immediately
if (!this->deferred_batch_.empty()) {
this->process_batch_();
@@ -203,7 +240,12 @@ void APIConnection::loop() {
// Release excess memory from buffers that grew during initial sync
this->deferred_batch_.release_buffer();
this->helper_->release_buffers();
} else {
this->process_iterator_batch_(this->iterator_storage_.initial_state);
}
break;
case ActiveIterator::NONE:
break;
}
if (this->flags_.sent_ping) {
@@ -228,33 +270,17 @@ void APIConnection::loop() {
}
}
#ifdef USE_CAMERA
if (this->image_reader_ && this->image_reader_->available() && this->helper_->can_write_without_blocking()) {
uint32_t to_send = std::min((size_t) MAX_BATCH_PACKET_SIZE, this->image_reader_->available());
bool done = this->image_reader_->available() == to_send;
CameraImageResponse msg;
msg.key = camera::Camera::instance()->get_object_id_hash();
msg.set_data(this->image_reader_->peek_data_buffer(), to_send);
msg.done = done;
#ifdef USE_DEVICES
msg.device_id = camera::Camera::instance()->get_device_id();
#endif
if (this->send_message_(msg, CameraImageResponse::MESSAGE_TYPE)) {
this->image_reader_->consume_data(to_send);
if (done) {
this->image_reader_->return_image();
}
}
}
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
if (state_subs_at_ >= 0) {
this->process_state_subscriptions_();
}
#endif
#ifdef USE_CAMERA
// Process camera last - state updates are higher priority
// (missing a frame is fine, missing a state update is not)
this->try_send_camera_image_();
#endif
}
bool APIConnection::send_disconnect_response(const DisconnectRequest &msg) {
@@ -447,7 +473,7 @@ void APIConnection::fan_command(const FanCommandRequest &msg) {
if (msg.has_direction)
call.set_direction(static_cast<fan::FanDirection>(msg.direction));
if (msg.has_preset_mode)
call.set_preset_mode(msg.preset_mode);
call.set_preset_mode(reinterpret_cast<const char *>(msg.preset_mode), msg.preset_mode_len);
call.perform();
}
#endif
@@ -712,11 +738,11 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) {
if (msg.has_fan_mode)
call.set_fan_mode(static_cast<climate::ClimateFanMode>(msg.fan_mode));
if (msg.has_custom_fan_mode)
call.set_fan_mode(msg.custom_fan_mode);
call.set_fan_mode(reinterpret_cast<const char *>(msg.custom_fan_mode), msg.custom_fan_mode_len);
if (msg.has_preset)
call.set_preset(static_cast<climate::ClimatePreset>(msg.preset));
if (msg.has_custom_preset)
call.set_preset(msg.custom_preset);
call.set_preset(reinterpret_cast<const char *>(msg.custom_preset), msg.custom_preset_len);
if (msg.has_swing_mode)
call.set_swing_mode(static_cast<climate::ClimateSwingMode>(msg.swing_mode));
call.perform();
@@ -1057,6 +1083,36 @@ void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) {
#endif
#ifdef USE_CAMERA
void APIConnection::try_send_camera_image_() {
if (!this->image_reader_)
return;
// Send as many chunks as possible without blocking
while (this->image_reader_->available()) {
if (!this->helper_->can_write_without_blocking())
return;
uint32_t to_send = std::min((size_t) MAX_BATCH_PACKET_SIZE, this->image_reader_->available());
bool done = this->image_reader_->available() == to_send;
CameraImageResponse msg;
msg.key = camera::Camera::instance()->get_object_id_hash();
msg.set_data(this->image_reader_->peek_data_buffer(), to_send);
msg.done = done;
#ifdef USE_DEVICES
msg.device_id = camera::Camera::instance()->get_device_id();
#endif
if (!this->send_message_(msg, CameraImageResponse::MESSAGE_TYPE)) {
return; // Send failed, try again later
}
this->image_reader_->consume_data(to_send);
if (done) {
this->image_reader_->return_image();
return;
}
}
}
void APIConnection::set_camera_state(std::shared_ptr<camera::CameraImage> image) {
if (!this->flags_.state_subscription)
return;
@@ -1064,8 +1120,11 @@ void APIConnection::set_camera_state(std::shared_ptr<camera::CameraImage> image)
return;
if (this->image_reader_->available())
return;
if (image->was_requested_by(esphome::camera::API_REQUESTER) || image->was_requested_by(esphome::camera::IDLE))
if (image->was_requested_by(esphome::camera::API_REQUESTER) || image->was_requested_by(esphome::camera::IDLE)) {
this->image_reader_->set_image(std::move(image));
// Try to send immediately to reduce latency
this->try_send_camera_image_();
}
}
uint16_t APIConnection::try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) {
@@ -1306,6 +1365,57 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe
}
#endif
#ifdef USE_WATER_HEATER
bool APIConnection::send_water_heater_state(water_heater::WaterHeater *water_heater) {
return this->send_message_smart_(water_heater, &APIConnection::try_send_water_heater_state,
WaterHeaterStateResponse::MESSAGE_TYPE, WaterHeaterStateResponse::ESTIMATED_SIZE);
}
uint16_t APIConnection::try_send_water_heater_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) {
auto *wh = static_cast<water_heater::WaterHeater *>(entity);
WaterHeaterStateResponse resp;
resp.mode = static_cast<enums::WaterHeaterMode>(wh->get_mode());
resp.current_temperature = wh->get_current_temperature();
resp.target_temperature = wh->get_target_temperature();
resp.target_temperature_low = wh->get_target_temperature_low();
resp.target_temperature_high = wh->get_target_temperature_high();
resp.state = wh->get_state();
resp.key = wh->get_object_id_hash();
return encode_message_to_buffer(resp, WaterHeaterStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
}
uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) {
auto *wh = static_cast<water_heater::WaterHeater *>(entity);
ListEntitiesWaterHeaterResponse msg;
auto traits = wh->get_traits();
msg.min_temperature = traits.get_min_temperature();
msg.max_temperature = traits.get_max_temperature();
msg.target_temperature_step = traits.get_target_temperature_step();
msg.supported_modes = &traits.get_supported_modes();
msg.supported_features = traits.get_feature_flags();
return fill_and_encode_entity_info(wh, msg, ListEntitiesWaterHeaterResponse::MESSAGE_TYPE, conn, remaining_size,
is_single);
}
void APIConnection::on_water_heater_command_request(const WaterHeaterCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(water_heater::WaterHeater, water_heater, water_heater)
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_MODE)
call.set_mode(static_cast<water_heater::WaterHeaterMode>(msg.mode));
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE)
call.set_target_temperature(msg.target_temperature);
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW)
call.set_target_temperature_low(msg.target_temperature_low);
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH)
call.set_target_temperature_high(msg.target_temperature_high);
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_STATE) {
call.set_away((msg.state & water_heater::WATER_HEATER_STATE_AWAY) != 0);
call.set_on((msg.state & water_heater::WATER_HEATER_STATE_ON) != 0);
}
call.perform();
}
#endif
#ifdef USE_EVENT
void APIConnection::send_event(event::Event *event, const char *event_type) {
this->send_message_smart_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE,
@@ -1472,7 +1582,10 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
resp.set_esphome_version(ESPHOME_VERSION_REF);
resp.set_compilation_time(App.get_compilation_time_ref());
// Stack buffer for build time string
char build_time_str[Application::BUILD_TIME_STR_SIZE];
App.get_build_time_string(build_time_str);
resp.set_compilation_time(StringRef(build_time_str));
// Manufacturer string - define once, handle ESP8266 PROGMEM separately
#if defined(USE_ESP8266) || defined(USE_ESP32)
@@ -1579,15 +1692,29 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
#ifdef USE_API_HOMEASSISTANT_STATES
void APIConnection::on_home_assistant_state_response(const HomeAssistantStateResponse &msg) {
for (auto &it : this->parent_->get_state_subs()) {
// Compare entity_id and attribute with message fields
bool entity_match = (strcmp(it.entity_id, msg.entity_id.c_str()) == 0);
bool attribute_match = (it.attribute != nullptr && strcmp(it.attribute, msg.attribute.c_str()) == 0) ||
(it.attribute == nullptr && msg.attribute.empty());
if (entity_match && attribute_match) {
it.callback(msg.state);
// Skip if entity_id is empty (invalid message)
if (msg.entity_id_len == 0) {
return;
}
for (auto &it : this->parent_->get_state_subs()) {
// Compare entity_id: check length matches and content matches
size_t entity_id_len = strlen(it.entity_id);
if (entity_id_len != msg.entity_id_len || memcmp(it.entity_id, msg.entity_id, msg.entity_id_len) != 0) {
continue;
}
// Compare attribute: either both have matching attribute, or both have none
size_t sub_attr_len = it.attribute != nullptr ? strlen(it.attribute) : 0;
if (sub_attr_len != msg.attribute_len ||
(sub_attr_len > 0 && memcmp(it.attribute, msg.attribute, sub_attr_len) != 0)) {
continue;
}
// Create temporary string for callback (callback takes const std::string &)
// Handle empty state (nullptr with len=0)
std::string state(msg.state_len > 0 ? reinterpret_cast<const char *>(msg.state) : "", msg.state_len);
it.callback(state);
}
}
#endif
@@ -1663,13 +1790,13 @@ bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryption
resp.success = false;
psk_t psk{};
if (msg.key.empty()) {
if (msg.key_len == 0) {
if (this->parent_->clear_noise_psk(true)) {
resp.success = true;
} else {
ESP_LOGW(TAG, "Failed to clear encryption key");
}
} else if (base64_decode(msg.key, psk.data(), psk.size()) != psk.size()) {
} else if (base64_decode(msg.key, msg.key_len, psk.data(), psk.size()) != psk.size()) {
ESP_LOGW(TAG, "Invalid encryption key length");
} else if (!this->parent_->save_noise_psk(psk, true)) {
ESP_LOGW(TAG, "Failed to save encryption key");

View File

@@ -176,6 +176,11 @@ class APIConnection final : public APIServerConnection {
void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override;
#endif
#ifdef USE_WATER_HEATER
bool send_water_heater_state(water_heater::WaterHeater *water_heater);
void on_water_heater_command_request(const WaterHeaterCommandRequest &msg) override;
#endif
#ifdef USE_EVENT
void send_event(event::Event *event, const char *event_type);
#endif
@@ -203,10 +208,14 @@ class APIConnection final : public APIServerConnection {
bool send_disconnect_response(const DisconnectRequest &msg) override;
bool send_ping_response(const PingRequest &msg) override;
bool send_device_info_response(const DeviceInfoRequest &msg) override;
void list_entities(const ListEntitiesRequest &msg) override { this->list_entities_iterator_.begin(); }
void list_entities(const ListEntitiesRequest &msg) override { this->begin_iterator_(ActiveIterator::LIST_ENTITIES); }
void subscribe_states(const SubscribeStatesRequest &msg) override {
this->flags_.state_subscription = true;
this->initial_state_iterator_.begin();
// Start initial state iterator only if no iterator is active
// If list_entities is running, we'll start initial_state when it completes
if (this->active_iterator_ == ActiveIterator::NONE) {
this->begin_iterator_(ActiveIterator::INITIAL_STATE);
}
}
void subscribe_logs(const SubscribeLogsRequest &msg) override {
this->flags_.log_subscription = msg.level;
@@ -287,6 +296,10 @@ class APIConnection final : public APIServerConnection {
// Helper function to handle authentication completion
void complete_authentication_();
#ifdef USE_CAMERA
void try_send_camera_image_();
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
void process_state_subscriptions_();
#endif
@@ -310,17 +323,10 @@ class APIConnection final : public APIServerConnection {
APIConnection *conn, uint32_t remaining_size, bool is_single) {
// Set common fields that are shared by all entity types
msg.key = entity->get_object_id_hash();
// Try to use static reference first to avoid allocation
StringRef static_ref = entity->get_object_id_ref_for_api_();
// Store dynamic string outside the if-else to maintain lifetime
std::string object_id;
if (!static_ref.empty()) {
msg.set_object_id(static_ref);
} else {
// Dynamic case - need to allocate
object_id = entity->get_object_id();
msg.set_object_id(StringRef(object_id));
}
// Get object_id with zero heap allocation
// Static case returns direct reference, dynamic case uses buffer
char object_id_buf[OBJECT_ID_MAX_LEN];
msg.set_object_id(entity->get_object_id_to(object_id_buf));
if (entity->has_own_name()) {
msg.set_name(entity->get_name());
@@ -456,6 +462,12 @@ class APIConnection final : public APIServerConnection {
static uint16_t try_send_alarm_control_panel_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single);
#endif
#ifdef USE_WATER_HEATER
static uint16_t try_send_water_heater_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single);
static uint16_t try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single);
#endif
#ifdef USE_EVENT
static uint16_t try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn,
uint32_t remaining_size, bool is_single);
@@ -490,10 +502,22 @@ class APIConnection final : public APIServerConnection {
std::unique_ptr<APIFrameHelper> helper_;
APIServer *parent_;
// Group 2: Larger objects (must be 4-byte aligned)
// These contain vectors/pointers internally, so putting them early ensures good alignment
InitialStateIterator initial_state_iterator_;
ListEntitiesIterator list_entities_iterator_;
// Group 2: Iterator union (saves ~16 bytes vs separate iterators)
// These iterators are never active simultaneously - list_entities runs to completion
// before initial_state begins, so we use a union with explicit construction/destruction.
enum class ActiveIterator : uint8_t { NONE, LIST_ENTITIES, INITIAL_STATE };
union IteratorUnion {
ListEntitiesIterator list_entities;
InitialStateIterator initial_state;
// Constructor/destructor do nothing - use placement new/explicit destructor
IteratorUnion() {}
~IteratorUnion() {}
} iterator_storage_;
// Helper methods for iterator lifecycle management
void destroy_active_iterator_();
void begin_iterator_(ActiveIterator type);
#ifdef USE_CAMERA
std::unique_ptr<camera::CameraImageReader> image_reader_;
#endif
@@ -608,7 +632,9 @@ class APIConnection final : public APIServerConnection {
// 2-byte types immediately after flags_ (no padding between them)
uint16_t client_api_version_major_{0};
uint16_t client_api_version_minor_{0};
// Total: 2 (flags) + 2 + 2 = 6 bytes, then 2 bytes padding to next 4-byte boundary
// 1-byte type to fill padding
ActiveIterator active_iterator_{ActiveIterator::NONE};
// Total: 2 (flags) + 2 + 2 + 1 = 7 bytes, then 1 byte padding to next 4-byte boundary
uint32_t get_batch_delay_ms_() const;
// Message will use 8 more bytes than the minimum size, and typical

View File

@@ -539,7 +539,8 @@ APIError APINoiseFrameHelper::init_handshake_() {
if (aerr != APIError::OK)
return aerr;
// set_prologue copies it into handshakestate, so we can get rid of it now
prologue_ = {};
// Use swap idiom to actually release memory (= {} only clears size, not capacity)
std::vector<uint8_t>().swap(prologue_);
err = noise_handshakestate_start(handshake_);
aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_start"), APIError::HANDSHAKESTATE_SETUP_FAILED);

View File

@@ -124,12 +124,12 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const {
#endif
#ifdef USE_DEVICES
for (const auto &it : this->devices) {
buffer.encode_message(20, it, true);
buffer.encode_message(20, it);
}
#endif
#ifdef USE_AREAS
for (const auto &it : this->areas) {
buffer.encode_message(21, it, true);
buffer.encode_message(21, it);
}
#endif
#ifdef USE_AREAS
@@ -447,9 +447,12 @@ bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
}
bool FanCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 13:
this->preset_mode = value.as_string();
case 13: {
// Use raw data directly to avoid allocation
this->preset_mode = value.data();
this->preset_mode_len = value.size();
break;
}
default:
return false;
}
@@ -855,9 +858,12 @@ void SubscribeLogsResponse::calculate_size(ProtoSize &size) const {
#ifdef USE_API_NOISE
bool NoiseEncryptionSetKeyRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 1:
this->key = value.as_string();
case 1: {
// Use raw data directly to avoid allocation
this->key = value.data();
this->key_len = value.size();
break;
}
default:
return false;
}
@@ -878,13 +884,13 @@ void HomeassistantServiceMap::calculate_size(ProtoSize &size) const {
void HomeassistantActionRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->service_ref_);
for (auto &it : this->data) {
buffer.encode_message(2, it, true);
buffer.encode_message(2, it);
}
for (auto &it : this->data_template) {
buffer.encode_message(3, it, true);
buffer.encode_message(3, it);
}
for (auto &it : this->variables) {
buffer.encode_message(4, it, true);
buffer.encode_message(4, it);
}
buffer.encode_bool(5, this->is_event);
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
@@ -960,15 +966,24 @@ void SubscribeHomeAssistantStateResponse::calculate_size(ProtoSize &size) const
}
bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 1:
this->entity_id = value.as_string();
case 1: {
// Use raw data directly to avoid allocation
this->entity_id = value.data();
this->entity_id_len = value.size();
break;
case 2:
this->state = value.as_string();
}
case 2: {
// Use raw data directly to avoid allocation
this->state = value.data();
this->state_len = value.size();
break;
case 3:
this->attribute = value.as_string();
}
case 3: {
// Use raw data directly to avoid allocation
this->attribute = value.data();
this->attribute_len = value.size();
break;
}
default:
return false;
}
@@ -1011,7 +1026,7 @@ void ListEntitiesServicesResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->name_ref_);
buffer.encode_fixed32(2, this->key);
for (auto &it : this->args) {
buffer.encode_message(3, it, true);
buffer.encode_message(3, it);
}
buffer.encode_uint32(4, static_cast<uint32_t>(this->supports_response));
}
@@ -1392,12 +1407,18 @@ bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value)
}
bool ClimateCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 17:
this->custom_fan_mode = value.as_string();
case 17: {
// Use raw data directly to avoid allocation
this->custom_fan_mode = value.data();
this->custom_fan_mode_len = value.size();
break;
case 21:
this->custom_preset = value.as_string();
}
case 21: {
// Use raw data directly to avoid allocation
this->custom_preset = value.data();
this->custom_preset_len = value.size();
break;
}
default:
return false;
}
@@ -1426,6 +1447,114 @@ bool ClimateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
return true;
}
#endif
#ifdef USE_WATER_HEATER
void ListEntitiesWaterHeaterResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->object_id_ref_);
buffer.encode_fixed32(2, this->key);
buffer.encode_string(3, this->name_ref_);
#ifdef USE_ENTITY_ICON
buffer.encode_string(4, this->icon_ref_);
#endif
buffer.encode_bool(5, this->disabled_by_default);
buffer.encode_uint32(6, static_cast<uint32_t>(this->entity_category));
#ifdef USE_DEVICES
buffer.encode_uint32(7, this->device_id);
#endif
buffer.encode_float(8, this->min_temperature);
buffer.encode_float(9, this->max_temperature);
buffer.encode_float(10, this->target_temperature_step);
for (const auto &it : *this->supported_modes) {
buffer.encode_uint32(11, static_cast<uint32_t>(it), true);
}
buffer.encode_uint32(12, this->supported_features);
}
void ListEntitiesWaterHeaterResponse::calculate_size(ProtoSize &size) const {
size.add_length(1, this->object_id_ref_.size());
size.add_fixed32(1, this->key);
size.add_length(1, this->name_ref_.size());
#ifdef USE_ENTITY_ICON
size.add_length(1, this->icon_ref_.size());
#endif
size.add_bool(1, this->disabled_by_default);
size.add_uint32(1, static_cast<uint32_t>(this->entity_category));
#ifdef USE_DEVICES
size.add_uint32(1, this->device_id);
#endif
size.add_float(1, this->min_temperature);
size.add_float(1, this->max_temperature);
size.add_float(1, this->target_temperature_step);
if (!this->supported_modes->empty()) {
for (const auto &it : *this->supported_modes) {
size.add_uint32_force(1, static_cast<uint32_t>(it));
}
}
size.add_uint32(1, this->supported_features);
}
void WaterHeaterStateResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_fixed32(1, this->key);
buffer.encode_float(2, this->current_temperature);
buffer.encode_float(3, this->target_temperature);
buffer.encode_uint32(4, static_cast<uint32_t>(this->mode));
#ifdef USE_DEVICES
buffer.encode_uint32(5, this->device_id);
#endif
buffer.encode_uint32(6, this->state);
buffer.encode_float(7, this->target_temperature_low);
buffer.encode_float(8, this->target_temperature_high);
}
void WaterHeaterStateResponse::calculate_size(ProtoSize &size) const {
size.add_fixed32(1, this->key);
size.add_float(1, this->current_temperature);
size.add_float(1, this->target_temperature);
size.add_uint32(1, static_cast<uint32_t>(this->mode));
#ifdef USE_DEVICES
size.add_uint32(1, this->device_id);
#endif
size.add_uint32(1, this->state);
size.add_float(1, this->target_temperature_low);
size.add_float(1, this->target_temperature_high);
}
bool WaterHeaterCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 2:
this->has_fields = value.as_uint32();
break;
case 3:
this->mode = static_cast<enums::WaterHeaterMode>(value.as_uint32());
break;
#ifdef USE_DEVICES
case 5:
this->device_id = value.as_uint32();
break;
#endif
case 6:
this->state = value.as_uint32();
break;
default:
return false;
}
return true;
}
bool WaterHeaterCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
switch (field_id) {
case 1:
this->key = value.as_fixed32();
break;
case 4:
this->target_temperature = value.as_float();
break;
case 7:
this->target_temperature_low = value.as_float();
break;
case 8:
this->target_temperature_high = value.as_float();
break;
default:
return false;
}
return true;
}
#endif
#ifdef USE_NUMBER
void ListEntitiesNumberResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->object_id_ref_);
@@ -1867,7 +1996,7 @@ void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_uint32(7, static_cast<uint32_t>(this->entity_category));
buffer.encode_bool(8, this->supports_pause);
for (auto &it : this->supported_formats) {
buffer.encode_message(9, it, true);
buffer.encode_message(9, it);
}
#ifdef USE_DEVICES
buffer.encode_uint32(10, this->device_id);
@@ -1987,7 +2116,7 @@ void BluetoothLERawAdvertisement::calculate_size(ProtoSize &size) const {
}
void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer buffer) const {
for (uint16_t i = 0; i < this->advertisements_len; i++) {
buffer.encode_message(1, this->advertisements[i], true);
buffer.encode_message(1, this->advertisements[i]);
}
}
void BluetoothLERawAdvertisementsResponse::calculate_size(ProtoSize &size) const {
@@ -2060,7 +2189,7 @@ void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer buffer) const {
buffer.encode_uint32(2, this->handle);
buffer.encode_uint32(3, this->properties);
for (auto &it : this->descriptors) {
buffer.encode_message(4, it, true);
buffer.encode_message(4, it);
}
buffer.encode_uint32(5, this->short_uuid);
}
@@ -2081,7 +2210,7 @@ void BluetoothGATTService::encode(ProtoWriteBuffer buffer) const {
}
buffer.encode_uint32(2, this->handle);
for (auto &it : this->characteristics) {
buffer.encode_message(3, it, true);
buffer.encode_message(3, it);
}
buffer.encode_uint32(4, this->short_uuid);
}
@@ -2097,7 +2226,7 @@ void BluetoothGATTService::calculate_size(ProtoSize &size) const {
void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_uint64(1, this->address);
for (auto &it : this->services) {
buffer.encode_message(2, it, true);
buffer.encode_message(2, it);
}
}
void BluetoothGATTGetServicesResponse::calculate_size(ProtoSize &size) const {
@@ -2557,7 +2686,7 @@ bool VoiceAssistantConfigurationRequest::decode_length(uint32_t field_id, ProtoL
}
void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer buffer) const {
for (auto &it : this->available_wake_words) {
buffer.encode_message(1, it, true);
buffer.encode_message(1, it);
}
for (const auto &it : *this->active_wake_words) {
buffer.encode_string(2, it, true);

View File

@@ -129,6 +129,25 @@ enum ClimatePreset : uint32_t {
CLIMATE_PRESET_ACTIVITY = 7,
};
#endif
#ifdef USE_WATER_HEATER
enum WaterHeaterMode : uint32_t {
WATER_HEATER_MODE_OFF = 0,
WATER_HEATER_MODE_ECO = 1,
WATER_HEATER_MODE_ELECTRIC = 2,
WATER_HEATER_MODE_PERFORMANCE = 3,
WATER_HEATER_MODE_HIGH_DEMAND = 4,
WATER_HEATER_MODE_HEAT_PUMP = 5,
WATER_HEATER_MODE_GAS = 6,
};
#endif
enum WaterHeaterCommandHasField : uint32_t {
WATER_HEATER_COMMAND_HAS_NONE = 0,
WATER_HEATER_COMMAND_HAS_MODE = 1,
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE = 2,
WATER_HEATER_COMMAND_HAS_STATE = 4,
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8,
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH = 16,
};
#ifdef USE_NUMBER
enum NumberMode : uint32_t {
NUMBER_MODE_AUTO = 0,
@@ -765,7 +784,7 @@ class FanStateResponse final : public StateResponseProtoMessage {
class FanCommandRequest final : public CommandProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 31;
static constexpr uint8_t ESTIMATED_SIZE = 38;
static constexpr uint8_t ESTIMATED_SIZE = 48;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "fan_command_request"; }
#endif
@@ -778,7 +797,8 @@ class FanCommandRequest final : public CommandProtoMessage {
bool has_speed_level{false};
int32_t speed_level{0};
bool has_preset_mode{false};
std::string preset_mode{};
const uint8_t *preset_mode{nullptr};
uint16_t preset_mode_len{0};
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1053,11 +1073,12 @@ class SubscribeLogsResponse final : public ProtoMessage {
class NoiseEncryptionSetKeyRequest final : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 124;
static constexpr uint8_t ESTIMATED_SIZE = 9;
static constexpr uint8_t ESTIMATED_SIZE = 19;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "noise_encryption_set_key_request"; }
#endif
std::string key{};
const uint8_t *key{nullptr};
uint16_t key_len{0};
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1201,13 +1222,16 @@ class SubscribeHomeAssistantStateResponse final : public ProtoMessage {
class HomeAssistantStateResponse final : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 40;
static constexpr uint8_t ESTIMATED_SIZE = 27;
static constexpr uint8_t ESTIMATED_SIZE = 57;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "home_assistant_state_response"; }
#endif
std::string entity_id{};
std::string state{};
std::string attribute{};
const uint8_t *entity_id{nullptr};
uint16_t entity_id_len{0};
const uint8_t *state{nullptr};
uint16_t state_len{0};
const uint8_t *attribute{nullptr};
uint16_t attribute_len{0};
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1475,7 +1499,7 @@ class ClimateStateResponse final : public StateResponseProtoMessage {
class ClimateCommandRequest final : public CommandProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 48;
static constexpr uint8_t ESTIMATED_SIZE = 84;
static constexpr uint8_t ESTIMATED_SIZE = 104;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "climate_command_request"; }
#endif
@@ -1492,11 +1516,13 @@ class ClimateCommandRequest final : public CommandProtoMessage {
bool has_swing_mode{false};
enums::ClimateSwingMode swing_mode{};
bool has_custom_fan_mode{false};
std::string custom_fan_mode{};
const uint8_t *custom_fan_mode{nullptr};
uint16_t custom_fan_mode_len{0};
bool has_preset{false};
enums::ClimatePreset preset{};
bool has_custom_preset{false};
std::string custom_preset{};
const uint8_t *custom_preset{nullptr};
uint16_t custom_preset_len{0};
bool has_target_humidity{false};
float target_humidity{0.0f};
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -1509,6 +1535,70 @@ class ClimateCommandRequest final : public CommandProtoMessage {
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
#endif
#ifdef USE_WATER_HEATER
class ListEntitiesWaterHeaterResponse final : public InfoResponseProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 132;
static constexpr uint8_t ESTIMATED_SIZE = 63;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_water_heater_response"; }
#endif
float min_temperature{0.0f};
float max_temperature{0.0f};
float target_temperature_step{0.0f};
const water_heater::WaterHeaterModeMask *supported_modes{};
uint32_t supported_features{0};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
};
class WaterHeaterStateResponse final : public StateResponseProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 133;
static constexpr uint8_t ESTIMATED_SIZE = 35;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "water_heater_state_response"; }
#endif
float current_temperature{0.0f};
float target_temperature{0.0f};
enums::WaterHeaterMode mode{};
uint32_t state{0};
float target_temperature_low{0.0f};
float target_temperature_high{0.0f};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
};
class WaterHeaterCommandRequest final : public CommandProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 134;
static constexpr uint8_t ESTIMATED_SIZE = 34;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "water_heater_command_request"; }
#endif
uint32_t has_fields{0};
enums::WaterHeaterMode mode{};
float target_temperature{0.0f};
uint32_t state{0};
float target_temperature_low{0.0f};
float target_temperature_high{0.0f};
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
#endif
#ifdef USE_NUMBER
class ListEntitiesNumberResponse final : public InfoResponseProtoMessage {
public:

View File

@@ -348,6 +348,47 @@ template<> const char *proto_enum_to_string<enums::ClimatePreset>(enums::Climate
}
}
#endif
#ifdef USE_WATER_HEATER
template<> const char *proto_enum_to_string<enums::WaterHeaterMode>(enums::WaterHeaterMode value) {
switch (value) {
case enums::WATER_HEATER_MODE_OFF:
return "WATER_HEATER_MODE_OFF";
case enums::WATER_HEATER_MODE_ECO:
return "WATER_HEATER_MODE_ECO";
case enums::WATER_HEATER_MODE_ELECTRIC:
return "WATER_HEATER_MODE_ELECTRIC";
case enums::WATER_HEATER_MODE_PERFORMANCE:
return "WATER_HEATER_MODE_PERFORMANCE";
case enums::WATER_HEATER_MODE_HIGH_DEMAND:
return "WATER_HEATER_MODE_HIGH_DEMAND";
case enums::WATER_HEATER_MODE_HEAT_PUMP:
return "WATER_HEATER_MODE_HEAT_PUMP";
case enums::WATER_HEATER_MODE_GAS:
return "WATER_HEATER_MODE_GAS";
default:
return "UNKNOWN";
}
}
#endif
template<>
const char *proto_enum_to_string<enums::WaterHeaterCommandHasField>(enums::WaterHeaterCommandHasField value) {
switch (value) {
case enums::WATER_HEATER_COMMAND_HAS_NONE:
return "WATER_HEATER_COMMAND_HAS_NONE";
case enums::WATER_HEATER_COMMAND_HAS_MODE:
return "WATER_HEATER_COMMAND_HAS_MODE";
case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE:
return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE";
case enums::WATER_HEATER_COMMAND_HAS_STATE:
return "WATER_HEATER_COMMAND_HAS_STATE";
case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW:
return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW";
case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH:
return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH";
default:
return "UNKNOWN";
}
}
#ifdef USE_NUMBER
template<> const char *proto_enum_to_string<enums::NumberMode>(enums::NumberMode value) {
switch (value) {
@@ -923,7 +964,9 @@ void FanCommandRequest::dump_to(std::string &out) const {
dump_field(out, "has_speed_level", this->has_speed_level);
dump_field(out, "speed_level", this->speed_level);
dump_field(out, "has_preset_mode", this->has_preset_mode);
dump_field(out, "preset_mode", this->preset_mode);
out.append(" preset_mode: ");
out.append(format_hex_pretty(this->preset_mode, this->preset_mode_len));
out.append("\n");
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
#endif
@@ -1113,7 +1156,7 @@ void SubscribeLogsResponse::dump_to(std::string &out) const {
void NoiseEncryptionSetKeyRequest::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "NoiseEncryptionSetKeyRequest");
out.append(" key: ");
out.append(format_hex_pretty(reinterpret_cast<const uint8_t *>(this->key.data()), this->key.size()));
out.append(format_hex_pretty(this->key, this->key_len));
out.append("\n");
}
void NoiseEncryptionSetKeyResponse::dump_to(std::string &out) const { dump_field(out, "success", this->success); }
@@ -1182,9 +1225,15 @@ void SubscribeHomeAssistantStateResponse::dump_to(std::string &out) const {
}
void HomeAssistantStateResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "HomeAssistantStateResponse");
dump_field(out, "entity_id", this->entity_id);
dump_field(out, "state", this->state);
dump_field(out, "attribute", this->attribute);
out.append(" entity_id: ");
out.append(format_hex_pretty(this->entity_id, this->entity_id_len));
out.append("\n");
out.append(" state: ");
out.append(format_hex_pretty(this->state, this->state_len));
out.append("\n");
out.append(" attribute: ");
out.append(format_hex_pretty(this->attribute, this->attribute_len));
out.append("\n");
}
#endif
void GetTimeRequest::dump_to(std::string &out) const { out.append("GetTimeRequest {}"); }
@@ -1374,11 +1423,15 @@ void ClimateCommandRequest::dump_to(std::string &out) const {
dump_field(out, "has_swing_mode", this->has_swing_mode);
dump_field(out, "swing_mode", static_cast<enums::ClimateSwingMode>(this->swing_mode));
dump_field(out, "has_custom_fan_mode", this->has_custom_fan_mode);
dump_field(out, "custom_fan_mode", this->custom_fan_mode);
out.append(" custom_fan_mode: ");
out.append(format_hex_pretty(this->custom_fan_mode, this->custom_fan_mode_len));
out.append("\n");
dump_field(out, "has_preset", this->has_preset);
dump_field(out, "preset", static_cast<enums::ClimatePreset>(this->preset));
dump_field(out, "has_custom_preset", this->has_custom_preset);
dump_field(out, "custom_preset", this->custom_preset);
out.append(" custom_preset: ");
out.append(format_hex_pretty(this->custom_preset, this->custom_preset_len));
out.append("\n");
dump_field(out, "has_target_humidity", this->has_target_humidity);
dump_field(out, "target_humidity", this->target_humidity);
#ifdef USE_DEVICES
@@ -1386,6 +1439,55 @@ void ClimateCommandRequest::dump_to(std::string &out) const {
#endif
}
#endif
#ifdef USE_WATER_HEATER
void ListEntitiesWaterHeaterResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ListEntitiesWaterHeaterResponse");
dump_field(out, "object_id", this->object_id_ref_);
dump_field(out, "key", this->key);
dump_field(out, "name", this->name_ref_);
#ifdef USE_ENTITY_ICON
dump_field(out, "icon", this->icon_ref_);
#endif
dump_field(out, "disabled_by_default", this->disabled_by_default);
dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category));
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
#endif
dump_field(out, "min_temperature", this->min_temperature);
dump_field(out, "max_temperature", this->max_temperature);
dump_field(out, "target_temperature_step", this->target_temperature_step);
for (const auto &it : *this->supported_modes) {
dump_field(out, "supported_modes", static_cast<enums::WaterHeaterMode>(it), 4);
}
dump_field(out, "supported_features", this->supported_features);
}
void WaterHeaterStateResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "WaterHeaterStateResponse");
dump_field(out, "key", this->key);
dump_field(out, "current_temperature", this->current_temperature);
dump_field(out, "target_temperature", this->target_temperature);
dump_field(out, "mode", static_cast<enums::WaterHeaterMode>(this->mode));
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
#endif
dump_field(out, "state", this->state);
dump_field(out, "target_temperature_low", this->target_temperature_low);
dump_field(out, "target_temperature_high", this->target_temperature_high);
}
void WaterHeaterCommandRequest::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "WaterHeaterCommandRequest");
dump_field(out, "key", this->key);
dump_field(out, "has_fields", this->has_fields);
dump_field(out, "mode", static_cast<enums::WaterHeaterMode>(this->mode));
dump_field(out, "target_temperature", this->target_temperature);
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
#endif
dump_field(out, "state", this->state);
dump_field(out, "target_temperature_low", this->target_temperature_low);
dump_field(out, "target_temperature_high", this->target_temperature_high);
}
#endif
#ifdef USE_NUMBER
void ListEntitiesNumberResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ListEntitiesNumberResponse");

View File

@@ -10,6 +10,10 @@
#include "esphome/components/climate/climate_traits.h"
#endif
#ifdef USE_WATER_HEATER
#include "esphome/components/water_heater/water_heater.h"
#endif
#ifdef USE_LIGHT
#include "esphome/components/light/light_traits.h"
#endif

View File

@@ -621,6 +621,17 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
this->on_homeassistant_action_response(msg);
break;
}
#endif
#ifdef USE_WATER_HEATER
case WaterHeaterCommandRequest::MESSAGE_TYPE: {
WaterHeaterCommandRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_water_heater_command_request: %s", msg.dump().c_str());
#endif
this->on_water_heater_command_request(msg);
break;
}
#endif
default:
break;

View File

@@ -91,6 +91,10 @@ class APIServerConnectionBase : public ProtoService {
virtual void on_climate_command_request(const ClimateCommandRequest &value){};
#endif
#ifdef USE_WATER_HEATER
virtual void on_water_heater_command_request(const WaterHeaterCommandRequest &value){};
#endif
#ifdef USE_NUMBER
virtual void on_number_command_request(const NumberCommandRequest &value){};
#endif

View File

@@ -335,6 +335,10 @@ API_DISPATCH_UPDATE(valve::Valve, valve)
API_DISPATCH_UPDATE(media_player::MediaPlayer, media_player)
#endif
#ifdef USE_WATER_HEATER
API_DISPATCH_UPDATE(water_heater::WaterHeater, water_heater)
#endif
#ifdef USE_EVENT
// Event is a special case - unlike other entities with simple state fields,
// events store their state in a member accessed via obj->get_last_event_type()

View File

@@ -133,6 +133,9 @@ class APIServer : public Component,
#ifdef USE_MEDIA_PLAYER
void on_media_player_update(media_player::MediaPlayer *obj) override;
#endif
#ifdef USE_WATER_HEATER
void on_water_heater_update(water_heater::WaterHeater *obj) override;
#endif
#ifdef USE_API_HOMEASSISTANT_SERVICES
void send_homeassistant_action(const HomeassistantActionRequest &call);

View File

@@ -73,6 +73,9 @@ LIST_ENTITIES_HANDLER(media_player, media_player::MediaPlayer, ListEntitiesMedia
LIST_ENTITIES_HANDLER(alarm_control_panel, alarm_control_panel::AlarmControlPanel,
ListEntitiesAlarmControlPanelResponse)
#endif
#ifdef USE_WATER_HEATER
LIST_ENTITIES_HANDLER(water_heater, water_heater::WaterHeater, ListEntitiesWaterHeaterResponse)
#endif
#ifdef USE_EVENT
LIST_ENTITIES_HANDLER(event, event::Event, ListEntitiesEventResponse)
#endif

View File

@@ -82,6 +82,9 @@ class ListEntitiesIterator : public ComponentIterator {
#ifdef USE_ALARM_CONTROL_PANEL
bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *entity) override;
#endif
#ifdef USE_WATER_HEATER
bool on_water_heater(water_heater::WaterHeater *entity) override;
#endif
#ifdef USE_EVENT
bool on_event(event::Event *entity) override;
#endif

View File

@@ -334,7 +334,7 @@ class ProtoWriteBuffer {
void encode_sint64(uint32_t field_id, int64_t value, bool force = false) {
this->encode_uint64(field_id, encode_zigzag64(value), force);
}
void encode_message(uint32_t field_id, const ProtoMessage &value, bool force = false);
void encode_message(uint32_t field_id, const ProtoMessage &value);
std::vector<uint8_t> *get_buffer() const { return buffer_; }
protected:
@@ -795,7 +795,7 @@ class ProtoSize {
};
// Implementation of encode_message - must be after ProtoMessage is defined
inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value, bool force) {
inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value) {
this->encode_field_raw(field_id, 2); // type 2: Length-delimited message
// Calculate the message size first

View File

@@ -60,6 +60,9 @@ INITIAL_STATE_HANDLER(media_player, media_player::MediaPlayer)
#ifdef USE_ALARM_CONTROL_PANEL
INITIAL_STATE_HANDLER(alarm_control_panel, alarm_control_panel::AlarmControlPanel)
#endif
#ifdef USE_WATER_HEATER
INITIAL_STATE_HANDLER(water_heater, water_heater::WaterHeater)
#endif
#ifdef USE_UPDATE
INITIAL_STATE_HANDLER(update, update::UpdateEntity)
#endif

View File

@@ -76,6 +76,9 @@ class InitialStateIterator : public ComponentIterator {
#ifdef USE_ALARM_CONTROL_PANEL
bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *entity) override;
#endif
#ifdef USE_WATER_HEATER
bool on_water_heater(water_heater::WaterHeater *entity) override;
#endif
#ifdef USE_EVENT
bool on_event(event::Event *event) override { return true; };
#endif

View File

@@ -0,0 +1,14 @@
import esphome.codegen as cg
CODEOWNERS = ["@jasstrong", "@ximex", "@freekode"]
aqi_ns = cg.esphome_ns.namespace("aqi")
AQICalculatorType = aqi_ns.enum("AQICalculatorType")
CONF_AQI = "aqi"
CONF_CALCULATION_TYPE = "calculation_type"
AQI_CALCULATION_TYPE = {
"CAQI": AQICalculatorType.CAQI_TYPE,
"AQI": AQICalculatorType.AQI_TYPE,
}

View File

@@ -2,13 +2,11 @@
#include <cstdint>
namespace esphome {
namespace hm3301 {
namespace esphome::aqi {
class AbstractAQICalculator {
public:
virtual uint16_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) = 0;
};
} // namespace hm3301
} // namespace esphome
} // namespace esphome::aqi

View File

@@ -1,10 +1,11 @@
#pragma once
#include <climits>
#include "abstract_aqi_calculator.h"
// https://document.airnow.gov/technical-assistance-document-for-the-reporting-of-daily-air-quailty.pdf
namespace esphome {
namespace hm3301 {
namespace esphome::aqi {
class AQICalculator : public AbstractAQICalculator {
public:
@@ -28,6 +29,9 @@ class AQICalculator : public AbstractAQICalculator {
int calculate_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) {
int grid_index = get_grid_index_(value, array);
if (grid_index == -1) {
return -1;
}
int aqi_lo = index_grid_[grid_index][0];
int aqi_hi = index_grid_[grid_index][1];
int conc_lo = array[grid_index][0];
@@ -46,5 +50,4 @@ class AQICalculator : public AbstractAQICalculator {
}
};
} // namespace hm3301
} // namespace esphome
} // namespace esphome::aqi

View File

@@ -3,8 +3,7 @@
#include "caqi_calculator.h"
#include "aqi_calculator.h"
namespace esphome {
namespace hm3301 {
namespace esphome::aqi {
enum AQICalculatorType { CAQI_TYPE = 0, AQI_TYPE = 1 };
@@ -12,18 +11,17 @@ class AQICalculatorFactory {
public:
AbstractAQICalculator *get_calculator(AQICalculatorType type) {
if (type == 0) {
return caqi_calculator_;
return &this->caqi_calculator_;
} else if (type == 1) {
return aqi_calculator_;
return &this->aqi_calculator_;
}
return nullptr;
}
protected:
CAQICalculator *caqi_calculator_ = new CAQICalculator();
AQICalculator *aqi_calculator_ = new AQICalculator();
CAQICalculator caqi_calculator_;
AQICalculator aqi_calculator_;
};
} // namespace hm3301
} // namespace esphome
} // namespace esphome::aqi

View File

@@ -3,8 +3,7 @@
#include "esphome/core/log.h"
#include "abstract_aqi_calculator.h"
namespace esphome {
namespace hm3301 {
namespace esphome::aqi {
class CAQICalculator : public AbstractAQICalculator {
public:
@@ -48,5 +47,4 @@ class CAQICalculator : public AbstractAQICalculator {
}
};
} // namespace hm3301
} // namespace esphome
} // namespace esphome::aqi

View File

@@ -1,6 +1,6 @@
#include "audio_reader.h"
#ifdef USE_ESP_IDF
#ifdef USE_ESP32
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"

View File

@@ -1,6 +1,6 @@
#pragma once
#ifdef USE_ESP_IDF
#ifdef USE_ESP32
#include "audio.h"
#include "audio_transfer_buffer.h"

View File

@@ -1,12 +1,7 @@
import esphome.codegen as cg
from esphome.components import ble_client, climate
from esphome.components import climate
import esphome.config_validation as cv
from esphome.const import (
CONF_HEAT_MODE,
CONF_RECEIVE_TIMEOUT,
CONF_TEMPERATURE_SOURCE,
CONF_TIME_ID,
)
from esphome.const import CONF_HEAT_MODE, CONF_TEMPERATURE_SOURCE
from .. import BEDJET_CLIENT_SCHEMA, bedjet_ns, register_bedjet_child
@@ -38,22 +33,6 @@ CONFIG_SCHEMA = (
}
)
.extend(cv.polling_component_schema("60s"))
.extend(
# TODO: remove compat layer.
{
cv.Optional(ble_client.CONF_BLE_CLIENT_ID): cv.invalid(
"The 'ble_client_id' option has been removed. Please migrate "
"to the new `bedjet_id` option in the `bedjet` component.\n"
"See https://esphome.io/components/climate/bedjet/"
),
cv.Optional(CONF_TIME_ID): cv.invalid(
"The 'time_id' option has been moved to the `bedjet` component."
),
cv.Optional(CONF_RECEIVE_TIMEOUT): cv.invalid(
"The 'receive_timeout' option has been moved to the `bedjet` component."
),
}
)
.extend(BEDJET_CLIENT_SCHEMA)
)

View File

@@ -20,16 +20,6 @@ CONFIG_SCHEMA = (
device_class=DEVICE_CLASS_ILLUMINANCE,
state_class=STATE_CLASS_MEASUREMENT,
)
.extend(
{
cv.Optional("resolution"): cv.invalid(
"The 'resolution' option has been removed. The optimal value is now dynamically calculated."
),
cv.Optional("measurement_duration"): cv.invalid(
"The 'measurement_duration' option has been removed. The optimal value is now dynamically calculated."
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x23))
)

View File

@@ -50,6 +50,7 @@ TYPES = [
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(cg.Component),
cv.GenerateID(CONF_BME68X_BSEC2_ID): cv.use_id(BME68xBSEC2Component),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,

View File

@@ -11,6 +11,7 @@ CODEOWNERS = ["@neffs", "@kbx81"]
AUTO_LOAD = ["bme68x_bsec2"]
DEPENDENCIES = ["i2c"]
MULTI_CONF = True
bme68x_bsec2_i2c_ns = cg.esphome_ns.namespace("bme68x_bsec2_i2c")
BME68xBSEC2I2CComponent = bme68x_bsec2_i2c_ns.class_(

View File

@@ -41,7 +41,7 @@ class Button : public EntityBase, public EntityBase_DeviceClass {
*/
virtual void press_action() = 0;
CallbackManager<void()> press_callback_{};
LazyCallbackManager<void()> press_callback_{};
};
} // namespace esphome::button

View File

@@ -25,7 +25,7 @@ _LOGGER = logging.getLogger(__name__)
def AUTO_LOAD() -> list[str]:
auto_load = ["web_server_base", "ota.web_server"]
if CORE.using_esp_idf:
if CORE.is_esp32:
auto_load.append("socket")
return auto_load
@@ -97,10 +97,6 @@ async def to_code(config):
cg.add_define("USE_CAPTIVE_PORTAL")
if CORE.using_arduino:
if CORE.is_esp32:
cg.add_library("ESP32 Async UDP", None)
cg.add_library("DNSServer", None)
cg.add_library("WiFi", None)
if CORE.is_esp8266:
cg.add_library("DNSServer", None)
if CORE.is_libretiny:
@@ -110,6 +106,9 @@ async def to_code(config):
# Only compile the ESP-IDF DNS server when using ESP-IDF framework
FILTER_SOURCE_FILES = filter_source_files_from_platform(
{
"dns_server_esp32_idf.cpp": {PlatformFramework.ESP32_IDF},
"dns_server_esp32_idf.cpp": {
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF,
},
}
)

View File

@@ -69,12 +69,11 @@ void CaptivePortal::start() {
network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip();
#ifdef USE_ESP_IDF
#if defined(USE_ESP32)
// Create DNS server instance for ESP-IDF
this->dns_server_ = make_unique<DNSServer>();
this->dns_server_->start(ip);
#endif
#ifdef USE_ARDUINO
#elif defined(USE_ARDUINO)
this->dns_server_ = make_unique<DNSServer>();
this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError);
this->dns_server_->start(53, ESPHOME_F("*"), ip);

View File

@@ -2,11 +2,10 @@
#include "esphome/core/defines.h"
#ifdef USE_CAPTIVE_PORTAL
#include <memory>
#ifdef USE_ARDUINO
#include <DNSServer.h>
#endif
#ifdef USE_ESP_IDF
#if defined(USE_ESP32)
#include "dns_server_esp32_idf.h"
#elif defined(USE_ARDUINO)
#include <DNSServer.h>
#endif
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
@@ -23,15 +22,14 @@ class CaptivePortal : public AsyncWebHandler, public Component {
void setup() override;
void dump_config() override;
void loop() override {
#ifdef USE_ARDUINO
if (this->dns_server_ != nullptr) {
this->dns_server_->processNextRequest();
}
#endif
#ifdef USE_ESP_IDF
#if defined(USE_ESP32)
if (this->dns_server_ != nullptr) {
this->dns_server_->process_next_request();
}
#elif defined(USE_ARDUINO)
if (this->dns_server_ != nullptr) {
this->dns_server_->processNextRequest();
}
#endif
}
float get_setup_priority() const override;
@@ -64,7 +62,7 @@ class CaptivePortal : public AsyncWebHandler, public Component {
web_server_base::WebServerBase *base_;
bool initialized_{false};
bool active_{false};
#if defined(USE_ARDUINO) || defined(USE_ESP_IDF)
#if defined(USE_ARDUINO) || defined(USE_ESP32)
std::unique_ptr<DNSServer> dns_server_{nullptr};
#endif
};

View File

@@ -1,5 +1,5 @@
#include "dns_server_esp32_idf.h"
#ifdef USE_ESP_IDF
#ifdef USE_ESP32
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
@@ -202,4 +202,4 @@ void DNSServer::process_next_request() {
} // namespace esphome::captive_portal
#endif // USE_ESP_IDF
#endif // USE_ESP32

View File

@@ -1,5 +1,5 @@
#pragma once
#ifdef USE_ESP_IDF
#ifdef USE_ESP32
#include <memory>
#include "esphome/core/helpers.h"
@@ -24,4 +24,4 @@ class DNSServer {
} // namespace esphome::captive_portal
#endif // USE_ESP_IDF
#endif // USE_ESP32

View File

@@ -160,41 +160,63 @@ HYST_LEVEL = {
"High": HystLevel.HYST_LEVEL_HIGH,
}
# Config key -> Validator mapping
# Optional settings to generate setter calls for
CONFIG_MAP = {
CONF_OUTPUT_POWER: cv.float_range(min=-30.0, max=11.0),
CONF_RX_ATTENUATION: cv.enum(RX_ATTENUATION, upper=False),
CONF_DC_BLOCKING_FILTER: cv.boolean,
CONF_FREQUENCY: cv.All(cv.frequency, cv.float_range(min=300000000, max=928000000)),
CONF_IF_FREQUENCY: cv.All(cv.frequency, cv.float_range(min=25000, max=788000)),
CONF_FILTER_BANDWIDTH: cv.All(cv.frequency, cv.float_range(min=58000, max=812000)),
CONF_CHANNEL: cv.uint8_t,
CONF_CHANNEL_SPACING: cv.All(cv.frequency, cv.float_range(min=25000, max=405000)),
CONF_FSK_DEVIATION: cv.All(cv.frequency, cv.float_range(min=1500, max=381000)),
CONF_MSK_DEVIATION: cv.int_range(min=1, max=8),
CONF_SYMBOL_RATE: cv.float_range(min=600, max=500000),
CONF_SYNC_MODE: cv.enum(SYNC_MODE, upper=False),
CONF_CARRIER_SENSE_ABOVE_THRESHOLD: cv.boolean,
CONF_MODULATION_TYPE: cv.enum(MODULATION, upper=False),
CONF_MANCHESTER: cv.boolean,
CONF_NUM_PREAMBLE: cv.int_range(min=0, max=7),
CONF_SYNC1: cv.hex_uint8_t,
CONF_SYNC0: cv.hex_uint8_t,
CONF_MAGN_TARGET: cv.enum(MAGN_TARGET, upper=False),
CONF_MAX_LNA_GAIN: cv.enum(MAX_LNA_GAIN, upper=False),
CONF_MAX_DVGA_GAIN: cv.enum(MAX_DVGA_GAIN, upper=False),
CONF_CARRIER_SENSE_ABS_THR: cv.int_range(min=-8, max=7),
CONF_CARRIER_SENSE_REL_THR: cv.enum(CARRIER_SENSE_REL_THR, upper=False),
CONF_LNA_PRIORITY: cv.boolean,
CONF_FILTER_LENGTH_FSK_MSK: cv.enum(FILTER_LENGTH_FSK_MSK, upper=False),
CONF_FILTER_LENGTH_ASK_OOK: cv.enum(FILTER_LENGTH_ASK_OOK, upper=False),
CONF_FREEZE: cv.enum(FREEZE, upper=False),
CONF_WAIT_TIME: cv.enum(WAIT_TIME, upper=False),
CONF_HYST_LEVEL: cv.enum(HYST_LEVEL, upper=False),
CONF_PACKET_MODE: cv.boolean,
CONF_PACKET_LENGTH: cv.uint8_t,
CONF_CRC_ENABLE: cv.boolean,
CONF_WHITENING: cv.boolean,
cv.Optional(CONF_OUTPUT_POWER, default=10): cv.float_range(min=-30.0, max=11.0),
cv.Optional(CONF_RX_ATTENUATION, default="0dB"): cv.enum(
RX_ATTENUATION, upper=False
),
cv.Optional(CONF_DC_BLOCKING_FILTER, default=True): cv.boolean,
cv.Optional(CONF_FREQUENCY, default="433.92MHz"): cv.All(
cv.frequency, cv.float_range(min=300.0e6, max=928.0e6)
),
cv.Optional(CONF_IF_FREQUENCY, default="153kHz"): cv.All(
cv.frequency, cv.float_range(min=25000, max=788000)
),
cv.Optional(CONF_FILTER_BANDWIDTH, default="203kHz"): cv.All(
cv.frequency, cv.float_range(min=58000, max=812000)
),
cv.Optional(CONF_CHANNEL, default=0): cv.uint8_t,
cv.Optional(CONF_CHANNEL_SPACING, default="200kHz"): cv.All(
cv.frequency, cv.float_range(min=25000, max=405000)
),
cv.Optional(CONF_FSK_DEVIATION): cv.All(
cv.frequency, cv.float_range(min=1500, max=381000)
),
cv.Optional(CONF_MSK_DEVIATION): cv.int_range(min=1, max=8),
cv.Optional(CONF_SYMBOL_RATE, default=5000): cv.float_range(min=600, max=500000),
cv.Optional(CONF_SYNC_MODE, default="16/16"): cv.enum(SYNC_MODE, upper=False),
cv.Optional(CONF_CARRIER_SENSE_ABOVE_THRESHOLD, default=False): cv.boolean,
cv.Optional(CONF_MODULATION_TYPE, default="ASK/OOK"): cv.enum(
MODULATION, upper=False
),
cv.Optional(CONF_MANCHESTER, default=False): cv.boolean,
cv.Optional(CONF_NUM_PREAMBLE, default=2): cv.int_range(min=0, max=7),
cv.Optional(CONF_SYNC1, default=0xD3): cv.hex_uint8_t,
cv.Optional(CONF_SYNC0, default=0x91): cv.hex_uint8_t,
cv.Optional(CONF_MAGN_TARGET, default="42dB"): cv.enum(MAGN_TARGET, upper=False),
cv.Optional(CONF_MAX_LNA_GAIN, default="Default"): cv.enum(
MAX_LNA_GAIN, upper=False
),
cv.Optional(CONF_MAX_DVGA_GAIN, default="-3"): cv.enum(MAX_DVGA_GAIN, upper=False),
cv.Optional(CONF_CARRIER_SENSE_ABS_THR): cv.int_range(min=-8, max=7),
cv.Optional(CONF_CARRIER_SENSE_REL_THR): cv.enum(
CARRIER_SENSE_REL_THR, upper=False
),
cv.Optional(CONF_LNA_PRIORITY, default=False): cv.boolean,
cv.Optional(CONF_FILTER_LENGTH_FSK_MSK): cv.enum(
FILTER_LENGTH_FSK_MSK, upper=False
),
cv.Optional(CONF_FILTER_LENGTH_ASK_OOK): cv.enum(
FILTER_LENGTH_ASK_OOK, upper=False
),
cv.Optional(CONF_FREEZE): cv.enum(FREEZE, upper=False),
cv.Optional(CONF_WAIT_TIME, default="32"): cv.enum(WAIT_TIME, upper=False),
cv.Optional(CONF_HYST_LEVEL): cv.enum(HYST_LEVEL, upper=False),
cv.Optional(CONF_PACKET_MODE, default=False): cv.boolean,
cv.Optional(CONF_PACKET_LENGTH): cv.uint8_t,
cv.Optional(CONF_CRC_ENABLE, default=False): cv.boolean,
cv.Optional(CONF_WHITENING, default=False): cv.boolean,
}
@@ -217,7 +239,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_ON_PACKET): automation.validate_automation(single=True),
}
)
.extend({cv.Optional(key): validator for key, validator in CONFIG_MAP.items()})
.extend(CONFIG_MAP)
.extend(cv.COMPONENT_SCHEMA)
.extend(spi.spi_device_schema(cs_pin_required=True)),
_validate_packet_mode,
@@ -229,7 +251,8 @@ async def to_code(config):
await cg.register_component(var, config)
await spi.register_spi_device(var, config)
for key in CONFIG_MAP:
for opt in CONFIG_MAP:
key = opt.schema
if key in config:
cg.add(getattr(var, f"set_{key}")(config[key]))

View File

@@ -98,25 +98,8 @@ CC1101Component::CC1101Component() {
this->state_.LENGTH_CONFIG = 2;
this->state_.FS_AUTOCAL = 1;
// Default Settings
this->set_frequency(433920);
this->set_if_frequency(153);
this->set_filter_bandwidth(203);
this->set_channel(0);
this->set_channel_spacing(200);
this->set_symbol_rate(5000);
this->set_sync_mode(SyncMode::SYNC_MODE_NONE);
this->set_carrier_sense_above_threshold(true);
this->set_modulation_type(Modulation::MODULATION_ASK_OOK);
this->set_magn_target(MagnTarget::MAGN_TARGET_42DB);
this->set_max_lna_gain(MaxLnaGain::MAX_LNA_GAIN_DEFAULT);
this->set_max_dvga_gain(MaxDvgaGain::MAX_DVGA_GAIN_MINUS_3);
this->set_lna_priority(false);
this->set_wait_time(WaitTime::WAIT_TIME_32_SAMPLES);
// CRITICAL: Initialize PA Table to avoid transmitting 0 power (Silence)
memset(this->pa_table_, 0, sizeof(this->pa_table_));
this->set_output_power(10.0f);
}
void CC1101Component::setup() {
@@ -186,14 +169,16 @@ void CC1101Component::loop() {
}
// Read packet
uint8_t payload_length;
uint8_t payload_length, expected_rx;
if (this->state_.LENGTH_CONFIG == static_cast<uint8_t>(LengthConfig::LENGTH_CONFIG_VARIABLE)) {
this->read_(Register::FIFO, &payload_length, 1);
expected_rx = payload_length + 1;
} else {
payload_length = this->state_.PKTLEN;
expected_rx = payload_length;
}
if (payload_length == 0 || payload_length > 64) {
ESP_LOGW(TAG, "Invalid payload length: %u", payload_length);
if (payload_length == 0 || payload_length > 64 || rx_bytes != expected_rx) {
ESP_LOGW(TAG, "Invalid packet: rx_bytes %u, payload_length %u", rx_bytes, payload_length);
this->enter_idle_();
this->strobe_(Command::FRX);
this->strobe_(Command::RX);
@@ -203,13 +188,12 @@ void CC1101Component::loop() {
this->packet_.resize(payload_length);
this->read_(Register::FIFO, this->packet_.data(), payload_length);
// Read status and trigger
uint8_t status[2];
this->read_(Register::FIFO, status, 2);
int8_t rssi_raw = static_cast<int8_t>(status[0]);
float rssi = (rssi_raw * RSSI_STEP) - RSSI_OFFSET;
bool crc_ok = (status[1] & STATUS_CRC_OK_MASK) != 0;
uint8_t lqi = status[1] & STATUS_LQI_MASK;
// Read status from registers (more reliable than FIFO status bytes due to timing issues)
this->read_(Register::RSSI);
this->read_(Register::LQI);
float rssi = (this->state_.RSSI * RSSI_STEP) - RSSI_OFFSET;
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_, rssi, lqi);
}
@@ -633,12 +617,15 @@ void CC1101Component::set_packet_mode(bool value) {
this->state_.GDO0_CFG = 0x01;
// Set max RX FIFO threshold to ensure we only trigger on end-of-packet
this->state_.FIFO_THR = 15;
// Don't append status bytes to FIFO - we read from registers instead
this->state_.APPEND_STATUS = 0;
} else {
// Configure GDO0 for serial data (async serial mode)
this->state_.GDO0_CFG = 0x0D;
}
if (this->initialized_) {
this->write_(Register::PKTCTRL0);
this->write_(Register::PKTCTRL1);
this->write_(Register::IOCFG0);
this->write_(Register::FIFOTHR);
}

View File

@@ -117,9 +117,7 @@ CONF_MIN_HUMIDITY = "min_humidity"
CONF_MAX_HUMIDITY = "max_humidity"
CONF_TARGET_HUMIDITY = "target_humidity"
visual_temperature = cv.float_with_unit(
"visual_temperature", "(°C|° C|°|C|°K|° K|K|°F|° F|F)?"
)
visual_temperature = cv.float_with_unit("visual_temperature", "(°|(° ?)?[CKF])?")
VISUAL_TEMPERATURE_STEP_SCHEMA = cv.Schema(
@@ -275,10 +273,13 @@ async def setup_climate_core_(var, config):
visual = config[CONF_VISUAL]
if (min_temp := visual.get(CONF_MIN_TEMPERATURE)) is not None:
cg.add_define("USE_CLIMATE_VISUAL_OVERRIDES")
cg.add(var.set_visual_min_temperature_override(min_temp))
if (max_temp := visual.get(CONF_MAX_TEMPERATURE)) is not None:
cg.add_define("USE_CLIMATE_VISUAL_OVERRIDES")
cg.add(var.set_visual_max_temperature_override(max_temp))
if (temp_step := visual.get(CONF_TEMPERATURE_STEP)) is not None:
cg.add_define("USE_CLIMATE_VISUAL_OVERRIDES")
cg.add(
var.set_visual_temperature_step_override(
temp_step[CONF_TARGET_TEMPERATURE],
@@ -286,8 +287,10 @@ async def setup_climate_core_(var, config):
)
)
if (min_humidity := visual.get(CONF_MIN_HUMIDITY)) is not None:
cg.add_define("USE_CLIMATE_VISUAL_OVERRIDES")
cg.add(var.set_visual_min_humidity_override(min_humidity))
if (max_humidity := visual.get(CONF_MAX_HUMIDITY)) is not None:
cg.add_define("USE_CLIMATE_VISUAL_OVERRIDES")
cg.add(var.set_visual_max_humidity_override(max_humidity))
if (mqtt_id := config.get(CONF_MQTT_ID)) is not None:

View File

@@ -2,6 +2,7 @@
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/macros.h"
#include <strings.h>
namespace esphome::climate {
@@ -190,24 +191,30 @@ ClimateCall &ClimateCall::set_fan_mode(ClimateFanMode fan_mode) {
}
ClimateCall &ClimateCall::set_fan_mode(const char *custom_fan_mode) {
return this->set_fan_mode(custom_fan_mode, strlen(custom_fan_mode));
}
ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) {
return this->set_fan_mode(fan_mode.data(), fan_mode.size());
}
ClimateCall &ClimateCall::set_fan_mode(const char *custom_fan_mode, size_t len) {
// Check if it's a standard enum mode first
for (const auto &mode_entry : CLIMATE_FAN_MODES_BY_STR) {
if (str_equals_case_insensitive(custom_fan_mode, mode_entry.str)) {
if (strncasecmp(custom_fan_mode, mode_entry.str, len) == 0 && mode_entry.str[len] == '\0') {
return this->set_fan_mode(static_cast<ClimateFanMode>(mode_entry.value));
}
}
// Find the matching pointer from parent climate device
if (const char *mode_ptr = this->parent_->find_custom_fan_mode_(custom_fan_mode)) {
if (const char *mode_ptr = this->parent_->find_custom_fan_mode_(custom_fan_mode, len)) {
this->custom_fan_mode_ = mode_ptr;
this->fan_mode_.reset();
return *this;
}
ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), custom_fan_mode);
ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %.*s", this->parent_->get_name().c_str(), (int) len, custom_fan_mode);
return *this;
}
ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) { return this->set_fan_mode(fan_mode.c_str()); }
ClimateCall &ClimateCall::set_fan_mode(optional<std::string> fan_mode) {
if (fan_mode.has_value()) {
this->set_fan_mode(fan_mode.value());
@@ -222,24 +229,30 @@ ClimateCall &ClimateCall::set_preset(ClimatePreset preset) {
}
ClimateCall &ClimateCall::set_preset(const char *custom_preset) {
return this->set_preset(custom_preset, strlen(custom_preset));
}
ClimateCall &ClimateCall::set_preset(const std::string &preset) {
return this->set_preset(preset.data(), preset.size());
}
ClimateCall &ClimateCall::set_preset(const char *custom_preset, size_t len) {
// Check if it's a standard enum preset first
for (const auto &preset_entry : CLIMATE_PRESETS_BY_STR) {
if (str_equals_case_insensitive(custom_preset, preset_entry.str)) {
if (strncasecmp(custom_preset, preset_entry.str, len) == 0 && preset_entry.str[len] == '\0') {
return this->set_preset(static_cast<ClimatePreset>(preset_entry.value));
}
}
// Find the matching pointer from parent climate device
if (const char *preset_ptr = this->parent_->find_custom_preset_(custom_preset)) {
if (const char *preset_ptr = this->parent_->find_custom_preset_(custom_preset, len)) {
this->custom_preset_ = preset_ptr;
this->preset_.reset();
return *this;
}
ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), custom_preset);
ESP_LOGW(TAG, "'%s' - Unrecognized preset %.*s", this->parent_->get_name().c_str(), (int) len, custom_preset);
return *this;
}
ClimateCall &ClimateCall::set_preset(const std::string &preset) { return this->set_preset(preset.c_str()); }
ClimateCall &ClimateCall::set_preset(optional<std::string> preset) {
if (preset.has_value()) {
this->set_preset(preset.value());
@@ -356,7 +369,7 @@ optional<ClimateDeviceRestoreState> Climate::restore_state_() {
}
void Climate::save_state_() {
#if (defined(USE_ESP_IDF) || (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0))) && \
#if (defined(USE_ESP32) || (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0))) && \
!defined(CLANG_TIDY)
#pragma GCC diagnostic ignored "-Wclass-memaccess"
#define TEMP_IGNORE_MEMACCESS
@@ -473,26 +486,28 @@ void Climate::publish_state() {
ClimateTraits Climate::get_traits() {
auto traits = this->traits();
if (this->visual_min_temperature_override_.has_value()) {
traits.set_visual_min_temperature(*this->visual_min_temperature_override_);
#ifdef USE_CLIMATE_VISUAL_OVERRIDES
if (!std::isnan(this->visual_min_temperature_override_)) {
traits.set_visual_min_temperature(this->visual_min_temperature_override_);
}
if (this->visual_max_temperature_override_.has_value()) {
traits.set_visual_max_temperature(*this->visual_max_temperature_override_);
if (!std::isnan(this->visual_max_temperature_override_)) {
traits.set_visual_max_temperature(this->visual_max_temperature_override_);
}
if (this->visual_target_temperature_step_override_.has_value()) {
traits.set_visual_target_temperature_step(*this->visual_target_temperature_step_override_);
traits.set_visual_current_temperature_step(*this->visual_current_temperature_step_override_);
if (!std::isnan(this->visual_target_temperature_step_override_)) {
traits.set_visual_target_temperature_step(this->visual_target_temperature_step_override_);
traits.set_visual_current_temperature_step(this->visual_current_temperature_step_override_);
}
if (this->visual_min_humidity_override_.has_value()) {
traits.set_visual_min_humidity(*this->visual_min_humidity_override_);
if (!std::isnan(this->visual_min_humidity_override_)) {
traits.set_visual_min_humidity(this->visual_min_humidity_override_);
}
if (this->visual_max_humidity_override_.has_value()) {
traits.set_visual_max_humidity(*this->visual_max_humidity_override_);
if (!std::isnan(this->visual_max_humidity_override_)) {
traits.set_visual_max_humidity(this->visual_max_humidity_override_);
}
#endif
return traits;
}
#ifdef USE_CLIMATE_VISUAL_OVERRIDES
void Climate::set_visual_min_temperature_override(float visual_min_temperature_override) {
this->visual_min_temperature_override_ = visual_min_temperature_override;
}
@@ -513,6 +528,7 @@ void Climate::set_visual_min_humidity_override(float visual_min_humidity_overrid
void Climate::set_visual_max_humidity_override(float visual_max_humidity_override) {
this->visual_max_humidity_override_ = visual_max_humidity_override;
}
#endif
ClimateCall Climate::make_call() { return ClimateCall(this); }
@@ -685,11 +701,19 @@ bool Climate::set_custom_preset_(const char *preset) {
void Climate::clear_custom_preset_() { this->custom_preset_ = nullptr; }
const char *Climate::find_custom_fan_mode_(const char *custom_fan_mode) {
return this->get_traits().find_custom_fan_mode_(custom_fan_mode);
return this->find_custom_fan_mode_(custom_fan_mode, strlen(custom_fan_mode));
}
const char *Climate::find_custom_fan_mode_(const char *custom_fan_mode, size_t len) {
return this->get_traits().find_custom_fan_mode_(custom_fan_mode, len);
}
const char *Climate::find_custom_preset_(const char *custom_preset) {
return this->get_traits().find_custom_preset_(custom_preset);
return this->find_custom_preset_(custom_preset, strlen(custom_preset));
}
const char *Climate::find_custom_preset_(const char *custom_preset, size_t len) {
return this->get_traits().find_custom_preset_(custom_preset, len);
}
void Climate::dump_traits_(const char *tag) {

View File

@@ -78,6 +78,8 @@ class ClimateCall {
ClimateCall &set_fan_mode(optional<std::string> fan_mode);
/// Set the custom fan mode of the climate device.
ClimateCall &set_fan_mode(const char *custom_fan_mode);
/// Set the custom fan mode of the climate device (zero-copy API path).
ClimateCall &set_fan_mode(const char *custom_fan_mode, size_t len);
/// Set the swing mode of the climate device.
ClimateCall &set_swing_mode(ClimateSwingMode swing_mode);
/// Set the swing mode of the climate device.
@@ -94,6 +96,8 @@ class ClimateCall {
ClimateCall &set_preset(optional<std::string> preset);
/// Set the custom preset of the climate device.
ClimateCall &set_preset(const char *custom_preset);
/// Set the custom preset of the climate device (zero-copy API path).
ClimateCall &set_preset(const char *custom_preset, size_t len);
void perform();
@@ -213,11 +217,13 @@ class Climate : public EntityBase {
*/
ClimateTraits get_traits();
#ifdef USE_CLIMATE_VISUAL_OVERRIDES
void set_visual_min_temperature_override(float visual_min_temperature_override);
void set_visual_max_temperature_override(float visual_max_temperature_override);
void set_visual_temperature_step_override(float target, float current);
void set_visual_min_humidity_override(float visual_min_humidity_override);
void set_visual_max_humidity_override(float visual_max_humidity_override);
#endif
/// Check if a custom fan mode is currently active.
bool has_custom_fan_mode() const { return this->custom_fan_mode_ != nullptr; }
@@ -288,9 +294,11 @@ class Climate : public EntityBase {
/// Find and return the matching custom fan mode pointer from traits, or nullptr if not found.
const char *find_custom_fan_mode_(const char *custom_fan_mode);
const char *find_custom_fan_mode_(const char *custom_fan_mode, size_t len);
/// Find and return the matching custom preset pointer from traits, or nullptr if not found.
const char *find_custom_preset_(const char *custom_preset);
const char *find_custom_preset_(const char *custom_preset, size_t len);
/** Get the default traits of this climate device.
*
@@ -318,15 +326,17 @@ class Climate : public EntityBase {
void dump_traits_(const char *tag);
CallbackManager<void(Climate &)> state_callback_{};
CallbackManager<void(ClimateCall &)> control_callback_{};
LazyCallbackManager<void(Climate &)> state_callback_{};
LazyCallbackManager<void(ClimateCall &)> control_callback_{};
ESPPreferenceObject rtc_;
optional<float> visual_min_temperature_override_{};
optional<float> visual_max_temperature_override_{};
optional<float> visual_target_temperature_step_override_{};
optional<float> visual_current_temperature_step_override_{};
optional<float> visual_min_humidity_override_{};
optional<float> visual_max_humidity_override_{};
#ifdef USE_CLIMATE_VISUAL_OVERRIDES
float visual_min_temperature_override_{NAN};
float visual_max_temperature_override_{NAN};
float visual_target_temperature_step_override_{NAN};
float visual_current_temperature_step_override_{NAN};
float visual_min_humidity_override_{NAN};
float visual_max_humidity_override_{NAN};
#endif
private:
/** The active custom fan mode (private - enforces use of safe setters).

View File

@@ -20,18 +20,22 @@ using ClimatePresetMask = FiniteSetMask<ClimatePreset, DefaultBitPolicy<ClimateP
// Lightweight linear search for small vectors (1-20 items) of const char* pointers
// Avoids std::find template overhead
inline bool vector_contains(const std::vector<const char *> &vec, const char *value) {
inline bool vector_contains(const std::vector<const char *> &vec, const char *value, size_t len) {
for (const char *item : vec) {
if (strcmp(item, value) == 0)
if (strncmp(item, value, len) == 0 && item[len] == '\0')
return true;
}
return false;
}
inline bool vector_contains(const std::vector<const char *> &vec, const char *value) {
return vector_contains(vec, value, strlen(value));
}
// Find and return matching pointer from vector, or nullptr if not found
inline const char *vector_find(const std::vector<const char *> &vec, const char *value) {
inline const char *vector_find(const std::vector<const char *> &vec, const char *value, size_t len) {
for (const char *item : vec) {
if (strcmp(item, value) == 0)
if (strncmp(item, value, len) == 0 && item[len] == '\0')
return item;
}
return nullptr;
@@ -257,13 +261,19 @@ class ClimateTraits {
/// Find and return the matching custom fan mode pointer from supported modes, or nullptr if not found
/// This is protected as it's an implementation detail - use Climate::find_custom_fan_mode_() instead
const char *find_custom_fan_mode_(const char *custom_fan_mode) const {
return vector_find(this->supported_custom_fan_modes_, custom_fan_mode);
return this->find_custom_fan_mode_(custom_fan_mode, strlen(custom_fan_mode));
}
const char *find_custom_fan_mode_(const char *custom_fan_mode, size_t len) const {
return vector_find(this->supported_custom_fan_modes_, custom_fan_mode, len);
}
/// Find and return the matching custom preset pointer from supported presets, or nullptr if not found
/// This is protected as it's an implementation detail - use Climate::find_custom_preset_() instead
const char *find_custom_preset_(const char *custom_preset) const {
return vector_find(this->supported_custom_presets_, custom_preset);
return this->find_custom_preset_(custom_preset, strlen(custom_preset));
}
const char *find_custom_preset_(const char *custom_preset, size_t len) const {
return vector_find(this->supported_custom_presets_, custom_preset, len);
}
uint32_t feature_flags_{0};

View File

@@ -7,7 +7,7 @@ namespace copy {
static const char *const TAG = "copy.select";
void CopySelect::setup() {
source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(index); });
source_->add_on_state_callback([this](size_t index) { this->publish_state(index); });
traits.set_options(source_->traits.get_options());

View File

@@ -152,7 +152,7 @@ class Cover : public EntityBase, public EntityBase_DeviceClass {
optional<CoverRestoreState> restore_state_();
CallbackManager<void()> state_callback_{};
LazyCallbackManager<void()> state_callback_{};
ESPPreferenceObject rtc_;
};

View File

@@ -51,7 +51,7 @@ void DallasTemperatureSensor::update() {
}
float tempc = this->get_temp_c_();
ESP_LOGD(TAG, "'%s': Got Temperature=%.1f°C", this->get_name().c_str(), tempc);
ESP_LOGD(TAG, "'%s': Got Temperature=%f°C", this->get_name().c_str(), tempc);
this->publish_state(tempc);
});
}

View File

@@ -22,7 +22,7 @@ class DateTimeBase : public EntityBase {
#endif
protected:
CallbackManager<void()> state_callback_;
LazyCallbackManager<void()> state_callback_;
#ifdef USE_TIME
time::RealTimeClock *rtc_;

View File

@@ -200,7 +200,7 @@ void DebugComponent::get_device_info_(std::string &device_info) {
#ifdef USE_ARDUINO
ESP_LOGD(TAG, "Framework: Arduino");
device_info += "Arduino";
#elif defined(USE_ESP_IDF)
#elif defined(USE_ESP32)
ESP_LOGD(TAG, "Framework: ESP-IDF");
device_info += "ESP-IDF";
#else

View File

@@ -1,4 +1,4 @@
from esphome import automation, pins
from esphome import automation, core, pins
import esphome.codegen as cg
from esphome.components import esp32, time
from esphome.components.esp32 import (
@@ -23,16 +23,20 @@ from esphome.const import (
CONF_MINUTE,
CONF_MODE,
CONF_NUMBER,
CONF_PIN,
CONF_PINS,
CONF_RUN_DURATION,
CONF_SECOND,
CONF_SLEEP_DURATION,
CONF_TIME_ID,
CONF_WAKEUP_PIN,
PLATFORM_BK72XX,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PlatformFramework,
)
from esphome.core import CORE
from esphome.types import ConfigType
WAKEUP_PINS = {
VARIANT_ESP32: [
@@ -113,7 +117,7 @@ WAKEUP_PINS = {
}
def validate_pin_number(value):
def validate_pin_number_esp32(value: ConfigType) -> ConfigType:
valid_pins = WAKEUP_PINS.get(get_esp32_variant(), WAKEUP_PINS[VARIANT_ESP32])
if value[CONF_NUMBER] not in valid_pins:
raise cv.Invalid(
@@ -122,6 +126,51 @@ def validate_pin_number(value):
return value
def validate_pin_number(value: ConfigType) -> ConfigType:
if not CORE.is_esp32:
return value
return validate_pin_number_esp32(value)
def validate_wakeup_pin(
value: ConfigType | list[ConfigType],
) -> list[ConfigType]:
if not isinstance(value, list):
processed_pins: list[ConfigType] = [{CONF_PIN: value}]
else:
processed_pins = list(value)
for i, pin_config in enumerate(processed_pins):
# now validate each item
validated_pin = WAKEUP_PIN_SCHEMA(pin_config)
validate_pin_number(validated_pin[CONF_PIN])
processed_pins[i] = validated_pin
return processed_pins
def validate_config(config: ConfigType) -> ConfigType:
# right now only BK72XX supports the list format for wakeup pins
if CORE.is_bk72xx:
if CONF_WAKEUP_PIN_MODE in config:
wakeup_pins = config.get(CONF_WAKEUP_PIN, [])
if len(wakeup_pins) > 1:
raise cv.Invalid(
"You need to remove the global wakeup_pin_mode and define it per pin"
)
if wakeup_pins:
wakeup_pins[0][CONF_WAKEUP_PIN_MODE] = config.pop(CONF_WAKEUP_PIN_MODE)
elif (
isinstance(config.get(CONF_WAKEUP_PIN), list)
and len(config[CONF_WAKEUP_PIN]) > 1
):
raise cv.Invalid(
"Your platform does not support providing multiple entries in wakeup_pin"
)
return config
def _validate_ex1_wakeup_mode(value):
if value == "ALL_LOW":
esp32.only_on_variant(supported=[VARIANT_ESP32], msg_prefix="ALL_LOW")(value)
@@ -141,6 +190,15 @@ def _validate_ex1_wakeup_mode(value):
return value
def _validate_sleep_duration(value: core.TimePeriod) -> core.TimePeriod:
if not CORE.is_bk72xx:
return value
max_duration = core.TimePeriod(hours=36)
if value > max_duration:
raise cv.Invalid("sleep duration cannot be more than 36 hours on BK72XX")
return value
deep_sleep_ns = cg.esphome_ns.namespace("deep_sleep")
DeepSleepComponent = deep_sleep_ns.class_("DeepSleepComponent", cg.Component)
EnterDeepSleepAction = deep_sleep_ns.class_("EnterDeepSleepAction", automation.Action)
@@ -186,6 +244,13 @@ WAKEUP_CAUSES_SCHEMA = cv.Schema(
}
)
WAKEUP_PIN_SCHEMA = cv.Schema(
{
cv.Required(CONF_PIN): pins.internal_gpio_input_pin_schema,
cv.Optional(CONF_WAKEUP_PIN_MODE): cv.enum(WAKEUP_PIN_MODES, upper=True),
}
)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
@@ -194,14 +259,15 @@ CONFIG_SCHEMA = cv.All(
cv.All(cv.only_on_esp32, WAKEUP_CAUSES_SCHEMA),
cv.positive_time_period_milliseconds,
),
cv.Optional(CONF_SLEEP_DURATION): cv.positive_time_period_milliseconds,
cv.Optional(CONF_WAKEUP_PIN): cv.All(
cv.only_on_esp32,
pins.internal_gpio_input_pin_schema,
validate_pin_number,
cv.Optional(CONF_SLEEP_DURATION): cv.All(
cv.positive_time_period_milliseconds,
_validate_sleep_duration,
),
cv.Optional(CONF_WAKEUP_PIN): validate_wakeup_pin,
cv.Optional(CONF_WAKEUP_PIN_MODE): cv.All(
cv.only_on_esp32, cv.enum(WAKEUP_PIN_MODES), upper=True
cv.only_on([PLATFORM_ESP32, PLATFORM_BK72XX]),
cv.enum(WAKEUP_PIN_MODES),
upper=True,
),
cv.Optional(CONF_ESP32_EXT1_WAKEUP): cv.All(
cv.only_on_esp32,
@@ -212,7 +278,8 @@ CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.Required(CONF_PINS): cv.ensure_list(
pins.internal_gpio_input_pin_schema, validate_pin_number
pins.internal_gpio_input_pin_schema,
validate_pin_number_esp32,
),
cv.Required(CONF_MODE): cv.All(
cv.enum(EXT1_WAKEUP_MODES, upper=True),
@@ -238,7 +305,8 @@ CONFIG_SCHEMA = cv.All(
),
}
).extend(cv.COMPONENT_SCHEMA),
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266]),
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX]),
validate_config,
)
@@ -249,7 +317,20 @@ async def to_code(config):
if CONF_SLEEP_DURATION in config:
cg.add(var.set_sleep_duration(config[CONF_SLEEP_DURATION]))
if CONF_WAKEUP_PIN in config:
pin = await cg.gpio_pin_expression(config[CONF_WAKEUP_PIN])
pins_as_list = config.get(CONF_WAKEUP_PIN, [])
if CORE.is_bk72xx:
cg.add(var.init_wakeup_pins_(len(pins_as_list)))
for item in pins_as_list:
cg.add(
var.add_wakeup_pin(
await cg.gpio_pin_expression(item[CONF_PIN]),
item.get(
CONF_WAKEUP_PIN_MODE, WakeupPinMode.WAKEUP_PIN_MODE_IGNORE
),
)
)
else:
pin = await cg.gpio_pin_expression(pins_as_list[0][CONF_PIN])
cg.add(var.set_wakeup_pin(pin))
if CONF_WAKEUP_PIN_MODE in config:
cg.add(var.set_wakeup_pin_mode(config[CONF_WAKEUP_PIN_MODE]))
@@ -305,7 +386,10 @@ DEEP_SLEEP_ENTER_SCHEMA = cv.All(
cv.Schema(
{
cv.Exclusive(CONF_SLEEP_DURATION, "time"): cv.templatable(
cv.positive_time_period_milliseconds
cv.All(
cv.positive_time_period_milliseconds,
_validate_sleep_duration,
)
),
# Only on ESP32 due to how long the RTC on ESP8266 can stay asleep
cv.Exclusive(CONF_UNTIL, "time"): cv.All(
@@ -363,5 +447,6 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
PlatformFramework.ESP32_IDF,
},
"deep_sleep_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
"deep_sleep_bk72xx.cpp": {PlatformFramework.BK72XX_ARDUINO},
}
)

View File

@@ -0,0 +1,64 @@
#ifdef USE_BK72XX
#include "deep_sleep_component.h"
#include "esphome/core/log.h"
namespace esphome::deep_sleep {
static const char *const TAG = "deep_sleep.bk72xx";
optional<uint32_t> DeepSleepComponent::get_run_duration_() const { return this->run_duration_; }
void DeepSleepComponent::dump_config_platform_() {
for (const WakeUpPinItem &item : this->wakeup_pins_) {
LOG_PIN(" Wakeup Pin: ", item.wakeup_pin);
}
}
bool DeepSleepComponent::pin_prevents_sleep_(WakeUpPinItem &pinItem) const {
return (pinItem.wakeup_pin_mode == WAKEUP_PIN_MODE_KEEP_AWAKE && pinItem.wakeup_pin != nullptr &&
!this->sleep_duration_.has_value() && (pinItem.wakeup_level == get_real_pin_state_(*pinItem.wakeup_pin)));
}
bool DeepSleepComponent::prepare_to_sleep_() {
if (wakeup_pins_.size() > 0) {
for (WakeUpPinItem &item : this->wakeup_pins_) {
if (pin_prevents_sleep_(item)) {
// Defer deep sleep until inactive
if (!this->next_enter_deep_sleep_) {
this->status_set_warning();
ESP_LOGV(TAG, "Waiting for pin to switch state to enter deep sleep...");
}
this->next_enter_deep_sleep_ = true;
return false;
}
}
}
return true;
}
void DeepSleepComponent::deep_sleep_() {
for (WakeUpPinItem &item : this->wakeup_pins_) {
if (item.wakeup_pin_mode == WAKEUP_PIN_MODE_INVERT_WAKEUP) {
if (item.wakeup_level == get_real_pin_state_(*item.wakeup_pin)) {
item.wakeup_level = !item.wakeup_level;
}
}
ESP_LOGI(TAG, "Wake-up on P%u %s (%d)", item.wakeup_pin->get_pin(), item.wakeup_level ? "HIGH" : "LOW",
static_cast<int32_t>(item.wakeup_pin_mode));
}
if (this->sleep_duration_.has_value())
lt_deep_sleep_config_timer((*this->sleep_duration_ / 1000) & 0xFFFFFFFF);
for (WakeUpPinItem &item : this->wakeup_pins_) {
lt_deep_sleep_config_gpio(1 << item.wakeup_pin->get_pin(), item.wakeup_level);
lt_deep_sleep_keep_floating_gpio(1 << item.wakeup_pin->get_pin(), true);
}
lt_deep_sleep_enter();
}
} // namespace esphome::deep_sleep
#endif // USE_BK72XX

View File

@@ -19,7 +19,7 @@
namespace esphome {
namespace deep_sleep {
#ifdef USE_ESP32
#if defined(USE_ESP32) || defined(USE_BK72XX)
/** The values of this enum define what should be done if deep sleep is set up with a wakeup pin on the ESP32
* and the scenario occurs that the wakeup pin is already in the wakeup state.
@@ -33,7 +33,17 @@ enum WakeupPinMode {
*/
WAKEUP_PIN_MODE_INVERT_WAKEUP,
};
#endif
#if defined(USE_BK72XX)
struct WakeUpPinItem {
InternalGPIOPin *wakeup_pin;
WakeupPinMode wakeup_pin_mode;
bool wakeup_level;
};
#endif // USE_BK72XX
#ifdef USE_ESP32
#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3)
struct Ext1Wakeup {
uint64_t mask;
@@ -75,6 +85,13 @@ class DeepSleepComponent : public Component {
void set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode);
#endif // USE_ESP32
#if defined(USE_BK72XX)
void init_wakeup_pins_(size_t capacity) { this->wakeup_pins_.init(capacity); }
void add_wakeup_pin(InternalGPIOPin *wakeup_pin, WakeupPinMode wakeup_pin_mode) {
this->wakeup_pins_.emplace_back(WakeUpPinItem{wakeup_pin, wakeup_pin_mode, !wakeup_pin->is_inverted()});
}
#endif // USE_BK72XX
#if defined(USE_ESP32)
#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3)
void set_ext1_wakeup(Ext1Wakeup ext1_wakeup);
@@ -114,7 +131,17 @@ class DeepSleepComponent : public Component {
bool prepare_to_sleep_();
void deep_sleep_();
#ifdef USE_BK72XX
bool pin_prevents_sleep_(WakeUpPinItem &pinItem) const;
bool get_real_pin_state_(InternalGPIOPin &pin) const { return (pin.digital_read() ^ pin.is_inverted()); }
#endif // USE_BK72XX
optional<uint64_t> sleep_duration_;
#ifdef USE_BK72XX
FixedVector<WakeUpPinItem> wakeup_pins_;
#endif // USE_BK72XX
#ifdef USE_ESP32
InternalGPIOPin *wakeup_pin_;
WakeupPinMode wakeup_pin_mode_{WAKEUP_PIN_MODE_IGNORE};
@@ -124,8 +151,10 @@ class DeepSleepComponent : public Component {
#endif
optional<bool> touch_wakeup_;
optional<WakeupCauseToRunDuration> wakeup_cause_to_run_duration_;
#endif // USE_ESP32
optional<uint32_t> run_duration_;
bool next_enter_deep_sleep_{false};
bool prevent_{false};

View File

@@ -8,17 +8,20 @@ namespace dht {
static const char *const TAG = "dht";
void DHT::setup() {
this->pin_->digital_write(true);
this->pin_->setup();
this->pin_->digital_write(true);
this->t_pin_->digital_write(true);
this->t_pin_->setup();
#ifdef USE_ESP32
this->t_pin_->pin_mode(this->t_pin_->get_flags() | gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN);
#endif
this->t_pin_->digital_write(true);
}
void DHT::dump_config() {
ESP_LOGCONFIG(TAG, "DHT:");
LOG_PIN(" Pin: ", this->pin_);
LOG_PIN(" Pin: ", this->t_pin_);
ESP_LOGCONFIG(TAG, " %sModel: %s", this->is_auto_detect_ ? "Auto-detected " : "",
this->model_ == DHT_MODEL_DHT11 ? "DHT11" : "DHT22 or equivalent");
ESP_LOGCONFIG(TAG, " Internal pull-up: %s", ONOFF(this->pin_->get_flags() & gpio::FLAG_PULLUP));
ESP_LOGCONFIG(TAG, " Internal pull-up: %s", ONOFF(this->t_pin_->get_flags() & gpio::FLAG_PULLUP));
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
@@ -72,21 +75,15 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
int8_t i = 0;
uint8_t data[5] = {0, 0, 0, 0, 0};
this->pin_->digital_write(false);
this->pin_->pin_mode(gpio::FLAG_OUTPUT);
this->pin_->digital_write(false);
#ifndef USE_ESP32
this->pin_.pin_mode(gpio::FLAG_OUTPUT);
#endif
this->pin_.digital_write(false);
if (this->model_ == DHT_MODEL_DHT11) {
delayMicroseconds(18000);
} else if (this->model_ == DHT_MODEL_SI7021) {
#ifdef USE_ESP8266
delayMicroseconds(500);
this->pin_->digital_write(true);
delayMicroseconds(40);
#else
delayMicroseconds(400);
this->pin_->digital_write(true);
#endif
} else if (this->model_ == DHT_MODEL_DHT22_TYPE2) {
delayMicroseconds(2000);
} else if (this->model_ == DHT_MODEL_AM2120 || this->model_ == DHT_MODEL_AM2302) {
@@ -94,7 +91,12 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
} else {
delayMicroseconds(800);
}
this->pin_->pin_mode(this->pin_->get_flags());
#ifdef USE_ESP32
this->pin_.digital_write(true);
#else
this->pin_.pin_mode(this->t_pin_->get_flags());
#endif
{
InterruptLock lock;
@@ -110,7 +112,7 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
uint32_t start_time = micros();
// Wait for rising edge
while (!this->pin_->digital_read()) {
while (!this->pin_.digital_read()) {
if (micros() - start_time > 90) {
if (i < 0) {
error_code = 1; // line didn't clear
@@ -127,7 +129,7 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
uint32_t end_time = start_time;
// Wait for falling edge
while (this->pin_->digital_read()) {
while (this->pin_.digital_read()) {
end_time = micros();
if (end_time - start_time > 90) {
if (i < 0) {

View File

@@ -38,7 +38,10 @@ class DHT : public PollingComponent {
*/
void set_dht_model(DHTModel model);
void set_pin(InternalGPIOPin *pin) { pin_ = pin; }
void set_pin(InternalGPIOPin *pin) {
this->t_pin_ = pin;
this->pin_ = pin->to_isr();
}
void set_model(DHTModel model) { model_ = model; }
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; }
@@ -54,7 +57,8 @@ class DHT : public PollingComponent {
protected:
bool read_sensor_(float *temperature, float *humidity, bool report_errors);
InternalGPIOPin *pin_;
InternalGPIOPin *t_pin_;
ISRInternalGPIOPin pin_;
DHTModel model_{DHT_MODEL_AUTO_DETECT};
bool is_auto_detect_{false};
sensor::Sensor *temperature_sensor_{nullptr};

View File

@@ -54,6 +54,7 @@ bool MenuItemSelect::select_next() {
if (this->select_var_ != nullptr) {
this->select_var_->make_call().select_next(true).perform();
this->on_value_();
changed = true;
}
@@ -65,6 +66,7 @@ bool MenuItemSelect::select_prev() {
if (this->select_var_ != nullptr) {
this->select_var_->make_call().select_previous(true).perform();
this->on_value_();
changed = true;
}

View File

@@ -13,6 +13,7 @@ from esphome.const import (
CONF_ADVANCED,
CONF_BOARD,
CONF_COMPONENTS,
CONF_DISABLED,
CONF_ESPHOME,
CONF_FRAMEWORK,
CONF_IGNORE_EFUSE_CUSTOM_MAC,
@@ -24,6 +25,7 @@ from esphome.const import (
CONF_PLATFORMIO_OPTIONS,
CONF_REF,
CONF_REFRESH,
CONF_SAFE_MODE,
CONF_SOURCE,
CONF_TYPE,
CONF_VARIANT,
@@ -81,6 +83,7 @@ CONF_ASSERTION_LEVEL = "assertion_level"
CONF_COMPILER_OPTIMIZATION = "compiler_optimization"
CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES = "enable_idf_experimental_features"
CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert"
CONF_ENABLE_OTA_ROLLBACK = "enable_ota_rollback"
CONF_EXECUTE_FROM_PSRAM = "execute_from_psram"
CONF_RELEASE = "release"
@@ -118,8 +121,8 @@ ARDUINO_ALLOWED_VARIANTS = [
]
def get_cpu_frequencies(*frequencies):
return [str(x) + "MHZ" for x in frequencies]
def get_cpu_frequencies(*frequencies: int) -> list[str]:
return [f"{frequency}MHZ" for frequency in frequencies]
CPU_FREQUENCIES = {
@@ -136,7 +139,7 @@ CPU_FREQUENCIES = {
}
# Make sure not missed here if a new variant added.
assert all(v in CPU_FREQUENCIES for v in VARIANTS)
assert all(variant in CPU_FREQUENCIES for variant in VARIANTS)
FULL_CPU_FREQUENCIES = set(itertools.chain.from_iterable(CPU_FREQUENCIES.values()))
@@ -250,10 +253,10 @@ def add_idf_sdkconfig_option(name: str, value: SdkconfigValueType):
def add_idf_component(
*,
name: str,
repo: str = None,
ref: str = None,
path: str = None,
refresh: TimePeriod = None,
repo: str | None = None,
ref: str | None = None,
path: str | None = None,
refresh: TimePeriod | None = None,
components: list[str] | None = None,
submodules: list[str] | None = None,
):
@@ -334,7 +337,7 @@ def _format_framework_espidf_version(ver: cv.Version, release: str) -> str:
return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.{ext}"
def _is_framework_url(source: str) -> str:
def _is_framework_url(source: str) -> bool:
# platformio accepts many URL schemes for framework repositories and archives including http, https, git, file, and symlink
import urllib.parse
@@ -354,11 +357,12 @@ def _is_framework_url(source: str) -> str:
# The default/recommended arduino framework version
# - https://github.com/espressif/arduino-esp32/releases
ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
"recommended": cv.Version(3, 3, 2),
"latest": cv.Version(3, 3, 4),
"dev": cv.Version(3, 3, 4),
"recommended": cv.Version(3, 3, 5),
"latest": cv.Version(3, 3, 5),
"dev": cv.Version(3, 3, 5),
}
ARDUINO_PLATFORM_VERSION_LOOKUP = {
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"),
cv.Version(3, 3, 2): cv.Version(55, 3, 31, "2"),
@@ -371,15 +375,33 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = {
cv.Version(3, 1, 1): cv.Version(53, 3, 11),
cv.Version(3, 1, 0): cv.Version(53, 3, 10),
}
# Maps Arduino framework versions to a compatible ESP-IDF version
# These versions correspond to pioarduino/esp-idf releases
# See: https://github.com/pioarduino/esp-idf/releases
ARDUINO_IDF_VERSION_LOOKUP = {
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),
cv.Version(3, 3, 2): cv.Version(5, 5, 1),
cv.Version(3, 3, 1): cv.Version(5, 5, 1),
cv.Version(3, 3, 0): cv.Version(5, 5, 0),
cv.Version(3, 2, 1): cv.Version(5, 4, 2),
cv.Version(3, 2, 0): cv.Version(5, 4, 2),
cv.Version(3, 1, 3): cv.Version(5, 3, 2),
cv.Version(3, 1, 2): cv.Version(5, 3, 2),
cv.Version(3, 1, 1): cv.Version(5, 3, 1),
cv.Version(3, 1, 0): cv.Version(5, 3, 0),
}
# The default/recommended esp-idf framework version
# - https://github.com/espressif/esp-idf/releases
ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
"recommended": cv.Version(5, 5, 1),
"latest": cv.Version(5, 5, 1),
"dev": cv.Version(5, 5, 1),
"recommended": cv.Version(5, 5, 2),
"latest": cv.Version(5, 5, 2),
"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, 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),
@@ -396,9 +418,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, 31, "2"),
"latest": cv.Version(55, 3, 31, "2"),
"dev": cv.Version(55, 3, 31, "2"),
"recommended": cv.Version(55, 3, 35),
"latest": cv.Version(55, 3, 35),
"dev": cv.Version(55, 3, 35),
}
@@ -571,6 +593,13 @@ def final_validate(config):
path=[CONF_FLASH_SIZE],
)
)
if advanced[CONF_ENABLE_OTA_ROLLBACK]:
safe_mode_config = full_config.get(CONF_SAFE_MODE)
if safe_mode_config is None or safe_mode_config.get(CONF_DISABLED, False):
_LOGGER.warning(
"OTA rollback requires safe_mode, disabling rollback support"
)
advanced[CONF_ENABLE_OTA_ROLLBACK] = False
if errs:
raise cv.MultipleInvalid(errs)
@@ -691,6 +720,7 @@ FRAMEWORK_SCHEMA = cv.Schema(
cv.Optional(CONF_LOOP_TASK_STACK_SIZE, default=8192): cv.int_range(
min=8192, max=32768
),
cv.Optional(CONF_ENABLE_OTA_ROLLBACK, default=True): cv.boolean,
}
),
cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list(
@@ -716,12 +746,14 @@ FRAMEWORK_SCHEMA = cv.Schema(
)
# Remove this class in 2026.7.0
class _FrameworkMigrationWarning:
shown = False
def _show_framework_migration_message(name: str, variant: str) -> None:
"""Show a friendly message about framework migration when defaulting to Arduino."""
"""Show a message about the framework default change and how to switch back to Arduino."""
# Remove this function in 2026.7.0
if _FrameworkMigrationWarning.shown:
return
_FrameworkMigrationWarning.shown = True
@@ -731,41 +763,27 @@ def _show_framework_migration_message(name: str, variant: str) -> None:
message = (
color(
AnsiFore.BOLD_CYAN,
f"💡 IMPORTANT: {name} doesn't have a framework specified!",
f"💡 NOTICE: {name} does not have a framework specified.",
)
+ "\n\n"
+ f"Currently, {variant} defaults to the Arduino framework.\n"
+ color(AnsiFore.YELLOW, "This will change to ESP-IDF in ESPHome 2026.1.0.\n")
+ f"Starting with ESPHome 2026.1.0, the default framework for {variant} is ESP-IDF.\n"
+ "(We've been warning about this change since ESPHome 2025.8.0)\n"
+ "\n"
+ "Note: Newer ESP32 variants (C6, H2, P4, etc.) already use ESP-IDF by default.\n"
+ "\n"
+ "Why change? ESP-IDF offers:\n"
+ color(AnsiFore.GREEN, " ✨ Up to 40% smaller binaries\n")
+ color(AnsiFore.GREEN, " 🚀 Better performance and optimization\n")
+ "Why we made this change:\n"
+ color(AnsiFore.GREEN, " ✨ Up to 40% smaller firmware binaries\n")
+ color(AnsiFore.GREEN, " ⚡ 2-3x faster compile times\n")
+ color(AnsiFore.GREEN, " 📦 Custom-built firmware for your exact needs\n")
+ color(
AnsiFore.GREEN,
" 🔧 Active development and testing by ESPHome developers\n",
)
+ color(AnsiFore.GREEN, " 🚀 Better performance and newer features\n")
+ color(AnsiFore.GREEN, " 🔧 More actively maintained by ESPHome\n")
+ "\n"
+ "Trade-offs:\n"
+ color(AnsiFore.YELLOW, " 🔄 Some components need migration\n")
+ "\n"
+ "What should I do?\n"
+ color(AnsiFore.CYAN, " Option 1")
+ ": Migrate to ESP-IDF (recommended)\n"
+ " Add this to your YAML under 'esp32:':\n"
+ color(AnsiFore.WHITE, " framework:\n")
+ color(AnsiFore.WHITE, " type: esp-idf\n")
+ "\n"
+ color(AnsiFore.CYAN, " Option 2")
+ ": Keep using Arduino (still supported)\n"
+ " Add this to your YAML under 'esp32:':\n"
+ "To continue using Arduino, add this to your YAML under 'esp32:':\n"
+ color(AnsiFore.WHITE, " framework:\n")
+ color(AnsiFore.WHITE, " type: arduino\n")
+ "\n"
+ "Need help? Check out the migration guide:\n"
+ "To silence this message with ESP-IDF, explicitly set:\n"
+ color(AnsiFore.WHITE, " framework:\n")
+ color(AnsiFore.WHITE, " type: esp-idf\n")
+ "\n"
+ "Migration guide: "
+ color(
AnsiFore.BLUE,
"https://esphome.io/guides/esp32_arduino_to_idf/",
@@ -780,13 +798,13 @@ def _set_default_framework(config):
config[CONF_FRAMEWORK] = FRAMEWORK_SCHEMA({})
if CONF_TYPE not in config[CONF_FRAMEWORK]:
variant = config[CONF_VARIANT]
config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ESP_IDF
# Show migration message for variants that previously defaulted to Arduino
# Remove this message in 2026.7.0
if variant in ARDUINO_ALLOWED_VARIANTS:
config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ARDUINO
_show_framework_migration_message(
config.get(CONF_NAME, "This device"), variant
)
else:
config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ESP_IDF
return config
@@ -971,29 +989,21 @@ async def to_code(config):
cg.add_platformio_option("framework", "arduino, espidf")
cg.add_build_flag("-DUSE_ARDUINO")
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO")
cg.add_platformio_option(
"board_build.embed_txtfiles",
[
"managed_components/espressif__esp_insights/server_certs/https_server.crt",
"managed_components/espressif__esp_rainmaker/server_certs/rmaker_mqtt_server.crt",
"managed_components/espressif__esp_rainmaker/server_certs/rmaker_claim_service_server.crt",
"managed_components/espressif__esp_rainmaker/server_certs/rmaker_ota_server.crt",
],
)
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_ARDUINO_LOOP_STACK_SIZE",
conf[CONF_ADVANCED][CONF_LOOP_TASK_STACK_SIZE],
)
add_idf_sdkconfig_option("CONFIG_AUTOSTART_ARDUINO", True)
add_idf_sdkconfig_option("CONFIG_MBEDTLS_PSK_MODES", True)
add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True)
add_idf_sdkconfig_option("CONFIG_ESP_PHY_REDUCE_TX_POWER", 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:
@@ -1164,6 +1174,11 @@ async def to_code(config):
"CONFIG_BOOTLOADER_CACHE_32BIT_ADDR_QUAD_FLASH", True
)
# Enable OTA rollback support
if advanced[CONF_ENABLE_OTA_ROLLBACK]:
add_idf_sdkconfig_option("CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE", True)
cg.add_define("USE_OTA_ROLLBACK")
cg.add_define("ESPHOME_LOOP_TASK_STACK_SIZE", advanced[CONF_LOOP_TASK_STACK_SIZE])
cg.add_define(
@@ -1193,7 +1208,7 @@ APP_PARTITION_SIZES = {
}
def get_arduino_partition_csv(flash_size):
def get_arduino_partition_csv(flash_size: str):
app_partition_size = APP_PARTITION_SIZES[flash_size]
eeprom_partition_size = 0x1000 # 4 KB
spiffs_partition_size = 0xF000 # 60 KB
@@ -1213,7 +1228,7 @@ spiffs, data, spiffs, 0x{spiffs_partition_start:X}, 0x{spiffs_partition_size:
"""
def get_idf_partition_csv(flash_size):
def get_idf_partition_csv(flash_size: str):
app_partition_size = APP_PARTITION_SIZES[flash_size]
return f"""\

View File

@@ -1488,6 +1488,10 @@ BOARDS = {
"name": "Arduino Nano ESP32",
"variant": VARIANT_ESP32S3,
},
"arduino_nesso_n1": {
"name": "Arduino Nesso-N1",
"variant": VARIANT_ESP32C6,
},
"atd147_s3": {
"name": "ArtronShop ATD1.47-S3",
"variant": VARIANT_ESP32S3,
@@ -1656,6 +1660,10 @@ BOARDS = {
"name": "Espressif ESP32-C6-DevKitM-1",
"variant": VARIANT_ESP32C6,
},
"esp32-c61-devkitc1-n8r2": {
"name": "Espressif ESP32-C61-DevKitC-1 N8R2 (8 MB Flash Quad, 2 MB PSRAM Quad)",
"variant": VARIANT_ESP32C61,
},
"esp32-devkitlipo": {
"name": "OLIMEX ESP32-DevKit-LiPo",
"variant": VARIANT_ESP32,
@@ -1673,11 +1681,15 @@ BOARDS = {
"variant": VARIANT_ESP32H2,
},
"esp32-p4": {
"name": "Espressif ESP32-P4 generic",
"name": "Espressif ESP32-P4 ES (pre rev.300) generic",
"variant": VARIANT_ESP32P4,
},
"esp32-p4-evboard": {
"name": "Espressif ESP32-P4 Function EV Board",
"name": "Espressif ESP32-P4 Function EV Board (ES pre rev.300)",
"variant": VARIANT_ESP32P4,
},
"esp32-p4_r3": {
"name": "Espressif ESP32-P4 rev.300 generic",
"variant": VARIANT_ESP32P4,
},
"esp32-pico-devkitm-2": {
@@ -2093,7 +2105,7 @@ BOARDS = {
"variant": VARIANT_ESP32,
},
"m5stack-tab5-p4": {
"name": "M5STACK Tab5 esp32-p4 Board",
"name": "M5STACK Tab5 esp32-p4 Board (ES pre rev.300)",
"variant": VARIANT_ESP32P4,
},
"m5stack-timer-cam": {

View File

@@ -4,25 +4,20 @@
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "preferences.h"
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <esp_clk_tree.h>
#include <esp_cpu.h>
#include <esp_idf_version.h>
#include <esp_ota_ops.h>
#include <esp_task_wdt.h>
#include <esp_timer.h>
#include <soc/rtc.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <hal/cpu_hal.h>
void setup(); // NOLINT(readability-redundant-declaration)
void loop(); // NOLINT(readability-redundant-declaration)
#ifdef USE_ARDUINO
#include <Esp.h>
#else
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
#include <esp_clk_tree.h>
#endif
void setup();
void loop();
#endif
// Weak stub for initArduino - overridden when the Arduino component is present
extern "C" __attribute__((weak)) void initArduino() {}
namespace esphome {
@@ -41,29 +36,13 @@ void arch_restart() {
void arch_init() {
// Enable the task watchdog only on the loop task (from which we're currently running)
#if defined(USE_ESP_IDF)
esp_task_wdt_add(nullptr);
// Idle task watchdog is disabled on ESP-IDF
#elif defined(USE_ARDUINO)
enableLoopWDT();
// Disable idle task watchdog on the core we're using (Arduino pins the task to a core)
#if defined(CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0) && CONFIG_ARDUINO_RUNNING_CORE == 0
disableCore0WDT();
#endif
#if defined(CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1) && CONFIG_ARDUINO_RUNNING_CORE == 1
disableCore1WDT();
#endif
#endif
// If the bootloader was compiled with CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE the current
// partition will get rolled back unless it is marked as valid.
esp_ota_img_states_t state;
const esp_partition_t *running = esp_ota_get_running_partition();
if (esp_ota_get_state_partition(running, &state) == ESP_OK) {
if (state == ESP_OTA_IMG_PENDING_VERIFY) {
// Handle OTA rollback: mark partition valid immediately unless USE_OTA_ROLLBACK is enabled,
// in which case safe_mode will mark it valid after confirming successful boot.
#ifndef USE_OTA_ROLLBACK
esp_ota_mark_app_valid_cancel_rollback();
}
}
#endif
}
void IRAM_ATTR HOT arch_feed_wdt() { esp_task_wdt_reset(); }
@@ -71,21 +50,10 @@ uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; }
uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); }
uint32_t arch_get_cpu_freq_hz() {
uint32_t freq = 0;
#ifdef USE_ESP_IDF
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
esp_clk_tree_src_get_freq_hz(SOC_MOD_CLK_CPU, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &freq);
#else
rtc_cpu_freq_config_t config;
rtc_clk_cpu_freq_get_config(&config);
freq = config.freq_mhz * 1000000U;
#endif
#elif defined(USE_ARDUINO)
freq = ESP.getCpuFreqMHz() * 1000000;
#endif
return freq;
}
#ifdef USE_ESP_IDF
TaskHandle_t loop_task_handle = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void loop_task(void *pv_params) {
@@ -96,6 +64,7 @@ void loop_task(void *pv_params) {
}
extern "C" void app_main() {
initArduino();
esp32::setup_preferences();
#if CONFIG_FREERTOS_UNICORE
xTaskCreate(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, &loop_task_handle);
@@ -103,11 +72,6 @@ extern "C" void app_main() {
xTaskCreatePinnedToCore(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, &loop_task_handle, 1);
#endif
}
#endif // USE_ESP_IDF
#ifdef USE_ARDUINO
extern "C" void init() { esp32::setup_preferences(); }
#endif // USE_ARDUINO
} // namespace esphome

View File

@@ -5,6 +5,7 @@ import json # noqa: E402
import os # noqa: E402
import pathlib # noqa: E402
import shutil # noqa: E402
from glob import glob # noqa: E402
def merge_factory_bin(source, target, env):
@@ -126,3 +127,14 @@ def esp32_copy_ota_bin(source, target, env):
# Run merge first, then ota copy second
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", merge_factory_bin) # noqa: F821
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_copy_ota_bin) # noqa: F821
# Find server certificates in managed components and generate .S files.
# Workaround for PlatformIO not processing target_add_binary_data() from managed component CMakeLists.
project_dir = env.subst("$PROJECT_DIR")
managed_components = os.path.join(project_dir, "managed_components")
if os.path.isdir(managed_components):
for cert_file in glob(os.path.join(managed_components, "**/server_certs/*.crt"), recursive=True):
try:
env.FileToAsm(cert_file, FILE_TYPE="TEXT")
except Exception as e:
print(f"Error processing {os.path.basename(cert_file)}: {e}")

View File

@@ -4,26 +4,28 @@
#include "esphome/core/log.h"
#include "esphome/core/preferences.h"
#include <nvs_flash.h>
#include <cstring>
#include <cinttypes>
#include <vector>
#include <string>
#include <cstring>
#include <memory>
#include <vector>
namespace esphome {
namespace esp32 {
static const char *const TAG = "esp32.preferences";
// Buffer size for converting uint32_t to string: max "4294967295" (10 chars) + null terminator + 1 padding
static constexpr size_t KEY_BUFFER_SIZE = 12;
struct NVSData {
std::string key;
uint32_t key;
std::unique_ptr<uint8_t[]> data;
size_t len;
void set_data(const uint8_t *src, size_t size) {
data = std::make_unique<uint8_t[]>(size);
memcpy(data.get(), src, size);
len = size;
this->data = std::make_unique<uint8_t[]>(size);
memcpy(this->data.get(), src, size);
this->len = size;
}
};
@@ -31,27 +33,27 @@ static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-n
class ESP32PreferenceBackend : public ESPPreferenceBackend {
public:
std::string key;
uint32_t key;
uint32_t nvs_handle;
bool save(const uint8_t *data, size_t len) override {
// try find in pending saves and update that
for (auto &obj : s_pending_save) {
if (obj.key == key) {
if (obj.key == this->key) {
obj.set_data(data, len);
return true;
}
}
NVSData save{};
save.key = key;
save.key = this->key;
save.set_data(data, len);
s_pending_save.emplace_back(std::move(save));
ESP_LOGVV(TAG, "s_pending_save: key: %s, len: %zu", key.c_str(), len);
ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len);
return true;
}
bool load(uint8_t *data, size_t len) override {
// try find in pending saves and load from that
for (auto &obj : s_pending_save) {
if (obj.key == key) {
if (obj.key == this->key) {
if (obj.len != len) {
// size mismatch
return false;
@@ -61,22 +63,24 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend {
}
}
char key_str[KEY_BUFFER_SIZE];
snprintf(key_str, sizeof(key_str), "%" PRIu32, this->key);
size_t actual_len;
esp_err_t err = nvs_get_blob(nvs_handle, key.c_str(), nullptr, &actual_len);
esp_err_t err = nvs_get_blob(this->nvs_handle, key_str, nullptr, &actual_len);
if (err != 0) {
ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key.c_str(), esp_err_to_name(err));
ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key_str, esp_err_to_name(err));
return false;
}
if (actual_len != len) {
ESP_LOGVV(TAG, "NVS length does not match (%zu!=%zu)", actual_len, len);
return false;
}
err = nvs_get_blob(nvs_handle, key.c_str(), data, &len);
err = nvs_get_blob(this->nvs_handle, key_str, data, &len);
if (err != 0) {
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key.c_str(), esp_err_to_name(err));
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err));
return false;
} else {
ESP_LOGVV(TAG, "nvs_get_blob: key: %s, len: %zu", key.c_str(), len);
ESP_LOGVV(TAG, "nvs_get_blob: key: %s, len: %zu", key_str, len);
}
return true;
}
@@ -103,14 +107,12 @@ class ESP32Preferences : public ESPPreferences {
}
}
ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override {
return make_preference(length, type);
return this->make_preference(length, type);
}
ESPPreferenceObject make_preference(size_t length, uint32_t type) override {
auto *pref = new ESP32PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory)
pref->nvs_handle = nvs_handle;
uint32_t keyval = type;
pref->key = str_sprintf("%" PRIu32, keyval);
pref->nvs_handle = this->nvs_handle;
pref->key = type;
return ESPPreferenceObject(pref);
}
@@ -123,17 +125,19 @@ class ESP32Preferences : public ESPPreferences {
// goal try write all pending saves even if one fails
int cached = 0, written = 0, failed = 0;
esp_err_t last_err = ESP_OK;
std::string last_key{};
uint32_t last_key = 0;
// go through vector from back to front (makes erase easier/more efficient)
for (ssize_t i = s_pending_save.size() - 1; i >= 0; i--) {
const auto &save = s_pending_save[i];
ESP_LOGVV(TAG, "Checking if NVS data %s has changed", save.key.c_str());
if (is_changed(nvs_handle, save)) {
esp_err_t err = nvs_set_blob(nvs_handle, save.key.c_str(), save.data.get(), save.len);
ESP_LOGV(TAG, "sync: key: %s, len: %zu", save.key.c_str(), save.len);
char key_str[KEY_BUFFER_SIZE];
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str);
if (this->is_changed_(this->nvs_handle, save, key_str)) {
esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.get(), save.len);
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.len);
if (err != 0) {
ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", save.key.c_str(), save.len, esp_err_to_name(err));
ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.len, esp_err_to_name(err));
failed++;
last_err = err;
last_key = save.key;
@@ -141,7 +145,7 @@ class ESP32Preferences : public ESPPreferences {
}
written++;
} else {
ESP_LOGV(TAG, "NVS data not changed skipping %s len=%zu", save.key.c_str(), save.len);
ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.len);
cached++;
}
s_pending_save.erase(s_pending_save.begin() + i);
@@ -149,12 +153,12 @@ class ESP32Preferences : public ESPPreferences {
ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written,
failed);
if (failed > 0) {
ESP_LOGE(TAG, "Writing %d items failed. Last error=%s for key=%s", failed, esp_err_to_name(last_err),
last_key.c_str());
ESP_LOGE(TAG, "Writing %d items failed. Last error=%s for key=%" PRIu32, failed, esp_err_to_name(last_err),
last_key);
}
// note: commit on esp-idf currently is a no-op, nvs_set_blob always writes
esp_err_t err = nvs_commit(nvs_handle);
esp_err_t err = nvs_commit(this->nvs_handle);
if (err != 0) {
ESP_LOGV(TAG, "nvs_commit() failed: %s", esp_err_to_name(err));
return false;
@@ -162,11 +166,13 @@ class ESP32Preferences : public ESPPreferences {
return failed == 0;
}
bool is_changed(const uint32_t nvs_handle, const NVSData &to_save) {
protected:
bool is_changed_(uint32_t nvs_handle, const NVSData &to_save, const char *key_str) {
size_t actual_len;
esp_err_t err = nvs_get_blob(nvs_handle, to_save.key.c_str(), nullptr, &actual_len);
esp_err_t err = nvs_get_blob(nvs_handle, key_str, nullptr, &actual_len);
if (err != 0) {
ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", to_save.key.c_str(), esp_err_to_name(err));
ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key_str, esp_err_to_name(err));
return true;
}
// Check size first before allocating memory
@@ -174,9 +180,9 @@ class ESP32Preferences : public ESPPreferences {
return true;
}
auto stored_data = std::make_unique<uint8_t[]>(actual_len);
err = nvs_get_blob(nvs_handle, to_save.key.c_str(), stored_data.get(), &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", to_save.key.c_str(), esp_err_to_name(err));
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err));
return true;
}
return memcmp(to_save.data.get(), stored_data.get(), to_save.len) != 0;

View File

@@ -24,7 +24,9 @@ extern "C" {
#include <nvs_flash.h>
#ifdef USE_ARDUINO
#include <esp32-hal-bt.h>
// Prevent Arduino from releasing BT memory at startup (esp32-hal-misc.c).
// Without this, esp_bt_controller_init() fails with ESP_ERR_INVALID_STATE.
extern "C" bool btInUse() { return true; } // NOLINT(readability-identifier-naming)
#endif
namespace esphome::esp32_ble {
@@ -165,12 +167,6 @@ void ESP32BLE::advertising_init_() {
bool ESP32BLE::ble_setup_() {
esp_err_t err;
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
#ifdef USE_ARDUINO
if (!btStart()) {
ESP_LOGE(TAG, "btStart failed: %d", esp_bt_controller_get_status());
return false;
}
#else
if (esp_bt_controller_get_status() != ESP_BT_CONTROLLER_STATUS_ENABLED) {
// start bt controller
if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_IDLE) {
@@ -195,7 +191,6 @@ bool ESP32BLE::ble_setup_() {
return false;
}
}
#endif
esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT);
#else
@@ -256,8 +251,11 @@ bool ESP32BLE::ble_setup_() {
}
#endif
// BLE device names are limited to 20 characters
// Buffer: 20 chars + null terminator
constexpr size_t ble_name_max_len = 21;
char name_buffer[ble_name_max_len];
const char *device_name;
std::string name_with_suffix;
if (this->name_ != nullptr) {
if (App.is_name_add_mac_suffix_enabled()) {
@@ -268,23 +266,28 @@ bool ESP32BLE::ble_setup_() {
char mac_addr[mac_address_len];
get_mac_address_into_buffer(mac_addr);
const char *mac_suffix_ptr = mac_addr + mac_address_suffix_len;
name_with_suffix =
make_name_with_suffix(this->name_, strlen(this->name_), '-', mac_suffix_ptr, mac_address_suffix_len);
device_name = name_with_suffix.c_str();
make_name_with_suffix_to(name_buffer, sizeof(name_buffer), this->name_, strlen(this->name_), '-', mac_suffix_ptr,
mac_address_suffix_len);
device_name = name_buffer;
} else {
device_name = this->name_;
}
} else {
name_with_suffix = App.get_name();
if (name_with_suffix.length() > 20) {
const std::string &app_name = App.get_name();
size_t name_len = app_name.length();
if (name_len > 20) {
if (App.is_name_add_mac_suffix_enabled()) {
// Keep first 13 chars and last 7 chars (MAC suffix), remove middle
name_with_suffix.erase(13, name_with_suffix.length() - 20);
memcpy(name_buffer, app_name.c_str(), 13);
memcpy(name_buffer + 13, app_name.c_str() + name_len - 7, 7);
} else {
name_with_suffix.resize(20);
memcpy(name_buffer, app_name.c_str(), 20);
}
name_buffer[20] = '\0';
} else {
memcpy(name_buffer, app_name.c_str(), name_len + 1); // Include null terminator
}
device_name = name_with_suffix.c_str();
device_name = name_buffer;
}
err = esp_ble_gap_set_device_name(device_name);
@@ -308,22 +311,24 @@ bool ESP32BLE::ble_setup_() {
bool ESP32BLE::ble_dismantle_() {
esp_err_t err = esp_bluedroid_disable();
if (err != ESP_OK) {
// ESP_ERR_INVALID_STATE means Bluedroid is already disabled, which is fine
if (err != ESP_ERR_INVALID_STATE) {
ESP_LOGE(TAG, "esp_bluedroid_disable failed: %d", err);
return false;
}
ESP_LOGD(TAG, "Already disabled");
}
err = esp_bluedroid_deinit();
if (err != ESP_OK) {
// ESP_ERR_INVALID_STATE means Bluedroid is already deinitialized, which is fine
if (err != ESP_ERR_INVALID_STATE) {
ESP_LOGE(TAG, "esp_bluedroid_deinit failed: %d", err);
return false;
}
ESP_LOGD(TAG, "Already deinitialized");
}
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
#ifdef USE_ARDUINO
if (!btStop()) {
ESP_LOGE(TAG, "btStop failed: %d", esp_bt_controller_get_status());
return false;
}
#else
if (esp_bt_controller_get_status() != ESP_BT_CONTROLLER_STATUS_IDLE) {
// stop bt controller
if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_ENABLED) {
@@ -347,7 +352,6 @@ bool ESP32BLE::ble_dismantle_() {
return false;
}
}
#endif
#else
if (esp_hosted_bt_controller_disable() != ESP_OK) {
ESP_LOGW(TAG, "esp_hosted_bt_controller_disable failed");

View File

@@ -212,17 +212,23 @@ extern ESP32BLE *global_ble;
template<typename... Ts> class BLEEnabledCondition : public Condition<Ts...> {
public:
bool check(const Ts &...x) override { return global_ble->is_active(); }
bool check(const Ts &...x) override { return global_ble != nullptr && global_ble->is_active(); }
};
template<typename... Ts> class BLEEnableAction : public Action<Ts...> {
public:
void play(const Ts &...x) override { global_ble->enable(); }
void play(const Ts &...x) override {
if (global_ble != nullptr)
global_ble->enable();
}
};
template<typename... Ts> class BLEDisableAction : public Action<Ts...> {
public:
void play(const Ts &...x) override { global_ble->disable(); }
void play(const Ts &...x) override {
if (global_ble != nullptr)
global_ble->disable();
}
};
} // namespace esphome::esp32_ble

View File

@@ -143,9 +143,8 @@ bool ESPBTUUID::operator==(const ESPBTUUID &uuid) const {
return this->as_128bit() == uuid.as_128bit();
}
esp_bt_uuid_t ESPBTUUID::get_uuid() const { return this->uuid_; }
std::string ESPBTUUID::to_string() const {
char buf[40]; // Enough for 128-bit UUID with dashes
char *pos = buf;
void ESPBTUUID::to_str(std::span<char, UUID_STR_LEN> output) const {
char *pos = output.data();
switch (this->uuid_.len) {
case ESP_UUID_LEN_16:
@@ -156,7 +155,7 @@ std::string ESPBTUUID::to_string() const {
*pos++ = format_hex_pretty_char((this->uuid_.uuid.uuid16 >> 4) & 0x0F);
*pos++ = format_hex_pretty_char(this->uuid_.uuid.uuid16 & 0x0F);
*pos = '\0';
return std::string(buf);
return;
case ESP_UUID_LEN_32:
*pos++ = '0';
@@ -165,7 +164,7 @@ std::string ESPBTUUID::to_string() const {
*pos++ = format_hex_pretty_char((this->uuid_.uuid.uuid32 >> shift) & 0x0F);
}
*pos = '\0';
return std::string(buf);
return;
default:
case ESP_UUID_LEN_128:
@@ -179,9 +178,13 @@ std::string ESPBTUUID::to_string() const {
}
}
*pos = '\0';
return std::string(buf);
return;
}
return "";
}
std::string ESPBTUUID::to_string() const {
char buf[UUID_STR_LEN];
this->to_str(buf);
return std::string(buf);
}
} // namespace esphome::esp32_ble

View File

@@ -7,11 +7,15 @@
#ifdef USE_ESP32
#ifdef USE_ESP32_BLE_UUID
#include <span>
#include <string>
#include <esp_bt_defs.h>
namespace esphome::esp32_ble {
/// Buffer size for UUID string: "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX\0"
static constexpr size_t UUID_STR_LEN = 37;
class ESPBTUUID {
public:
ESPBTUUID();
@@ -37,6 +41,7 @@ class ESPBTUUID {
esp_bt_uuid_t get_uuid() const;
std::string to_string() const;
void to_str(std::span<char, UUID_STR_LEN> output) const;
protected:
esp_bt_uuid_t uuid_;

View File

@@ -50,8 +50,12 @@ void BLECharacteristic::parse_descriptors() {
desc->handle = result.handle;
desc->characteristic = this;
this->descriptors.push_back(desc);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char uuid_buf[espbt::UUID_STR_LEN];
desc->uuid.to_str(uuid_buf);
ESP_LOGV(TAG, "[%d] [%s] descriptor %s, handle 0x%x", this->service->client->get_connection_index(),
this->service->client->address_str(), desc->uuid.to_string().c_str(), desc->handle);
this->service->client->address_str(), uuid_buf, desc->handle);
#endif
offset++;
}
}

View File

@@ -411,12 +411,15 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
this->update_conn_params_(MEDIUM_MIN_CONN_INTERVAL, MEDIUM_MAX_CONN_INTERVAL, 0, MEDIUM_CONN_TIMEOUT, "medium");
} else if (this->connection_type_ != espbt::ConnectionType::V3_WITH_CACHE) {
#ifdef USE_ESP32_BLE_DEVICE
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
for (auto &svc : this->services_) {
ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_,
svc->uuid.to_string().c_str());
char uuid_buf[espbt::UUID_STR_LEN];
svc->uuid.to_str(uuid_buf);
ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_, uuid_buf);
ESP_LOGV(TAG, "[%d] [%s] start_handle: 0x%x end_handle: 0x%x", this->connection_index_, this->address_str_,
svc->start_handle, svc->end_handle);
}
#endif
#endif
}
ESP_LOGI(TAG, "[%d] [%s] Service discovery complete", this->connection_index_, this->address_str_);
@@ -524,10 +527,9 @@ void BLEClientBase::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_
case ESP_GAP_BLE_AUTH_CMPL_EVT:
if (!this->check_addr(param->ble_security.auth_cmpl.bd_addr))
return;
esp_bd_addr_t bd_addr;
memcpy(bd_addr, param->ble_security.auth_cmpl.bd_addr, sizeof(esp_bd_addr_t));
ESP_LOGI(TAG, "[%d] [%s] auth complete addr: %s", this->connection_index_, this->address_str_,
format_hex(bd_addr, 6).c_str());
char addr_str[MAC_ADDR_STR_LEN];
format_mac_addr_upper(param->ble_security.auth_cmpl.bd_addr, addr_str);
ESP_LOGI(TAG, "[%d] [%s] auth complete addr: %s", this->connection_index_, this->address_str_, addr_str);
if (!param->ble_security.auth_cmpl.success) {
this->log_error_("auth fail reason", param->ble_security.auth_cmpl.fail_reason);
} else {

View File

@@ -64,9 +64,12 @@ void BLEService::parse_characteristics() {
characteristic->handle = result.char_handle;
characteristic->service = this;
this->characteristics.push_back(characteristic);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char uuid_buf[espbt::UUID_STR_LEN];
characteristic->uuid.to_str(uuid_buf);
ESP_LOGV(TAG, "[%d] [%s] characteristic %s, handle 0x%x, properties 0x%x", this->client->get_connection_index(),
this->client->address_str(), characteristic->uuid.to_string().c_str(), characteristic->handle,
characteristic->properties);
this->client->address_str(), uuid_buf, characteristic->handle, characteristic->properties);
#endif
offset++;
}
}

View File

@@ -109,7 +109,11 @@ void BLECharacteristic::do_create(BLEService *service) {
esp_attr_control_t control;
control.auto_rsp = ESP_GATT_RSP_BY_APP;
ESP_LOGV(TAG, "Creating characteristic - %s", this->uuid_.to_string().c_str());
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char uuid_buf[esp32_ble::UUID_STR_LEN];
this->uuid_.to_str(uuid_buf);
ESP_LOGV(TAG, "Creating characteristic - %s", uuid_buf);
#endif
esp_bt_uuid_t uuid = this->uuid_.get_uuid();
esp_err_t err = esp_ble_gatts_add_char(service->get_handle(), &uuid, static_cast<esp_gatt_perm_t>(this->permissions_),

View File

@@ -34,7 +34,11 @@ void BLEDescriptor::do_create(BLECharacteristic *characteristic) {
esp_attr_control_t control;
control.auto_rsp = ESP_GATT_AUTO_RSP;
ESP_LOGV(TAG, "Creating descriptor - %s", this->uuid_.to_string().c_str());
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char uuid_buf[esp32_ble::UUID_STR_LEN];
this->uuid_.to_str(uuid_buf);
ESP_LOGV(TAG, "Creating descriptor - %s", uuid_buf);
#endif
esp_bt_uuid_t uuid = this->uuid_.get_uuid();
esp_err_t err = esp_ble_gatts_add_char_descr(this->characteristic_->get_service()->get_handle(), &uuid,
this->permissions_, &this->value_, &control);

View File

@@ -106,7 +106,11 @@ void BLEServer::restart_advertising_() {
}
BLEService *BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t num_handles) {
ESP_LOGV(TAG, "Creating BLE service - %s", uuid.to_string().c_str());
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char uuid_buf[esp32_ble::UUID_STR_LEN];
uuid.to_str(uuid_buf);
ESP_LOGV(TAG, "Creating BLE service - %s", uuid_buf);
#endif
// Calculate the inst_id for the service
uint8_t inst_id = 0;
for (; inst_id < 0xFF; inst_id++) {
@@ -115,7 +119,9 @@ BLEService *BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t n
}
}
if (inst_id == 0xFF) {
ESP_LOGW(TAG, "Could not create BLE service %s, too many instances", uuid.to_string().c_str());
char warn_uuid_buf[esp32_ble::UUID_STR_LEN];
uuid.to_str(warn_uuid_buf);
ESP_LOGW(TAG, "Could not create BLE service %s, too many instances", warn_uuid_buf);
return nullptr;
}
BLEService *service = // NOLINT(cppcoreguidelines-owning-memory)
@@ -128,7 +134,11 @@ BLEService *BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t n
}
void BLEServer::remove_service(ESPBTUUID uuid, uint8_t inst_id) {
ESP_LOGV(TAG, "Removing BLE service - %s %d", uuid.to_string().c_str(), inst_id);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char uuid_buf[esp32_ble::UUID_STR_LEN];
uuid.to_str(uuid_buf);
ESP_LOGV(TAG, "Removing BLE service - %s %d", uuid_buf, inst_id);
#endif
for (auto it = this->services_.begin(); it != this->services_.end(); ++it) {
if (it->uuid == uuid && it->inst_id == inst_id) {
it->service->do_delete();
@@ -137,7 +147,9 @@ void BLEServer::remove_service(ESPBTUUID uuid, uint8_t inst_id) {
return;
}
}
ESP_LOGW(TAG, "BLE service %s %d does not exist", uuid.to_string().c_str(), inst_id);
char warn_uuid_buf[esp32_ble::UUID_STR_LEN];
uuid.to_str(warn_uuid_buf);
ESP_LOGW(TAG, "BLE service %s %d does not exist", warn_uuid_buf, inst_id);
}
BLEService *BLEServer::get_service(ESPBTUUID uuid, uint8_t inst_id) {

View File

@@ -5,7 +5,7 @@ import logging
from esphome import automation
import esphome.codegen as cg
from esphome.components import esp32_ble
from esphome.components import esp32_ble, ota
from esphome.components.esp32 import add_idf_sdkconfig_option
from esphome.components.esp32_ble import (
IDF_MAX_CONNECTIONS,
@@ -328,7 +328,7 @@ async def to_code(config):
# Note: CONFIG_BT_ACL_CONNECTIONS and CONFIG_BTDM_CTRL_BLE_MAX_CONN are now
# configured in esp32_ble component based on max_connections setting
cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts
ota.request_ota_state_listeners() # To be notified when an OTA update starts
cg.add_define("USE_ESP32_BLE_CLIENT")
CORE.add_job(_add_ble_features)

View File

@@ -71,9 +71,13 @@ void ESP32BLETracker::setup() {
global_esp32_ble_tracker = this;
#ifdef USE_OTA
ota::get_global_ota_callback()->add_on_state_callback(
[this](ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) {
#ifdef USE_OTA_STATE_LISTENER
ota::get_global_ota_callback()->add_global_state_listener(this);
#endif
}
#ifdef USE_OTA_STATE_LISTENER
void ESP32BLETracker::on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) {
if (state == ota::OTA_STARTED) {
this->stop_scan();
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
@@ -82,9 +86,8 @@ void ESP32BLETracker::setup() {
}
#endif
}
});
#endif
}
#endif
void ESP32BLETracker::loop() {
if (!this->parent_->is_active()) {
@@ -185,7 +188,10 @@ void ESP32BLETracker::ble_before_disabled_event_handler() { this->stop_scan_();
void ESP32BLETracker::stop_scan_() {
if (this->scanner_state_ != ScannerState::RUNNING && this->scanner_state_ != ScannerState::FAILED) {
// If scanner is already idle, there's nothing to stop - this is not an error
if (this->scanner_state_ != ScannerState::IDLE) {
ESP_LOGE(TAG, "Cannot stop scan: %s", this->scanner_state_to_string_(this->scanner_state_));
}
return;
}
// Reset timeout state machine when stopping scan
@@ -435,24 +441,31 @@ void ESPBTDevice::parse_scan_rst(const BLEScanResult &scan_result) {
ESP_LOGVV(TAG, " Ad Flag: %u", *this->ad_flag_);
}
for (auto &uuid : this->service_uuids_) {
ESP_LOGVV(TAG, " Service UUID: %s", uuid.to_string().c_str());
char uuid_buf[esp32_ble::UUID_STR_LEN];
uuid.to_str(uuid_buf);
ESP_LOGVV(TAG, " Service UUID: %s", uuid_buf);
}
for (auto &data : this->manufacturer_datas_) {
auto ibeacon = ESPBLEiBeacon::from_manufacturer_data(data);
if (ibeacon.has_value()) {
ESP_LOGVV(TAG, " Manufacturer iBeacon:");
ESP_LOGVV(TAG, " UUID: %s", ibeacon.value().get_uuid().to_string().c_str());
char uuid_buf[esp32_ble::UUID_STR_LEN];
ibeacon.value().get_uuid().to_str(uuid_buf);
ESP_LOGVV(TAG, " UUID: %s", uuid_buf);
ESP_LOGVV(TAG, " Major: %u", ibeacon.value().get_major());
ESP_LOGVV(TAG, " Minor: %u", ibeacon.value().get_minor());
ESP_LOGVV(TAG, " TXPower: %d", ibeacon.value().get_signal_power());
} else {
ESP_LOGVV(TAG, " Manufacturer ID: %s, data: %s", data.uuid.to_string().c_str(),
format_hex_pretty(data.data).c_str());
char uuid_buf[esp32_ble::UUID_STR_LEN];
data.uuid.to_str(uuid_buf);
ESP_LOGVV(TAG, " Manufacturer ID: %s, data: %s", uuid_buf, format_hex_pretty(data.data).c_str());
}
}
for (auto &data : this->service_datas_) {
ESP_LOGVV(TAG, " Service data:");
ESP_LOGVV(TAG, " UUID: %s", data.uuid.to_string().c_str());
char uuid_buf[esp32_ble::UUID_STR_LEN];
data.uuid.to_str(uuid_buf);
ESP_LOGVV(TAG, " UUID: %s", uuid_buf);
ESP_LOGVV(TAG, " Data: %s", format_hex_pretty(data.data).c_str());
}

View File

@@ -22,6 +22,10 @@
#include "esphome/components/esp32_ble/ble_uuid.h"
#include "esphome/components/esp32_ble/ble_scan_result.h"
#ifdef USE_OTA_STATE_LISTENER
#include "esphome/components/ota/ota_backend.h"
#endif
namespace esphome::esp32_ble_tracker {
using namespace esp32_ble;
@@ -241,6 +245,9 @@ class ESP32BLETracker : public Component,
public GAPScanEventHandler,
public GATTcEventHandler,
public BLEStatusEventHandler,
#ifdef USE_OTA_STATE_LISTENER
public ota::OTAGlobalStateListener,
#endif
public Parented<ESP32BLE> {
public:
void set_scan_duration(uint32_t scan_duration) { scan_duration_ = scan_duration; }
@@ -274,6 +281,10 @@ class ESP32BLETracker : public Component,
void gap_scan_event_handler(const BLEScanResult &scan_result) override;
void ble_before_disabled_event_handler() override;
#ifdef USE_OTA_STATE_LISTENER
void on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) override;
#endif
/// Add a listener for scanner state changes
void add_scanner_state_listener(BLEScannerStateListener *listener) {
this->scanner_state_listeners_.push_back(listener);

View File

@@ -2,8 +2,8 @@ import logging
from esphome import automation, pins
import esphome.codegen as cg
from esphome.components import i2c
from esphome.components.esp32 import add_idf_component
from esphome.components import i2c, socket
from esphome.components.esp32 import add_idf_component, add_idf_sdkconfig_option
from esphome.components.psram import DOMAIN as psram_domain
import esphome.config_validation as cv
from esphome.const import (
@@ -27,7 +27,7 @@ import esphome.final_validate as fv
_LOGGER = logging.getLogger(__name__)
AUTO_LOAD = ["camera"]
AUTO_LOAD = ["camera", "socket"]
DEPENDENCIES = ["esp32"]
esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera")
@@ -186,7 +186,7 @@ CONFIG_SCHEMA = cv.All(
{
cv.Required(CONF_PIN): pins.internal_gpio_input_pin_number,
cv.Optional(CONF_FREQUENCY, default="20MHz"): cv.All(
cv.frequency, cv.Range(min=8e6, max=20e6)
cv.frequency, cv.float_range(min=8e6, max=20e6)
),
}
),
@@ -324,6 +324,7 @@ SETTERS = {
async def to_code(config):
cg.add_define("USE_CAMERA")
socket.require_wake_loop_threadsafe()
var = cg.new_Pvariable(config[CONF_ID])
await setup_entity(var, config, "camera")
await cg.register_component(var, config)
@@ -352,6 +353,8 @@ async def to_code(config):
cg.add_define("USE_CAMERA")
add_idf_component(name="espressif/esp32-camera", ref="2.1.1")
add_idf_sdkconfig_option("CONFIG_SCCB_HARDWARE_I2C_DRIVER_NEW", True)
add_idf_sdkconfig_option("CONFIG_SCCB_HARDWARE_I2C_DRIVER_LEGACY", False)
for conf in config.get(CONF_ON_STREAM_START, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)

View File

@@ -11,6 +11,10 @@ namespace esphome {
namespace esp32_camera {
static const char *const TAG = "esp32_camera";
static constexpr size_t FRAMEBUFFER_TASK_STACK_SIZE = 1792;
#if ESPHOME_LOG_LEVEL < ESPHOME_LOG_LEVEL_VERBOSE
static constexpr uint32_t FRAME_LOG_INTERVAL_MS = 60000;
#endif
/* ---------------- public API (derivated) ---------------- */
void ESP32Camera::setup() {
@@ -40,7 +44,7 @@ void ESP32Camera::setup() {
this->framebuffer_return_queue_ = xQueueCreate(1, sizeof(camera_fb_t *));
xTaskCreatePinnedToCore(&ESP32Camera::framebuffer_task,
"framebuffer_task", // name
1024, // stack size
FRAMEBUFFER_TASK_STACK_SIZE, // stack size
this, // task pv params
1, // priority
nullptr, // handle
@@ -164,6 +168,19 @@ void ESP32Camera::dump_config() {
}
void ESP32Camera::loop() {
// Fast path: skip all work when truly idle
// (no current image, no pending requests, and not time for idle request yet)
const uint32_t now = App.get_loop_component_start_time();
if (!this->current_image_ && !this->has_requested_image_()) {
// Only check idle interval when we're otherwise idle
if (this->idle_update_interval_ != 0 && now - this->last_idle_request_ > this->idle_update_interval_) {
this->last_idle_request_ = now;
this->request_image(camera::IDLE);
} else {
return;
}
}
// check if we can return the image
if (this->can_return_image_()) {
// return image
@@ -172,13 +189,6 @@ void ESP32Camera::loop() {
this->current_image_.reset();
}
// request idle image every idle_update_interval
const uint32_t now = App.get_loop_component_start_time();
if (this->idle_update_interval_ != 0 && now - this->last_idle_request_ > this->idle_update_interval_) {
this->last_idle_request_ = now;
this->request_image(camera::IDLE);
}
// Check if we should fetch a new image
if (!this->has_requested_image_())
return;
@@ -204,7 +214,20 @@ void ESP32Camera::loop() {
}
this->current_image_ = std::make_shared<ESP32CameraImage>(fb, this->single_requesters_ | this->stream_requesters_);
ESP_LOGD(TAG, "Got Image: len=%u", fb->len);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
ESP_LOGV(TAG, "Got Image: len=%u", fb->len);
#else
// Initialize log time on first frame to ensure accurate interval measurement
if (this->frame_count_ == 0) {
this->last_log_time_ = now;
}
this->frame_count_++;
if (now - this->last_log_time_ >= FRAME_LOG_INTERVAL_MS) {
ESP_LOGD(TAG, "Received %u images in last %us", this->frame_count_, FRAME_LOG_INTERVAL_MS / 1000);
this->last_log_time_ = now;
this->frame_count_ = 0;
}
#endif
for (auto *listener : this->listeners_) {
listener->on_camera_image(this->current_image_);
}
@@ -405,6 +428,10 @@ void ESP32Camera::framebuffer_task(void *pv) {
while (true) {
camera_fb_t *framebuffer = esp_camera_fb_get();
xQueueSend(that->framebuffer_get_queue_, &framebuffer, portMAX_DELAY);
// Only wake the main loop if there's a pending request to consume the frame
if (that->has_requested_image_()) {
App.wake_loop_threadsafe();
}
// return is no-op for config with 1 fb
xQueueReceive(that->framebuffer_return_queue_, &framebuffer, portMAX_DELAY);
esp_camera_fb_return(framebuffer);

View File

@@ -2,6 +2,7 @@
#ifdef USE_ESP32
#include <atomic>
#include <esp_camera.h>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
@@ -205,14 +206,18 @@ class ESP32Camera : public camera::Camera {
esp_err_t init_error_{ESP_OK};
std::shared_ptr<ESP32CameraImage> current_image_;
uint8_t single_requesters_{0};
uint8_t stream_requesters_{0};
std::atomic<uint8_t> single_requesters_{0};
std::atomic<uint8_t> stream_requesters_{0};
QueueHandle_t framebuffer_get_queue_;
QueueHandle_t framebuffer_return_queue_;
std::vector<camera::CameraListener *> listeners_;
uint32_t last_idle_request_{0};
uint32_t last_update_{0};
#if ESPHOME_LOG_LEVEL < ESPHOME_LOG_LEVEL_VERBOSE
uint32_t last_log_time_{0};
uint16_t frame_count_{0};
#endif
#ifdef USE_I2C
i2c::InternalI2CBus *i2c_bus_{nullptr};
#endif // USE_I2C

View File

@@ -191,7 +191,8 @@ async def to_code(config):
cg.add_define(ThreadModel.SINGLE)
cg.add_platformio_option(
"extra_scripts", ["pre:testing_mode.py", "post:post_build.py"]
"extra_scripts",
["pre:testing_mode.py", "pre:exclude_updater.py", "post:post_build.py"],
)
conf = config[CONF_FRAMEWORK]
@@ -278,3 +279,8 @@ def copy_files():
testing_mode_file,
CORE.relative_build_path("testing_mode.py"),
)
exclude_updater_file = dir / "exclude_updater.py.script"
copy_file_if_changed(
exclude_updater_file,
CORE.relative_build_path("exclude_updater.py"),
)

View File

@@ -0,0 +1,21 @@
# pylint: disable=E0602
Import("env") # noqa
import os
# Filter out Updater.cpp from the Arduino core build
# This saves 228 bytes of .bss by not instantiating the global Update object
# ESPHome uses its own native OTA backend instead
def filter_updater_from_core(env, node):
"""Filter callback to exclude Updater.cpp from framework build."""
path = node.get_path()
if path.endswith("Updater.cpp"):
print(f"ESPHome: Excluding {os.path.basename(path)} from build (using native OTA backend)")
return None
return node
# Apply the filter to framework sources
env.AddBuildMiddleware(filter_updater_from_core, "**/cores/esp8266/Updater.cpp")

View File

@@ -16,7 +16,7 @@ def valid_pwm_pin(value):
esp8266_pwm_ns = cg.esphome_ns.namespace("esp8266_pwm")
ESP8266PWM = esp8266_pwm_ns.class_("ESP8266PWM", output.FloatOutput, cg.Component)
SetFrequencyAction = esp8266_pwm_ns.class_("SetFrequencyAction", automation.Action)
validate_frequency = cv.All(cv.frequency, cv.Range(min=1.0e-6))
validate_frequency = cv.All(cv.frequency, cv.float_range(min=1.0e-6))
CONFIG_SCHEMA = cv.All(
output.FLOAT_OUTPUT_SCHEMA.extend(

View File

@@ -31,7 +31,7 @@ CONFIG_SCHEMA = cv.All(
}
)
),
cv.only_with_esp_idf,
cv.only_on_esp32,
only_on_variant(supported=[VARIANT_ESP32P4]),
)

View File

@@ -10,7 +10,7 @@
#endif
#include "esphome/components/network/util.h"
#include "esphome/components/ota/ota_backend.h"
#include "esphome/components/ota/ota_backend_arduino_esp8266.h"
#include "esphome/components/ota/ota_backend_esp8266.h"
#include "esphome/components/ota/ota_backend_arduino_libretiny.h"
#include "esphome/components/ota/ota_backend_arduino_rp2040.h"
#include "esphome/components/ota/ota_backend_esp_idf.h"
@@ -41,10 +41,6 @@ static constexpr size_t SHA256_HEX_SIZE = 64; // SHA256 hash as hex string (32
#endif // USE_OTA_PASSWORD
void ESPHomeOTAComponent::setup() {
#ifdef USE_OTA_STATE_CALLBACK
ota::register_ota_platform(this);
#endif
this->server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections
if (this->server_ == nullptr) {
this->log_socket_error_(LOG_STR("creation"));
@@ -297,8 +293,8 @@ void ESPHomeOTAComponent::handle_data_() {
// accidentally trigger the update process.
this->log_start_(LOG_STR("update"));
this->status_set_warning();
#ifdef USE_OTA_STATE_CALLBACK
this->state_callback_.call(ota::OTA_STARTED, 0.0f, 0);
#ifdef USE_OTA_STATE_LISTENER
this->notify_state_(ota::OTA_STARTED, 0.0f, 0);
#endif
// This will block for a few seconds as it locks flash
@@ -357,8 +353,8 @@ void ESPHomeOTAComponent::handle_data_() {
last_progress = now;
float percentage = (total * 100.0f) / ota_size;
ESP_LOGD(TAG, "Progress: %0.1f%%", percentage);
#ifdef USE_OTA_STATE_CALLBACK
this->state_callback_.call(ota::OTA_IN_PROGRESS, percentage, 0);
#ifdef USE_OTA_STATE_LISTENER
this->notify_state_(ota::OTA_IN_PROGRESS, percentage, 0);
#endif
// feed watchdog and give other tasks a chance to run
this->yield_and_feed_watchdog_();
@@ -387,8 +383,8 @@ void ESPHomeOTAComponent::handle_data_() {
delay(10);
ESP_LOGI(TAG, "Update complete");
this->status_clear_warning();
#ifdef USE_OTA_STATE_CALLBACK
this->state_callback_.call(ota::OTA_COMPLETED, 100.0f, 0);
#ifdef USE_OTA_STATE_LISTENER
this->notify_state_(ota::OTA_COMPLETED, 100.0f, 0);
#endif
delay(100); // NOLINT
App.safe_reboot();
@@ -402,8 +398,8 @@ error:
}
this->status_momentary_error("err", 5000);
#ifdef USE_OTA_STATE_CALLBACK
this->state_callback_.call(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
#ifdef USE_OTA_STATE_LISTENER
this->notify_state_(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
#endif
}
@@ -658,12 +654,7 @@ bool ESPHomeOTAComponent::handle_auth_send_() {
this->auth_buf_[0] = this->auth_type_;
hasher->get_hex(buf);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char log_buf[65]; // Fixed size for SHA256 hex (64) + null, works for MD5 (32) too
memcpy(log_buf, buf, hex_size);
log_buf[hex_size] = '\0';
ESP_LOGV(TAG, "Auth: Nonce is %s", log_buf);
#endif
ESP_LOGV(TAG, "Auth: Nonce is %.*s", hex_size, buf);
}
// Try to write auth_type + nonce
@@ -743,23 +734,13 @@ bool ESPHomeOTAComponent::handle_auth_read_() {
hasher->add(nonce, hex_size * 2); // Add both nonce and cnonce (contiguous in buffer)
hasher->calculate();
ESP_LOGV(TAG, "Auth: CNonce is %.*s", hex_size, cnonce);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char log_buf[65]; // Fixed size for SHA256 hex (64) + null, works for MD5 (32) too
// Log CNonce
memcpy(log_buf, cnonce, hex_size);
log_buf[hex_size] = '\0';
ESP_LOGV(TAG, "Auth: CNonce is %s", log_buf);
// Log computed hash
hasher->get_hex(log_buf);
log_buf[hex_size] = '\0';
ESP_LOGV(TAG, "Auth: Result is %s", log_buf);
// Log received response
memcpy(log_buf, response, hex_size);
log_buf[hex_size] = '\0';
ESP_LOGV(TAG, "Auth: Response is %s", log_buf);
char computed_hash[65]; // Buffer for hex-encoded hash (max expected length + null terminator)
hasher->get_hex(computed_hash);
ESP_LOGV(TAG, "Auth: Result is %.*s", hex_size, computed_hash);
#endif
ESP_LOGV(TAG, "Auth: Response is %.*s", hex_size, response);
// Compare response
bool matches = hasher->equals_hex(response);

View File

@@ -80,6 +80,7 @@ class ESPHomeOTAComponent : public ota::OTAComponent {
#ifdef USE_OTA_PASSWORD
std::string password_;
std::unique_ptr<uint8_t[]> auth_buf_;
#endif // USE_OTA_PASSWORD
std::unique_ptr<socket::Socket> server_;
@@ -93,7 +94,6 @@ class ESPHomeOTAComponent : public ota::OTAComponent {
uint8_t handshake_buf_pos_{0};
uint8_t ota_features_{0};
#ifdef USE_OTA_PASSWORD
std::unique_ptr<uint8_t[]> auth_buf_;
uint8_t auth_buf_pos_{0};
uint8_t auth_type_{0}; // Store auth type to know which hasher to use
#endif // USE_OTA_PASSWORD

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