1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-05 17:41:49 +00:00

Compare commits

...

187 Commits

Author SHA1 Message Date
J. Nick Koston
58fb37ead3 Merge branch 'dev' into select_options_convert_remaining 2025-11-04 18:15:28 -06:00
Gnuspice
64f8963566 [const] Move CONF_ENABLED to const.py (#11719) 2025-11-05 12:46:06 +13:00
Jesse Hills
de01cb13de Merge branch 'dev' into select_options_convert_remaining 2025-11-05 11:33:18 +13:00
J. Nick Koston
6f7e54c3f3 [select] Refactor to index-based operations for immediate and future RAM savings (#11623) 2025-11-05 11:33:01 +13:00
J. Nick Koston
c7ae424613 [display] Optimize display writers with function pointers for stateless lambdas (#11629) 2025-11-05 11:14:54 +13:00
Clyde Stubbs
c5e5609e92 [lvgl] Fix case sensitivity in flex layout (#11717) 2025-11-05 09:00:12 +11:00
J. Nick Koston
885508775f [fan] Remove duplicate preset mode storage to save RAM (#11632) 2025-11-05 10:55:37 +13:00
J. Nick Koston
531b27582a [network] Store use_address in RODATA to save RAM (#11707) 2025-11-05 10:52:10 +13:00
J. Nick Koston
aed7505f53 [automations] Reduce memory usage in if/while/repeat actions (32-36 bytes per instance) (#11650)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-11-05 10:48:20 +13:00
Javier Peletier
191a88c2dc [gt911] Fix gt911 touchscreen with reset pin not initializing when loglevel is set to NONE (#11715) 2025-11-04 13:38:59 -05:00
SeByDocKy
968df6cb3f [gp8403] Add gp8413 (15 bits) DAC model (#7726)
Co-authored-by: Djordje Mandic <6750655+DjordjeMandic@users.noreply.github.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
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-11-04 12:16:11 -05:00
Cameron Steel
71fa88c9d4 [max7219digit] support flip_x when rotate_chip is 90° or 270° (#6109)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-11-04 16:32:23 +00:00
Chaser Huang
84f7cacef9 [sgp30] Fix reading from preexisting stored baseline even with store_baseline:false (#7922)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-11-04 15:41:30 +00:00
leejoow
13e3c03a61 [dallas_temp] add support for index (#11346) 2025-11-03 22:30:53 -08:00
J. Nick Koston
060bb4159f [ci] Cache component dependency graph for up to 3.4x faster determine-jobs (#11648) 2025-11-04 17:38:57 +13:00
J. Nick Koston
980098ca77 [ci] Fix non-component files incorrectly detected as components (#11701) 2025-11-04 16:47:11 +13:00
J. Nick Koston
4d2f9db861 [esp32_ble] Remove leftover lwip/sockets.h include (#11702) 2025-11-04 16:46:34 +13:00
J. Nick Koston
4c31cb57ea [espnow] Add wake_loop_threadsafe() for low-latency event processing (#11696) 2025-11-04 16:45:57 +13:00
J. Nick Koston
5257900495 [mqtt] Add wake_loop_threadsafe() for low-latency event processing on ESP32 (#11695) 2025-11-04 16:45:20 +13:00
Clyde Stubbs
3e086c2127 [lvgl] Fix rotation with unusual width (#11680) 2025-11-04 16:43:27 +13:00
Clyde Stubbs
0b04361fc0 [lvgl] Layout improvements (#10149)
Co-authored-by: clydeps <U5yx99dok9>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-04 16:39:27 +13:00
Clyde Stubbs
758ac58343 [psram] Require mode for S3 (#11470)
Co-authored-by: clydeps <U5yx99dok9>
2025-11-04 16:38:43 +13:00
Jesse Hills
ce63137565 Merge branch 'release' into dev 2025-11-04 16:04:48 +13:00
Jesse Hills
00155989af Merge pull request #11703 from esphome/bump-2025.10.4
2025.10.4
2025-11-04 16:04:04 +13:00
Jonathan Swoboda
326975ccad [core] Fix ESPTime crash (#11705) 2025-11-03 21:09:34 -05:00
J. Nick Koston
6220084fe6 [ci] Fix memory impact analysis to filter incompatible platform components (#11706) 2025-11-04 12:23:04 +11:00
Keith Burzinski
59326f137e [tinyusb] New component (#11678) 2025-11-03 18:29:30 -06:00
Keith Burzinski
266e4ae91f [helpers] Add get_mac_address_into_buffer() (#11700) 2025-11-03 23:30:37 +00:00
Clyde Stubbs
99d1a9cf6e [usb_uart] Fixes for transfer queue allocation (#11548) 2025-11-04 10:23:45 +11:00
J. Nick Koston
99ce989eae [micro_wake_word] Add wake_loop_threadsafe() for low-latency wake word detection (#11698) 2025-11-03 16:30:35 -06:00
Jesse Hills
a3583da17d Bump version to 2025.10.4 2025-11-04 11:25:33 +13:00
Clyde Stubbs
0f6fd91304 [sdl] Fix keymappings (#11635) 2025-11-04 11:25:33 +13:00
Clyde Stubbs
2f5f1da16f [lvgl] Fix event for binary sensor (#11636) 2025-11-04 11:25:33 +13:00
Clyde Stubbs
51745d1d5e [image] Catch and report svg load errors (#11619) 2025-11-04 11:25:33 +13:00
J. Nick Koston
fecc8399a5 [lvgl] Fix nested lambdas in automations unable to access parameters (#11583)
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2025-11-04 11:25:33 +13:00
Clyde Stubbs
db395a662d [mipi_rgb] Fix rotation with custom model (#11585) 2025-11-04 11:25:33 +13:00
Anton Sergunov
641dd24b21 Fix the LiberTiny bug with UART pin setup (#11518) 2025-11-04 11:25:32 +13:00
Keith Burzinski
57f2e32b00 [uart] Fix order of initialization calls (#11510) 2025-11-04 11:25:32 +13:00
Clyde Stubbs
8aa8bb8f98 [epaper_spi] Refactoring (#11540)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-04 10:45:32 +13:00
Jonathan Swoboda
9c7cb30ae5 [esp32_hosted] Initial OTA implementation (#11562) 2025-11-03 14:08:50 -06:00
J. Nick Koston
fb7dbc9910 [usb_host] Add wake_loop_threadsafe() for low-latency USB event processing (#11683) 2025-11-03 13:50:39 -06:00
J. Nick Koston
3f12630a6b [core][esp32_ble][socket] Add wake_loop_threadsafe() helper for background thread wakeups (#11681) 2025-11-04 08:13:37 +13:00
tomaszduda23
06d0787ee0 [nrf52, i2c] i2c support for nrf52 (#8150)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: Ludovic BOUÉ <lboue@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-11-03 16:42:49 +00:00
Paul Strawder
cb039b42aa [esp32] Make the loop task's stack size configurable (#10564)
Co-authored-by: Paul Strawder <paul@korro.ai>
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: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-11-03 16:34:53 +00:00
Nathan Bernard
f05f45af74 Add support for Mopeka standard check alternate ID (#10907)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-11-03 15:17:28 +00:00
J. Nick Koston
772ebe9db2 [select] Convert remaining components to use index-based control() 2025-11-03 08:34:35 -06:00
J. Nick Koston
014722bcbb Merge branch 'dev' into select_options 2025-11-03 08:23:34 -06:00
J. Nick Koston
1ec1692c77 [mqtt] Fix climate custom fan mode and preset compilation errors (#11692) 2025-11-03 08:23:04 -06:00
Kent Gibson
7e1cea8e69 [template] alarm_control_panel more ESP_LOGCONFIG reductions (#11691) 2025-11-03 08:05:33 -06:00
J. Nick Koston
a6d98cec0b address review comments 2025-11-02 23:41:24 -06:00
J. Nick Koston
8e3d53cb03 address review comments 2025-11-02 23:36:42 -06:00
J. Nick Koston
1150f8e6ba address review comments 2025-11-02 23:33:08 -06:00
tomaszduda23
0e792d0791 [nrf52,debug] fix status of nRESET pin, add extra registry from UICR (#11667)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2025-11-03 05:20:08 +00:00
J. Nick Koston
55b89faac8 Merge branch 'dev' into select_options 2025-11-02 23:17:04 -06:00
J. Nick Koston
42833c85f5 [climate] Replace std::vector<std::string> with const char* for custom fan modes and presets (#11621) 2025-11-02 23:16:39 -06:00
dependabot[bot]
a41c7b2b3c Bump aioesphomeapi from 42.5.0 to 42.6.0 (#11682)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-02 22:16:38 -06:00
J. Nick Koston
4dd3c90663 [esp32_ble] Wake main loop for GAP security events (#11677) 2025-11-03 15:55:17 +13:00
J. Nick Koston
0f0cd1f706 [core] Avoid redundant millis() calls in base_automation loop methods (#11676) 2025-11-03 01:40:13 +00:00
J. Nick Koston
4a5e6576c8 [scheduler] Refactor call() for improved code organization (#11643) 2025-11-03 14:29:29 +13:00
J. Nick Koston
cf76c3a747 [web_server_idf] Reduce flash by eliminating temporary string allocations in event formatting (#11658)
Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
2025-11-03 14:23:03 +13:00
J. Nick Koston
3f05fd82e5 [fan] Use std::vector for preset modes, preserve config order (#11483) 2025-11-03 14:18:59 +13:00
J. Nick Koston
34244afea1 [esp32_ble] Reduce GATT event latency from 8ms to 12μs with notification socket (#11663) 2025-11-03 14:16:26 +13:00
J. Nick Koston
4838eff382 [web_server] Use zero-copy entity ID comparison in request handlers (#11644) 2025-11-03 14:12:56 +13:00
J. Nick Koston
712421b82b [web_server] Eliminate nested lambdas in DeferredUpdateEventSourceList (#11641) 2025-11-03 14:10:18 +13:00
J. Nick Koston
7a1297ec84 [web_server] Remove redundant assignment in deq_push_back_with_dedup_ (#11642) 2025-11-03 14:08:12 +13:00
J. Nick Koston
40f919eaa6 Add action continuation tests (#11674) 2025-11-03 14:07:03 +13:00
J. Nick Koston
01ae86145a [ble_client] Fix premature disconnections by reading characteristics immediately after service discovery (#11410) 2025-11-03 14:06:40 +13:00
J. Nick Koston
17ab20ef61 [esp32_ble] Optimize loop() to reduce flash usage by ~104 bytes (#11627) 2025-11-03 14:05:36 +13:00
J. Nick Koston
1509ed8d23 [esphome][ota] Add write_byte_() helper to reduce code duplication (#11511) 2025-11-03 14:04:06 +13:00
Clyde Stubbs
3e17767f6a [font][image] Use ESPHome urls for remote images (#11675) 2025-11-03 00:50:15 +00:00
Clyde Stubbs
19e275dc02 [component] Add is_idle method and condition (#11651) 2025-11-03 11:33:44 +11:00
Kjell Braden
86402be9e3 actions: fix loop re-entry (#7972)
Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
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: J. Nick Koston <nick@home-assistant.io>
2025-11-02 17:02:13 -06:00
tomaszduda23
8a8a80e107 [nrf52, zigbee] OnlyWith support list of components (#11533)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-11-02 16:44:52 -06:00
Juan Antonio Aldea
79378a930e Use lists inits initialization instead of std::fill (#11532) 2025-11-02 16:26:20 -06:00
Jimmy Hedman
c822ec152f Enable IPv6 for host (#11630) 2025-11-02 16:22:49 -06:00
tomaszduda23
50e7ce55e7 [nrf52] enable nrf52 test (#11379) 2025-11-02 16:20:30 -06:00
tomaszduda23
70ea3af578 [nrf52,gpio] switch input gpio to polling mode (#11664) 2025-11-02 16:19:28 -06:00
Guillermo Ruffino
338190abec ESP32 Pin loopTask to CORE 1 (#11669) 2025-11-02 16:11:02 -06:00
Edward Firmo
425c88ee94 [nextion] Send auto_wake_on_touch as part of startup commands on loop (#11670) 2025-11-02 16:06:13 -06:00
Kjell Braden
f6946c0b9a add integration test for script re-entry argument issue (#11652)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-11-02 15:08:45 -06:00
J. Nick Koston
edde2fc94c Add basic tests for web_server_idf (#11659) 2025-11-02 08:18:17 -06:00
J. Nick Koston
1fc3165b58 [api] Remove unnecessary intermediate variable in frame helpers (#11668) 2025-11-01 22:43:39 -05:00
J. Nick Koston
d25121a55c [core] Remove redundant fd bounds check in yield_with_select_() (#11666) 2025-11-01 22:43:08 -05:00
tomaszduda23
55af818629 [nrf52] fix compilation warning (#11656) 2025-11-01 11:18:38 -05:00
J. Nick Koston
c662697ca7 [json] Fix component test compilation errors (#11647) 2025-11-01 11:18:10 -05:00
J. Nick Koston
e28c152298 [cpp_generator] Align isinstance() with codebase style (tuple vs PEP 604) (#11645) 2025-11-01 20:48:58 +11:00
Clyde Stubbs
0b4d445794 [sdl] Fix keymappings (#11635) 2025-11-01 17:45:42 +11:00
Clyde Stubbs
4d1d37a911 [lvgl] Fix event for binary sensor (#11636) 2025-11-01 17:37:07 +11:00
Clyde Stubbs
8df5a3a630 [lvgl] Trigger improvements and additions (#11628) 2025-11-01 17:27:28 +11:00
J. Nick Koston
5a5894eaa3 [ruff] Remove deprecated UP038 rule from ignore list (#11646) 2025-11-01 17:05:26 +11:00
Clyde Stubbs
d9d2d2f6b9 [automations] Update error message (#11640) 2025-11-01 15:17:23 +11:00
Clyde Stubbs
30f2a4395f [image] Catch and report svg load errors (#11619) 2025-11-01 11:08:28 +11:00
dependabot[bot]
292abd1187 Bump ruff from 0.14.2 to 0.14.3 (#11633)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-10-31 19:46:50 +00:00
Javier Peletier
6d0527ff2a [substitutions] fix jinja parsing strings that look like sets as sets (#11611) 2025-10-31 14:04:55 -05:00
dependabot[bot]
fd64585f99 Bump github/codeql-action from 4.31.0 to 4.31.2 (#11626)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-30 16:50:06 -05:00
J. Nick Koston
19e1427d92 wip 2025-10-30 15:40:10 -05:00
J. Nick Koston
2a73fd3fd6 esp8266 2025-10-30 15:38:40 -05:00
J. Nick Koston
10b9ec32a8 preen 2025-10-30 15:33:19 -05:00
J. Nick Koston
774cdd33bc cleaner 2025-10-30 15:27:44 -05:00
J. Nick Koston
394d50a328 esphom prefers this-> 2025-10-30 15:24:02 -05:00
J. Nick Koston
f86c74ff02 preen 2025-10-30 15:20:50 -05:00
J. Nick Koston
3552d29167 preen 2025-10-30 14:30:58 -05:00
J. Nick Koston
567672171a force inline 2025-10-30 14:28:09 -05:00
J. Nick Koston
f447aaed8d force inline 2025-10-30 14:26:37 -05:00
J. Nick Koston
1a9aa23ae9 force inline 2025-10-30 14:25:35 -05:00
J. Nick Koston
6cab143db2 break it out, logic was too hard to follow 2025-10-30 14:20:28 -05:00
J. Nick Koston
867ff200ce break it out, logic was too hard to follow 2025-10-30 14:18:56 -05:00
J. Nick Koston
9f62df1456 break it out, logic was too hard to follow 2025-10-30 14:16:56 -05:00
J. Nick Koston
c02d316866 tidy 2025-10-30 14:07:49 -05:00
J. Nick Koston
54c536cbe2 missed some 2025-10-30 13:40:33 -05:00
J. Nick Koston
849483eb3b silience warning 2025-10-30 13:35:35 -05:00
J. Nick Koston
d496676c84 preen 2025-10-30 13:30:22 -05:00
J. Nick Koston
7d2ebabec7 give people time to migrate since we can 2025-10-30 13:28:27 -05:00
J. Nick Koston
ad5752f68e give people time to migrate since we can 2025-10-30 13:25:31 -05:00
J. Nick Koston
2e6dab89ff preen 2025-10-30 13:19:45 -05:00
J. Nick Koston
6dff2d6240 cleanups 2025-10-30 13:17:25 -05:00
J. Nick Koston
b6d178b8c1 cleanups 2025-10-30 13:12:28 -05:00
J. Nick Koston
fd8726b479 comment it 2025-10-30 13:07:03 -05:00
J. Nick Koston
f6aee64ec1 preen 2025-10-30 13:02:37 -05:00
J. Nick Koston
58a517afa6 preen 2025-10-30 13:01:32 -05:00
J. Nick Koston
a02b90129d preen 2025-10-30 13:00:02 -05:00
J. Nick Koston
d1adf79fc3 preen 2025-10-30 12:45:41 -05:00
J. Nick Koston
29887e1da5 preen 2025-10-30 12:43:50 -05:00
J. Nick Koston
5f4f6ced32 preen 2025-10-30 12:39:18 -05:00
J. Nick Koston
cf99bab87b preen 2025-10-30 12:38:12 -05:00
J. Nick Koston
c2902c9671 preen 2025-10-30 12:33:10 -05:00
J. Nick Koston
1c0a5a9765 preen 2025-10-30 12:32:37 -05:00
J. Nick Koston
df014f0217 preen 2025-10-30 12:28:19 -05:00
J. Nick Koston
18783ff20b preen 2025-10-30 12:26:47 -05:00
J. Nick Koston
0db55ef2dd select by index 2025-10-30 12:14:53 -05:00
Markus
077cce9848 [core] .local addresses are only resolvable if mDNS is enabled (#11508) 2025-10-30 10:08:08 -05:00
J. Nick Koston
bd87e56bc7 [e131] Replace std::set with std::vector to reduce flash usage (#11598) 2025-10-30 15:14:03 +13:00
J. Nick Koston
58235049e3 [template] Eliminate optional wrapper to save 4 bytes RAM per instance (#11610) 2025-10-30 15:06:21 +13:00
J. Nick Koston
29ed3c20af [gpio] Skip set_use_interrupt call when using default value (#11612) 2025-10-30 14:28:38 +13:00
J. Nick Koston
08aae39ea4 [ci] Consolidate component splitting into determine-jobs (#11614) 2025-10-30 14:27:28 +13:00
J. Nick Koston
03fd114371 [ci] Restore parallel execution for clang-tidy split mode (#11613) 2025-10-30 14:26:37 +13:00
Stuart Parmenter
918650f15a [lvgl] memset canvas buffer to prevent display of random garbage (#11582)
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2025-10-29 21:06:45 +00:00
Stuart Parmenter
287f65cbaf [lvgl] fix typo from previous refactor (#11596) 2025-10-30 07:27:31 +11:00
Javier Peletier
f18c70a256 [core] Fix substitution id redefinition false positive (#11603) 2025-10-30 07:06:55 +13:00
Jonathan Swoboda
6fb490f49b [remote_transmitter] Add non-blocking mode (#11524) 2025-10-29 12:40:22 -04:00
J. Nick Koston
66cf7c3a3b [lvgl] Fix nested lambdas in automations unable to access parameters (#11583)
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2025-10-29 17:07:48 +11:00
dependabot[bot]
f29021b5ef Bump ruff from 0.14.1 to 0.14.2 (#11519)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-10-29 05:23:42 +00:00
dependabot[bot]
7549ca4d39 Bump actions/download-artifact from 5.0.0 to 6.0.0 (#11521)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-29 00:20:13 -05:00
dependabot[bot]
33e7a2101b Bump actions/upload-artifact from 4.6.2 to 5.0.0 (#11520)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-29 00:20:05 -05:00
dependabot[bot]
59a216bfcb Bump github/codeql-action from 4.30.9 to 4.31.0 (#11522)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-29 00:19:47 -05:00
Jesse Hills
09d89000ad [core] Remove deprecated schema constants (#11591) 2025-10-29 00:14:02 -05:00
Kent Gibson
b6c9ece0e6 template_alarm_control_panel readability improvements (#11593) 2025-10-29 00:10:36 -05:00
dependabot[bot]
7169556722 Bump aioesphomeapi from 42.4.0 to 42.5.0 (#11597)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-29 04:46:47 +00:00
J. Nick Koston
f6e4c0cb52 [ci] Fix component tests not running when only test files change (#11580) 2025-10-29 16:22:28 +13:00
J. Nick Koston
f3634edc22 [select] Store options in flash to reduce RAM usage (#11514) 2025-10-29 15:28:16 +13:00
Jesse Hills
a609343cb6 [fan] Remove deprecated set_speed function (#11590) 2025-10-28 21:06:30 -05:00
Clyde Stubbs
5528c3c765 [mipi_rgb] Fix rotation with custom model (#11585) 2025-10-29 14:37:14 +13:00
Anton Sergunov
0d805355f5 Fix the LiberTiny bug with UART pin setup (#11518) 2025-10-29 14:33:16 +13:00
Jesse Hills
99f48ae51c [logger] Improve level validation errors (#11589)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-29 01:29:40 +00:00
Jesse Hills
25e4aafd71 [ci] Fix auto labeller workflow with wrong comment for too-big with labels (#11592) 2025-10-29 14:28:29 +13:00
Kent Gibson
4f2d54be4e template_alarm_control_panel cleanups (#11469) 2025-10-29 13:48:26 +13:00
dependabot[bot]
249cd7415b Bump aioesphomeapi from 42.3.0 to 42.4.0 (#11586)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-29 00:32:41 +00:00
J. Nick Koston
78d780105b [ci] Change upper Python version being tested to 3.13 (#11587) 2025-10-28 19:24:37 -05:00
Jesse Hills
466d4522bc [http_request] Pass trigger variables into on_response/on_error (#11464) 2025-10-29 12:17:16 +13:00
Javier Peletier
e462217500 [packages] Tighten package validation (#11584) 2025-10-29 11:18:47 +13:00
J. Nick Koston
f1bce262ed [uart] Optimize UART components to eliminate temporary vector allocations (#11570)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-10-29 09:48:20 +13:00
J. Nick Koston
7ed7e7ad26 [climate] Replace std::set with FiniteSetMask for trait storage (#11466) 2025-10-29 08:46:44 +13:00
J. Nick Koston
08b8454555 [ble_client] Use function pointers for lambda actions and sensors (#11564)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-10-29 08:10:32 +13:00
J. Nick Koston
0119e17f04 [ci] Remove base bus components exclusion from memory impact analysis (#11572) 2025-10-29 08:08:13 +13:00
J. Nick Koston
c3f40de844 [modbus_controller] Optimize lambdas to use function pointers instead of std::function (#11566) 2025-10-29 08:06:13 +13:00
J. Nick Koston
7dd829cfca [esp32_ble_server][esp32_improv] Eliminate unnecessary heap allocations (#11569) 2025-10-29 08:05:12 +13:00
J. Nick Koston
da19673f51 Add additional uart test coverage (#11571) 2025-10-29 08:03:09 +13:00
rwrozelle
f5e32d03d0 [http_request] update timeout to be uint32_t (#11577) 2025-10-28 12:41:48 -04:00
J. Nick Koston
f3b69383fd Add additional modbus compile tests (#11567) 2025-10-28 16:43:16 +13:00
J. Nick Koston
aba72809d3 Additional tests for ble_client lambdas (#11565) 2025-10-28 16:43:10 +13:00
aanban
85205a28d2 [remote_base] add support for Dyson cool AM07 tower fan (#10163)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-10-27 22:49:16 -04:00
Edward Firmo
285e006637 [nextion] Add set_component_visibility() method for dynamic visibility control (#11530)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-10-28 13:22:28 +13:00
Edward Firmo
5647f36900 [nextion] Remove TFT upload baud rate validation to reduce flash usage (#11012) 2025-10-28 13:21:17 +13:00
Samuel Sieb
1e9309ffff [tuya] allow enum for eco id (#11544)
Co-authored-by: Samuel Sieb <samuel@sieb.net>
2025-10-28 13:20:21 +13:00
Daniel Herrmann
ce8a6a6c43 fix: load_cert_chain requires the path, not a file object (#11543) 2025-10-28 12:24:13 +13:00
J. Nick Koston
dfb4b31bf9 [template] Store initial option as index in template select (#11523) 2025-10-28 11:37:40 +13:00
J. Nick Koston
31b1b50ad9 [number] Skip set_mode call when using default AUTO mode (#11537) 2025-10-28 11:16:38 +13:00
J. Nick Koston
3377080272 [core] Simplify ESPTime::strftime() and save 20 bytes flash (#11539) 2025-10-28 11:16:09 +13:00
Keith Burzinski
d65ad69338 [uart] Fix order of initialization calls (#11510) 2025-10-27 17:09:45 -05:00
J. Nick Koston
dfa69173ea [api] Use FixedVector const references for service array arguments (#11546) 2025-10-28 11:03:44 +13:00
J. Nick Koston
f44615cc8d [template] Optimize all template platforms to use function pointers for stateless lambdas (#11555) 2025-10-28 11:00:02 +13:00
J. Nick Koston
bda4769bd3 [core] Optimize TemplatableValue to use function pointers for stateless lambdas (#11554) 2025-10-27 21:05:40 +00:00
J. Nick Koston
14b057f54e [light] Optimize LambdaLightEffect and AddressableLambdaLightEffect with function pointers (#11556) 2025-10-27 20:14:16 +00:00
J. Nick Koston
e26b5874d7 [api] Register user services with initializer_list (#11545) 2025-10-28 09:07:31 +13:00
J. Nick Koston
00f22e5c36 [network] Eliminate runtime string parsing for IP address initialization (#11561) 2025-10-28 08:51:08 +13:00
Javier Peletier
51e080c2d3 [substitutions] fix #11077 Preserve ESPHomeDatabase (document metadata) in substitutions (#11087)
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-10-27 19:46:26 +00:00
J. Nick Koston
3c18558003 Optimize stateless lambdas to use function pointers (#11551) 2025-10-28 08:06:22 +13:00
449 changed files with 8432 additions and 3087 deletions

View File

@@ -416,7 +416,7 @@ jobs:
} }
// Generate review messages // Generate review messages
function generateReviewMessages(finalLabels) { function generateReviewMessages(finalLabels, originalLabelCount) {
const messages = []; const messages = [];
const prAuthor = context.payload.pull_request.user.login; const prAuthor = context.payload.pull_request.user.login;
@@ -430,15 +430,15 @@ jobs:
.reduce((sum, file) => sum + (file.deletions || 0), 0); .reduce((sum, file) => sum + (file.deletions || 0), 0);
const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions); const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions);
const tooManyLabels = finalLabels.length > MAX_LABELS; const tooManyLabels = originalLabelCount > MAX_LABELS;
const tooManyChanges = nonTestChanges > TOO_BIG_THRESHOLD; const tooManyChanges = nonTestChanges > TOO_BIG_THRESHOLD;
let message = `${TOO_BIG_MARKER}\n### 📦 Pull Request Size\n\n`; let message = `${TOO_BIG_MARKER}\n### 📦 Pull Request Size\n\n`;
if (tooManyLabels && tooManyChanges) { if (tooManyLabels && tooManyChanges) {
message += `This PR is too large with ${nonTestChanges} line changes (excluding tests) and affects ${finalLabels.length} different components/areas.`; message += `This PR is too large with ${nonTestChanges} line changes (excluding tests) and affects ${originalLabelCount} different components/areas.`;
} else if (tooManyLabels) { } else if (tooManyLabels) {
message += `This PR affects ${finalLabels.length} different components/areas.`; message += `This PR affects ${originalLabelCount} different components/areas.`;
} else { } else {
message += `This PR is too large with ${nonTestChanges} line changes (excluding tests).`; message += `This PR is too large with ${nonTestChanges} line changes (excluding tests).`;
} }
@@ -466,8 +466,8 @@ jobs:
} }
// Handle reviews // Handle reviews
async function handleReviews(finalLabels) { async function handleReviews(finalLabels, originalLabelCount) {
const reviewMessages = generateReviewMessages(finalLabels); const reviewMessages = generateReviewMessages(finalLabels, originalLabelCount);
const hasReviewableLabels = finalLabels.some(label => const hasReviewableLabels = finalLabels.some(label =>
['too-big', 'needs-codeowners'].includes(label) ['too-big', 'needs-codeowners'].includes(label)
); );
@@ -627,6 +627,7 @@ jobs:
// Handle too many labels (only for non-mega PRs) // Handle too many labels (only for non-mega PRs)
const tooManyLabels = finalLabels.length > MAX_LABELS; const tooManyLabels = finalLabels.length > MAX_LABELS;
const originalLabelCount = finalLabels.length;
if (tooManyLabels && !isMegaPR && !finalLabels.includes('too-big')) { if (tooManyLabels && !isMegaPR && !finalLabels.includes('too-big')) {
finalLabels = ['too-big']; finalLabels = ['too-big'];
@@ -635,7 +636,7 @@ jobs:
console.log('Computed labels:', finalLabels.join(', ')); console.log('Computed labels:', finalLabels.join(', '));
// Handle reviews // Handle reviews
await handleReviews(finalLabels); await handleReviews(finalLabels, originalLabelCount);
// Apply labels // Apply labels
if (finalLabels.length > 0) { if (finalLabels.length > 0) {

View File

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

View File

@@ -114,7 +114,7 @@ jobs:
matrix: matrix:
python-version: python-version:
- "3.11" - "3.11"
- "3.14" - "3.13"
os: os:
- ubuntu-latest - ubuntu-latest
- macOS-latest - macOS-latest
@@ -123,9 +123,9 @@ jobs:
# Minimize CI resource usage # Minimize CI resource usage
# by only running the Python version # by only running the Python version
# version used for docker images on Windows and macOS # version used for docker images on Windows and macOS
- python-version: "3.14" - python-version: "3.13"
os: windows-latest os: windows-latest
- python-version: "3.14" - python-version: "3.13"
os: macOS-latest os: macOS-latest
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
needs: needs:
@@ -180,6 +180,7 @@ jobs:
memory_impact: ${{ steps.determine.outputs.memory-impact }} memory_impact: ${{ steps.determine.outputs.memory-impact }}
cpp-unit-tests-run-all: ${{ steps.determine.outputs.cpp-unit-tests-run-all }} cpp-unit-tests-run-all: ${{ steps.determine.outputs.cpp-unit-tests-run-all }}
cpp-unit-tests-components: ${{ steps.determine.outputs.cpp-unit-tests-components }} cpp-unit-tests-components: ${{ steps.determine.outputs.cpp-unit-tests-components }}
component-test-batches: ${{ steps.determine.outputs.component-test-batches }}
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -191,6 +192,11 @@ jobs:
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }} cache-key: ${{ needs.common.outputs.cache-key }}
- name: Restore components graph cache
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: .temp/components_graph.json
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
- name: Determine which tests to run - name: Determine which tests to run
id: determine id: determine
env: env:
@@ -214,6 +220,13 @@ jobs:
echo "memory-impact=$(echo "$output" | jq -c '.memory_impact')" >> $GITHUB_OUTPUT echo "memory-impact=$(echo "$output" | jq -c '.memory_impact')" >> $GITHUB_OUTPUT
echo "cpp-unit-tests-run-all=$(echo "$output" | jq -r '.cpp_unit_tests_run_all')" >> $GITHUB_OUTPUT echo "cpp-unit-tests-run-all=$(echo "$output" | jq -r '.cpp_unit_tests_run_all')" >> $GITHUB_OUTPUT
echo "cpp-unit-tests-components=$(echo "$output" | jq -c '.cpp_unit_tests_components')" >> $GITHUB_OUTPUT echo "cpp-unit-tests-components=$(echo "$output" | jq -c '.cpp_unit_tests_components')" >> $GITHUB_OUTPUT
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
with:
path: .temp/components_graph.json
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
integration-tests: integration-tests:
name: Run integration tests name: Run integration tests
@@ -458,7 +471,7 @@ jobs:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
strategy: strategy:
fail-fast: false fail-fast: false
max-parallel: 1 max-parallel: 2
matrix: matrix:
include: include:
- id: clang-tidy - id: clang-tidy
@@ -536,59 +549,18 @@ jobs:
run: script/ci-suggest-changes run: script/ci-suggest-changes
if: always() if: always()
test-build-components-splitter:
name: Split components for intelligent grouping (40 weighted per batch)
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0
outputs:
matrix: ${{ steps.split.outputs.components }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Split components intelligently based on bus configurations
id: split
run: |
. venv/bin/activate
# Use intelligent splitter that groups components with same bus configs
components='${{ needs.determine-jobs.outputs.changed-components-with-tests }}'
# Only isolate directly changed components when targeting dev branch
# For beta/release branches, group everything for faster CI
if [[ "${{ github.base_ref }}" == beta* ]] || [[ "${{ github.base_ref }}" == release* ]]; then
directly_changed='[]'
echo "Target branch: ${{ github.base_ref }} - grouping all components"
else
directly_changed='${{ needs.determine-jobs.outputs.directly-changed-components-with-tests }}'
echo "Target branch: ${{ github.base_ref }} - isolating directly changed components"
fi
echo "Splitting components intelligently..."
output=$(python3 script/split_components_for_ci.py --components "$components" --directly-changed "$directly_changed" --batch-size 40 --output github)
echo "$output" >> $GITHUB_OUTPUT
test-build-components-split: test-build-components-split:
name: Test components batch (${{ matrix.components }}) name: Test components batch (${{ matrix.components }})
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: needs:
- common - common
- determine-jobs - determine-jobs
- test-build-components-splitter
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0 if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0
strategy: strategy:
fail-fast: false fail-fast: false
max-parallel: ${{ (startsWith(github.base_ref, 'beta') || startsWith(github.base_ref, 'release')) && 8 || 4 }} max-parallel: ${{ (startsWith(github.base_ref, 'beta') || startsWith(github.base_ref, 'release')) && 8 || 4 }}
matrix: matrix:
components: ${{ fromJson(needs.test-build-components-splitter.outputs.matrix) }} components: ${{ fromJson(needs.determine-jobs.outputs.component-test-batches) }}
steps: steps:
- name: Show disk space - name: Show disk space
run: | run: |
@@ -849,7 +821,7 @@ jobs:
fi fi
- name: Upload memory analysis JSON - name: Upload memory analysis JSON
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with: with:
name: memory-analysis-target name: memory-analysis-target
path: memory-analysis-target.json path: memory-analysis-target.json
@@ -913,7 +885,7 @@ jobs:
--platform "$platform" --platform "$platform"
- name: Upload memory analysis JSON - name: Upload memory analysis JSON
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with: with:
name: memory-analysis-pr name: memory-analysis-pr
path: memory-analysis-pr.json path: memory-analysis-pr.json
@@ -943,13 +915,13 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }} cache-key: ${{ needs.common.outputs.cache-key }}
- name: Download target analysis JSON - name: Download target analysis JSON
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with: with:
name: memory-analysis-target name: memory-analysis-target
path: ./memory-analysis path: ./memory-analysis
continue-on-error: true continue-on-error: true
- name: Download PR analysis JSON - name: Download PR analysis JSON
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with: with:
name: memory-analysis-pr name: memory-analysis-pr
path: ./memory-analysis path: ./memory-analysis
@@ -980,7 +952,6 @@ jobs:
- clang-tidy-nosplit - clang-tidy-nosplit
- clang-tidy-split - clang-tidy-split
- determine-jobs - determine-jobs
- test-build-components-splitter
- test-build-components-split - test-build-components-split
- pre-commit-ci-lite - pre-commit-ci-lite
- memory-impact-target-branch - memory-impact-target-branch

View File

@@ -58,7 +58,7 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 # v4.30.9 uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }} build-mode: ${{ matrix.build-mode }}
@@ -86,6 +86,6 @@ jobs:
exit 1 exit 1
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@16140ae1a102900babc80a33c44059580f687047 # v4.30.9 uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
with: with:
category: "/language:${{matrix.language}}" category: "/language:${{matrix.language}}"

View File

@@ -138,7 +138,7 @@ jobs:
# version: ${{ needs.init.outputs.tag }} # version: ${{ needs.init.outputs.tag }}
- name: Upload digests - name: Upload digests
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with: with:
name: digests-${{ matrix.platform.arch }} name: digests-${{ matrix.platform.arch }}
path: /tmp/digests path: /tmp/digests
@@ -171,7 +171,7 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Download digests - name: Download digests
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with: with:
pattern: digests-* pattern: digests-*
path: /tmp/digests path: /tmp/digests

View File

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

View File

@@ -155,6 +155,7 @@ esphome/components/esp32_ble_tracker/* @bdraco
esphome/components/esp32_camera_web_server/* @ayufan esphome/components/esp32_camera_web_server/* @ayufan
esphome/components/esp32_can/* @Sympatron esphome/components/esp32_can/* @Sympatron
esphome/components/esp32_hosted/* @swoboda1337 esphome/components/esp32_hosted/* @swoboda1337
esphome/components/esp32_hosted/update/* @swoboda1337
esphome/components/esp32_improv/* @jesserockz esphome/components/esp32_improv/* @jesserockz
esphome/components/esp32_rmt/* @jesserockz esphome/components/esp32_rmt/* @jesserockz
esphome/components/esp32_rmt_led_strip/* @jesserockz esphome/components/esp32_rmt_led_strip/* @jesserockz
@@ -180,7 +181,7 @@ esphome/components/gdk101/* @Szewcson
esphome/components/gl_r01_i2c/* @pkejval esphome/components/gl_r01_i2c/* @pkejval
esphome/components/globals/* @esphome/core esphome/components/globals/* @esphome/core
esphome/components/gp2y1010au0f/* @zry98 esphome/components/gp2y1010au0f/* @zry98
esphome/components/gp8403/* @jesserockz esphome/components/gp8403/* @jesserockz @sebydocky
esphome/components/gpio/* @esphome/core esphome/components/gpio/* @esphome/core
esphome/components/gpio/one_wire/* @ssieb esphome/components/gpio/one_wire/* @ssieb
esphome/components/gps/* @coogle @ximex esphome/components/gps/* @coogle @ximex
@@ -479,6 +480,7 @@ esphome/components/template/fan/* @ssieb
esphome/components/text/* @mauritskorse esphome/components/text/* @mauritskorse
esphome/components/thermostat/* @kbx81 esphome/components/thermostat/* @kbx81
esphome/components/time/* @esphome/core esphome/components/time/* @esphome/core
esphome/components/tinyusb/* @kbx81
esphome/components/tlc5947/* @rnauber esphome/components/tlc5947/* @rnauber
esphome/components/tlc5971/* @IJIJI esphome/components/tlc5971/* @IJIJI
esphome/components/tm1621/* @Philippe12 esphome/components/tm1621/* @Philippe12

View File

@@ -207,14 +207,14 @@ def choose_upload_log_host(
if has_mqtt_logging(): if has_mqtt_logging():
resolved.append("MQTT") resolved.append("MQTT")
if has_api() and has_non_ip_address(): if has_api() and has_non_ip_address() and has_resolvable_address():
resolved.extend(_resolve_with_cache(CORE.address, purpose)) resolved.extend(_resolve_with_cache(CORE.address, purpose))
elif purpose == Purpose.UPLOADING: elif purpose == Purpose.UPLOADING:
if has_ota() and has_mqtt_ip_lookup(): if has_ota() and has_mqtt_ip_lookup():
resolved.append("MQTTIP") resolved.append("MQTTIP")
if has_ota() and has_non_ip_address(): if has_ota() and has_non_ip_address() and has_resolvable_address():
resolved.extend(_resolve_with_cache(CORE.address, purpose)) resolved.extend(_resolve_with_cache(CORE.address, purpose))
else: else:
resolved.append(device) resolved.append(device)
@@ -318,7 +318,17 @@ def has_resolvable_address() -> bool:
"""Check if CORE.address is resolvable (via mDNS, DNS, or is an IP address).""" """Check if CORE.address is resolvable (via mDNS, DNS, or is an IP address)."""
# Any address (IP, mDNS hostname, or regular DNS hostname) is resolvable # Any address (IP, mDNS hostname, or regular DNS hostname) is resolvable
# The resolve_ip_address() function in helpers.py handles all types via AsyncResolver # The resolve_ip_address() function in helpers.py handles all types via AsyncResolver
return CORE.address is not None if CORE.address is None:
return False
if has_ip_address():
return True
if has_mdns():
return True
# .local mDNS hostnames are only resolvable if mDNS is enabled
return not CORE.address.endswith(".local")
def mqtt_get_ip(config: ConfigType, username: str, password: str, client_id: str): def mqtt_get_ip(config: ConfigType, username: str, password: str, client_id: str):

View File

@@ -15,8 +15,13 @@ from esphome.const import (
CONF_TYPE_ID, CONF_TYPE_ID,
CONF_UPDATE_INTERVAL, CONF_UPDATE_INTERVAL,
) )
from esphome.core import ID from esphome.core import ID, Lambda
from esphome.cpp_generator import MockObj, MockObjClass, TemplateArgsType from esphome.cpp_generator import (
LambdaExpression,
MockObj,
MockObjClass,
TemplateArgsType,
)
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from esphome.types import ConfigType from esphome.types import ConfigType
from esphome.util import Registry from esphome.util import Registry
@@ -87,6 +92,7 @@ def validate_potentially_or_condition(value):
DelayAction = cg.esphome_ns.class_("DelayAction", Action, cg.Component) DelayAction = cg.esphome_ns.class_("DelayAction", Action, cg.Component)
LambdaAction = cg.esphome_ns.class_("LambdaAction", Action) LambdaAction = cg.esphome_ns.class_("LambdaAction", Action)
StatelessLambdaAction = cg.esphome_ns.class_("StatelessLambdaAction", Action)
IfAction = cg.esphome_ns.class_("IfAction", Action) IfAction = cg.esphome_ns.class_("IfAction", Action)
WhileAction = cg.esphome_ns.class_("WhileAction", Action) WhileAction = cg.esphome_ns.class_("WhileAction", Action)
RepeatAction = cg.esphome_ns.class_("RepeatAction", Action) RepeatAction = cg.esphome_ns.class_("RepeatAction", Action)
@@ -97,9 +103,40 @@ ResumeComponentAction = cg.esphome_ns.class_("ResumeComponentAction", Action)
Automation = cg.esphome_ns.class_("Automation") Automation = cg.esphome_ns.class_("Automation")
LambdaCondition = cg.esphome_ns.class_("LambdaCondition", Condition) LambdaCondition = cg.esphome_ns.class_("LambdaCondition", Condition)
StatelessLambdaCondition = cg.esphome_ns.class_("StatelessLambdaCondition", Condition)
ForCondition = cg.esphome_ns.class_("ForCondition", Condition, cg.Component) ForCondition = cg.esphome_ns.class_("ForCondition", Condition, cg.Component)
def new_lambda_pvariable(
id_obj: ID,
lambda_expr: LambdaExpression,
stateless_class: MockObjClass,
template_arg: cg.TemplateArguments | None = None,
) -> MockObj:
"""Create Pvariable for lambda, using stateless class if applicable.
Combines ID selection and Pvariable creation in one call. For stateless
lambdas (empty capture), uses function pointer instead of std::function.
Args:
id_obj: The ID object (action_id, condition_id, or filter_id)
lambda_expr: The lambda expression object
stateless_class: The stateless class to use for stateless lambdas
template_arg: Optional template arguments (for actions/conditions)
Returns:
The created Pvariable
"""
# For stateless lambdas, use function pointer instead of std::function
if lambda_expr.capture == "":
id_obj = id_obj.copy()
id_obj.type = stateless_class
if template_arg is not None:
return cg.new_Pvariable(id_obj, template_arg, lambda_expr)
return cg.new_Pvariable(id_obj, lambda_expr)
def validate_automation(extra_schema=None, extra_validators=None, single=False): def validate_automation(extra_schema=None, extra_validators=None, single=False):
if extra_schema is None: if extra_schema is None:
extra_schema = {} extra_schema = {}
@@ -145,7 +182,7 @@ def validate_automation(extra_schema=None, extra_validators=None, single=False):
value = cv.Schema([extra_validators])(value) value = cv.Schema([extra_validators])(value)
if single: if single:
if len(value) != 1: if len(value) != 1:
raise cv.Invalid("Cannot have more than 1 automation for templates") raise cv.Invalid("This trigger allows only a single automation")
return value[0] return value[0]
return value return value
@@ -240,7 +277,9 @@ async def lambda_condition_to_code(
args: TemplateArgsType, args: TemplateArgsType,
) -> MockObj: ) -> MockObj:
lambda_ = await cg.process_lambda(config, args, return_type=bool) lambda_ = await cg.process_lambda(config, args, return_type=bool)
return cg.new_Pvariable(condition_id, template_arg, lambda_) return new_lambda_pvariable(
condition_id, lambda_, StatelessLambdaCondition, template_arg
)
@register_condition( @register_condition(
@@ -271,6 +310,30 @@ async def for_condition_to_code(
return var return var
@register_condition(
"component.is_idle",
LambdaCondition,
maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(cg.Component),
}
),
)
async def component_is_idle_condition_to_code(
config: ConfigType,
condition_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
comp = await cg.get_variable(config[CONF_ID])
lambda_ = await cg.process_lambda(
Lambda(f"return {comp}->is_idle();"), args, return_type=bool
)
return new_lambda_pvariable(
condition_id, lambda_, StatelessLambdaCondition, template_arg
)
@register_action( @register_action(
"delay", DelayAction, cv.templatable(cv.positive_time_period_milliseconds) "delay", DelayAction, cv.templatable(cv.positive_time_period_milliseconds)
) )
@@ -406,7 +469,7 @@ async def lambda_action_to_code(
args: TemplateArgsType, args: TemplateArgsType,
) -> MockObj: ) -> MockObj:
lambda_ = await cg.process_lambda(config, args, return_type=cg.void) lambda_ = await cg.process_lambda(config, args, return_type=cg.void)
return cg.new_Pvariable(action_id, template_arg, lambda_) return new_lambda_pvariable(action_id, lambda_, StatelessLambdaAction, template_arg)
@register_action( @register_action(

View File

@@ -62,6 +62,7 @@ from esphome.cpp_types import ( # noqa: F401
EntityBase, EntityBase,
EntityCategory, EntityCategory,
ESPTime, ESPTime,
FixedVector,
GPIOPin, GPIOPin,
InternalGPIOPin, InternalGPIOPin,
JsonObject, JsonObject,

View File

@@ -172,12 +172,6 @@ def alarm_control_panel_schema(
return _ALARM_CONTROL_PANEL_SCHEMA.extend(schema) return _ALARM_CONTROL_PANEL_SCHEMA.extend(schema)
# Remove before 2025.11.0
ALARM_CONTROL_PANEL_SCHEMA = alarm_control_panel_schema(AlarmControlPanel)
ALARM_CONTROL_PANEL_SCHEMA.add_extra(
cv.deprecated_schema_constant("alarm_control_panel")
)
ALARM_CONTROL_PANEL_ACTION_SCHEMA = maybe_simple_id( ALARM_CONTROL_PANEL_ACTION_SCHEMA = maybe_simple_id(
{ {
cv.GenerateID(): cv.use_id(AlarmControlPanel), cv.GenerateID(): cv.use_id(AlarmControlPanel),

View File

@@ -71,10 +71,12 @@ SERVICE_ARG_NATIVE_TYPES = {
"int": cg.int32, "int": cg.int32,
"float": float, "float": float,
"string": cg.std_string, "string": cg.std_string,
"bool[]": cg.std_vector.template(bool), "bool[]": cg.FixedVector.template(bool).operator("const").operator("ref"),
"int[]": cg.std_vector.template(cg.int32), "int[]": cg.FixedVector.template(cg.int32).operator("const").operator("ref"),
"float[]": cg.std_vector.template(float), "float[]": cg.FixedVector.template(float).operator("const").operator("ref"),
"string[]": cg.std_vector.template(cg.std_string), "string[]": cg.FixedVector.template(cg.std_string)
.operator("const")
.operator("ref"),
} }
CONF_ENCRYPTION = "encryption" CONF_ENCRYPTION = "encryption"
CONF_BATCH_DELAY = "batch_delay" CONF_BATCH_DELAY = "batch_delay"
@@ -258,6 +260,10 @@ async def to_code(config):
if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]: if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]:
cg.add_define("USE_API_SERVICES") cg.add_define("USE_API_SERVICES")
# Set USE_API_CUSTOM_SERVICES if external components need dynamic service registration
if config[CONF_CUSTOM_SERVICES]:
cg.add_define("USE_API_CUSTOM_SERVICES")
if config[CONF_HOMEASSISTANT_SERVICES]: if config[CONF_HOMEASSISTANT_SERVICES]:
cg.add_define("USE_API_HOMEASSISTANT_SERVICES") cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
@@ -265,6 +271,8 @@ async def to_code(config):
cg.add_define("USE_API_HOMEASSISTANT_STATES") cg.add_define("USE_API_HOMEASSISTANT_STATES")
if actions := config.get(CONF_ACTIONS, []): if actions := config.get(CONF_ACTIONS, []):
# Collect all triggers first, then register all at once with initializer_list
triggers: list[cg.Pvariable] = []
for conf in actions: for conf in actions:
template_args = [] template_args = []
func_args = [] func_args = []
@@ -278,8 +286,10 @@ async def to_code(config):
trigger = cg.new_Pvariable( trigger = cg.new_Pvariable(
conf[CONF_TRIGGER_ID], templ, conf[CONF_ACTION], service_arg_names conf[CONF_TRIGGER_ID], templ, conf[CONF_ACTION], service_arg_names
) )
cg.add(var.register_user_service(trigger)) triggers.append(trigger)
await automation.build_automation(trigger, func_args, conf) await automation.build_automation(trigger, func_args, conf)
# Register all services at once - single allocation, no reallocations
cg.add(var.initialize_user_services(triggers))
if CONF_ON_CLIENT_CONNECTED in config: if CONF_ON_CLIENT_CONNECTED in config:
cg.add_define("USE_API_CLIENT_CONNECTED_TRIGGER") cg.add_define("USE_API_CLIENT_CONNECTED_TRIGGER")

View File

@@ -425,7 +425,7 @@ message ListEntitiesFanResponse {
bool disabled_by_default = 9; bool disabled_by_default = 9;
string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"]; string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"];
EntityCategory entity_category = 11; EntityCategory entity_category = 11;
repeated string supported_preset_modes = 12 [(container_pointer) = "std::set"]; repeated string supported_preset_modes = 12 [(container_pointer_no_template) = "std::vector<const char *>"];
uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"]; uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"];
} }
// Deprecated in API version 1.6 - only used in deprecated fields // Deprecated in API version 1.6 - only used in deprecated fields
@@ -989,7 +989,7 @@ message ListEntitiesClimateResponse {
bool supports_current_temperature = 5; // Deprecated: use feature_flags bool supports_current_temperature = 5; // Deprecated: use feature_flags
bool supports_two_point_target_temperature = 6; // Deprecated: use feature_flags bool supports_two_point_target_temperature = 6; // Deprecated: use feature_flags
repeated ClimateMode supported_modes = 7 [(container_pointer) = "std::set<climate::ClimateMode>"]; repeated ClimateMode supported_modes = 7 [(container_pointer_no_template) = "climate::ClimateModeMask"];
float visual_min_temperature = 8; float visual_min_temperature = 8;
float visual_max_temperature = 9; float visual_max_temperature = 9;
float visual_target_temperature_step = 10; float visual_target_temperature_step = 10;
@@ -998,11 +998,11 @@ message ListEntitiesClimateResponse {
// Deprecated in API version 1.5 // Deprecated in API version 1.5
bool legacy_supports_away = 11 [deprecated=true]; bool legacy_supports_away = 11 [deprecated=true];
bool supports_action = 12; // Deprecated: use feature_flags bool supports_action = 12; // Deprecated: use feature_flags
repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer) = "std::set<climate::ClimateFanMode>"]; repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer_no_template) = "climate::ClimateFanModeMask"];
repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer) = "std::set<climate::ClimateSwingMode>"]; repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer_no_template) = "climate::ClimateSwingModeMask"];
repeated string supported_custom_fan_modes = 15 [(container_pointer) = "std::set"]; repeated string supported_custom_fan_modes = 15 [(container_pointer_no_template) = "std::vector<const char *>"];
repeated ClimatePreset supported_presets = 16 [(container_pointer) = "std::set<climate::ClimatePreset>"]; repeated ClimatePreset supported_presets = 16 [(container_pointer_no_template) = "climate::ClimatePresetMask"];
repeated string supported_custom_presets = 17 [(container_pointer) = "std::set"]; repeated string supported_custom_presets = 17 [(container_pointer_no_template) = "std::vector<const char *>"];
bool disabled_by_default = 18; bool disabled_by_default = 18;
string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"]; string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"];
EntityCategory entity_category = 20; EntityCategory entity_category = 20;
@@ -1143,7 +1143,7 @@ message ListEntitiesSelectResponse {
reserved 4; // Deprecated: was string unique_id reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
repeated string options = 6 [(container_pointer) = "std::vector"]; repeated string options = 6 [(container_pointer_no_template) = "FixedVector<const char *>"];
bool disabled_by_default = 7; bool disabled_by_default = 7;
EntityCategory entity_category = 8; EntityCategory entity_category = 8;
uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"]; uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"];

View File

@@ -410,8 +410,8 @@ uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *co
} }
if (traits.supports_direction()) if (traits.supports_direction())
msg.direction = static_cast<enums::FanDirection>(fan->direction); msg.direction = static_cast<enums::FanDirection>(fan->direction);
if (traits.supports_preset_modes()) if (traits.supports_preset_modes() && fan->has_preset_mode())
msg.set_preset_mode(StringRef(fan->preset_mode)); msg.set_preset_mode(StringRef(fan->get_preset_mode()));
return fill_and_encode_entity_state(fan, msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return fill_and_encode_entity_state(fan, msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@@ -423,7 +423,7 @@ uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *con
msg.supports_speed = traits.supports_speed(); msg.supports_speed = traits.supports_speed();
msg.supports_direction = traits.supports_direction(); msg.supports_direction = traits.supports_direction();
msg.supported_speed_count = traits.supported_speed_count(); msg.supported_speed_count = traits.supported_speed_count();
msg.supported_preset_modes = &traits.supported_preset_modes_for_api_(); msg.supported_preset_modes = &traits.supported_preset_modes();
return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::fan_command(const FanCommandRequest &msg) { void APIConnection::fan_command(const FanCommandRequest &msg) {
@@ -637,14 +637,14 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection
} }
if (traits.get_supports_fan_modes() && climate->fan_mode.has_value()) if (traits.get_supports_fan_modes() && climate->fan_mode.has_value())
resp.fan_mode = static_cast<enums::ClimateFanMode>(climate->fan_mode.value()); resp.fan_mode = static_cast<enums::ClimateFanMode>(climate->fan_mode.value());
if (!traits.get_supported_custom_fan_modes().empty() && climate->custom_fan_mode.has_value()) { if (!traits.get_supported_custom_fan_modes().empty() && climate->has_custom_fan_mode()) {
resp.set_custom_fan_mode(StringRef(climate->custom_fan_mode.value())); resp.set_custom_fan_mode(StringRef(climate->get_custom_fan_mode()));
} }
if (traits.get_supports_presets() && climate->preset.has_value()) { if (traits.get_supports_presets() && climate->preset.has_value()) {
resp.preset = static_cast<enums::ClimatePreset>(climate->preset.value()); resp.preset = static_cast<enums::ClimatePreset>(climate->preset.value());
} }
if (!traits.get_supported_custom_presets().empty() && climate->custom_preset.has_value()) { if (!traits.get_supported_custom_presets().empty() && climate->has_custom_preset()) {
resp.set_custom_preset(StringRef(climate->custom_preset.value())); resp.set_custom_preset(StringRef(climate->get_custom_preset()));
} }
if (traits.get_supports_swing_modes()) if (traits.get_supports_swing_modes())
resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode); resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode);
@@ -669,18 +669,18 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection
msg.supports_action = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION); msg.supports_action = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION);
// Current feature flags and other supported parameters // Current feature flags and other supported parameters
msg.feature_flags = traits.get_feature_flags(); msg.feature_flags = traits.get_feature_flags();
msg.supported_modes = &traits.get_supported_modes_for_api_(); msg.supported_modes = &traits.get_supported_modes();
msg.visual_min_temperature = traits.get_visual_min_temperature(); msg.visual_min_temperature = traits.get_visual_min_temperature();
msg.visual_max_temperature = traits.get_visual_max_temperature(); msg.visual_max_temperature = traits.get_visual_max_temperature();
msg.visual_target_temperature_step = traits.get_visual_target_temperature_step(); msg.visual_target_temperature_step = traits.get_visual_target_temperature_step();
msg.visual_current_temperature_step = traits.get_visual_current_temperature_step(); msg.visual_current_temperature_step = traits.get_visual_current_temperature_step();
msg.visual_min_humidity = traits.get_visual_min_humidity(); msg.visual_min_humidity = traits.get_visual_min_humidity();
msg.visual_max_humidity = traits.get_visual_max_humidity(); msg.visual_max_humidity = traits.get_visual_max_humidity();
msg.supported_fan_modes = &traits.get_supported_fan_modes_for_api_(); msg.supported_fan_modes = &traits.get_supported_fan_modes();
msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes_for_api_(); msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes();
msg.supported_presets = &traits.get_supported_presets_for_api_(); msg.supported_presets = &traits.get_supported_presets();
msg.supported_custom_presets = &traits.get_supported_custom_presets_for_api_(); msg.supported_custom_presets = &traits.get_supported_custom_presets();
msg.supported_swing_modes = &traits.get_supported_swing_modes_for_api_(); msg.supported_swing_modes = &traits.get_supported_swing_modes();
return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size, return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size,
is_single); is_single);
} }
@@ -877,7 +877,7 @@ uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection
bool is_single) { bool is_single) {
auto *select = static_cast<select::Select *>(entity); auto *select = static_cast<select::Select *>(entity);
SelectStateResponse resp; SelectStateResponse resp;
resp.set_state(StringRef(select->state)); resp.set_state(StringRef(select->current_option()));
resp.missing_state = !select->has_state(); resp.missing_state = !select->has_state();
return fill_and_encode_entity_state(select, resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return fill_and_encode_entity_state(select, resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }

View File

@@ -434,8 +434,7 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, st
return APIError::OK; return APIError::OK;
} }
std::vector<uint8_t> *raw_buffer = buffer.get_buffer(); uint8_t *buffer_data = buffer.get_buffer()->data();
uint8_t *buffer_data = raw_buffer->data(); // Cache buffer pointer
this->reusable_iovs_.clear(); this->reusable_iovs_.clear();
this->reusable_iovs_.reserve(packets.size()); this->reusable_iovs_.reserve(packets.size());

View File

@@ -230,8 +230,7 @@ APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer
return APIError::OK; return APIError::OK;
} }
std::vector<uint8_t> *raw_buffer = buffer.get_buffer(); uint8_t *buffer_data = buffer.get_buffer()->data();
uint8_t *buffer_data = raw_buffer->data(); // Cache buffer pointer
this->reusable_iovs_.clear(); this->reusable_iovs_.clear();
this->reusable_iovs_.reserve(packets.size()); this->reusable_iovs_.reserve(packets.size());

View File

@@ -355,8 +355,8 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(10, this->icon_ref_); buffer.encode_string(10, this->icon_ref_);
#endif #endif
buffer.encode_uint32(11, static_cast<uint32_t>(this->entity_category)); buffer.encode_uint32(11, static_cast<uint32_t>(this->entity_category));
for (const auto &it : *this->supported_preset_modes) { for (const char *it : *this->supported_preset_modes) {
buffer.encode_string(12, it, true); buffer.encode_string(12, it, strlen(it), true);
} }
#ifdef USE_DEVICES #ifdef USE_DEVICES
buffer.encode_uint32(13, this->device_id); buffer.encode_uint32(13, this->device_id);
@@ -376,8 +376,8 @@ void ListEntitiesFanResponse::calculate_size(ProtoSize &size) const {
#endif #endif
size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); size.add_uint32(1, static_cast<uint32_t>(this->entity_category));
if (!this->supported_preset_modes->empty()) { if (!this->supported_preset_modes->empty()) {
for (const auto &it : *this->supported_preset_modes) { for (const char *it : *this->supported_preset_modes) {
size.add_length_force(1, it.size()); size.add_length_force(1, strlen(it));
} }
} }
#ifdef USE_DEVICES #ifdef USE_DEVICES
@@ -1179,14 +1179,14 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const {
for (const auto &it : *this->supported_swing_modes) { for (const auto &it : *this->supported_swing_modes) {
buffer.encode_uint32(14, static_cast<uint32_t>(it), true); buffer.encode_uint32(14, static_cast<uint32_t>(it), true);
} }
for (const auto &it : *this->supported_custom_fan_modes) { for (const char *it : *this->supported_custom_fan_modes) {
buffer.encode_string(15, it, true); buffer.encode_string(15, it, strlen(it), true);
} }
for (const auto &it : *this->supported_presets) { for (const auto &it : *this->supported_presets) {
buffer.encode_uint32(16, static_cast<uint32_t>(it), true); buffer.encode_uint32(16, static_cast<uint32_t>(it), true);
} }
for (const auto &it : *this->supported_custom_presets) { for (const char *it : *this->supported_custom_presets) {
buffer.encode_string(17, it, true); buffer.encode_string(17, it, strlen(it), true);
} }
buffer.encode_bool(18, this->disabled_by_default); buffer.encode_bool(18, this->disabled_by_default);
#ifdef USE_ENTITY_ICON #ifdef USE_ENTITY_ICON
@@ -1229,8 +1229,8 @@ void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const {
} }
} }
if (!this->supported_custom_fan_modes->empty()) { if (!this->supported_custom_fan_modes->empty()) {
for (const auto &it : *this->supported_custom_fan_modes) { for (const char *it : *this->supported_custom_fan_modes) {
size.add_length_force(1, it.size()); size.add_length_force(1, strlen(it));
} }
} }
if (!this->supported_presets->empty()) { if (!this->supported_presets->empty()) {
@@ -1239,8 +1239,8 @@ void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const {
} }
} }
if (!this->supported_custom_presets->empty()) { if (!this->supported_custom_presets->empty()) {
for (const auto &it : *this->supported_custom_presets) { for (const char *it : *this->supported_custom_presets) {
size.add_length_force(2, it.size()); size.add_length_force(2, strlen(it));
} }
} }
size.add_bool(2, this->disabled_by_default); size.add_bool(2, this->disabled_by_default);
@@ -1475,8 +1475,8 @@ void ListEntitiesSelectResponse::encode(ProtoWriteBuffer buffer) const {
#ifdef USE_ENTITY_ICON #ifdef USE_ENTITY_ICON
buffer.encode_string(5, this->icon_ref_); buffer.encode_string(5, this->icon_ref_);
#endif #endif
for (const auto &it : *this->options) { for (const char *it : *this->options) {
buffer.encode_string(6, it, true); buffer.encode_string(6, it, strlen(it), true);
} }
buffer.encode_bool(7, this->disabled_by_default); buffer.encode_bool(7, this->disabled_by_default);
buffer.encode_uint32(8, static_cast<uint32_t>(this->entity_category)); buffer.encode_uint32(8, static_cast<uint32_t>(this->entity_category));
@@ -1492,8 +1492,8 @@ void ListEntitiesSelectResponse::calculate_size(ProtoSize &size) const {
size.add_length(1, this->icon_ref_.size()); size.add_length(1, this->icon_ref_.size());
#endif #endif
if (!this->options->empty()) { if (!this->options->empty()) {
for (const auto &it : *this->options) { for (const char *it : *this->options) {
size.add_length_force(1, it.size()); size.add_length_force(1, strlen(it));
} }
} }
size.add_bool(1, this->disabled_by_default); size.add_bool(1, this->disabled_by_default);

View File

@@ -725,7 +725,7 @@ class ListEntitiesFanResponse final : public InfoResponseProtoMessage {
bool supports_speed{false}; bool supports_speed{false};
bool supports_direction{false}; bool supports_direction{false};
int32_t supported_speed_count{0}; int32_t supported_speed_count{0};
const std::set<std::string> *supported_preset_modes{}; const std::vector<const char *> *supported_preset_modes{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@@ -1377,16 +1377,16 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage {
#endif #endif
bool supports_current_temperature{false}; bool supports_current_temperature{false};
bool supports_two_point_target_temperature{false}; bool supports_two_point_target_temperature{false};
const std::set<climate::ClimateMode> *supported_modes{}; const climate::ClimateModeMask *supported_modes{};
float visual_min_temperature{0.0f}; float visual_min_temperature{0.0f};
float visual_max_temperature{0.0f}; float visual_max_temperature{0.0f};
float visual_target_temperature_step{0.0f}; float visual_target_temperature_step{0.0f};
bool supports_action{false}; bool supports_action{false};
const std::set<climate::ClimateFanMode> *supported_fan_modes{}; const climate::ClimateFanModeMask *supported_fan_modes{};
const std::set<climate::ClimateSwingMode> *supported_swing_modes{}; const climate::ClimateSwingModeMask *supported_swing_modes{};
const std::set<std::string> *supported_custom_fan_modes{}; const std::vector<const char *> *supported_custom_fan_modes{};
const std::set<climate::ClimatePreset> *supported_presets{}; const climate::ClimatePresetMask *supported_presets{};
const std::set<std::string> *supported_custom_presets{}; const std::vector<const char *> *supported_custom_presets{};
float visual_current_temperature_step{0.0f}; float visual_current_temperature_step{0.0f};
bool supports_current_humidity{false}; bool supports_current_humidity{false};
bool supports_target_humidity{false}; bool supports_target_humidity{false};
@@ -1534,7 +1534,7 @@ class ListEntitiesSelectResponse final : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_select_response"; } const char *message_name() const override { return "list_entities_select_response"; }
#endif #endif
const std::vector<std::string> *options{}; const FixedVector<const char *> *options{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP

View File

@@ -88,6 +88,12 @@ static void dump_field(std::string &out, const char *field_name, StringRef value
out.append("\n"); out.append("\n");
} }
static void dump_field(std::string &out, const char *field_name, const char *value, int indent = 2) {
append_field_prefix(out, field_name, indent);
out.append("'").append(value).append("'");
out.append("\n");
}
template<typename T> static void dump_field(std::string &out, const char *field_name, T value, int indent = 2) { template<typename T> static void dump_field(std::string &out, const char *field_name, T value, int indent = 2) {
append_field_prefix(out, field_name, indent); append_field_prefix(out, field_name, indent);
out.append(proto_enum_to_string<T>(value)); out.append(proto_enum_to_string<T>(value));

View File

@@ -224,7 +224,7 @@ void APIServer::dump_config() {
" Address: %s:%u\n" " Address: %s:%u\n"
" Listen backlog: %u\n" " Listen backlog: %u\n"
" Max connections: %u", " Max connections: %u",
network::get_use_address().c_str(), this->port_, this->listen_backlog_, this->max_connections_); network::get_use_address(), this->port_, this->listen_backlog_, this->max_connections_);
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_->has_psk())); ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_->has_psk()));
if (!this->noise_ctx_->has_psk()) { if (!this->noise_ctx_->has_psk()) {

View File

@@ -125,8 +125,14 @@ class APIServer : public Component, public Controller {
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES #endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
#endif // USE_API_HOMEASSISTANT_SERVICES #endif // USE_API_HOMEASSISTANT_SERVICES
#ifdef USE_API_SERVICES #ifdef USE_API_SERVICES
void initialize_user_services(std::initializer_list<UserServiceDescriptor *> services) {
this->user_services_.assign(services);
}
#ifdef USE_API_CUSTOM_SERVICES
// Only compile push_back method when custom_services: true (external components)
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
#endif #endif
#endif
#ifdef USE_HOMEASSISTANT_TIME #ifdef USE_HOMEASSISTANT_TIME
void request_time(); void request_time();
#endif #endif

View File

@@ -53,8 +53,14 @@ class CustomAPIDevice {
template<typename T, typename... Ts> template<typename T, typename... Ts>
void register_service(void (T::*callback)(Ts...), const std::string &name, void register_service(void (T::*callback)(Ts...), const std::string &name,
const std::array<std::string, sizeof...(Ts)> &arg_names) { const std::array<std::string, sizeof...(Ts)> &arg_names) {
#ifdef USE_API_CUSTOM_SERVICES
auto *service = new CustomAPIDeviceService<T, Ts...>(name, arg_names, (T *) this, callback); // NOLINT auto *service = new CustomAPIDeviceService<T, Ts...>(name, arg_names, (T *) this, callback); // NOLINT
global_api_server->register_user_service(service); global_api_server->register_user_service(service);
#else
static_assert(
sizeof(T) == 0,
"register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration");
#endif
} }
#else #else
template<typename T, typename... Ts> template<typename T, typename... Ts>
@@ -86,8 +92,14 @@ class CustomAPIDevice {
*/ */
#ifdef USE_API_SERVICES #ifdef USE_API_SERVICES
template<typename T> void register_service(void (T::*callback)(), const std::string &name) { template<typename T> void register_service(void (T::*callback)(), const std::string &name) {
#ifdef USE_API_CUSTOM_SERVICES
auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback); // NOLINT auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback); // NOLINT
global_api_server->register_user_service(service); global_api_server->register_user_service(service);
#else
static_assert(
sizeof(T) == 0,
"register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration");
#endif
} }
#else #else
template<typename T> void register_service(void (T::*callback)(), const std::string &name) { template<typename T> void register_service(void (T::*callback)(), const std::string &name) {

View File

@@ -11,23 +11,58 @@ template<> int32_t get_execute_arg_value<int32_t>(const ExecuteServiceArgument &
} }
template<> float get_execute_arg_value<float>(const ExecuteServiceArgument &arg) { return arg.float_; } template<> float get_execute_arg_value<float>(const ExecuteServiceArgument &arg) { return arg.float_; }
template<> std::string get_execute_arg_value<std::string>(const ExecuteServiceArgument &arg) { return arg.string_; } template<> std::string get_execute_arg_value<std::string>(const ExecuteServiceArgument &arg) { return arg.string_; }
// Legacy std::vector versions for external components using custom_api_device.h - optimized with reserve
template<> std::vector<bool> get_execute_arg_value<std::vector<bool>>(const ExecuteServiceArgument &arg) { template<> std::vector<bool> get_execute_arg_value<std::vector<bool>>(const ExecuteServiceArgument &arg) {
return std::vector<bool>(arg.bool_array.begin(), arg.bool_array.end()); std::vector<bool> result;
result.reserve(arg.bool_array.size());
result.insert(result.end(), arg.bool_array.begin(), arg.bool_array.end());
return result;
} }
template<> std::vector<int32_t> get_execute_arg_value<std::vector<int32_t>>(const ExecuteServiceArgument &arg) { template<> std::vector<int32_t> get_execute_arg_value<std::vector<int32_t>>(const ExecuteServiceArgument &arg) {
return std::vector<int32_t>(arg.int_array.begin(), arg.int_array.end()); std::vector<int32_t> result;
result.reserve(arg.int_array.size());
result.insert(result.end(), arg.int_array.begin(), arg.int_array.end());
return result;
} }
template<> std::vector<float> get_execute_arg_value<std::vector<float>>(const ExecuteServiceArgument &arg) { template<> std::vector<float> get_execute_arg_value<std::vector<float>>(const ExecuteServiceArgument &arg) {
return std::vector<float>(arg.float_array.begin(), arg.float_array.end()); std::vector<float> result;
result.reserve(arg.float_array.size());
result.insert(result.end(), arg.float_array.begin(), arg.float_array.end());
return result;
} }
template<> std::vector<std::string> get_execute_arg_value<std::vector<std::string>>(const ExecuteServiceArgument &arg) { template<> std::vector<std::string> get_execute_arg_value<std::vector<std::string>>(const ExecuteServiceArgument &arg) {
return std::vector<std::string>(arg.string_array.begin(), arg.string_array.end()); std::vector<std::string> result;
result.reserve(arg.string_array.size());
result.insert(result.end(), arg.string_array.begin(), arg.string_array.end());
return result;
}
// New FixedVector const reference versions for YAML-generated services - zero-copy
template<>
const FixedVector<bool> &get_execute_arg_value<const FixedVector<bool> &>(const ExecuteServiceArgument &arg) {
return arg.bool_array;
}
template<>
const FixedVector<int32_t> &get_execute_arg_value<const FixedVector<int32_t> &>(const ExecuteServiceArgument &arg) {
return arg.int_array;
}
template<>
const FixedVector<float> &get_execute_arg_value<const FixedVector<float> &>(const ExecuteServiceArgument &arg) {
return arg.float_array;
}
template<>
const FixedVector<std::string> &get_execute_arg_value<const FixedVector<std::string> &>(
const ExecuteServiceArgument &arg) {
return arg.string_array;
} }
template<> enums::ServiceArgType to_service_arg_type<bool>() { return enums::SERVICE_ARG_TYPE_BOOL; } template<> enums::ServiceArgType to_service_arg_type<bool>() { return enums::SERVICE_ARG_TYPE_BOOL; }
template<> enums::ServiceArgType to_service_arg_type<int32_t>() { return enums::SERVICE_ARG_TYPE_INT; } template<> enums::ServiceArgType to_service_arg_type<int32_t>() { return enums::SERVICE_ARG_TYPE_INT; }
template<> enums::ServiceArgType to_service_arg_type<float>() { return enums::SERVICE_ARG_TYPE_FLOAT; } template<> enums::ServiceArgType to_service_arg_type<float>() { return enums::SERVICE_ARG_TYPE_FLOAT; }
template<> enums::ServiceArgType to_service_arg_type<std::string>() { return enums::SERVICE_ARG_TYPE_STRING; } template<> enums::ServiceArgType to_service_arg_type<std::string>() { return enums::SERVICE_ARG_TYPE_STRING; }
// Legacy std::vector versions for external components using custom_api_device.h
template<> enums::ServiceArgType to_service_arg_type<std::vector<bool>>() { return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; } template<> enums::ServiceArgType to_service_arg_type<std::vector<bool>>() { return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; }
template<> enums::ServiceArgType to_service_arg_type<std::vector<int32_t>>() { template<> enums::ServiceArgType to_service_arg_type<std::vector<int32_t>>() {
return enums::SERVICE_ARG_TYPE_INT_ARRAY; return enums::SERVICE_ARG_TYPE_INT_ARRAY;
@@ -39,4 +74,18 @@ template<> enums::ServiceArgType to_service_arg_type<std::vector<std::string>>()
return enums::SERVICE_ARG_TYPE_STRING_ARRAY; return enums::SERVICE_ARG_TYPE_STRING_ARRAY;
} }
// New FixedVector const reference versions for YAML-generated services
template<> enums::ServiceArgType to_service_arg_type<const FixedVector<bool> &>() {
return enums::SERVICE_ARG_TYPE_BOOL_ARRAY;
}
template<> enums::ServiceArgType to_service_arg_type<const FixedVector<int32_t> &>() {
return enums::SERVICE_ARG_TYPE_INT_ARRAY;
}
template<> enums::ServiceArgType to_service_arg_type<const FixedVector<float> &>() {
return enums::SERVICE_ARG_TYPE_FLOAT_ARRAY;
}
template<> enums::ServiceArgType to_service_arg_type<const FixedVector<std::string> &>() {
return enums::SERVICE_ARG_TYPE_STRING_ARRAY;
}
} // namespace esphome::api } // namespace esphome::api

View File

@@ -99,9 +99,7 @@ enum BedjetCommand : uint8_t {
static const uint8_t BEDJET_FAN_SPEED_COUNT = 20; static const uint8_t BEDJET_FAN_SPEED_COUNT = 20;
static const char *const BEDJET_FAN_STEP_NAMES[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_; static constexpr const char *const BEDJET_FAN_STEP_NAMES[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_;
static const std::string BEDJET_FAN_STEP_NAME_STRINGS[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_;
static const std::set<std::string> BEDJET_FAN_STEP_NAMES_SET BEDJET_FAN_STEP_NAMES_;
} // namespace bedjet } // namespace bedjet
} // namespace esphome } // namespace esphome

View File

@@ -8,15 +8,15 @@ namespace bedjet {
using namespace esphome::climate; using namespace esphome::climate;
static const std::string *bedjet_fan_step_to_fan_mode(const uint8_t fan_step) { static const char *bedjet_fan_step_to_fan_mode(const uint8_t fan_step) {
if (fan_step < BEDJET_FAN_SPEED_COUNT) if (fan_step < BEDJET_FAN_SPEED_COUNT)
return &BEDJET_FAN_STEP_NAME_STRINGS[fan_step]; return BEDJET_FAN_STEP_NAMES[fan_step];
return nullptr; return nullptr;
} }
static uint8_t bedjet_fan_speed_to_step(const std::string &fan_step_percent) { static uint8_t bedjet_fan_speed_to_step(const char *fan_step_percent) {
for (int i = 0; i < BEDJET_FAN_SPEED_COUNT; i++) { for (int i = 0; i < BEDJET_FAN_SPEED_COUNT; i++) {
if (fan_step_percent == BEDJET_FAN_STEP_NAME_STRINGS[i]) { if (strcmp(BEDJET_FAN_STEP_NAMES[i], fan_step_percent) == 0) {
return i; return i;
} }
} }
@@ -48,7 +48,7 @@ void BedJetClimate::dump_config() {
ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(mode))); ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(mode)));
} }
for (const auto &mode : traits.get_supported_custom_fan_modes()) { for (const auto &mode : traits.get_supported_custom_fan_modes()) {
ESP_LOGCONFIG(TAG, " - %s (c)", mode.c_str()); ESP_LOGCONFIG(TAG, " - %s (c)", mode);
} }
ESP_LOGCONFIG(TAG, " Supported presets:"); ESP_LOGCONFIG(TAG, " Supported presets:");
@@ -56,7 +56,7 @@ void BedJetClimate::dump_config() {
ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_preset_to_string(preset))); ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_preset_to_string(preset)));
} }
for (const auto &preset : traits.get_supported_custom_presets()) { for (const auto &preset : traits.get_supported_custom_presets()) {
ESP_LOGCONFIG(TAG, " - %s (c)", preset.c_str()); ESP_LOGCONFIG(TAG, " - %s (c)", preset);
} }
} }
@@ -79,7 +79,7 @@ void BedJetClimate::reset_state_() {
this->target_temperature = NAN; this->target_temperature = NAN;
this->current_temperature = NAN; this->current_temperature = NAN;
this->preset.reset(); this->preset.reset();
this->custom_preset.reset(); this->clear_custom_preset_();
this->publish_state(); this->publish_state();
} }
@@ -120,7 +120,7 @@ void BedJetClimate::control(const ClimateCall &call) {
if (button_result) { if (button_result) {
this->mode = mode; this->mode = mode;
// We're using (custom) preset for Turbo, EXT HT, & M1-3 presets, so changing climate mode will clear those // We're using (custom) preset for Turbo, EXT HT, & M1-3 presets, so changing climate mode will clear those
this->custom_preset.reset(); this->clear_custom_preset_();
this->preset.reset(); this->preset.reset();
} }
} }
@@ -144,8 +144,7 @@ void BedJetClimate::control(const ClimateCall &call) {
if (result) { if (result) {
this->mode = CLIMATE_MODE_HEAT; this->mode = CLIMATE_MODE_HEAT;
this->preset = CLIMATE_PRESET_BOOST; this->set_preset_(CLIMATE_PRESET_BOOST);
this->custom_preset.reset();
} }
} else if (preset == CLIMATE_PRESET_NONE && this->preset.has_value()) { } else if (preset == CLIMATE_PRESET_NONE && this->preset.has_value()) {
if (this->mode == CLIMATE_MODE_HEAT && this->preset == CLIMATE_PRESET_BOOST) { if (this->mode == CLIMATE_MODE_HEAT && this->preset == CLIMATE_PRESET_BOOST) {
@@ -153,7 +152,7 @@ void BedJetClimate::control(const ClimateCall &call) {
result = this->parent_->send_button(heat_button(this->heating_mode_)); result = this->parent_->send_button(heat_button(this->heating_mode_));
if (result) { if (result) {
this->preset.reset(); this->preset.reset();
this->custom_preset.reset(); this->clear_custom_preset_();
} }
} else { } else {
ESP_LOGD(TAG, "Ignoring preset '%s' call; with current mode '%s' and preset '%s'", ESP_LOGD(TAG, "Ignoring preset '%s' call; with current mode '%s' and preset '%s'",
@@ -164,28 +163,27 @@ void BedJetClimate::control(const ClimateCall &call) {
ESP_LOGW(TAG, "Unsupported preset: %d", preset); ESP_LOGW(TAG, "Unsupported preset: %d", preset);
return; return;
} }
} else if (call.get_custom_preset().has_value()) { } else if (call.has_custom_preset()) {
std::string preset = *call.get_custom_preset(); const char *preset = call.get_custom_preset();
bool result; bool result;
if (preset == "M1") { if (strcmp(preset, "M1") == 0) {
result = this->parent_->button_memory1(); result = this->parent_->button_memory1();
} else if (preset == "M2") { } else if (strcmp(preset, "M2") == 0) {
result = this->parent_->button_memory2(); result = this->parent_->button_memory2();
} else if (preset == "M3") { } else if (strcmp(preset, "M3") == 0) {
result = this->parent_->button_memory3(); result = this->parent_->button_memory3();
} else if (preset == "LTD HT") { } else if (strcmp(preset, "LTD HT") == 0) {
result = this->parent_->button_heat(); result = this->parent_->button_heat();
} else if (preset == "EXT HT") { } else if (strcmp(preset, "EXT HT") == 0) {
result = this->parent_->button_ext_heat(); result = this->parent_->button_ext_heat();
} else { } else {
ESP_LOGW(TAG, "Unsupported preset: %s", preset.c_str()); ESP_LOGW(TAG, "Unsupported preset: %s", preset);
return; return;
} }
if (result) { if (result) {
this->custom_preset = preset; this->set_custom_preset_(preset);
this->preset.reset();
} }
} }
@@ -207,19 +205,16 @@ void BedJetClimate::control(const ClimateCall &call) {
} }
if (result) { if (result) {
this->fan_mode = fan_mode; this->set_fan_mode_(fan_mode);
this->custom_fan_mode.reset();
} }
} else if (call.get_custom_fan_mode().has_value()) { } else if (call.has_custom_fan_mode()) {
auto fan_mode = *call.get_custom_fan_mode(); const char *fan_mode = call.get_custom_fan_mode();
auto fan_index = bedjet_fan_speed_to_step(fan_mode); auto fan_index = bedjet_fan_speed_to_step(fan_mode);
if (fan_index <= 19) { if (fan_index <= 19) {
ESP_LOGV(TAG, "[%s] Converted fan mode %s to bedjet fan step %d", this->get_name().c_str(), fan_mode.c_str(), ESP_LOGV(TAG, "[%s] Converted fan mode %s to bedjet fan step %d", this->get_name().c_str(), fan_mode, fan_index);
fan_index);
bool result = this->parent_->set_fan_index(fan_index); bool result = this->parent_->set_fan_index(fan_index);
if (result) { if (result) {
this->custom_fan_mode = fan_mode; this->set_custom_fan_mode_(fan_mode);
this->fan_mode.reset();
} }
} }
} }
@@ -245,7 +240,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) {
const auto *fan_mode_name = bedjet_fan_step_to_fan_mode(data->fan_step); const auto *fan_mode_name = bedjet_fan_step_to_fan_mode(data->fan_step);
if (fan_mode_name != nullptr) { if (fan_mode_name != nullptr) {
this->custom_fan_mode = *fan_mode_name; this->set_custom_fan_mode_(fan_mode_name);
} }
// TODO: Get biorhythm data to determine which preset (M1-3) is running, if any. // TODO: Get biorhythm data to determine which preset (M1-3) is running, if any.
@@ -255,7 +250,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) {
this->mode = CLIMATE_MODE_OFF; this->mode = CLIMATE_MODE_OFF;
this->action = CLIMATE_ACTION_IDLE; this->action = CLIMATE_ACTION_IDLE;
this->fan_mode = CLIMATE_FAN_OFF; this->fan_mode = CLIMATE_FAN_OFF;
this->custom_preset.reset(); this->clear_custom_preset_();
this->preset.reset(); this->preset.reset();
break; break;
@@ -266,7 +261,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) {
if (this->heating_mode_ == HEAT_MODE_EXTENDED) { if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
this->set_custom_preset_("LTD HT"); this->set_custom_preset_("LTD HT");
} else { } else {
this->custom_preset.reset(); this->clear_custom_preset_();
} }
break; break;
@@ -275,7 +270,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) {
this->action = CLIMATE_ACTION_HEATING; this->action = CLIMATE_ACTION_HEATING;
this->preset.reset(); this->preset.reset();
if (this->heating_mode_ == HEAT_MODE_EXTENDED) { if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
this->custom_preset.reset(); this->clear_custom_preset_();
} else { } else {
this->set_custom_preset_("EXT HT"); this->set_custom_preset_("EXT HT");
} }
@@ -284,20 +279,19 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) {
case MODE_COOL: case MODE_COOL:
this->mode = CLIMATE_MODE_FAN_ONLY; this->mode = CLIMATE_MODE_FAN_ONLY;
this->action = CLIMATE_ACTION_COOLING; this->action = CLIMATE_ACTION_COOLING;
this->custom_preset.reset(); this->clear_custom_preset_();
this->preset.reset(); this->preset.reset();
break; break;
case MODE_DRY: case MODE_DRY:
this->mode = CLIMATE_MODE_DRY; this->mode = CLIMATE_MODE_DRY;
this->action = CLIMATE_ACTION_DRYING; this->action = CLIMATE_ACTION_DRYING;
this->custom_preset.reset(); this->clear_custom_preset_();
this->preset.reset(); this->preset.reset();
break; break;
case MODE_TURBO: case MODE_TURBO:
this->preset = CLIMATE_PRESET_BOOST; this->set_preset_(CLIMATE_PRESET_BOOST);
this->custom_preset.reset();
this->mode = CLIMATE_MODE_HEAT; this->mode = CLIMATE_MODE_HEAT;
this->action = CLIMATE_ACTION_HEATING; this->action = CLIMATE_ACTION_HEATING;
break; break;

View File

@@ -43,28 +43,20 @@ class BedJetClimate : public climate::Climate, public BedJetClient, public Polli
}); });
// It would be better if we had a slider for the fan modes. // It would be better if we had a slider for the fan modes.
traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES_SET); traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES);
traits.set_supported_presets({ traits.set_supported_presets({
// If we support NONE, then have to decide what happens if the user switches to it (turn off?) // If we support NONE, then have to decide what happens if the user switches to it (turn off?)
// climate::CLIMATE_PRESET_NONE, // climate::CLIMATE_PRESET_NONE,
// Climate doesn't have a "TURBO" mode, but we can use the BOOST preset instead. // Climate doesn't have a "TURBO" mode, but we can use the BOOST preset instead.
climate::CLIMATE_PRESET_BOOST, climate::CLIMATE_PRESET_BOOST,
}); });
// String literals are stored in rodata and valid for program lifetime
traits.set_supported_custom_presets({ traits.set_supported_custom_presets({
// We could fetch biodata from bedjet and set these names that way. this->heating_mode_ == HEAT_MODE_EXTENDED ? "LTD HT" : "EXT HT",
// But then we have to invert the lookup in order to send the right preset.
// For now, we can leave them as M1-3 to match the remote buttons.
// EXT HT added to match remote button.
"EXT HT",
"M1", "M1",
"M2", "M2",
"M3", "M3",
}); });
if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
traits.add_supported_custom_preset("LTD HT");
} else {
traits.add_supported_custom_preset("EXT HT");
}
traits.set_visual_min_temperature(19.0); traits.set_visual_min_temperature(19.0);
traits.set_visual_max_temperature(43.0); traits.set_visual_max_temperature(43.0);
traits.set_visual_temperature_step(1.0); traits.set_visual_temperature_step(1.0);

View File

@@ -155,6 +155,7 @@ DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter, cg.Compon
InvertFilter = binary_sensor_ns.class_("InvertFilter", Filter) InvertFilter = binary_sensor_ns.class_("InvertFilter", Filter)
AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Component) AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Component)
LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter) LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter)
StatelessLambdaFilter = binary_sensor_ns.class_("StatelessLambdaFilter", Filter)
SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter, cg.Component) SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter, cg.Component)
_LOGGER = getLogger(__name__) _LOGGER = getLogger(__name__)
@@ -299,7 +300,7 @@ async def lambda_filter_to_code(config, filter_id):
lambda_ = await cg.process_lambda( lambda_ = await cg.process_lambda(
config, [(bool, "x")], return_type=cg.optional.template(bool) config, [(bool, "x")], return_type=cg.optional.template(bool)
) )
return cg.new_Pvariable(filter_id, lambda_) return automation.new_lambda_pvariable(filter_id, lambda_, StatelessLambdaFilter)
@register_filter( @register_filter(
@@ -547,11 +548,6 @@ def binary_sensor_schema(
return _BINARY_SENSOR_SCHEMA.extend(schema) return _BINARY_SENSOR_SCHEMA.extend(schema)
# Remove before 2025.11.0
BINARY_SENSOR_SCHEMA = binary_sensor_schema()
BINARY_SENSOR_SCHEMA.add_extra(cv.deprecated_schema_constant("binary_sensor"))
async def setup_binary_sensor_core_(var, config): async def setup_binary_sensor_core_(var, config):
await setup_entity(var, config, "binary_sensor") await setup_entity(var, config, "binary_sensor")

View File

@@ -111,6 +111,21 @@ class LambdaFilter : public Filter {
std::function<optional<bool>(bool)> f_; std::function<optional<bool>(bool)> f_;
}; };
/** Optimized lambda filter for stateless lambdas (no capture).
*
* Uses function pointer instead of std::function to reduce memory overhead.
* Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function).
*/
class StatelessLambdaFilter : public Filter {
public:
explicit StatelessLambdaFilter(optional<bool> (*f)(bool)) : f_(f) {}
optional<bool> new_value(bool value) override { return this->f_(value); }
protected:
optional<bool> (*f_)(bool);
};
class SettleFilter : public Filter, public Component { class SettleFilter : public Filter, public Component {
public: public:
optional<bool> new_value(bool value) override; optional<bool> new_value(bool value) override;

View File

@@ -96,8 +96,11 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
BLEClientWriteAction(BLEClient *ble_client) { BLEClientWriteAction(BLEClient *ble_client) {
ble_client->register_ble_node(this); ble_client->register_ble_node(this);
ble_client_ = ble_client; ble_client_ = ble_client;
this->construct_simple_value_();
} }
~BLEClientWriteAction() { this->destroy_simple_value_(); }
void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); }
void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
@@ -106,14 +109,18 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
void set_char_uuid32(uint32_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } void set_char_uuid32(uint32_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
void set_value_template(std::function<std::vector<uint8_t>(Ts...)> func) { void set_value_template(std::vector<uint8_t> (*func)(Ts...)) {
this->value_template_ = std::move(func); this->destroy_simple_value_();
has_simple_value_ = false; this->value_.template_func = func;
this->has_simple_value_ = false;
} }
void set_value_simple(const std::vector<uint8_t> &value) { void set_value_simple(const std::vector<uint8_t> &value) {
this->value_simple_ = value; if (!this->has_simple_value_) {
has_simple_value_ = true; this->construct_simple_value_();
}
this->value_.simple = value;
this->has_simple_value_ = true;
} }
void play(Ts... x) override {} void play(Ts... x) override {}
@@ -121,7 +128,7 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
void play_complex(Ts... x) override { void play_complex(Ts... x) override {
this->num_running_++; this->num_running_++;
this->var_ = std::make_tuple(x...); this->var_ = std::make_tuple(x...);
auto value = this->has_simple_value_ ? this->value_simple_ : this->value_template_(x...); auto value = this->has_simple_value_ ? this->value_.simple : this->value_.template_func(x...);
// on write failure, continue the automation chain rather than stopping so that e.g. disconnect can work. // on write failure, continue the automation chain rather than stopping so that e.g. disconnect can work.
if (!write(value)) if (!write(value))
this->play_next_(x...); this->play_next_(x...);
@@ -194,10 +201,22 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
} }
private: private:
void construct_simple_value_() { new (&this->value_.simple) std::vector<uint8_t>(); }
void destroy_simple_value_() {
if (this->has_simple_value_) {
this->value_.simple.~vector();
}
}
BLEClient *ble_client_; BLEClient *ble_client_;
bool has_simple_value_ = true; bool has_simple_value_ = true;
std::vector<uint8_t> value_simple_; union Value {
std::function<std::vector<uint8_t>(Ts...)> value_template_{}; std::vector<uint8_t> simple;
std::vector<uint8_t> (*template_func)(Ts...);
Value() {} // trivial constructor
~Value() {} // trivial destructor - we manage lifetime via discriminator
} value_;
espbt::ESPBTUUID service_uuid_; espbt::ESPBTUUID service_uuid_;
espbt::ESPBTUUID char_uuid_; espbt::ESPBTUUID char_uuid_;
std::tuple<Ts...> var_{}; std::tuple<Ts...> var_{};
@@ -213,9 +232,9 @@ template<typename... Ts> class BLEClientPasskeyReplyAction : public Action<Ts...
void play(Ts... x) override { void play(Ts... x) override {
uint32_t passkey; uint32_t passkey;
if (has_simple_value_) { if (has_simple_value_) {
passkey = this->value_simple_; passkey = this->value_.simple;
} else { } else {
passkey = this->value_template_(x...); passkey = this->value_.template_func(x...);
} }
if (passkey > 999999) if (passkey > 999999)
return; return;
@@ -224,21 +243,23 @@ template<typename... Ts> class BLEClientPasskeyReplyAction : public Action<Ts...
esp_ble_passkey_reply(remote_bda, true, passkey); esp_ble_passkey_reply(remote_bda, true, passkey);
} }
void set_value_template(std::function<uint32_t(Ts...)> func) { void set_value_template(uint32_t (*func)(Ts...)) {
this->value_template_ = std::move(func); this->value_.template_func = func;
has_simple_value_ = false; this->has_simple_value_ = false;
} }
void set_value_simple(const uint32_t &value) { void set_value_simple(const uint32_t &value) {
this->value_simple_ = value; this->value_.simple = value;
has_simple_value_ = true; this->has_simple_value_ = true;
} }
private: private:
BLEClient *parent_{nullptr}; BLEClient *parent_{nullptr};
bool has_simple_value_ = true; bool has_simple_value_ = true;
uint32_t value_simple_{0}; union {
std::function<uint32_t(Ts...)> value_template_{}; uint32_t simple;
uint32_t (*template_func)(Ts...);
} value_{.simple = 0};
}; };
template<typename... Ts> class BLEClientNumericComparisonReplyAction : public Action<Ts...> { template<typename... Ts> class BLEClientNumericComparisonReplyAction : public Action<Ts...> {
@@ -249,27 +270,29 @@ template<typename... Ts> class BLEClientNumericComparisonReplyAction : public Ac
esp_bd_addr_t remote_bda; esp_bd_addr_t remote_bda;
memcpy(remote_bda, parent_->get_remote_bda(), sizeof(esp_bd_addr_t)); memcpy(remote_bda, parent_->get_remote_bda(), sizeof(esp_bd_addr_t));
if (has_simple_value_) { if (has_simple_value_) {
esp_ble_confirm_reply(remote_bda, this->value_simple_); esp_ble_confirm_reply(remote_bda, this->value_.simple);
} else { } else {
esp_ble_confirm_reply(remote_bda, this->value_template_(x...)); esp_ble_confirm_reply(remote_bda, this->value_.template_func(x...));
} }
} }
void set_value_template(std::function<bool(Ts...)> func) { void set_value_template(bool (*func)(Ts...)) {
this->value_template_ = std::move(func); this->value_.template_func = func;
has_simple_value_ = false; this->has_simple_value_ = false;
} }
void set_value_simple(const bool &value) { void set_value_simple(const bool &value) {
this->value_simple_ = value; this->value_.simple = value;
has_simple_value_ = true; this->has_simple_value_ = true;
} }
private: private:
BLEClient *parent_{nullptr}; BLEClient *parent_{nullptr};
bool has_simple_value_ = true; bool has_simple_value_ = true;
bool value_simple_{false}; union {
std::function<bool(Ts...)> value_template_{}; bool simple;
bool (*template_func)(Ts...);
} value_{.simple = false};
}; };
template<typename... Ts> class BLEClientRemoveBondAction : public Action<Ts...> { template<typename... Ts> class BLEClientRemoveBondAction : public Action<Ts...> {

View File

@@ -77,6 +77,9 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
} }
} else { } else {
this->node_state = espbt::ClientState::ESTABLISHED; this->node_state = espbt::ClientState::ESTABLISHED;
// For non-notify characteristics, trigger an immediate read after service discovery
// to avoid peripherals disconnecting due to inactivity
this->update();
} }
break; break;
} }
@@ -117,9 +120,9 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
} }
float BLESensor::parse_data_(uint8_t *value, uint16_t value_len) { float BLESensor::parse_data_(uint8_t *value, uint16_t value_len) {
if (this->data_to_value_func_.has_value()) { if (this->has_data_to_value_) {
std::vector<uint8_t> data(value, value + value_len); std::vector<uint8_t> data(value, value + value_len);
return (*this->data_to_value_func_)(data); return this->data_to_value_func_(data);
} else { } else {
return value[0]; return value[0];
} }

View File

@@ -15,8 +15,6 @@ namespace ble_client {
namespace espbt = esphome::esp32_ble_tracker; namespace espbt = esphome::esp32_ble_tracker;
using data_to_value_t = std::function<float(std::vector<uint8_t>)>;
class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClientNode { class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClientNode {
public: public:
void loop() override; void loop() override;
@@ -33,13 +31,17 @@ class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClie
void set_descr_uuid16(uint16_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } void set_descr_uuid16(uint16_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); }
void set_descr_uuid32(uint32_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } void set_descr_uuid32(uint32_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
void set_descr_uuid128(uint8_t *uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } void set_descr_uuid128(uint8_t *uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
void set_data_to_value(data_to_value_t &&lambda) { this->data_to_value_func_ = lambda; } void set_data_to_value(float (*lambda)(const std::vector<uint8_t> &)) {
this->data_to_value_func_ = lambda;
this->has_data_to_value_ = true;
}
void set_enable_notify(bool notify) { this->notify_ = notify; } void set_enable_notify(bool notify) { this->notify_ = notify; }
uint16_t handle; uint16_t handle;
protected: protected:
float parse_data_(uint8_t *value, uint16_t value_len); float parse_data_(uint8_t *value, uint16_t value_len);
optional<data_to_value_t> data_to_value_func_{}; bool has_data_to_value_{false};
float (*data_to_value_func_)(const std::vector<uint8_t> &){};
bool notify_; bool notify_;
espbt::ESPBTUUID service_uuid_; espbt::ESPBTUUID service_uuid_;
espbt::ESPBTUUID char_uuid_; espbt::ESPBTUUID char_uuid_;

View File

@@ -79,6 +79,9 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
} }
} else { } else {
this->node_state = espbt::ClientState::ESTABLISHED; this->node_state = espbt::ClientState::ESTABLISHED;
// For non-notify characteristics, trigger an immediate read after service discovery
// to avoid peripherals disconnecting due to inactivity
this->update();
} }
break; break;
} }

View File

@@ -84,11 +84,6 @@ def button_schema(
return _BUTTON_SCHEMA.extend(schema) return _BUTTON_SCHEMA.extend(schema)
# Remove before 2025.11.0
BUTTON_SCHEMA = button_schema(Button)
BUTTON_SCHEMA.add_extra(cv.deprecated_schema_constant("button"))
async def setup_button_core_(var, config): async def setup_button_core_(var, config):
await setup_entity(var, config, "button") await setup_entity(var, config, "button")

View File

@@ -270,11 +270,6 @@ def climate_schema(
return _CLIMATE_SCHEMA.extend(schema) return _CLIMATE_SCHEMA.extend(schema)
# Remove before 2025.11.0
CLIMATE_SCHEMA = climate_schema(Climate)
CLIMATE_SCHEMA.add_extra(cv.deprecated_schema_constant("climate"))
async def setup_climate_core_(var, config): async def setup_climate_core_(var, config):
await setup_entity(var, config, "climate") await setup_entity(var, config, "climate")

View File

@@ -50,21 +50,21 @@ void ClimateCall::perform() {
const LogString *mode_s = climate_mode_to_string(*this->mode_); const LogString *mode_s = climate_mode_to_string(*this->mode_);
ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(mode_s)); ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(mode_s));
} }
if (this->custom_fan_mode_.has_value()) { if (this->custom_fan_mode_ != nullptr) {
this->fan_mode_.reset(); this->fan_mode_.reset();
ESP_LOGD(TAG, " Custom Fan: %s", this->custom_fan_mode_.value().c_str()); ESP_LOGD(TAG, " Custom Fan: %s", this->custom_fan_mode_);
} }
if (this->fan_mode_.has_value()) { if (this->fan_mode_.has_value()) {
this->custom_fan_mode_.reset(); this->custom_fan_mode_ = nullptr;
const LogString *fan_mode_s = climate_fan_mode_to_string(*this->fan_mode_); const LogString *fan_mode_s = climate_fan_mode_to_string(*this->fan_mode_);
ESP_LOGD(TAG, " Fan: %s", LOG_STR_ARG(fan_mode_s)); ESP_LOGD(TAG, " Fan: %s", LOG_STR_ARG(fan_mode_s));
} }
if (this->custom_preset_.has_value()) { if (this->custom_preset_ != nullptr) {
this->preset_.reset(); this->preset_.reset();
ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset_.value().c_str()); ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset_);
} }
if (this->preset_.has_value()) { if (this->preset_.has_value()) {
this->custom_preset_.reset(); this->custom_preset_ = nullptr;
const LogString *preset_s = climate_preset_to_string(*this->preset_); const LogString *preset_s = climate_preset_to_string(*this->preset_);
ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(preset_s)); ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(preset_s));
} }
@@ -96,11 +96,10 @@ void ClimateCall::validate_() {
this->mode_.reset(); this->mode_.reset();
} }
} }
if (this->custom_fan_mode_.has_value()) { if (this->custom_fan_mode_ != nullptr) {
auto custom_fan_mode = *this->custom_fan_mode_; if (!traits.supports_custom_fan_mode(this->custom_fan_mode_)) {
if (!traits.supports_custom_fan_mode(custom_fan_mode)) { ESP_LOGW(TAG, " Fan Mode %s not supported", this->custom_fan_mode_);
ESP_LOGW(TAG, " Fan Mode %s not supported", custom_fan_mode.c_str()); this->custom_fan_mode_ = nullptr;
this->custom_fan_mode_.reset();
} }
} else if (this->fan_mode_.has_value()) { } else if (this->fan_mode_.has_value()) {
auto fan_mode = *this->fan_mode_; auto fan_mode = *this->fan_mode_;
@@ -109,11 +108,10 @@ void ClimateCall::validate_() {
this->fan_mode_.reset(); this->fan_mode_.reset();
} }
} }
if (this->custom_preset_.has_value()) { if (this->custom_preset_ != nullptr) {
auto custom_preset = *this->custom_preset_; if (!traits.supports_custom_preset(this->custom_preset_)) {
if (!traits.supports_custom_preset(custom_preset)) { ESP_LOGW(TAG, " Preset %s not supported", this->custom_preset_);
ESP_LOGW(TAG, " Preset %s not supported", custom_preset.c_str()); this->custom_preset_ = nullptr;
this->custom_preset_.reset();
} }
} else if (this->preset_.has_value()) { } else if (this->preset_.has_value()) {
auto preset = *this->preset_; auto preset = *this->preset_;
@@ -186,26 +184,29 @@ ClimateCall &ClimateCall::set_mode(const std::string &mode) {
ClimateCall &ClimateCall::set_fan_mode(ClimateFanMode fan_mode) { ClimateCall &ClimateCall::set_fan_mode(ClimateFanMode fan_mode) {
this->fan_mode_ = fan_mode; this->fan_mode_ = fan_mode;
this->custom_fan_mode_.reset(); this->custom_fan_mode_ = nullptr;
return *this; return *this;
} }
ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) { ClimateCall &ClimateCall::set_fan_mode(const char *custom_fan_mode) {
// Check if it's a standard enum mode first
for (const auto &mode_entry : CLIMATE_FAN_MODES_BY_STR) { for (const auto &mode_entry : CLIMATE_FAN_MODES_BY_STR) {
if (str_equals_case_insensitive(fan_mode, mode_entry.str)) { if (str_equals_case_insensitive(custom_fan_mode, mode_entry.str)) {
this->set_fan_mode(static_cast<ClimateFanMode>(mode_entry.value)); return this->set_fan_mode(static_cast<ClimateFanMode>(mode_entry.value));
return *this;
} }
} }
if (this->parent_->get_traits().supports_custom_fan_mode(fan_mode)) { // Find the matching pointer from parent climate device
this->custom_fan_mode_ = fan_mode; if (const char *mode_ptr = this->parent_->find_custom_fan_mode_(custom_fan_mode)) {
this->custom_fan_mode_ = mode_ptr;
this->fan_mode_.reset(); this->fan_mode_.reset();
} else { return *this;
ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), fan_mode.c_str());
} }
ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), custom_fan_mode);
return *this; 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) { ClimateCall &ClimateCall::set_fan_mode(optional<std::string> fan_mode) {
if (fan_mode.has_value()) { if (fan_mode.has_value()) {
this->set_fan_mode(fan_mode.value()); this->set_fan_mode(fan_mode.value());
@@ -215,26 +216,29 @@ ClimateCall &ClimateCall::set_fan_mode(optional<std::string> fan_mode) {
ClimateCall &ClimateCall::set_preset(ClimatePreset preset) { ClimateCall &ClimateCall::set_preset(ClimatePreset preset) {
this->preset_ = preset; this->preset_ = preset;
this->custom_preset_.reset(); this->custom_preset_ = nullptr;
return *this; return *this;
} }
ClimateCall &ClimateCall::set_preset(const std::string &preset) { ClimateCall &ClimateCall::set_preset(const char *custom_preset) {
// Check if it's a standard enum preset first
for (const auto &preset_entry : CLIMATE_PRESETS_BY_STR) { for (const auto &preset_entry : CLIMATE_PRESETS_BY_STR) {
if (str_equals_case_insensitive(preset, preset_entry.str)) { if (str_equals_case_insensitive(custom_preset, preset_entry.str)) {
this->set_preset(static_cast<ClimatePreset>(preset_entry.value)); return this->set_preset(static_cast<ClimatePreset>(preset_entry.value));
return *this;
} }
} }
if (this->parent_->get_traits().supports_custom_preset(preset)) { // Find the matching pointer from parent climate device
this->custom_preset_ = preset; if (const char *preset_ptr = this->parent_->find_custom_preset_(custom_preset)) {
this->custom_preset_ = preset_ptr;
this->preset_.reset(); this->preset_.reset();
} else { return *this;
ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), preset.c_str());
} }
ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), custom_preset);
return *this; 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) { ClimateCall &ClimateCall::set_preset(optional<std::string> preset) {
if (preset.has_value()) { if (preset.has_value()) {
this->set_preset(preset.value()); this->set_preset(preset.value());
@@ -287,8 +291,6 @@ const optional<ClimateMode> &ClimateCall::get_mode() const { return this->mode_;
const optional<ClimateFanMode> &ClimateCall::get_fan_mode() const { return this->fan_mode_; } const optional<ClimateFanMode> &ClimateCall::get_fan_mode() const { return this->fan_mode_; }
const optional<ClimateSwingMode> &ClimateCall::get_swing_mode() const { return this->swing_mode_; } const optional<ClimateSwingMode> &ClimateCall::get_swing_mode() const { return this->swing_mode_; }
const optional<ClimatePreset> &ClimateCall::get_preset() const { return this->preset_; } const optional<ClimatePreset> &ClimateCall::get_preset() const { return this->preset_; }
const optional<std::string> &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; }
const optional<std::string> &ClimateCall::get_custom_preset() const { return this->custom_preset_; }
ClimateCall &ClimateCall::set_target_temperature_high(optional<float> target_temperature_high) { ClimateCall &ClimateCall::set_target_temperature_high(optional<float> target_temperature_high) {
this->target_temperature_high_ = target_temperature_high; this->target_temperature_high_ = target_temperature_high;
@@ -317,13 +319,13 @@ ClimateCall &ClimateCall::set_mode(optional<ClimateMode> mode) {
ClimateCall &ClimateCall::set_fan_mode(optional<ClimateFanMode> fan_mode) { ClimateCall &ClimateCall::set_fan_mode(optional<ClimateFanMode> fan_mode) {
this->fan_mode_ = fan_mode; this->fan_mode_ = fan_mode;
this->custom_fan_mode_.reset(); this->custom_fan_mode_ = nullptr;
return *this; return *this;
} }
ClimateCall &ClimateCall::set_preset(optional<ClimatePreset> preset) { ClimateCall &ClimateCall::set_preset(optional<ClimatePreset> preset) {
this->preset_ = preset; this->preset_ = preset;
this->custom_preset_.reset(); this->custom_preset_ = nullptr;
return *this; return *this;
} }
@@ -382,13 +384,13 @@ void Climate::save_state_() {
state.uses_custom_fan_mode = false; state.uses_custom_fan_mode = false;
state.fan_mode = this->fan_mode.value(); state.fan_mode = this->fan_mode.value();
} }
if (!traits.get_supported_custom_fan_modes().empty() && custom_fan_mode.has_value()) { if (!traits.get_supported_custom_fan_modes().empty() && this->has_custom_fan_mode()) {
state.uses_custom_fan_mode = true; state.uses_custom_fan_mode = true;
const auto &supported = traits.get_supported_custom_fan_modes(); const auto &supported = traits.get_supported_custom_fan_modes();
// std::set has consistent order (lexicographic for strings) // std::vector maintains insertion order
size_t i = 0; size_t i = 0;
for (const auto &mode : supported) { for (const char *mode : supported) {
if (mode == custom_fan_mode) { if (strcmp(mode, this->custom_fan_mode_) == 0) {
state.custom_fan_mode = i; state.custom_fan_mode = i;
break; break;
} }
@@ -399,13 +401,13 @@ void Climate::save_state_() {
state.uses_custom_preset = false; state.uses_custom_preset = false;
state.preset = this->preset.value(); state.preset = this->preset.value();
} }
if (!traits.get_supported_custom_presets().empty() && custom_preset.has_value()) { if (!traits.get_supported_custom_presets().empty() && this->has_custom_preset()) {
state.uses_custom_preset = true; state.uses_custom_preset = true;
const auto &supported = traits.get_supported_custom_presets(); const auto &supported = traits.get_supported_custom_presets();
// std::set has consistent order (lexicographic for strings) // std::vector maintains insertion order
size_t i = 0; size_t i = 0;
for (const auto &preset : supported) { for (const char *preset : supported) {
if (preset == custom_preset) { if (strcmp(preset, this->custom_preset_) == 0) {
state.custom_preset = i; state.custom_preset = i;
break; break;
} }
@@ -430,14 +432,14 @@ void Climate::publish_state() {
if (traits.get_supports_fan_modes() && this->fan_mode.has_value()) { if (traits.get_supports_fan_modes() && this->fan_mode.has_value()) {
ESP_LOGD(TAG, " Fan Mode: %s", LOG_STR_ARG(climate_fan_mode_to_string(this->fan_mode.value()))); ESP_LOGD(TAG, " Fan Mode: %s", LOG_STR_ARG(climate_fan_mode_to_string(this->fan_mode.value())));
} }
if (!traits.get_supported_custom_fan_modes().empty() && this->custom_fan_mode.has_value()) { if (!traits.get_supported_custom_fan_modes().empty() && this->has_custom_fan_mode()) {
ESP_LOGD(TAG, " Custom Fan Mode: %s", this->custom_fan_mode.value().c_str()); ESP_LOGD(TAG, " Custom Fan Mode: %s", this->custom_fan_mode_);
} }
if (traits.get_supports_presets() && this->preset.has_value()) { if (traits.get_supports_presets() && this->preset.has_value()) {
ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(climate_preset_to_string(this->preset.value()))); ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(climate_preset_to_string(this->preset.value())));
} }
if (!traits.get_supported_custom_presets().empty() && this->custom_preset.has_value()) { if (!traits.get_supported_custom_presets().empty() && this->has_custom_preset()) {
ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset.value().c_str()); ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset_);
} }
if (traits.get_supports_swing_modes()) { if (traits.get_supports_swing_modes()) {
ESP_LOGD(TAG, " Swing Mode: %s", LOG_STR_ARG(climate_swing_mode_to_string(this->swing_mode))); ESP_LOGD(TAG, " Swing Mode: %s", LOG_STR_ARG(climate_swing_mode_to_string(this->swing_mode)));
@@ -527,7 +529,7 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) {
if (this->uses_custom_fan_mode) { if (this->uses_custom_fan_mode) {
if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) { if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) {
call.fan_mode_.reset(); call.fan_mode_.reset();
call.custom_fan_mode_ = *std::next(traits.get_supported_custom_fan_modes().cbegin(), this->custom_fan_mode); call.custom_fan_mode_ = traits.get_supported_custom_fan_modes()[this->custom_fan_mode];
} }
} else if (traits.supports_fan_mode(this->fan_mode)) { } else if (traits.supports_fan_mode(this->fan_mode)) {
call.set_fan_mode(this->fan_mode); call.set_fan_mode(this->fan_mode);
@@ -535,7 +537,7 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) {
if (this->uses_custom_preset) { if (this->uses_custom_preset) {
if (this->custom_preset < traits.get_supported_custom_presets().size()) { if (this->custom_preset < traits.get_supported_custom_presets().size()) {
call.preset_.reset(); call.preset_.reset();
call.custom_preset_ = *std::next(traits.get_supported_custom_presets().cbegin(), this->custom_preset); call.custom_preset_ = traits.get_supported_custom_presets()[this->custom_preset];
} }
} else if (traits.supports_preset(this->preset)) { } else if (traits.supports_preset(this->preset)) {
call.set_preset(this->preset); call.set_preset(this->preset);
@@ -562,20 +564,20 @@ void ClimateDeviceRestoreState::apply(Climate *climate) {
if (this->uses_custom_fan_mode) { if (this->uses_custom_fan_mode) {
if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) { if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) {
climate->fan_mode.reset(); climate->fan_mode.reset();
climate->custom_fan_mode = *std::next(traits.get_supported_custom_fan_modes().cbegin(), this->custom_fan_mode); climate->custom_fan_mode_ = traits.get_supported_custom_fan_modes()[this->custom_fan_mode];
} }
} else if (traits.supports_fan_mode(this->fan_mode)) { } else if (traits.supports_fan_mode(this->fan_mode)) {
climate->fan_mode = this->fan_mode; climate->fan_mode = this->fan_mode;
climate->custom_fan_mode.reset(); climate->clear_custom_fan_mode_();
} }
if (this->uses_custom_preset) { if (this->uses_custom_preset) {
if (this->custom_preset < traits.get_supported_custom_presets().size()) { if (this->custom_preset < traits.get_supported_custom_presets().size()) {
climate->preset.reset(); climate->preset.reset();
climate->custom_preset = *std::next(traits.get_supported_custom_presets().cbegin(), this->custom_preset); climate->custom_preset_ = traits.get_supported_custom_presets()[this->custom_preset];
} }
} else if (traits.supports_preset(this->preset)) { } else if (traits.supports_preset(this->preset)) {
climate->preset = this->preset; climate->preset = this->preset;
climate->custom_preset.reset(); climate->clear_custom_preset_();
} }
if (traits.supports_swing_mode(this->swing_mode)) { if (traits.supports_swing_mode(this->swing_mode)) {
climate->swing_mode = this->swing_mode; climate->swing_mode = this->swing_mode;
@@ -583,28 +585,107 @@ void ClimateDeviceRestoreState::apply(Climate *climate) {
climate->publish_state(); climate->publish_state();
} }
template<typename T1, typename T2> bool set_alternative(optional<T1> &dst, optional<T2> &alt, const T1 &src) { /** Template helper for setting primary modes (fan_mode, preset) with mutual exclusion.
bool is_changed = alt.has_value(); *
alt.reset(); * Climate devices have mutually exclusive mode pairs:
if (is_changed || dst != src) { * - fan_mode (enum) vs custom_fan_mode_ (const char*)
dst = src; * - preset (enum) vs custom_preset_ (const char*)
is_changed = true; *
* Only one mode in each pair can be active at a time. This helper ensures setting a primary
* mode automatically clears its corresponding custom mode.
*
* Example state transitions:
* Before: custom_fan_mode_="Turbo", fan_mode=nullopt
* Call: set_fan_mode_(CLIMATE_FAN_HIGH)
* After: custom_fan_mode_=nullptr, fan_mode=CLIMATE_FAN_HIGH
*
* @param primary The primary mode optional (fan_mode or preset)
* @param custom_ptr Reference to the custom mode pointer (custom_fan_mode_ or custom_preset_)
* @param value The new primary mode value to set
* @return true if state changed, false if already set to this value
*/
template<typename T> bool set_primary_mode(optional<T> &primary, const char *&custom_ptr, T value) {
// Clear the custom mode (mutual exclusion)
bool changed = custom_ptr != nullptr;
custom_ptr = nullptr;
// Set the primary mode
if (changed || !primary.has_value() || primary.value() != value) {
primary = value;
return true;
} }
return is_changed; return false;
}
/** Template helper for setting custom modes (custom_fan_mode_, custom_preset_) with mutual exclusion.
*
* This helper ensures setting a custom mode automatically clears its corresponding primary mode.
* It also validates that the custom mode exists in the device's supported modes (lifetime safety).
*
* Example state transitions:
* Before: fan_mode=CLIMATE_FAN_HIGH, custom_fan_mode_=nullptr
* Call: set_custom_fan_mode_("Turbo")
* After: fan_mode=nullopt, custom_fan_mode_="Turbo" (pointer from traits)
*
* Lifetime Safety:
* - found_ptr must come from traits.find_custom_*_mode_()
* - Only pointers found in traits are stored, ensuring they remain valid
* - Prevents dangling pointers from temporary strings
*
* @param custom_ptr Reference to the custom mode pointer to set
* @param primary The primary mode optional to clear
* @param found_ptr The validated pointer from traits (nullptr if not found)
* @param has_custom Whether a custom mode is currently active
* @return true if state changed, false otherwise
*/
template<typename T>
bool set_custom_mode(const char *&custom_ptr, optional<T> &primary, const char *found_ptr, bool has_custom) {
if (found_ptr != nullptr) {
// Clear the primary mode (mutual exclusion)
bool changed = primary.has_value();
primary.reset();
// Set the custom mode (pointer is validated by caller from traits)
if (changed || custom_ptr != found_ptr) {
custom_ptr = found_ptr;
return true;
}
return false;
}
// Mode not found in supported modes, clear it if currently set
if (has_custom) {
custom_ptr = nullptr;
return true;
}
return false;
} }
bool Climate::set_fan_mode_(ClimateFanMode mode) { bool Climate::set_fan_mode_(ClimateFanMode mode) {
return set_alternative(this->fan_mode, this->custom_fan_mode, mode); return set_primary_mode(this->fan_mode, this->custom_fan_mode_, mode);
} }
bool Climate::set_custom_fan_mode_(const std::string &mode) { bool Climate::set_custom_fan_mode_(const char *mode) {
return set_alternative(this->custom_fan_mode, this->fan_mode, mode); auto traits = this->get_traits();
return set_custom_mode<ClimateFanMode>(this->custom_fan_mode_, this->fan_mode, traits.find_custom_fan_mode_(mode),
this->has_custom_fan_mode());
} }
bool Climate::set_preset_(ClimatePreset preset) { return set_alternative(this->preset, this->custom_preset, preset); } void Climate::clear_custom_fan_mode_() { this->custom_fan_mode_ = nullptr; }
bool Climate::set_custom_preset_(const std::string &preset) { bool Climate::set_preset_(ClimatePreset preset) { return set_primary_mode(this->preset, this->custom_preset_, preset); }
return set_alternative(this->custom_preset, this->preset, preset);
bool Climate::set_custom_preset_(const char *preset) {
auto traits = this->get_traits();
return set_custom_mode<ClimatePreset>(this->custom_preset_, this->preset, traits.find_custom_preset_(preset),
this->has_custom_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);
}
const char *Climate::find_custom_preset_(const char *custom_preset) {
return this->get_traits().find_custom_preset_(custom_preset);
} }
void Climate::dump_traits_(const char *tag) { void Climate::dump_traits_(const char *tag) {
@@ -656,8 +737,8 @@ void Climate::dump_traits_(const char *tag) {
} }
if (!traits.get_supported_custom_fan_modes().empty()) { if (!traits.get_supported_custom_fan_modes().empty()) {
ESP_LOGCONFIG(tag, " Supported custom fan modes:"); ESP_LOGCONFIG(tag, " Supported custom fan modes:");
for (const std::string &s : traits.get_supported_custom_fan_modes()) for (const char *s : traits.get_supported_custom_fan_modes())
ESP_LOGCONFIG(tag, " - %s", s.c_str()); ESP_LOGCONFIG(tag, " - %s", s);
} }
if (!traits.get_supported_presets().empty()) { if (!traits.get_supported_presets().empty()) {
ESP_LOGCONFIG(tag, " Supported presets:"); ESP_LOGCONFIG(tag, " Supported presets:");
@@ -666,8 +747,8 @@ void Climate::dump_traits_(const char *tag) {
} }
if (!traits.get_supported_custom_presets().empty()) { if (!traits.get_supported_custom_presets().empty()) {
ESP_LOGCONFIG(tag, " Supported custom presets:"); ESP_LOGCONFIG(tag, " Supported custom presets:");
for (const std::string &s : traits.get_supported_custom_presets()) for (const char *s : traits.get_supported_custom_presets())
ESP_LOGCONFIG(tag, " - %s", s.c_str()); ESP_LOGCONFIG(tag, " - %s", s);
} }
if (!traits.get_supported_swing_modes().empty()) { if (!traits.get_supported_swing_modes().empty()) {
ESP_LOGCONFIG(tag, " Supported swing modes:"); ESP_LOGCONFIG(tag, " Supported swing modes:");

View File

@@ -77,6 +77,8 @@ class ClimateCall {
ClimateCall &set_fan_mode(const std::string &fan_mode); ClimateCall &set_fan_mode(const std::string &fan_mode);
/// Set the fan mode of the climate device based on a string. /// Set the fan mode of the climate device based on a string.
ClimateCall &set_fan_mode(optional<std::string> fan_mode); 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 swing mode of the climate device. /// Set the swing mode of the climate device.
ClimateCall &set_swing_mode(ClimateSwingMode swing_mode); ClimateCall &set_swing_mode(ClimateSwingMode swing_mode);
/// Set the swing mode of the climate device. /// Set the swing mode of the climate device.
@@ -91,6 +93,8 @@ class ClimateCall {
ClimateCall &set_preset(const std::string &preset); ClimateCall &set_preset(const std::string &preset);
/// Set the preset of the climate device based on a string. /// Set the preset of the climate device based on a string.
ClimateCall &set_preset(optional<std::string> preset); ClimateCall &set_preset(optional<std::string> preset);
/// Set the custom preset of the climate device.
ClimateCall &set_preset(const char *custom_preset);
void perform(); void perform();
@@ -103,8 +107,10 @@ class ClimateCall {
const optional<ClimateFanMode> &get_fan_mode() const; const optional<ClimateFanMode> &get_fan_mode() const;
const optional<ClimateSwingMode> &get_swing_mode() const; const optional<ClimateSwingMode> &get_swing_mode() const;
const optional<ClimatePreset> &get_preset() const; const optional<ClimatePreset> &get_preset() const;
const optional<std::string> &get_custom_fan_mode() const; const char *get_custom_fan_mode() const { return this->custom_fan_mode_; }
const optional<std::string> &get_custom_preset() const; const char *get_custom_preset() const { return this->custom_preset_; }
bool has_custom_fan_mode() const { return this->custom_fan_mode_ != nullptr; }
bool has_custom_preset() const { return this->custom_preset_ != nullptr; }
protected: protected:
void validate_(); void validate_();
@@ -118,8 +124,10 @@ class ClimateCall {
optional<ClimateFanMode> fan_mode_; optional<ClimateFanMode> fan_mode_;
optional<ClimateSwingMode> swing_mode_; optional<ClimateSwingMode> swing_mode_;
optional<ClimatePreset> preset_; optional<ClimatePreset> preset_;
optional<std::string> custom_fan_mode_;
optional<std::string> custom_preset_; private:
const char *custom_fan_mode_{nullptr};
const char *custom_preset_{nullptr};
}; };
/// Struct used to save the state of the climate device in restore memory. /// Struct used to save the state of the climate device in restore memory.
@@ -212,6 +220,12 @@ class Climate : public EntityBase {
void set_visual_min_humidity_override(float visual_min_humidity_override); void set_visual_min_humidity_override(float visual_min_humidity_override);
void set_visual_max_humidity_override(float visual_max_humidity_override); void set_visual_max_humidity_override(float visual_max_humidity_override);
/// Check if a custom fan mode is currently active.
bool has_custom_fan_mode() const { return this->custom_fan_mode_ != nullptr; }
/// Check if a custom preset is currently active.
bool has_custom_preset() const { return this->custom_preset_ != nullptr; }
/// The current temperature of the climate device, as reported from the integration. /// The current temperature of the climate device, as reported from the integration.
float current_temperature{NAN}; float current_temperature{NAN};
@@ -238,12 +252,6 @@ class Climate : public EntityBase {
/// The active preset of the climate device. /// The active preset of the climate device.
optional<ClimatePreset> preset; optional<ClimatePreset> preset;
/// The active custom fan mode of the climate device.
optional<std::string> custom_fan_mode;
/// The active custom preset mode of the climate device.
optional<std::string> custom_preset;
/// The active mode of the climate device. /// The active mode of the climate device.
ClimateMode mode{CLIMATE_MODE_OFF}; ClimateMode mode{CLIMATE_MODE_OFF};
@@ -253,20 +261,37 @@ class Climate : public EntityBase {
/// The active swing mode of the climate device. /// The active swing mode of the climate device.
ClimateSwingMode swing_mode{CLIMATE_SWING_OFF}; ClimateSwingMode swing_mode{CLIMATE_SWING_OFF};
/// Get the active custom fan mode (read-only access).
const char *get_custom_fan_mode() const { return this->custom_fan_mode_; }
/// Get the active custom preset (read-only access).
const char *get_custom_preset() const { return this->custom_preset_; }
protected: protected:
friend ClimateCall; friend ClimateCall;
friend struct ClimateDeviceRestoreState;
/// Set fan mode. Reset custom fan mode. Return true if fan mode has been changed. /// Set fan mode. Reset custom fan mode. Return true if fan mode has been changed.
bool set_fan_mode_(ClimateFanMode mode); bool set_fan_mode_(ClimateFanMode mode);
/// Set custom fan mode. Reset primary fan mode. Return true if fan mode has been changed. /// Set custom fan mode. Reset primary fan mode. Return true if fan mode has been changed.
bool set_custom_fan_mode_(const std::string &mode); bool set_custom_fan_mode_(const char *mode);
/// Clear custom fan mode.
void clear_custom_fan_mode_();
/// Set preset. Reset custom preset. Return true if preset has been changed. /// Set preset. Reset custom preset. Return true if preset has been changed.
bool set_preset_(ClimatePreset preset); bool set_preset_(ClimatePreset preset);
/// Set custom preset. Reset primary preset. Return true if preset has been changed. /// Set custom preset. Reset primary preset. Return true if preset has been changed.
bool set_custom_preset_(const std::string &preset); bool set_custom_preset_(const char *preset);
/// Clear custom preset.
void clear_custom_preset_();
/// 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);
/// Find and return the matching custom preset pointer from traits, or nullptr if not found.
const char *find_custom_preset_(const char *custom_preset);
/** Get the default traits of this climate device. /** Get the default traits of this climate device.
* *
@@ -303,6 +328,21 @@ class Climate : public EntityBase {
optional<float> visual_current_temperature_step_override_{}; optional<float> visual_current_temperature_step_override_{};
optional<float> visual_min_humidity_override_{}; optional<float> visual_min_humidity_override_{};
optional<float> visual_max_humidity_override_{}; optional<float> visual_max_humidity_override_{};
private:
/** The active custom fan mode (private - enforces use of safe setters).
*
* Points to an entry in traits.supported_custom_fan_modes_ or nullptr.
* Use get_custom_fan_mode() to read, set_custom_fan_mode_() to modify.
*/
const char *custom_fan_mode_{nullptr};
/** The active custom preset (private - enforces use of safe setters).
*
* Points to an entry in traits.supported_custom_presets_ or nullptr.
* Use get_custom_preset() to read, set_custom_preset_() to modify.
*/
const char *custom_preset_{nullptr};
}; };
} // namespace climate } // namespace climate

View File

@@ -7,6 +7,7 @@ namespace esphome {
namespace climate { namespace climate {
/// Enum for all modes a climate device can be in. /// Enum for all modes a climate device can be in.
/// NOTE: If adding values, update ClimateModeMask in climate_traits.h to use the new last value
enum ClimateMode : uint8_t { enum ClimateMode : uint8_t {
/// The climate device is off /// The climate device is off
CLIMATE_MODE_OFF = 0, CLIMATE_MODE_OFF = 0,
@@ -24,7 +25,7 @@ enum ClimateMode : uint8_t {
* For example, the target temperature can be adjusted based on a schedule, or learned behavior. * For example, the target temperature can be adjusted based on a schedule, or learned behavior.
* The target temperature can't be adjusted when in this mode. * The target temperature can't be adjusted when in this mode.
*/ */
CLIMATE_MODE_AUTO = 6 CLIMATE_MODE_AUTO = 6 // Update ClimateModeMask in climate_traits.h if adding values after this
}; };
/// Enum for the current action of the climate device. Values match those of ClimateMode. /// Enum for the current action of the climate device. Values match those of ClimateMode.
@@ -43,6 +44,7 @@ enum ClimateAction : uint8_t {
CLIMATE_ACTION_FAN = 6, CLIMATE_ACTION_FAN = 6,
}; };
/// NOTE: If adding values, update ClimateFanModeMask in climate_traits.h to use the new last value
enum ClimateFanMode : uint8_t { enum ClimateFanMode : uint8_t {
/// The fan mode is set to On /// The fan mode is set to On
CLIMATE_FAN_ON = 0, CLIMATE_FAN_ON = 0,
@@ -63,10 +65,11 @@ enum ClimateFanMode : uint8_t {
/// The fan mode is set to Diffuse /// The fan mode is set to Diffuse
CLIMATE_FAN_DIFFUSE = 8, CLIMATE_FAN_DIFFUSE = 8,
/// The fan mode is set to Quiet /// The fan mode is set to Quiet
CLIMATE_FAN_QUIET = 9, CLIMATE_FAN_QUIET = 9, // Update ClimateFanModeMask in climate_traits.h if adding values after this
}; };
/// Enum for all modes a climate swing can be in /// Enum for all modes a climate swing can be in
/// NOTE: If adding values, update ClimateSwingModeMask in climate_traits.h to use the new last value
enum ClimateSwingMode : uint8_t { enum ClimateSwingMode : uint8_t {
/// The swing mode is set to Off /// The swing mode is set to Off
CLIMATE_SWING_OFF = 0, CLIMATE_SWING_OFF = 0,
@@ -75,10 +78,11 @@ enum ClimateSwingMode : uint8_t {
/// The fan mode is set to Vertical /// The fan mode is set to Vertical
CLIMATE_SWING_VERTICAL = 2, CLIMATE_SWING_VERTICAL = 2,
/// The fan mode is set to Horizontal /// The fan mode is set to Horizontal
CLIMATE_SWING_HORIZONTAL = 3, CLIMATE_SWING_HORIZONTAL = 3, // Update ClimateSwingModeMask in climate_traits.h if adding values after this
}; };
/// Enum for all preset modes /// Enum for all preset modes
/// NOTE: If adding values, update ClimatePresetMask in climate_traits.h to use the new last value
enum ClimatePreset : uint8_t { enum ClimatePreset : uint8_t {
/// No preset is active /// No preset is active
CLIMATE_PRESET_NONE = 0, CLIMATE_PRESET_NONE = 0,
@@ -95,7 +99,7 @@ enum ClimatePreset : uint8_t {
/// Device is prepared for sleep /// Device is prepared for sleep
CLIMATE_PRESET_SLEEP = 6, CLIMATE_PRESET_SLEEP = 6,
/// Device is reacting to activity (e.g., movement sensors) /// Device is reacting to activity (e.g., movement sensors)
CLIMATE_PRESET_ACTIVITY = 7, CLIMATE_PRESET_ACTIVITY = 7, // Update ClimatePresetMask in climate_traits.h if adding values after this
}; };
enum ClimateFeature : uint32_t { enum ClimateFeature : uint32_t {

View File

@@ -1,19 +1,43 @@
#pragma once #pragma once
#include <set> #include <cstring>
#include <vector>
#include "climate_mode.h" #include "climate_mode.h"
#include "esphome/core/finite_set_mask.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
namespace esphome { namespace esphome {
#ifdef USE_API
namespace api {
class APIConnection;
} // namespace api
#endif
namespace climate { namespace climate {
// Type aliases for climate enum bitmasks
// These replace std::set<EnumType> to eliminate red-black tree overhead
// For contiguous enums starting at 0, DefaultBitPolicy provides 1:1 mapping (enum value = bit position)
// Bitmask size is automatically calculated from the last enum value
using ClimateModeMask = FiniteSetMask<ClimateMode, DefaultBitPolicy<ClimateMode, CLIMATE_MODE_AUTO + 1>>;
using ClimateFanModeMask = FiniteSetMask<ClimateFanMode, DefaultBitPolicy<ClimateFanMode, CLIMATE_FAN_QUIET + 1>>;
using ClimateSwingModeMask =
FiniteSetMask<ClimateSwingMode, DefaultBitPolicy<ClimateSwingMode, CLIMATE_SWING_HORIZONTAL + 1>>;
using ClimatePresetMask = FiniteSetMask<ClimatePreset, DefaultBitPolicy<ClimatePreset, CLIMATE_PRESET_ACTIVITY + 1>>;
// 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) {
for (const char *item : vec) {
if (strcmp(item, value) == 0)
return true;
}
return false;
}
// 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) {
for (const char *item : vec) {
if (strcmp(item, value) == 0)
return item;
}
return nullptr;
}
/** This class contains all static data for climate devices. /** This class contains all static data for climate devices.
* *
* All climate devices must support these features: * All climate devices must support these features:
@@ -41,7 +65,11 @@ namespace climate {
* - temperature step - the step with which to increase/decrease target temperature. * - temperature step - the step with which to increase/decrease target temperature.
* This also affects with how many decimal places the temperature is shown * This also affects with how many decimal places the temperature is shown
*/ */
class Climate; // Forward declaration
class ClimateTraits { class ClimateTraits {
friend class Climate; // Allow Climate to access protected find methods
public: public:
/// Get/set feature flags (see ClimateFeatures enum in climate_mode.h) /// Get/set feature flags (see ClimateFeatures enum in climate_mode.h)
uint32_t get_feature_flags() const { return this->feature_flags_; } uint32_t get_feature_flags() const { return this->feature_flags_; }
@@ -107,48 +135,74 @@ class ClimateTraits {
} }
} }
void set_supported_modes(std::set<ClimateMode> modes) { this->supported_modes_ = std::move(modes); } void set_supported_modes(ClimateModeMask modes) { this->supported_modes_ = modes; }
void add_supported_mode(ClimateMode mode) { this->supported_modes_.insert(mode); } void add_supported_mode(ClimateMode mode) { this->supported_modes_.insert(mode); }
bool supports_mode(ClimateMode mode) const { return this->supported_modes_.count(mode); } bool supports_mode(ClimateMode mode) const { return this->supported_modes_.count(mode); }
const std::set<ClimateMode> &get_supported_modes() const { return this->supported_modes_; } const ClimateModeMask &get_supported_modes() const { return this->supported_modes_; }
void set_supported_fan_modes(std::set<ClimateFanMode> modes) { this->supported_fan_modes_ = std::move(modes); } void set_supported_fan_modes(ClimateFanModeMask modes) { this->supported_fan_modes_ = modes; }
void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); } void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); }
void add_supported_custom_fan_mode(const std::string &mode) { this->supported_custom_fan_modes_.insert(mode); }
bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode); } bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode); }
bool get_supports_fan_modes() const { bool get_supports_fan_modes() const {
return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty(); return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty();
} }
const std::set<ClimateFanMode> &get_supported_fan_modes() const { return this->supported_fan_modes_; } const ClimateFanModeMask &get_supported_fan_modes() const { return this->supported_fan_modes_; }
void set_supported_custom_fan_modes(std::set<std::string> supported_custom_fan_modes) { void set_supported_custom_fan_modes(std::initializer_list<const char *> modes) {
this->supported_custom_fan_modes_ = std::move(supported_custom_fan_modes); this->supported_custom_fan_modes_ = modes;
}
void set_supported_custom_fan_modes(const std::vector<const char *> &modes) {
this->supported_custom_fan_modes_ = modes;
}
template<size_t N> void set_supported_custom_fan_modes(const char *const (&modes)[N]) {
this->supported_custom_fan_modes_.assign(modes, modes + N);
}
// Deleted overloads to catch incorrect std::string usage at compile time with clear error messages
void set_supported_custom_fan_modes(const std::vector<std::string> &modes) = delete;
void set_supported_custom_fan_modes(std::initializer_list<std::string> modes) = delete;
const std::vector<const char *> &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; }
bool supports_custom_fan_mode(const char *custom_fan_mode) const {
return vector_contains(this->supported_custom_fan_modes_, custom_fan_mode);
} }
const std::set<std::string> &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; }
bool supports_custom_fan_mode(const std::string &custom_fan_mode) const { bool supports_custom_fan_mode(const std::string &custom_fan_mode) const {
return this->supported_custom_fan_modes_.count(custom_fan_mode); return this->supports_custom_fan_mode(custom_fan_mode.c_str());
} }
void set_supported_presets(std::set<ClimatePreset> presets) { this->supported_presets_ = std::move(presets); } void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; }
void add_supported_preset(ClimatePreset preset) { this->supported_presets_.insert(preset); } void add_supported_preset(ClimatePreset preset) { this->supported_presets_.insert(preset); }
void add_supported_custom_preset(const std::string &preset) { this->supported_custom_presets_.insert(preset); }
bool supports_preset(ClimatePreset preset) const { return this->supported_presets_.count(preset); } bool supports_preset(ClimatePreset preset) const { return this->supported_presets_.count(preset); }
bool get_supports_presets() const { return !this->supported_presets_.empty(); } bool get_supports_presets() const { return !this->supported_presets_.empty(); }
const std::set<climate::ClimatePreset> &get_supported_presets() const { return this->supported_presets_; } const ClimatePresetMask &get_supported_presets() const { return this->supported_presets_; }
void set_supported_custom_presets(std::set<std::string> supported_custom_presets) { void set_supported_custom_presets(std::initializer_list<const char *> presets) {
this->supported_custom_presets_ = std::move(supported_custom_presets); this->supported_custom_presets_ = presets;
}
void set_supported_custom_presets(const std::vector<const char *> &presets) {
this->supported_custom_presets_ = presets;
}
template<size_t N> void set_supported_custom_presets(const char *const (&presets)[N]) {
this->supported_custom_presets_.assign(presets, presets + N);
}
// Deleted overloads to catch incorrect std::string usage at compile time with clear error messages
void set_supported_custom_presets(const std::vector<std::string> &presets) = delete;
void set_supported_custom_presets(std::initializer_list<std::string> presets) = delete;
const std::vector<const char *> &get_supported_custom_presets() const { return this->supported_custom_presets_; }
bool supports_custom_preset(const char *custom_preset) const {
return vector_contains(this->supported_custom_presets_, custom_preset);
} }
const std::set<std::string> &get_supported_custom_presets() const { return this->supported_custom_presets_; }
bool supports_custom_preset(const std::string &custom_preset) const { bool supports_custom_preset(const std::string &custom_preset) const {
return this->supported_custom_presets_.count(custom_preset); return this->supports_custom_preset(custom_preset.c_str());
} }
void set_supported_swing_modes(std::set<ClimateSwingMode> modes) { this->supported_swing_modes_ = std::move(modes); } void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; }
void add_supported_swing_mode(ClimateSwingMode mode) { this->supported_swing_modes_.insert(mode); } void add_supported_swing_mode(ClimateSwingMode mode) { this->supported_swing_modes_.insert(mode); }
bool supports_swing_mode(ClimateSwingMode swing_mode) const { return this->supported_swing_modes_.count(swing_mode); } bool supports_swing_mode(ClimateSwingMode swing_mode) const { return this->supported_swing_modes_.count(swing_mode); }
bool get_supports_swing_modes() const { return !this->supported_swing_modes_.empty(); } bool get_supports_swing_modes() const { return !this->supported_swing_modes_.empty(); }
const std::set<ClimateSwingMode> &get_supported_swing_modes() const { return this->supported_swing_modes_; } const ClimateSwingModeMask &get_supported_swing_modes() const { return this->supported_swing_modes_; }
float get_visual_min_temperature() const { return this->visual_min_temperature_; } float get_visual_min_temperature() const { return this->visual_min_temperature_; }
void set_visual_min_temperature(float visual_min_temperature) { void set_visual_min_temperature(float visual_min_temperature) {
@@ -179,23 +233,6 @@ class ClimateTraits {
void set_visual_max_humidity(float visual_max_humidity) { this->visual_max_humidity_ = visual_max_humidity; } void set_visual_max_humidity(float visual_max_humidity) { this->visual_max_humidity_ = visual_max_humidity; }
protected: protected:
#ifdef USE_API
// The API connection is a friend class to access internal methods
friend class api::APIConnection;
// These methods return references to internal data structures.
// They are used by the API to avoid copying data when encoding messages.
// Warning: Do not use these methods outside of the API connection code.
// They return references to internal data that can be invalidated.
const std::set<ClimateMode> &get_supported_modes_for_api_() const { return this->supported_modes_; }
const std::set<ClimateFanMode> &get_supported_fan_modes_for_api_() const { return this->supported_fan_modes_; }
const std::set<std::string> &get_supported_custom_fan_modes_for_api_() const {
return this->supported_custom_fan_modes_;
}
const std::set<climate::ClimatePreset> &get_supported_presets_for_api_() const { return this->supported_presets_; }
const std::set<std::string> &get_supported_custom_presets_for_api_() const { return this->supported_custom_presets_; }
const std::set<ClimateSwingMode> &get_supported_swing_modes_for_api_() const { return this->supported_swing_modes_; }
#endif
void set_mode_support_(climate::ClimateMode mode, bool supported) { void set_mode_support_(climate::ClimateMode mode, bool supported) {
if (supported) { if (supported) {
this->supported_modes_.insert(mode); this->supported_modes_.insert(mode);
@@ -218,6 +255,18 @@ 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);
}
/// 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);
}
uint32_t feature_flags_{0}; uint32_t feature_flags_{0};
float visual_min_temperature_{10}; float visual_min_temperature_{10};
float visual_max_temperature_{30}; float visual_max_temperature_{30};
@@ -226,12 +275,21 @@ class ClimateTraits {
float visual_min_humidity_{30}; float visual_min_humidity_{30};
float visual_max_humidity_{99}; float visual_max_humidity_{99};
std::set<climate::ClimateMode> supported_modes_ = {climate::CLIMATE_MODE_OFF}; climate::ClimateModeMask supported_modes_{climate::CLIMATE_MODE_OFF};
std::set<climate::ClimateFanMode> supported_fan_modes_; climate::ClimateFanModeMask supported_fan_modes_;
std::set<climate::ClimateSwingMode> supported_swing_modes_; climate::ClimateSwingModeMask supported_swing_modes_;
std::set<climate::ClimatePreset> supported_presets_; climate::ClimatePresetMask supported_presets_;
std::set<std::string> supported_custom_fan_modes_;
std::set<std::string> supported_custom_presets_; /** Custom mode storage using const char* pointers to eliminate std::string overhead.
*
* Pointers must remain valid for the ClimateTraits lifetime. Safe patterns:
* - String literals: set_supported_custom_fan_modes({"Turbo", "Silent"})
* - Static const data: static const char* MODE = "Eco";
*
* Climate class setters validate pointers are from these vectors before storing.
*/
std::vector<const char *> supported_custom_fan_modes_;
std::vector<const char *> supported_custom_presets_;
}; };
} // namespace climate } // namespace climate

View File

@@ -1,10 +1,9 @@
import logging import logging
from esphome import core
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import climate, remote_base, sensor from esphome.components import climate, remote_base, sensor
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT from esphome.const import CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT
from esphome.cpp_generator import MockObjClass from esphome.cpp_generator import MockObjClass
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -52,26 +51,6 @@ def climate_ir_with_receiver_schema(
) )
# Remove before 2025.11.0
def deprecated_schema_constant(config):
type: str = "unknown"
if (id := config.get(CONF_ID)) is not None and isinstance(id, core.ID):
type = str(id.type).split("::", maxsplit=1)[0]
_LOGGER.warning(
"Using `climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA` is deprecated and will be removed in ESPHome 2025.11.0. "
"Please use `climate_ir.climate_ir_with_receiver_schema(...)` instead. "
"If you are seeing this, report an issue to the external_component author and ask them to update it. "
"https://developers.esphome.io/blog/2025/05/14/_schema-deprecations/. "
"Component using this schema: %s",
type,
)
return config
CLIMATE_IR_WITH_RECEIVER_SCHEMA = climate_ir_with_receiver_schema(ClimateIR)
CLIMATE_IR_WITH_RECEIVER_SCHEMA.add_extra(deprecated_schema_constant)
async def register_climate_ir(var, config): async def register_climate_ir(var, config):
await cg.register_component(var, config) await cg.register_component(var, config)
await remote_base.register_transmittable(var, config) await remote_base.register_transmittable(var, config)

View File

@@ -24,16 +24,18 @@ class ClimateIR : public Component,
public remote_base::RemoteTransmittable { public remote_base::RemoteTransmittable {
public: public:
ClimateIR(float minimum_temperature, float maximum_temperature, float temperature_step = 1.0f, ClimateIR(float minimum_temperature, float maximum_temperature, float temperature_step = 1.0f,
bool supports_dry = false, bool supports_fan_only = false, std::set<climate::ClimateFanMode> fan_modes = {}, bool supports_dry = false, bool supports_fan_only = false,
std::set<climate::ClimateSwingMode> swing_modes = {}, std::set<climate::ClimatePreset> presets = {}) { climate::ClimateFanModeMask fan_modes = climate::ClimateFanModeMask(),
climate::ClimateSwingModeMask swing_modes = climate::ClimateSwingModeMask(),
climate::ClimatePresetMask presets = climate::ClimatePresetMask()) {
this->minimum_temperature_ = minimum_temperature; this->minimum_temperature_ = minimum_temperature;
this->maximum_temperature_ = maximum_temperature; this->maximum_temperature_ = maximum_temperature;
this->temperature_step_ = temperature_step; this->temperature_step_ = temperature_step;
this->supports_dry_ = supports_dry; this->supports_dry_ = supports_dry;
this->supports_fan_only_ = supports_fan_only; this->supports_fan_only_ = supports_fan_only;
this->fan_modes_ = std::move(fan_modes); this->fan_modes_ = fan_modes;
this->swing_modes_ = std::move(swing_modes); this->swing_modes_ = swing_modes;
this->presets_ = std::move(presets); this->presets_ = presets;
} }
void setup() override; void setup() override;
@@ -60,9 +62,9 @@ class ClimateIR : public Component,
bool supports_heat_{true}; bool supports_heat_{true};
bool supports_dry_{false}; bool supports_dry_{false};
bool supports_fan_only_{false}; bool supports_fan_only_{false};
std::set<climate::ClimateFanMode> fan_modes_ = {}; climate::ClimateFanModeMask fan_modes_{};
std::set<climate::ClimateSwingMode> swing_modes_ = {}; climate::ClimateSwingModeMask swing_modes_{};
std::set<climate::ClimatePreset> presets_ = {}; climate::ClimatePresetMask presets_{};
sensor::Sensor *sensor_{nullptr}; sensor::Sensor *sensor_{nullptr};
}; };

View File

@@ -8,7 +8,9 @@ BYTE_ORDER_BIG = "big_endian"
CONF_COLOR_DEPTH = "color_depth" CONF_COLOR_DEPTH = "color_depth"
CONF_DRAW_ROUNDING = "draw_rounding" CONF_DRAW_ROUNDING = "draw_rounding"
CONF_ENABLED = "enabled"
CONF_ON_RECEIVE = "on_receive" CONF_ON_RECEIVE = "on_receive"
CONF_ON_STATE_CHANGE = "on_state_change" CONF_ON_STATE_CHANGE = "on_state_change"
CONF_REQUEST_HEADERS = "request_headers" CONF_REQUEST_HEADERS = "request_headers"
CONF_ROWS = "rows"
CONF_USE_PSRAM = "use_psram" CONF_USE_PSRAM = "use_psram"

View File

@@ -12,7 +12,7 @@ void CopyFan::setup() {
this->oscillating = source_->oscillating; this->oscillating = source_->oscillating;
this->speed = source_->speed; this->speed = source_->speed;
this->direction = source_->direction; this->direction = source_->direction;
this->preset_mode = source_->preset_mode; this->set_preset_mode_(source_->get_preset_mode());
this->publish_state(); this->publish_state();
}); });
@@ -20,7 +20,7 @@ void CopyFan::setup() {
this->oscillating = source_->oscillating; this->oscillating = source_->oscillating;
this->speed = source_->speed; this->speed = source_->speed;
this->direction = source_->direction; this->direction = source_->direction;
this->preset_mode = source_->preset_mode; this->set_preset_mode_(source_->get_preset_mode());
this->publish_state(); this->publish_state();
} }
@@ -49,7 +49,7 @@ void CopyFan::control(const fan::FanCall &call) {
call2.set_speed(*call.get_speed()); call2.set_speed(*call.get_speed());
if (call.get_direction().has_value()) if (call.get_direction().has_value())
call2.set_direction(*call.get_direction()); call2.set_direction(*call.get_direction());
if (!call.get_preset_mode().empty()) if (call.has_preset_mode())
call2.set_preset_mode(call.get_preset_mode()); call2.set_preset_mode(call.get_preset_mode());
call2.perform(); call2.perform();
} }

View File

@@ -7,19 +7,19 @@ namespace copy {
static const char *const TAG = "copy.select"; static const char *const TAG = "copy.select";
void CopySelect::setup() { void CopySelect::setup() {
source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(value); }); source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(index); });
traits.set_options(source_->traits.get_options()); traits.set_options(source_->traits.get_options());
if (source_->has_state()) if (source_->has_state())
this->publish_state(source_->state); this->publish_state(source_->active_index().value());
} }
void CopySelect::dump_config() { LOG_SELECT("", "Copy Select", this); } void CopySelect::dump_config() { LOG_SELECT("", "Copy Select", this); }
void CopySelect::control(const std::string &value) { void CopySelect::control(size_t index) {
auto call = source_->make_call(); auto call = source_->make_call();
call.set_option(value); call.set_index(index);
call.perform(); call.perform();
} }

View File

@@ -13,7 +13,7 @@ class CopySelect : public select::Select, public Component {
void dump_config() override; void dump_config() override;
protected: protected:
void control(const std::string &value) override; void control(size_t index) override;
select::Select *source_; select::Select *source_;
}; };

View File

@@ -151,11 +151,6 @@ def cover_schema(
return _COVER_SCHEMA.extend(schema) return _COVER_SCHEMA.extend(schema)
# Remove before 2025.11.0
COVER_SCHEMA = cover_schema(Cover)
COVER_SCHEMA.add_extra(cv.deprecated_schema_constant("cover"))
async def setup_cover_core_(var, config): async def setup_cover_core_(var, config):
await setup_entity(var, config, "cover") await setup_entity(var, config, "cover")

View File

@@ -70,7 +70,7 @@ bool DallasTemperatureSensor::read_scratch_pad_() {
} }
void DallasTemperatureSensor::setup() { void DallasTemperatureSensor::setup() {
if (!this->check_address_()) if (!this->check_address_or_index_())
return; return;
if (!this->read_scratch_pad_()) if (!this->read_scratch_pad_())
return; return;

View File

@@ -8,8 +8,7 @@
#define BOOTLOADER_VERSION_REGISTER NRF_TIMER2->CC[0] #define BOOTLOADER_VERSION_REGISTER NRF_TIMER2->CC[0]
namespace esphome { namespace esphome::debug {
namespace debug {
static const char *const TAG = "debug"; static const char *const TAG = "debug";
constexpr std::uintptr_t MBR_PARAM_PAGE_ADDR = 0xFFC; constexpr std::uintptr_t MBR_PARAM_PAGE_ADDR = 0xFFC;
@@ -281,14 +280,18 @@ void DebugComponent::get_device_info_(std::string &device_info) {
NRF_FICR->INFO.VARIANT & 0xFF, package(NRF_FICR->INFO.PACKAGE)); NRF_FICR->INFO.VARIANT & 0xFF, package(NRF_FICR->INFO.PACKAGE));
ESP_LOGD(TAG, "RAM: %ukB, Flash: %ukB, production test: %sdone", NRF_FICR->INFO.RAM, NRF_FICR->INFO.FLASH, ESP_LOGD(TAG, "RAM: %ukB, Flash: %ukB, production test: %sdone", NRF_FICR->INFO.RAM, NRF_FICR->INFO.FLASH,
(NRF_FICR->PRODTEST[0] == 0xBB42319F ? "" : "not ")); (NRF_FICR->PRODTEST[0] == 0xBB42319F ? "" : "not "));
bool n_reset_enabled = NRF_UICR->PSELRESET[0] == NRF_UICR->PSELRESET[1] &&
(NRF_UICR->PSELRESET[0] & UICR_PSELRESET_CONNECT_Msk) == UICR_PSELRESET_CONNECT_Connected
<< UICR_PSELRESET_CONNECT_Pos;
ESP_LOGD( ESP_LOGD(
TAG, "GPIO as NFC pins: %s, GPIO as nRESET pin: %s", TAG, "GPIO as NFC pins: %s, GPIO as nRESET pin: %s",
YESNO((NRF_UICR->NFCPINS & UICR_NFCPINS_PROTECT_Msk) == (UICR_NFCPINS_PROTECT_NFC << UICR_NFCPINS_PROTECT_Pos)), YESNO((NRF_UICR->NFCPINS & UICR_NFCPINS_PROTECT_Msk) == (UICR_NFCPINS_PROTECT_NFC << UICR_NFCPINS_PROTECT_Pos)),
YESNO(((NRF_UICR->PSELRESET[0] & UICR_PSELRESET_CONNECT_Msk) != YESNO(n_reset_enabled));
(UICR_PSELRESET_CONNECT_Connected << UICR_PSELRESET_CONNECT_Pos)) || if (n_reset_enabled) {
((NRF_UICR->PSELRESET[1] & UICR_PSELRESET_CONNECT_Msk) != uint8_t port = (NRF_UICR->PSELRESET[0] & UICR_PSELRESET_PORT_Msk) >> UICR_PSELRESET_PORT_Pos;
(UICR_PSELRESET_CONNECT_Connected << UICR_PSELRESET_CONNECT_Pos)))); uint8_t pin = (NRF_UICR->PSELRESET[0] & UICR_PSELRESET_PIN_Msk) >> UICR_PSELRESET_PIN_Pos;
ESP_LOGD(TAG, "nRESET port P%u.%02u", port, pin);
}
#ifdef USE_BOOTLOADER_MCUBOOT #ifdef USE_BOOTLOADER_MCUBOOT
ESP_LOGD(TAG, "bootloader: mcuboot"); ESP_LOGD(TAG, "bootloader: mcuboot");
#else #else
@@ -322,10 +325,22 @@ void DebugComponent::get_device_info_(std::string &device_info) {
#endif #endif
} }
#endif #endif
auto uicr = [](volatile uint32_t *data, uint8_t size) {
std::string res;
char buf[sizeof(uint32_t) * 2 + 1];
for (size_t i = 0; i < size; i++) {
if (i > 0) {
res += ' ';
}
res += format_hex_pretty<uint32_t>(data[i], '\0', false);
}
return res;
};
ESP_LOGD(TAG, "NRFFW %s", uicr(NRF_UICR->NRFFW, 13).c_str());
ESP_LOGD(TAG, "NRFHW %s", uicr(NRF_UICR->NRFHW, 12).c_str());
} }
void DebugComponent::update_platform_() {} void DebugComponent::update_platform_() {}
} // namespace debug } // namespace esphome::debug
} // namespace esphome
#endif #endif

View File

@@ -28,16 +28,16 @@ class DemoClimate : public climate::Climate, public Component {
this->mode = climate::CLIMATE_MODE_AUTO; this->mode = climate::CLIMATE_MODE_AUTO;
this->action = climate::CLIMATE_ACTION_COOLING; this->action = climate::CLIMATE_ACTION_COOLING;
this->fan_mode = climate::CLIMATE_FAN_HIGH; this->fan_mode = climate::CLIMATE_FAN_HIGH;
this->custom_preset = {"My Preset"}; this->set_custom_preset_("My Preset");
break; break;
case DemoClimateType::TYPE_3: case DemoClimateType::TYPE_3:
this->current_temperature = 21.5; this->current_temperature = 21.5;
this->target_temperature_low = 21.0; this->target_temperature_low = 21.0;
this->target_temperature_high = 22.5; this->target_temperature_high = 22.5;
this->mode = climate::CLIMATE_MODE_HEAT_COOL; this->mode = climate::CLIMATE_MODE_HEAT_COOL;
this->custom_fan_mode = {"Auto Low"}; this->set_custom_fan_mode_("Auto Low");
this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL; this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL;
this->preset = climate::CLIMATE_PRESET_AWAY; this->set_preset_(climate::CLIMATE_PRESET_AWAY);
break; break;
} }
this->publish_state(); this->publish_state();
@@ -58,23 +58,19 @@ class DemoClimate : public climate::Climate, public Component {
this->target_temperature_high = *call.get_target_temperature_high(); this->target_temperature_high = *call.get_target_temperature_high();
} }
if (call.get_fan_mode().has_value()) { if (call.get_fan_mode().has_value()) {
this->fan_mode = *call.get_fan_mode(); this->set_fan_mode_(*call.get_fan_mode());
this->custom_fan_mode.reset();
} }
if (call.get_swing_mode().has_value()) { if (call.get_swing_mode().has_value()) {
this->swing_mode = *call.get_swing_mode(); this->swing_mode = *call.get_swing_mode();
} }
if (call.get_custom_fan_mode().has_value()) { if (call.has_custom_fan_mode()) {
this->custom_fan_mode = *call.get_custom_fan_mode(); this->set_custom_fan_mode_(call.get_custom_fan_mode());
this->fan_mode.reset();
} }
if (call.get_preset().has_value()) { if (call.get_preset().has_value()) {
this->preset = *call.get_preset(); this->set_preset_(*call.get_preset());
this->custom_preset.reset();
} }
if (call.get_custom_preset().has_value()) { if (call.has_custom_preset()) {
this->custom_preset = *call.get_custom_preset(); this->set_custom_preset_(call.get_custom_preset());
this->preset.reset();
} }
this->publish_state(); this->publish_state();
} }

View File

@@ -8,7 +8,7 @@ namespace demo {
class DemoSelect : public select::Select, public Component { class DemoSelect : public select::Select, public Component {
protected: protected:
void control(const std::string &value) override { this->publish_state(value); } void control(size_t index) override { this->publish_state(index); }
}; };
} // namespace demo } // namespace demo

View File

@@ -176,7 +176,117 @@ class Display;
class DisplayPage; class DisplayPage;
class DisplayOnPageChangeTrigger; class DisplayOnPageChangeTrigger;
using display_writer_t = std::function<void(Display &)>; /** Optimized display writer that uses function pointers for stateless lambdas.
*
* Similar to TemplatableValue but specialized for display writer callbacks.
* Saves ~8 bytes per stateless lambda on 32-bit platforms (16 bytes std::function → ~8 bytes discriminator+pointer).
*
* Supports both:
* - Stateless lambdas (from YAML) → function pointer (4 bytes)
* - Stateful lambdas/std::function (from C++ code) → std::function* (heap allocated)
*
* @tparam T The display type (e.g., Display, Nextion, GPIOLCDDisplay)
*/
template<typename T> class DisplayWriter {
public:
DisplayWriter() : type_(NONE) {}
// For stateless lambdas (convertible to function pointer): use function pointer (4 bytes)
template<typename F>
DisplayWriter(F f) requires std::invocable<F, T &> && std::convertible_to<F, void (*)(T &)>
: type_(STATELESS_LAMBDA) {
this->stateless_f_ = f; // Implicit conversion to function pointer
}
// For stateful lambdas and std::function (not convertible to function pointer): use std::function* (heap allocated)
// This handles backwards compatibility with external components
template<typename F>
DisplayWriter(F f) requires std::invocable<F, T &> &&(!std::convertible_to<F, void (*)(T &)>) : type_(LAMBDA) {
this->f_ = new std::function<void(T &)>(std::move(f));
}
// Copy constructor
DisplayWriter(const DisplayWriter &other) : type_(other.type_) {
if (type_ == LAMBDA) {
this->f_ = new std::function<void(T &)>(*other.f_);
} else if (type_ == STATELESS_LAMBDA) {
this->stateless_f_ = other.stateless_f_;
}
}
// Move constructor
DisplayWriter(DisplayWriter &&other) noexcept : type_(other.type_) {
if (type_ == LAMBDA) {
this->f_ = other.f_;
other.f_ = nullptr;
} else if (type_ == STATELESS_LAMBDA) {
this->stateless_f_ = other.stateless_f_;
}
other.type_ = NONE;
}
// Assignment operators
DisplayWriter &operator=(const DisplayWriter &other) {
if (this != &other) {
this->~DisplayWriter();
new (this) DisplayWriter(other);
}
return *this;
}
DisplayWriter &operator=(DisplayWriter &&other) noexcept {
if (this != &other) {
this->~DisplayWriter();
new (this) DisplayWriter(std::move(other));
}
return *this;
}
~DisplayWriter() {
if (type_ == LAMBDA) {
delete this->f_;
}
// STATELESS_LAMBDA/NONE: no cleanup needed (function pointer or empty)
}
bool has_value() const { return this->type_ != NONE; }
void call(T &display) const {
switch (this->type_) {
case STATELESS_LAMBDA:
this->stateless_f_(display); // Direct function pointer call
break;
case LAMBDA:
(*this->f_)(display); // std::function call
break;
case NONE:
default:
break;
}
}
// Operator() for convenience
void operator()(T &display) const { this->call(display); }
// Operator* for backwards compatibility with (*writer_)(*this) pattern
DisplayWriter &operator*() { return *this; }
const DisplayWriter &operator*() const { return *this; }
protected:
enum : uint8_t {
NONE,
LAMBDA,
STATELESS_LAMBDA,
} type_;
union {
std::function<void(T &)> *f_;
void (*stateless_f_)(T &);
};
};
// Type alias for Display writer - uses optimized DisplayWriter instead of std::function
using display_writer_t = DisplayWriter<Display>;
#define LOG_DISPLAY(prefix, type, obj) \ #define LOG_DISPLAY(prefix, type, obj) \
if ((obj) != nullptr) { \ if ((obj) != nullptr) { \
@@ -210,7 +320,7 @@ class Display : public PollingComponent {
/// Fill the entire screen with the given color. /// Fill the entire screen with the given color.
virtual void fill(Color color); virtual void fill(Color color);
/// Clear the entire screen by filling it with OFF pixels. /// Clear the entire screen by filling it with OFF pixels.
void clear(); virtual void clear();
/// Get the calculated width of the display in pixels with rotation applied. /// Get the calculated width of the display in pixels with rotation applied.
virtual int get_width() { return this->get_width_internal(); } virtual int get_width() { return this->get_width_internal(); }
@@ -678,7 +788,7 @@ class Display : public PollingComponent {
void sort_triangle_points_by_y_(int *x1, int *y1, int *x2, int *y2, int *x3, int *y3); void sort_triangle_points_by_y_(int *x1, int *y1, int *x2, int *y2, int *x3, int *y3);
DisplayRotation rotation_{DISPLAY_ROTATION_0_DEGREES}; DisplayRotation rotation_{DISPLAY_ROTATION_0_DEGREES};
optional<display_writer_t> writer_{}; display_writer_t writer_{};
DisplayPage *page_{nullptr}; DisplayPage *page_{nullptr};
DisplayPage *previous_page_{nullptr}; DisplayPage *previous_page_{nullptr};
std::vector<DisplayOnPageChangeTrigger *> on_page_change_triggers_; std::vector<DisplayOnPageChangeTrigger *> on_page_change_triggers_;

View File

@@ -42,7 +42,7 @@ std::string MenuItemSelect::get_value_text() const {
result = this->value_getter_.value()(this); result = this->value_getter_.value()(this);
} else { } else {
if (this->select_var_ != nullptr) { if (this->select_var_ != nullptr) {
result = this->select_var_->state; result = this->select_var_->current_option();
} }
} }

View File

@@ -3,6 +3,8 @@
#include "e131_addressable_light_effect.h" #include "e131_addressable_light_effect.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <algorithm>
namespace esphome { namespace esphome {
namespace e131 { namespace e131 {
@@ -76,14 +78,14 @@ void E131Component::loop() {
} }
void E131Component::add_effect(E131AddressableLightEffect *light_effect) { void E131Component::add_effect(E131AddressableLightEffect *light_effect) {
if (light_effects_.count(light_effect)) { if (std::find(light_effects_.begin(), light_effects_.end(), light_effect) != light_effects_.end()) {
return; return;
} }
ESP_LOGD(TAG, "Registering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(), ESP_LOGD(TAG, "Registering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(),
light_effect->get_last_universe()); light_effect->get_last_universe());
light_effects_.insert(light_effect); light_effects_.push_back(light_effect);
for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) { for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) {
join_(universe); join_(universe);
@@ -91,14 +93,17 @@ void E131Component::add_effect(E131AddressableLightEffect *light_effect) {
} }
void E131Component::remove_effect(E131AddressableLightEffect *light_effect) { void E131Component::remove_effect(E131AddressableLightEffect *light_effect) {
if (!light_effects_.count(light_effect)) { auto it = std::find(light_effects_.begin(), light_effects_.end(), light_effect);
if (it == light_effects_.end()) {
return; return;
} }
ESP_LOGD(TAG, "Unregistering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(), ESP_LOGD(TAG, "Unregistering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(),
light_effect->get_last_universe()); light_effect->get_last_universe());
light_effects_.erase(light_effect); // Swap with last element and pop for O(1) removal (order doesn't matter)
*it = light_effects_.back();
light_effects_.pop_back();
for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) { for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) {
leave_(universe); leave_(universe);

View File

@@ -7,7 +7,6 @@
#include <cinttypes> #include <cinttypes>
#include <map> #include <map>
#include <memory> #include <memory>
#include <set>
#include <vector> #include <vector>
namespace esphome { namespace esphome {
@@ -47,9 +46,8 @@ class E131Component : public esphome::Component {
E131ListenMethod listen_method_{E131_MULTICAST}; E131ListenMethod listen_method_{E131_MULTICAST};
std::unique_ptr<socket::Socket> socket_; std::unique_ptr<socket::Socket> socket_;
std::set<E131AddressableLightEffect *> light_effects_; std::vector<E131AddressableLightEffect *> light_effects_;
std::map<int, int> universe_consumers_; std::map<int, int> universe_consumers_;
std::map<int, E131Packet> universe_packets_;
}; };
} // namespace e131 } // namespace e131

View File

@@ -1,21 +1,35 @@
import importlib
import pkgutil
from esphome import core, pins from esphome import core, pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import display, spi from esphome.components import display, spi
from esphome.components.mipi import flatten_sequence, map_sequence
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_BUSY_PIN, CONF_BUSY_PIN,
CONF_CS_PIN,
CONF_DATA_RATE,
CONF_DC_PIN, CONF_DC_PIN,
CONF_DIMENSIONS,
CONF_ENABLE_PIN,
CONF_HEIGHT,
CONF_ID, CONF_ID,
CONF_INIT_SEQUENCE,
CONF_LAMBDA, CONF_LAMBDA,
CONF_MODEL, CONF_MODEL,
CONF_PAGES,
CONF_RESET_DURATION, CONF_RESET_DURATION,
CONF_RESET_PIN, CONF_RESET_PIN,
CONF_WIDTH,
) )
from . import models
AUTO_LOAD = ["split_buffer"] AUTO_LOAD = ["split_buffer"]
DEPENDENCIES = ["spi"] DEPENDENCIES = ["spi"]
CONF_INIT_SEQUENCE_ID = "init_sequence_id"
epaper_spi_ns = cg.esphome_ns.namespace("epaper_spi") epaper_spi_ns = cg.esphome_ns.namespace("epaper_spi")
EPaperBase = epaper_spi_ns.class_( EPaperBase = epaper_spi_ns.class_(
"EPaperBase", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer "EPaperBase", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer
@@ -24,30 +38,79 @@ EPaperBase = epaper_spi_ns.class_(
EPaperSpectraE6 = epaper_spi_ns.class_("EPaperSpectraE6", EPaperBase) EPaperSpectraE6 = epaper_spi_ns.class_("EPaperSpectraE6", EPaperBase)
EPaper7p3InSpectraE6 = epaper_spi_ns.class_("EPaper7p3InSpectraE6", EPaperSpectraE6) EPaper7p3InSpectraE6 = epaper_spi_ns.class_("EPaper7p3InSpectraE6", EPaperSpectraE6)
MODELS = {
"7.3in-spectra-e6": EPaper7p3InSpectraE6,
}
# Import all models dynamically from the models package
for module_info in pkgutil.iter_modules(models.__path__):
importlib.import_module(f".models.{module_info.name}", package=__package__)
CONFIG_SCHEMA = cv.All( MODELS = models.EpaperModel.models
display.FULL_DISPLAY_SCHEMA.extend(
{ DIMENSION_SCHEMA = cv.Schema(
cv.GenerateID(): cv.declare_id(EPaperBase), {
cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, cv.Required(CONF_WIDTH): cv.int_,
cv.Required(CONF_MODEL): cv.one_of(*MODELS, lower=True, space="-"), cv.Required(CONF_HEIGHT): cv.int_,
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, }
cv.Optional(CONF_BUSY_PIN): pins.gpio_input_pin_schema,
cv.Optional(CONF_RESET_DURATION): cv.All(
cv.positive_time_period_milliseconds,
cv.Range(max=core.TimePeriod(milliseconds=500)),
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(spi.spi_device_schema()),
cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
) )
def model_schema(config):
model = MODELS[config[CONF_MODEL]]
class_name = epaper_spi_ns.class_(model.class_name, EPaperBase)
cv_dimensions = cv.Optional if model.get_default(CONF_WIDTH) else cv.Required
return (
display.FULL_DISPLAY_SCHEMA.extend(
spi.spi_device_schema(
cs_pin_required=False,
default_mode="MODE0",
default_data_rate=model.get_default(CONF_DATA_RATE, 10_000_000),
)
)
.extend(
{
model.option(pin): pins.gpio_output_pin_schema
for pin in (CONF_RESET_PIN, CONF_CS_PIN, CONF_BUSY_PIN)
}
)
.extend(
{
cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True),
model.option(CONF_DC_PIN, fallback=None): pins.gpio_output_pin_schema,
cv.GenerateID(): cv.declare_id(class_name),
cv.GenerateID(CONF_INIT_SEQUENCE_ID): cv.declare_id(cg.uint8),
cv_dimensions(CONF_DIMENSIONS): DIMENSION_SCHEMA,
model.option(CONF_ENABLE_PIN): cv.ensure_list(
pins.gpio_output_pin_schema
),
model.option(CONF_INIT_SEQUENCE, cv.UNDEFINED): cv.ensure_list(
map_sequence
),
model.option(CONF_RESET_DURATION, cv.UNDEFINED): cv.All(
cv.positive_time_period_milliseconds,
cv.Range(max=core.TimePeriod(milliseconds=500)),
),
}
)
)
def customise_schema(config):
"""
Create a customised config schema for a specific model and validate the configuration.
:param config: The configuration dictionary to validate
:return: The validated configuration dictionary
:raises cv.Invalid: If the configuration is invalid
"""
config = cv.Schema(
{
cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True),
},
extra=cv.ALLOW_EXTRA,
)(config)
return model_schema(config)(config)
CONFIG_SCHEMA = customise_schema
FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
"epaper_spi", require_miso=False, require_mosi=True "epaper_spi", require_miso=False, require_mosi=True
) )
@@ -56,8 +119,23 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
async def to_code(config): async def to_code(config):
model = MODELS[config[CONF_MODEL]] model = MODELS[config[CONF_MODEL]]
rhs = model.new() init_sequence = config.get(CONF_INIT_SEQUENCE)
var = cg.Pvariable(config[CONF_ID], rhs, model) if init_sequence is None:
init_sequence = model.get_init_sequence(config)
init_sequence = flatten_sequence(init_sequence)
init_sequence_length = len(init_sequence)
init_sequence_id = cg.static_const_array(
config[CONF_INIT_SEQUENCE_ID], init_sequence
)
width, height = model.get_dimensions(config)
var = cg.new_Pvariable(
config[CONF_ID],
model.name,
width,
height,
init_sequence_id,
init_sequence_length,
)
await display.register_display(var, config) await display.register_display(var, config)
await spi.register_spi_device(var, config) await spi.register_spi_device(var, config)

View File

@@ -8,33 +8,20 @@ namespace esphome::epaper_spi {
static const char *const TAG = "epaper_spi"; static const char *const TAG = "epaper_spi";
static const LogString *epaper_state_to_string(EPaperState state) { static constexpr const char *const EPAPER_STATE_STRINGS[] = {
switch (state) { "IDLE", "UPDATE", "RESET", "RESET_END",
case EPaperState::IDLE:
return LOG_STR("IDLE"); "SHOULD_WAIT", "INITIALISE", "TRANSFER_DATA", "POWER_ON", "REFRESH_SCREEN", "POWER_OFF", "DEEP_SLEEP",
case EPaperState::UPDATE: };
return LOG_STR("UPDATE");
case EPaperState::RESET: const char *EPaperBase::epaper_state_to_string_() {
return LOG_STR("RESET"); if (auto idx = static_cast<unsigned>(this->state_); idx < std::size(EPAPER_STATE_STRINGS))
case EPaperState::INITIALISE: return EPAPER_STATE_STRINGS[idx];
return LOG_STR("INITIALISE"); return "Unknown";
case EPaperState::TRANSFER_DATA:
return LOG_STR("TRANSFER_DATA");
case EPaperState::POWER_ON:
return LOG_STR("POWER_ON");
case EPaperState::REFRESH_SCREEN:
return LOG_STR("REFRESH_SCREEN");
case EPaperState::POWER_OFF:
return LOG_STR("POWER_OFF");
case EPaperState::DEEP_SLEEP:
return LOG_STR("DEEP_SLEEP");
default:
return LOG_STR("UNKNOWN");
}
} }
void EPaperBase::setup() { void EPaperBase::setup() {
if (!this->init_buffer_(this->get_buffer_length())) { if (!this->init_buffer_(this->buffer_length_)) {
this->mark_failed("Failed to initialise buffer"); this->mark_failed("Failed to initialise buffer");
return; return;
} }
@@ -50,7 +37,7 @@ bool EPaperBase::init_buffer_(size_t buffer_length) {
return true; return true;
} }
void EPaperBase::setup_pins_() { void EPaperBase::setup_pins_() const {
this->dc_pin_->setup(); // OUTPUT this->dc_pin_->setup(); // OUTPUT
this->dc_pin_->digital_write(false); this->dc_pin_->digital_write(false);
@@ -81,11 +68,7 @@ void EPaperBase::data(uint8_t value) {
// write a command followed by zero or more bytes of data. // write a command followed by zero or more bytes of data.
// The command is the first byte, length is the length of data only in the second byte, followed by the data. // The command is the first byte, length is the length of data only in the second byte, followed by the data.
// [COMMAND, LENGTH, DATA...] // [COMMAND, LENGTH, DATA...]
void EPaperBase::cmd_data(const uint8_t *data) { void EPaperBase::cmd_data(uint8_t command, const uint8_t *ptr, size_t length) {
const uint8_t command = data[0];
const uint8_t length = data[1];
const uint8_t *ptr = data + 2;
ESP_LOGVV(TAG, "Command: 0x%02X, Length: %d, Data: %s", command, length, ESP_LOGVV(TAG, "Command: 0x%02X, Length: %d, Data: %s", command, length,
format_hex_pretty(ptr, length, '.', false).c_str()); format_hex_pretty(ptr, length, '.', false).c_str());
@@ -99,91 +82,146 @@ void EPaperBase::cmd_data(const uint8_t *data) {
this->disable(); this->disable();
} }
bool EPaperBase::is_idle_() { bool EPaperBase::is_idle_() const {
if (this->busy_pin_ == nullptr) { if (this->busy_pin_ == nullptr) {
return true; return true;
} }
return this->busy_pin_->digital_read(); return !this->busy_pin_->digital_read();
} }
void EPaperBase::reset() { bool EPaperBase::reset_() const {
if (this->reset_pin_ != nullptr) { if (this->reset_pin_ != nullptr) {
this->reset_pin_->digital_write(false); if (this->state_ == EPaperState::RESET) {
this->disable_loop(); this->reset_pin_->digital_write(false);
this->set_timeout(this->reset_duration_, [this] { return false;
this->reset_pin_->digital_write(true); }
this->set_timeout(20, [this] { this->enable_loop(); }); this->reset_pin_->digital_write(true);
});
} }
return true;
} }
void EPaperBase::update() { void EPaperBase::update() {
if (!this->state_queue_.empty()) { if (this->state_ != EPaperState::IDLE) {
ESP_LOGE(TAG, "Display update already in progress - %s", ESP_LOGE(TAG, "Display already in state %s", epaper_state_to_string_());
LOG_STR_ARG(epaper_state_to_string(this->state_queue_.front())));
return; return;
} }
this->set_state_(EPaperState::RESET);
this->state_queue_.push(EPaperState::UPDATE);
this->state_queue_.push(EPaperState::RESET);
this->state_queue_.push(EPaperState::INITIALISE);
this->state_queue_.push(EPaperState::TRANSFER_DATA);
this->state_queue_.push(EPaperState::POWER_ON);
this->state_queue_.push(EPaperState::REFRESH_SCREEN);
this->state_queue_.push(EPaperState::POWER_OFF);
this->state_queue_.push(EPaperState::DEEP_SLEEP);
this->state_queue_.push(EPaperState::IDLE);
this->enable_loop(); this->enable_loop();
} }
void EPaperBase::wait_for_idle_(bool should_wait) {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
if (should_wait) {
this->waiting_for_idle_start_ = millis();
this->waiting_for_idle_last_print_ = this->waiting_for_idle_start_;
}
#endif
this->waiting_for_idle_ = should_wait;
}
/**
* Called during the loop task.
* First defer for any pending delays, then check if we are waiting for the display to become idle.
* If not waiting for idle, process the state machine.
*/
void EPaperBase::loop() { void EPaperBase::loop() {
auto now = millis();
if (this->delay_until_ != 0) {
// using modulus arithmetic to handle wrap-around
int diff = now - this->delay_until_;
if (diff < 0) {
return;
}
this->delay_until_ = 0;
}
if (this->waiting_for_idle_) { if (this->waiting_for_idle_) {
if (this->is_idle_()) { if (this->is_idle_()) {
this->waiting_for_idle_ = false; this->waiting_for_idle_ = false;
ESP_LOGV(TAG, "Screen now idle after %u ms", (unsigned) (millis() - this->waiting_for_idle_start_));
} else { } else {
if (App.get_loop_component_start_time() - this->waiting_for_idle_last_print_ >= 1000) { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
ESP_LOGV(TAG, "Waiting for idle"); if (now - this->waiting_for_idle_last_print_ >= 1000) {
this->waiting_for_idle_last_print_ = App.get_loop_component_start_time(); ESP_LOGV(TAG, "Waiting for idle in state %s", this->epaper_state_to_string_());
this->waiting_for_idle_last_print_ = millis();
} }
#endif
return; return;
} }
} }
this->process_state_();
}
auto state = this->state_queue_.front(); /**
* Process the state machine.
switch (state) { * Typical state sequence:
* IDLE -> RESET -> RESET_END -> UPDATE -> INITIALISE -> TRANSFER_DATA -> POWER_ON -> REFRESH_SCREEN -> POWER_OFF ->
* DEEP_SLEEP -> IDLE
*
* Should a subclassed class need to override this, the method will need to be made virtual.
*/
void EPaperBase::process_state_() {
ESP_LOGV(TAG, "Process state entered in state %s", epaper_state_to_string_());
switch (this->state_) {
default:
ESP_LOGD(TAG, "Display is in unhandled state %s", epaper_state_to_string_());
this->disable_loop();
break;
case EPaperState::IDLE: case EPaperState::IDLE:
this->disable_loop(); this->disable_loop();
break; break;
case EPaperState::RESET:
case EPaperState::RESET_END:
if (this->reset_()) {
this->set_state_(EPaperState::UPDATE);
} else {
this->set_state_(EPaperState::RESET_END);
}
break;
case EPaperState::UPDATE: case EPaperState::UPDATE:
this->do_update_(); // Calls ESPHome (current page) lambda this->do_update_(); // Calls ESPHome (current page) lambda
break; this->set_state_(EPaperState::INITIALISE);
case EPaperState::RESET:
this->reset();
break; break;
case EPaperState::INITIALISE: case EPaperState::INITIALISE:
this->initialise_(); this->initialise_();
this->set_state_(EPaperState::TRANSFER_DATA);
break; break;
case EPaperState::TRANSFER_DATA: case EPaperState::TRANSFER_DATA:
if (!this->transfer_data()) { if (!this->transfer_data()) {
return; // Not done yet, come back next loop return; // Not done yet, come back next loop
} }
this->set_state_(EPaperState::POWER_ON);
break; break;
case EPaperState::POWER_ON: case EPaperState::POWER_ON:
this->power_on(); this->power_on();
this->set_state_(EPaperState::REFRESH_SCREEN);
break; break;
case EPaperState::REFRESH_SCREEN: case EPaperState::REFRESH_SCREEN:
this->refresh_screen(); this->refresh_screen();
this->set_state_(EPaperState::POWER_OFF);
break; break;
case EPaperState::POWER_OFF: case EPaperState::POWER_OFF:
this->power_off(); this->power_off();
this->set_state_(EPaperState::DEEP_SLEEP);
break; break;
case EPaperState::DEEP_SLEEP: case EPaperState::DEEP_SLEEP:
this->deep_sleep(); this->deep_sleep();
this->set_state_(EPaperState::IDLE);
break; break;
} }
this->state_queue_.pop(); }
void EPaperBase::set_state_(EPaperState state, uint16_t delay) {
ESP_LOGV(TAG, "Exit state %s", this->epaper_state_to_string_());
this->state_ = state;
this->wait_for_idle_(state > EPaperState::SHOULD_WAIT);
if (delay != 0) {
this->delay_until_ = millis() + delay;
} else {
this->delay_until_ = 0;
}
ESP_LOGV(TAG, "Enter state %s, delay %u, wait_for_idle=%s", this->epaper_state_to_string_(), delay,
TRUEFALSE(this->waiting_for_idle_));
} }
void EPaperBase::start_command_() { void EPaperBase::start_command_() {
@@ -203,25 +241,39 @@ void EPaperBase::on_safe_shutdown() { this->deep_sleep(); }
void EPaperBase::initialise_() { void EPaperBase::initialise_() {
size_t index = 0; size_t index = 0;
const auto &sequence = this->init_sequence_;
const size_t sequence_size = this->init_sequence_length_;
while (index != sequence_size) {
if (sequence_size - index < 2) {
this->mark_failed("Malformed init sequence");
return;
}
const auto *ptr = sequence + index;
const uint8_t length = ptr[1];
if (sequence_size - index < length + 2) {
this->mark_failed("Malformed init sequence");
return;
}
this->cmd_data(ptr); auto *sequence = this->init_sequence_;
index += length + 2; auto length = this->init_sequence_length_;
while (index != length) {
if (length - index < 2) {
this->mark_failed("Malformed init sequence");
return;
}
const uint8_t cmd = sequence[index++];
if (const uint8_t x = sequence[index++]; x == DELAY_FLAG) {
ESP_LOGV(TAG, "Delay %dms", cmd);
delay(cmd);
} else {
const uint8_t num_args = x & 0x7F;
if (length - index < num_args) {
ESP_LOGE(TAG, "Malformed init sequence, cmd = %X, num_args = %u", cmd, num_args);
this->mark_failed();
return;
}
ESP_LOGV(TAG, "Command %02X, length %d", cmd, num_args);
this->cmd_data(cmd, sequence + index, num_args);
index += num_args;
}
} }
}
this->power_on(); void EPaperBase::dump_config() {
LOG_DISPLAY("", "E-Paper SPI", this);
ESP_LOGCONFIG(TAG, " Model: %s", this->name_);
LOG_PIN(" Reset Pin: ", this->reset_pin_);
LOG_PIN(" DC Pin: ", this->dc_pin_);
LOG_PIN(" Busy Pin: ", this->busy_pin_);
LOG_UPDATE_INTERVAL(this);
} }
} // namespace esphome::epaper_spi } // namespace esphome::epaper_spi

View File

@@ -8,36 +8,48 @@
#include <queue> #include <queue>
namespace esphome::epaper_spi { namespace esphome::epaper_spi {
using namespace display;
enum class EPaperState : uint8_t { enum class EPaperState : uint8_t {
IDLE, IDLE, // not doing anything
UPDATE, UPDATE, // update the buffer
RESET, RESET, // drive reset low (active)
INITIALISE, RESET_END, // drive reset high (inactive)
TRANSFER_DATA,
POWER_ON, SHOULD_WAIT, // states higher than this should wait for the display to be not busy
REFRESH_SCREEN, INITIALISE, // send the init sequence
POWER_OFF, TRANSFER_DATA, // transfer data to the display
DEEP_SLEEP, POWER_ON, // power on the display
REFRESH_SCREEN, // send refresh command
POWER_OFF, // power off the display
DEEP_SLEEP, // deep sleep the display
}; };
static const uint8_t MAX_TRANSFER_TIME = 10; // Transfer in 10ms blocks to allow the loop to run static constexpr uint8_t MAX_TRANSFER_TIME = 10; // Transfer in 10ms blocks to allow the loop to run
static constexpr uint8_t DELAY_FLAG = 0xFF;
class EPaperBase : public display::DisplayBuffer, class EPaperBase : public DisplayBuffer,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING, public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
spi::DATA_RATE_2MHZ> { spi::DATA_RATE_2MHZ> {
public: public:
EPaperBase(const uint8_t *init_sequence, const size_t init_sequence_length) EPaperBase(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
: init_sequence_length_(init_sequence_length), init_sequence_(init_sequence) {} size_t init_sequence_length, DisplayType display_type = DISPLAY_TYPE_BINARY)
: name_(name),
width_(width),
height_(height),
init_sequence_(init_sequence),
init_sequence_length_(init_sequence_length),
display_type_(display_type) {}
void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; }
float get_setup_priority() const override; float get_setup_priority() const override;
void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; } void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; }
void set_busy_pin(GPIOPin *busy) { this->busy_pin_ = busy; } void set_busy_pin(GPIOPin *busy) { this->busy_pin_ = busy; }
void set_reset_duration(uint32_t reset_duration) { this->reset_duration_ = reset_duration; } void set_reset_duration(uint32_t reset_duration) { this->reset_duration_ = reset_duration; }
void dump_config() override;
void command(uint8_t value); void command(uint8_t value);
void data(uint8_t value); void data(uint8_t value);
void cmd_data(const uint8_t *data); void cmd_data(uint8_t command, const uint8_t *ptr, size_t length);
void update() override; void update() override;
void loop() override; void loop() override;
@@ -46,48 +58,84 @@ class EPaperBase : public display::DisplayBuffer,
void on_safe_shutdown() override; void on_safe_shutdown() override;
DisplayType get_display_type() override { return this->display_type_; };
protected: protected:
bool is_idle_(); int get_height_internal() override { return this->height_; };
void setup_pins_(); int get_width_internal() override { return this->width_; };
virtual void reset(); void process_state_();
const char *epaper_state_to_string_();
bool is_idle_() const;
void setup_pins_() const;
bool reset_() const;
void initialise_(); void initialise_();
void wait_for_idle_(bool should_wait);
bool init_buffer_(size_t buffer_length); bool init_buffer_(size_t buffer_length);
virtual int get_width_controller() { return this->get_width_internal(); }; virtual int get_width_controller() { return this->get_width_internal(); };
virtual void deep_sleep() = 0;
/**
* Methods that must be implemented by concrete classes to control the display
*/
/** /**
* Send data to the device via SPI * Send data to the device via SPI
* @return true if done, false if should be called next loop * @return true if done, false if it should be called next loop
*/ */
virtual bool transfer_data() = 0; virtual bool transfer_data() = 0;
/**
* Refresh the screen after data transfer
*/
virtual void refresh_screen() = 0; virtual void refresh_screen() = 0;
/**
* Power the display on
*/
virtual void power_on() = 0; virtual void power_on() = 0;
/**
* Power the display off
*/
virtual void power_off() = 0; virtual void power_off() = 0;
virtual uint32_t get_buffer_length() = 0;
/**
* Place the display into deep sleep
*/
virtual void deep_sleep() = 0;
void set_state_(EPaperState state, uint16_t delay = 0);
void start_command_(); void start_command_();
void end_command_(); void end_command_();
void start_data_(); void start_data_();
void end_data_(); void end_data_();
const size_t init_sequence_length_{0}; // properties initialised in the constructor
const char *name_;
uint16_t width_;
uint16_t height_;
const uint8_t *init_sequence_;
size_t init_sequence_length_;
DisplayType display_type_;
size_t current_data_index_{0}; size_t buffer_length_{};
size_t current_data_index_{0}; // used by data transfer to track progress
uint32_t reset_duration_{200}; uint32_t reset_duration_{200};
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
uint32_t transfer_start_time_{};
uint32_t waiting_for_idle_last_print_{0}; uint32_t waiting_for_idle_last_print_{0};
uint32_t waiting_for_idle_start_{0};
#endif
GPIOPin *dc_pin_; GPIOPin *dc_pin_{};
GPIOPin *busy_pin_{nullptr}; GPIOPin *busy_pin_{};
GPIOPin *reset_pin_{nullptr}; GPIOPin *reset_pin_{};
const uint8_t *init_sequence_{nullptr};
bool waiting_for_idle_{false}; bool waiting_for_idle_{false};
uint32_t delay_until_{0};
split_buffer::SplitBuffer buffer_; split_buffer::SplitBuffer buffer_;
std::queue<EPaperState> state_queue_{{EPaperState::IDLE}}; EPaperState state_{EPaperState::IDLE};
}; };
} // namespace esphome::epaper_spi } // namespace esphome::epaper_spi

View File

@@ -1,42 +0,0 @@
#include "epaper_spi_model_7p3in_spectra_e6.h"
namespace esphome::epaper_spi {
static constexpr const char *const TAG = "epaper_spi.7.3in-spectra-e6";
void EPaper7p3InSpectraE6::power_on() {
ESP_LOGI(TAG, "Power on");
this->command(0x04);
this->waiting_for_idle_ = true;
}
void EPaper7p3InSpectraE6::power_off() {
ESP_LOGI(TAG, "Power off");
this->command(0x02);
this->data(0x00);
this->waiting_for_idle_ = true;
}
void EPaper7p3InSpectraE6::refresh_screen() {
ESP_LOGI(TAG, "Refresh");
this->command(0x12);
this->data(0x00);
this->waiting_for_idle_ = true;
}
void EPaper7p3InSpectraE6::deep_sleep() {
ESP_LOGI(TAG, "Deep sleep");
this->command(0x07);
this->data(0xA5);
}
void EPaper7p3InSpectraE6::dump_config() {
LOG_DISPLAY("", "E-Paper SPI", this);
ESP_LOGCONFIG(TAG, " Model: 7.3in Spectra E6");
LOG_PIN(" Reset Pin: ", this->reset_pin_);
LOG_PIN(" DC Pin: ", this->dc_pin_);
LOG_PIN(" Busy Pin: ", this->busy_pin_);
LOG_UPDATE_INTERVAL(this);
}
} // namespace esphome::epaper_spi

View File

@@ -1,45 +0,0 @@
#pragma once
#include "epaper_spi_spectra_e6.h"
namespace esphome::epaper_spi {
class EPaper7p3InSpectraE6 : public EPaperSpectraE6 {
static constexpr const uint16_t WIDTH = 800;
static constexpr const uint16_t HEIGHT = 480;
// clang-format off
// Command, data length, data
static constexpr uint8_t INIT_SEQUENCE[] = {
0xAA, 6, 0x49, 0x55, 0x20, 0x08, 0x09, 0x18,
0x01, 1, 0x3F,
0x00, 2, 0x5F, 0x69,
0x03, 4, 0x00, 0x54, 0x00, 0x44,
0x05, 4, 0x40, 0x1F, 0x1F, 0x2C,
0x06, 4, 0x6F, 0x1F, 0x17, 0x49,
0x08, 4, 0x6F, 0x1F, 0x1F, 0x22,
0x30, 1, 0x03,
0x50, 1, 0x3F,
0x60, 2, 0x02, 0x00,
0x61, 4, WIDTH / 256, WIDTH % 256, HEIGHT / 256, HEIGHT % 256,
0x84, 1, 0x01,
0xE3, 1, 0x2F,
};
// clang-format on
public:
EPaper7p3InSpectraE6() : EPaperSpectraE6(INIT_SEQUENCE, sizeof(INIT_SEQUENCE)) {}
void dump_config() override;
protected:
int get_width_internal() override { return WIDTH; };
int get_height_internal() override { return HEIGHT; };
void refresh_screen() override;
void power_on() override;
void power_off() override;
void deep_sleep() override;
};
} // namespace esphome::epaper_spi

View File

@@ -1,135 +1,166 @@
#include "epaper_spi_spectra_e6.h" #include "epaper_spi_spectra_e6.h"
#include <algorithm>
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome::epaper_spi { namespace esphome::epaper_spi {
static constexpr const char *const TAG = "epaper_spi.6c"; static constexpr const char *const TAG = "epaper_spi.6c";
static constexpr size_t MAX_TRANSFER_SIZE = 128;
static constexpr unsigned char GRAY_THRESHOLD = 50;
static inline uint8_t color_to_hex(Color color) { enum E6Color {
if (color.red > 127) { BLACK,
if (color.green > 170) { WHITE,
if (color.blue > 127) { YELLOW,
return 0x1; // White RED,
} else { SKIP_1,
return 0x2; // Yellow BLUE,
} GREEN,
} else { CYAN,
return 0x3; // Red (or Magenta) SKIP_2,
} };
} else {
if (color.green > 127) { static uint8_t color_to_hex(Color color) {
if (color.blue > 127) { // --- Step 1: Check for Grayscale (Black or White) ---
return 0x5; // Cyan -> Blue // We define "grayscale" as a color where the min and max components
} else { // are close to each other.
return 0x6; // Green unsigned char max_rgb = std::max({color.r, color.g, color.b});
} unsigned char min_rgb = std::min({color.r, color.g, color.b});
} else {
if (color.blue > 127) { if ((max_rgb - min_rgb) < GRAY_THRESHOLD) {
return 0x5; // Blue // It's a shade of gray. Map to BLACK or WHITE.
} else { // We split the luminance at the halfway point (382 = (255*3)/2)
return 0x0; // Black if ((static_cast<int>(color.r) + color.g + color.b) > 382) {
} return WHITE;
} }
return BLACK;
} }
// --- Step 2: Check for Primary/Secondary Colors ---
// If it's not gray, it's a color. We check which components are
// "on" (over 128) vs "off". This divides the RGB cube into 8 corners.
bool r_on = (color.r > 128);
bool g_on = (color.g > 128);
bool b_on = (color.b > 128);
if (r_on && g_on && !b_on) {
return YELLOW;
}
if (r_on && !g_on && !b_on) {
return RED;
}
if (!r_on && g_on && !b_on) {
return GREEN;
}
if (!r_on && !g_on && b_on) {
return BLUE;
}
// Handle "impure" colors (Cyan, Magenta)
if (!r_on && g_on && b_on) {
// Cyan (G+B) -> Closest is Green or Blue. Pick Green.
return GREEN;
}
if (r_on && !g_on) {
// Magenta (R+B) -> Closest is Red or Blue. Pick Red.
return RED;
}
// Handle the remaining corners (White-ish, Black-ish)
if (r_on) {
// All high (but not gray) -> White
return WHITE;
}
// !r_on && !g_on && !b_on
// All low (but not gray) -> Black
return BLACK;
}
void EPaperSpectraE6::power_on() {
ESP_LOGD(TAG, "Power on");
this->command(0x04);
}
void EPaperSpectraE6::power_off() {
ESP_LOGD(TAG, "Power off");
this->command(0x02);
this->data(0x00);
}
void EPaperSpectraE6::refresh_screen() {
ESP_LOGD(TAG, "Refresh");
this->command(0x12);
this->data(0x00);
}
void EPaperSpectraE6::deep_sleep() {
ESP_LOGD(TAG, "Deep sleep");
this->command(0x07);
this->data(0xA5);
} }
void EPaperSpectraE6::fill(Color color) { void EPaperSpectraE6::fill(Color color) {
uint8_t pixel_color; auto pixel_color = color_to_hex(color);
if (color.is_on()) {
pixel_color = color_to_hex(color);
} else {
pixel_color = 0x1;
}
// We store 8 bitset<3> in 3 bytes // We store 2 pixels per byte
// | byte 1 | byte 2 | byte 3 | this->buffer_.fill(pixel_color + (pixel_color << 4));
// |aaabbbaa|abbbaaab|bbaaabbb|
uint8_t byte_1 = pixel_color << 5 | pixel_color << 2 | pixel_color >> 1;
uint8_t byte_2 = pixel_color << 7 | pixel_color << 4 | pixel_color << 1 | pixel_color >> 2;
uint8_t byte_3 = pixel_color << 6 | pixel_color << 3 | pixel_color << 0;
const size_t buffer_length = this->get_buffer_length();
for (size_t i = 0; i < buffer_length; i += 3) {
this->buffer_[i + 0] = byte_1;
this->buffer_[i + 1] = byte_2;
this->buffer_[i + 2] = byte_3;
}
} }
uint32_t EPaperSpectraE6::get_buffer_length() { void EPaperSpectraE6::clear() {
// 6 colors buffer, 1 pixel = 3 bits, we will store 8 pixels in 24 bits = 3 bytes // clear buffer to white, just like real paper.
return this->get_width_controller() * this->get_height_internal() / 8u * 3u; this->fill(COLOR_ON);
} }
void HOT EPaperSpectraE6::draw_absolute_pixel_internal(int x, int y, Color color) { void HOT EPaperSpectraE6::draw_absolute_pixel_internal(int x, int y, Color color) {
if (x >= this->get_width_internal() || y >= this->get_height_internal() || x < 0 || y < 0) if (x >= this->width_ || y >= this->height_ || x < 0 || y < 0)
return; return;
uint8_t pixel_bits = color_to_hex(color); auto pixel_bits = color_to_hex(color);
uint32_t pixel_position = x + y * this->get_width_controller(); uint32_t pixel_position = x + y * this->get_width_controller();
uint32_t first_bit_position = pixel_position * 3; uint32_t byte_position = pixel_position / 2;
uint32_t byte_position = first_bit_position / 8u; auto original = this->buffer_[byte_position];
uint32_t byte_subposition = first_bit_position % 8u; if ((pixel_position & 1) != 0) {
this->buffer_[byte_position] = (original & 0xF0) | pixel_bits;
if (byte_subposition <= 5) {
this->buffer_[byte_position] = (this->buffer_[byte_position] & (0xFF ^ (0b111 << (5 - byte_subposition)))) |
(pixel_bits << (5 - byte_subposition));
} else { } else {
this->buffer_[byte_position] = (this->buffer_[byte_position] & (0xFF ^ (0b111 >> (byte_subposition - 5)))) | this->buffer_[byte_position] = (original & 0x0F) | (pixel_bits << 4);
(pixel_bits >> (byte_subposition - 5));
this->buffer_[byte_position + 1] =
(this->buffer_[byte_position + 1] & (0xFF ^ (0xFF & (0b111 << (13 - byte_subposition))))) |
(pixel_bits << (13 - byte_subposition));
} }
} }
bool HOT EPaperSpectraE6::transfer_data() { bool HOT EPaperSpectraE6::transfer_data() {
const uint32_t start_time = App.get_loop_component_start_time(); const uint32_t start_time = App.get_loop_component_start_time();
const size_t buffer_length = this->buffer_length_;
if (this->current_data_index_ == 0) { if (this->current_data_index_ == 0) {
ESP_LOGV(TAG, "Sending data"); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
this->transfer_start_time_ = millis();
#endif
ESP_LOGV(TAG, "Start sending data at %ums", (unsigned) millis());
this->command(0x10); this->command(0x10);
} }
uint8_t bytes_to_send[4]{0}; size_t buf_idx = 0;
const size_t buffer_length = this->get_buffer_length(); uint8_t bytes_to_send[MAX_TRANSFER_SIZE];
for (size_t i = this->current_data_index_; i < buffer_length; i += 3) { while (this->current_data_index_ != buffer_length) {
const uint32_t triplet = encode_uint24(this->buffer_[i + 0], this->buffer_[i + 1], this->buffer_[i + 2]); bytes_to_send[buf_idx++] = this->buffer_[this->current_data_index_++];
// 8 pixels are stored in 3 bytes
// |aaabbbaa|abbbaaab|bbaaabbb|
// | byte 1 | byte 2 | byte 3 |
bytes_to_send[0] = ((triplet >> 17) & 0b01110000) | ((triplet >> 18) & 0b00000111);
bytes_to_send[1] = ((triplet >> 11) & 0b01110000) | ((triplet >> 12) & 0b00000111);
bytes_to_send[2] = ((triplet >> 5) & 0b01110000) | ((triplet >> 6) & 0b00000111);
bytes_to_send[3] = ((triplet << 1) & 0b01110000) | ((triplet << 0) & 0b00000111);
this->start_data_(); if (buf_idx == sizeof bytes_to_send) {
this->write_array(bytes_to_send, sizeof(bytes_to_send)); this->start_data_();
this->end_data_(); this->write_array(bytes_to_send, buf_idx);
this->end_data_();
ESP_LOGV(TAG, "Wrote %d bytes at %ums", buf_idx, (unsigned) millis());
buf_idx = 0;
if (millis() - start_time > MAX_TRANSFER_TIME) { if (millis() - start_time > MAX_TRANSFER_TIME) {
// Let the main loop run and come back next loop // Let the main loop run and come back next loop
this->current_data_index_ = i + 3; return false;
return false; }
} }
} }
// Finished the entire dataset // Finished the entire dataset
if (buf_idx != 0) {
this->start_data_();
this->write_array(bytes_to_send, buf_idx);
this->end_data_();
}
this->current_data_index_ = 0; this->current_data_index_ = 0;
ESP_LOGV(TAG, "Sent data in %" PRIu32 " ms", millis() - this->transfer_start_time_);
return true; return true;
} }
void EPaperSpectraE6::reset() {
if (this->reset_pin_ != nullptr) {
this->disable_loop();
this->reset_pin_->digital_write(true);
this->set_timeout(20, [this] {
this->reset_pin_->digital_write(false);
delay(2);
this->reset_pin_->digital_write(true);
this->set_timeout(20, [this] { this->enable_loop(); });
});
}
}
} // namespace esphome::epaper_spi } // namespace esphome::epaper_spi

View File

@@ -6,18 +6,23 @@ namespace esphome::epaper_spi {
class EPaperSpectraE6 : public EPaperBase { class EPaperSpectraE6 : public EPaperBase {
public: public:
EPaperSpectraE6(const uint8_t *init_sequence, const size_t init_sequence_length) EPaperSpectraE6(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
: EPaperBase(init_sequence, init_sequence_length) {} size_t init_sequence_length)
: EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_COLOR) {
this->buffer_length_ = width * height / 2; // 2 pixels per byte
}
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
void fill(Color color) override; void fill(Color color) override;
void clear() override;
protected: protected:
void refresh_screen() override;
void power_on() override;
void power_off() override;
void deep_sleep() override;
void draw_absolute_pixel_internal(int x, int y, Color color) override; void draw_absolute_pixel_internal(int x, int y, Color color) override;
uint32_t get_buffer_length() override;
bool transfer_data() override; bool transfer_data() override;
void reset() override;
}; };
} // namespace esphome::epaper_spi } // namespace esphome::epaper_spi

View File

@@ -0,0 +1,65 @@
from typing import Any, Self
import esphome.config_validation as cv
from esphome.const import CONF_DIMENSIONS, CONF_HEIGHT, CONF_WIDTH
class EpaperModel:
models: dict[str, Self] = {}
def __init__(
self,
name: str,
class_name: str,
initsequence=None,
**defaults,
):
name = name.upper()
self.name = name
self.class_name = class_name
self.initsequence = initsequence
self.defaults = defaults
EpaperModel.models[name] = self
def get_default(self, key, fallback: Any = False) -> Any:
return self.defaults.get(key, fallback)
def get_init_sequence(self, config: dict):
return self.initsequence
def option(self, name, fallback=cv.UNDEFINED) -> cv.Optional | cv.Required:
if fallback is None and self.get_default(name, None) is None:
return cv.Required(name)
return cv.Optional(name, default=self.get_default(name, fallback))
def get_dimensions(self, config) -> tuple[int, int]:
if CONF_DIMENSIONS in config:
# Explicit dimensions, just use as is
dimensions = config[CONF_DIMENSIONS]
if isinstance(dimensions, dict):
width = dimensions[CONF_WIDTH]
height = dimensions[CONF_HEIGHT]
else:
(width, height) = dimensions
else:
# Default dimensions, use model defaults
width = self.get_default(CONF_WIDTH)
height = self.get_default(CONF_HEIGHT)
return width, height
def extend(self, name, **kwargs) -> "EpaperModel":
"""
Extend the current model with additional parameters or a modified init sequence.
Parameters supplied here will override the defaults of the current model.
if the initsequence is not provided, the current model's initsequence will be used.
If add_init_sequence is provided, it will be appended to the current initsequence.
:param name:
:param kwargs:
:return:
"""
initsequence = list(kwargs.pop("initsequence", self.initsequence) or ())
initsequence.extend(kwargs.pop("add_init_sequence", ()))
defaults = self.defaults.copy()
defaults.update(kwargs)
return self.__class__(name, initsequence=tuple(initsequence), **defaults)

View File

@@ -0,0 +1,51 @@
from typing import Any
from . import EpaperModel
class SpectraE6(EpaperModel):
def __init__(self, name, class_name="EPaperSpectraE6", **kwargs):
super().__init__(name, class_name, **kwargs)
# fmt: off
def get_init_sequence(self, config: dict):
width, height = self.get_dimensions(config)
return (
(0xAA, 0x49, 0x55, 0x20, 0x08, 0x09, 0x18,),
(0x01, 0x3F,),
(0x00, 0x5F, 0x69,),
(0x03, 0x00, 0x54, 0x00, 0x44,),
(0x05, 0x40, 0x1F, 0x1F, 0x2C,),
(0x06, 0x6F, 0x1F, 0x17, 0x49,),
(0x08, 0x6F, 0x1F, 0x1F, 0x22,),
(0x30, 0x03,),
(0x50, 0x3F,),
(0x60, 0x02, 0x00,),
(0x61, width // 256, width % 256, height // 256, height % 256,),
(0x84, 0x01,),
(0xE3, 0x2F,),
)
def get_default(self, key, fallback: Any = False) -> Any:
return self.defaults.get(key, fallback)
spectra_e6 = SpectraE6("spectra-e6")
spectra_e6.extend(
"Seeed-reTerminal-E1002",
width=800,
height=480,
data_rate="20MHz",
cs_pin=10,
dc_pin=11,
reset_pin=12,
busy_pin={
"number": 13,
"inverted": True,
"mode": {
"input": True,
"pullup": True,
},
},
)

View File

@@ -3,9 +3,9 @@
namespace esphome { namespace esphome {
namespace es8388 { namespace es8388 {
void ADCInputMicSelect::control(const std::string &value) { void ADCInputMicSelect::control(size_t index) {
this->publish_state(value); this->publish_state(index);
this->parent_->set_adc_input_mic(static_cast<AdcInputMicLine>(this->index_of(value).value())); this->parent_->set_adc_input_mic(static_cast<AdcInputMicLine>(index));
} }
} // namespace es8388 } // namespace es8388

View File

@@ -8,7 +8,7 @@ namespace es8388 {
class ADCInputMicSelect : public select::Select, public Parented<ES8388> { class ADCInputMicSelect : public select::Select, public Parented<ES8388> {
protected: protected:
void control(const std::string &value) override; void control(size_t index) override;
}; };
} // namespace es8388 } // namespace es8388

View File

@@ -3,9 +3,9 @@
namespace esphome { namespace esphome {
namespace es8388 { namespace es8388 {
void DacOutputSelect::control(const std::string &value) { void DacOutputSelect::control(size_t index) {
this->publish_state(value); this->publish_state(index);
this->parent_->set_dac_output(static_cast<DacOutputLine>(this->index_of(value).value())); this->parent_->set_dac_output(static_cast<DacOutputLine>(index));
} }
} // namespace es8388 } // namespace es8388

View File

@@ -8,7 +8,7 @@ namespace es8388 {
class DacOutputSelect : public select::Select, public Parented<ES8388> { class DacOutputSelect : public select::Select, public Parented<ES8388> {
protected: protected:
void control(const std::string &value) override; void control(size_t index) override;
}; };
} // namespace es8388 } // namespace es8388

View File

@@ -558,6 +558,7 @@ CONF_DISABLE_LIBC_LOCKS_IN_IRAM = "disable_libc_locks_in_iram"
CONF_DISABLE_VFS_SUPPORT_TERMIOS = "disable_vfs_support_termios" CONF_DISABLE_VFS_SUPPORT_TERMIOS = "disable_vfs_support_termios"
CONF_DISABLE_VFS_SUPPORT_SELECT = "disable_vfs_support_select" CONF_DISABLE_VFS_SUPPORT_SELECT = "disable_vfs_support_select"
CONF_DISABLE_VFS_SUPPORT_DIR = "disable_vfs_support_dir" CONF_DISABLE_VFS_SUPPORT_DIR = "disable_vfs_support_dir"
CONF_LOOP_TASK_STACK_SIZE = "loop_task_stack_size"
# VFS requirement tracking # VFS requirement tracking
# Components that need VFS features can call require_vfs_select() or require_vfs_dir() # Components that need VFS features can call require_vfs_select() or require_vfs_dir()
@@ -654,6 +655,9 @@ FRAMEWORK_SCHEMA = cv.All(
): cv.boolean, ): cv.boolean,
cv.Optional(CONF_DISABLE_VFS_SUPPORT_DIR, default=True): cv.boolean, cv.Optional(CONF_DISABLE_VFS_SUPPORT_DIR, default=True): cv.boolean,
cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean, cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean,
cv.Optional(CONF_LOOP_TASK_STACK_SIZE, default=8192): cv.int_range(
min=8192, max=32768
),
} }
), ),
cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list( cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list(
@@ -926,6 +930,10 @@ async def to_code(config):
f"VERSION_CODE({framework_ver.major}, {framework_ver.minor}, {framework_ver.patch})" f"VERSION_CODE({framework_ver.major}, {framework_ver.minor}, {framework_ver.patch})"
), ),
) )
add_idf_sdkconfig_option(
"CONFIG_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_AUTOSTART_ARDUINO", True)
add_idf_sdkconfig_option("CONFIG_MBEDTLS_PSK_MODES", True) add_idf_sdkconfig_option("CONFIG_MBEDTLS_PSK_MODES", True)
add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True) add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True)
@@ -1071,6 +1079,10 @@ async def to_code(config):
) )
add_idf_sdkconfig_option("CONFIG_IDF_EXPERIMENTAL_FEATURES", True) add_idf_sdkconfig_option("CONFIG_IDF_EXPERIMENTAL_FEATURES", True)
cg.add_define(
"ESPHOME_LOOP_TASK_STACK_SIZE", advanced.get(CONF_LOOP_TASK_STACK_SIZE)
)
cg.add_define( cg.add_define(
"USE_ESP_IDF_VERSION_CODE", "USE_ESP_IDF_VERSION_CODE",
cg.RawExpression( cg.RawExpression(

View File

@@ -1,5 +1,6 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
#include "esphome/core/defines.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "preferences.h" #include "preferences.h"
@@ -96,7 +97,11 @@ void loop_task(void *pv_params) {
extern "C" void app_main() { extern "C" void app_main() {
esp32::setup_preferences(); esp32::setup_preferences();
xTaskCreate(loop_task, "loopTask", 8192, nullptr, 1, &loop_task_handle); #if CONFIG_FREERTOS_UNICORE
xTaskCreate(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, &loop_task_handle);
#else
xTaskCreatePinnedToCore(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, &loop_task_handle, 1);
#endif
} }
#endif // USE_ESP_IDF #endif // USE_ESP_IDF

View File

@@ -7,6 +7,7 @@ from typing import Any
from esphome import automation from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import socket
from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
@@ -21,6 +22,7 @@ from esphome.core import CORE, CoroPriority, TimePeriod, coroutine_with_priority
import esphome.final_validate as fv import esphome.final_validate as fv
DEPENDENCIES = ["esp32"] DEPENDENCIES = ["esp32"]
AUTO_LOAD = ["socket"]
CODEOWNERS = ["@jesserockz", "@Rapsssito", "@bdraco"] CODEOWNERS = ["@jesserockz", "@Rapsssito", "@bdraco"]
DOMAIN = "esp32_ble" DOMAIN = "esp32_ble"
@@ -481,6 +483,11 @@ async def to_code(config):
cg.add(var.set_name(name)) cg.add(var.set_name(name))
await cg.register_component(var, config) await cg.register_component(var, config)
# BLE uses the socket wake_loop_threadsafe() mechanism to wake the main loop from BLE tasks
# This enables low-latency (~12μs) BLE event processing instead of waiting for
# select() timeout (0-16ms). The wake socket is shared across all components.
socket.require_wake_loop_threadsafe()
# Define max connections for use in C++ code (e.g., ble_server.h) # Define max connections for use in C++ code (e.g., ble_server.h)
max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS) max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS)
cg.add_define("USE_ESP32_BLE_MAX_CONNECTIONS", max_connections) cg.add_define("USE_ESP32_BLE_MAX_CONNECTIONS", max_connections)

View File

@@ -31,6 +31,26 @@ namespace esphome::esp32_ble {
static const char *const TAG = "esp32_ble"; static const char *const TAG = "esp32_ble";
// GAP event groups for deduplication across gap_event_handler and dispatch_gap_event_
#define GAP_SCAN_COMPLETE_EVENTS \
case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: \
case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: \
case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT
#define GAP_ADV_COMPLETE_EVENTS \
case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: \
case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT: \
case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: \
case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: \
case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT
#define GAP_SECURITY_EVENTS \
case ESP_GAP_BLE_AUTH_CMPL_EVT: \
case ESP_GAP_BLE_SEC_REQ_EVT: \
case ESP_GAP_BLE_PASSKEY_NOTIF_EVT: \
case ESP_GAP_BLE_PASSKEY_REQ_EVT: \
case ESP_GAP_BLE_NC_REQ_EVT
void ESP32BLE::setup() { void ESP32BLE::setup() {
global_ble = this; global_ble = this;
if (!ble_pre_setup_()) { if (!ble_pre_setup_()) {
@@ -414,60 +434,48 @@ void ESP32BLE::loop() {
break; break;
// Scan complete events // Scan complete events
case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: GAP_SCAN_COMPLETE_EVENTS:
case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
// All three scan complete events have the same structure with just status
// The scan_complete struct matches ESP-IDF's layout exactly, so this reinterpret_cast is safe
// This is verified at compile-time by static_assert checks in ble_event.h
// The struct already contains our copy of the status (copied in BLEEvent constructor)
ESP_LOGV(TAG, "gap_event_handler - %d", gap_event);
#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT
for (auto *gap_handler : this->gap_event_handlers_) {
gap_handler->gap_event_handler(
gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.scan_complete));
}
#endif
break;
// Advertising complete events // Advertising complete events
case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: GAP_ADV_COMPLETE_EVENTS:
case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT:
case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
// All advertising complete events have the same structure with just status
ESP_LOGV(TAG, "gap_event_handler - %d", gap_event);
#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT
for (auto *gap_handler : this->gap_event_handlers_) {
gap_handler->gap_event_handler(
gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.adv_complete));
}
#endif
break;
// RSSI complete event // RSSI complete event
case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT:
ESP_LOGV(TAG, "gap_event_handler - %d", gap_event);
#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT
for (auto *gap_handler : this->gap_event_handlers_) {
gap_handler->gap_event_handler(
gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.read_rssi_complete));
}
#endif
break;
// Security events // Security events
case ESP_GAP_BLE_AUTH_CMPL_EVT: GAP_SECURITY_EVENTS:
case ESP_GAP_BLE_SEC_REQ_EVT:
case ESP_GAP_BLE_PASSKEY_NOTIF_EVT:
case ESP_GAP_BLE_PASSKEY_REQ_EVT:
case ESP_GAP_BLE_NC_REQ_EVT:
ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); ESP_LOGV(TAG, "gap_event_handler - %d", gap_event);
#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT #ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT
for (auto *gap_handler : this->gap_event_handlers_) { {
gap_handler->gap_event_handler( esp_ble_gap_cb_param_t *param;
gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.security)); // clang-format off
switch (gap_event) {
// All three scan complete events have the same structure with just status
// The scan_complete struct matches ESP-IDF's layout exactly, so this reinterpret_cast is safe
// This is verified at compile-time by static_assert checks in ble_event.h
// The struct already contains our copy of the status (copied in BLEEvent constructor)
GAP_SCAN_COMPLETE_EVENTS:
param = reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.scan_complete);
break;
// All advertising complete events have the same structure with just status
GAP_ADV_COMPLETE_EVENTS:
param = reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.adv_complete);
break;
case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT:
param = reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.read_rssi_complete);
break;
GAP_SECURITY_EVENTS:
param = reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.security);
break;
default:
break;
}
// clang-format on
// Dispatch to all registered handlers
for (auto *gap_handler : this->gap_event_handlers_) {
gap_handler->gap_event_handler(gap_event, param);
}
} }
#endif #endif
break; break;
@@ -547,26 +555,24 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa
// Queue GAP events that components need to handle // Queue GAP events that components need to handle
// Scanning events - used by esp32_ble_tracker // Scanning events - used by esp32_ble_tracker
case ESP_GAP_BLE_SCAN_RESULT_EVT: case ESP_GAP_BLE_SCAN_RESULT_EVT:
case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: GAP_SCAN_COMPLETE_EVENTS:
case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
// Advertising events - used by esp32_ble_beacon and esp32_ble server // Advertising events - used by esp32_ble_beacon and esp32_ble server
case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: GAP_ADV_COMPLETE_EVENTS:
case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT:
case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
// Connection events - used by ble_client // Connection events - used by ble_client
case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT:
// Security events - used by ble_client and bluetooth_proxy
case ESP_GAP_BLE_AUTH_CMPL_EVT:
case ESP_GAP_BLE_SEC_REQ_EVT:
case ESP_GAP_BLE_PASSKEY_NOTIF_EVT:
case ESP_GAP_BLE_PASSKEY_REQ_EVT:
case ESP_GAP_BLE_NC_REQ_EVT:
enqueue_ble_event(event, param); enqueue_ble_event(event, param);
return; return;
// Security events - used by ble_client and bluetooth_proxy
// These are rare but interactive (pairing/bonding), so notify immediately
GAP_SECURITY_EVENTS:
enqueue_ble_event(event, param);
// Wake up main loop to process security event immediately
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
App.wake_loop_threadsafe();
#endif
return;
// Ignore these GAP events as they are not relevant for our use case // Ignore these GAP events as they are not relevant for our use case
case ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT: case ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT:
case ESP_GAP_BLE_SET_PKT_LENGTH_COMPLETE_EVT: case ESP_GAP_BLE_SET_PKT_LENGTH_COMPLETE_EVT:
@@ -584,6 +590,10 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa
void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
esp_ble_gatts_cb_param_t *param) { esp_ble_gatts_cb_param_t *param) {
enqueue_ble_event(event, gatts_if, param); enqueue_ble_event(event, gatts_if, param);
// Wake up main loop to process GATT event immediately
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
App.wake_loop_threadsafe();
#endif
} }
#endif #endif
@@ -591,6 +601,10 @@ void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gat
void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) { esp_ble_gattc_cb_param_t *param) {
enqueue_ble_event(event, gattc_if, param); enqueue_ble_event(event, gattc_if, param);
// Wake up main loop to process GATT event immediately
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
App.wake_loop_threadsafe();
#endif
} }
#endif #endif

View File

@@ -162,6 +162,11 @@ class ESP32BLE : public Component {
void advertising_init_(); void advertising_init_();
#endif #endif
// BLE uses the core wake_loop_threadsafe() mechanism to wake the main event loop
// from BLE tasks. This enables low-latency (~12μs) event processing instead of
// waiting for select() timeout (0-16ms). The wake socket is shared with other
// components that need this functionality.
private: private:
template<typename... Args> friend void enqueue_ble_event(Args... args); template<typename... Args> friend void enqueue_ble_event(Args... args);

View File

@@ -461,7 +461,9 @@ async def parse_value(value_config, args):
if isinstance(value, str): if isinstance(value, str):
value = list(value.encode(value_config[CONF_STRING_ENCODING])) value = list(value.encode(value_config[CONF_STRING_ENCODING]))
if isinstance(value, list): if isinstance(value, list):
return cg.std_vector.template(cg.uint8)(value) # Generate initializer list {1, 2, 3} instead of std::vector<uint8_t>({1, 2, 3})
# This calls the set_value(std::initializer_list<uint8_t>) overload
return cg.ArrayInitializer(*value)
val = cg.RawExpression(f"{value_config[CONF_TYPE]}({cg.safe_exp(value)})") val = cg.RawExpression(f"{value_config[CONF_TYPE]}({cg.safe_exp(value)})")
return ByteBuffer_ns.wrap(val, value_config[CONF_ENDIANNESS]) return ByteBuffer_ns.wrap(val, value_config[CONF_ENDIANNESS])

View File

@@ -35,13 +35,18 @@ BLECharacteristic::BLECharacteristic(const ESPBTUUID uuid, uint32_t properties)
void BLECharacteristic::set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); } void BLECharacteristic::set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); }
void BLECharacteristic::set_value(const std::vector<uint8_t> &buffer) { void BLECharacteristic::set_value(std::vector<uint8_t> &&buffer) {
xSemaphoreTake(this->set_value_lock_, 0L); xSemaphoreTake(this->set_value_lock_, 0L);
this->value_ = buffer; this->value_ = std::move(buffer);
xSemaphoreGive(this->set_value_lock_); xSemaphoreGive(this->set_value_lock_);
} }
void BLECharacteristic::set_value(std::initializer_list<uint8_t> data) {
this->set_value(std::vector<uint8_t>(data)); // Delegate to move overload
}
void BLECharacteristic::set_value(const std::string &buffer) { void BLECharacteristic::set_value(const std::string &buffer) {
this->set_value(std::vector<uint8_t>(buffer.begin(), buffer.end())); this->set_value(std::vector<uint8_t>(buffer.begin(), buffer.end())); // Delegate to move overload
} }
void BLECharacteristic::notify() { void BLECharacteristic::notify() {

View File

@@ -33,7 +33,8 @@ class BLECharacteristic {
~BLECharacteristic(); ~BLECharacteristic();
void set_value(ByteBuffer buffer); void set_value(ByteBuffer buffer);
void set_value(const std::vector<uint8_t> &buffer); void set_value(std::vector<uint8_t> &&buffer);
void set_value(std::initializer_list<uint8_t> data);
void set_value(const std::string &buffer); void set_value(const std::string &buffer);
void set_broadcast_property(bool value); void set_broadcast_property(bool value);

View File

@@ -46,15 +46,17 @@ void BLEDescriptor::do_create(BLECharacteristic *characteristic) {
this->state_ = CREATING; this->state_ = CREATING;
} }
void BLEDescriptor::set_value(std::vector<uint8_t> buffer) { void BLEDescriptor::set_value(std::vector<uint8_t> &&buffer) { this->set_value_impl_(buffer.data(), buffer.size()); }
size_t length = buffer.size();
void BLEDescriptor::set_value(std::initializer_list<uint8_t> data) { this->set_value_impl_(data.begin(), data.size()); }
void BLEDescriptor::set_value_impl_(const uint8_t *data, size_t length) {
if (length > this->value_.attr_max_len) { if (length > this->value_.attr_max_len) {
ESP_LOGE(TAG, "Size %d too large, must be no bigger than %d", length, this->value_.attr_max_len); ESP_LOGE(TAG, "Size %d too large, must be no bigger than %d", length, this->value_.attr_max_len);
return; return;
} }
this->value_.attr_len = length; this->value_.attr_len = length;
memcpy(this->value_.attr_value, buffer.data(), length); memcpy(this->value_.attr_value, data, length);
} }
void BLEDescriptor::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, void BLEDescriptor::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,

View File

@@ -27,7 +27,8 @@ class BLEDescriptor {
void do_create(BLECharacteristic *characteristic); void do_create(BLECharacteristic *characteristic);
ESPBTUUID get_uuid() const { return this->uuid_; } ESPBTUUID get_uuid() const { return this->uuid_; }
void set_value(std::vector<uint8_t> buffer); void set_value(std::vector<uint8_t> &&buffer);
void set_value(std::initializer_list<uint8_t> data);
void set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); } void set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); }
void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param);
@@ -42,6 +43,8 @@ class BLEDescriptor {
} }
protected: protected:
void set_value_impl_(const uint8_t *data, size_t length);
BLECharacteristic *characteristic_{nullptr}; BLECharacteristic *characteristic_{nullptr};
ESPBTUUID uuid_; ESPBTUUID uuid_;
uint16_t handle_{0xFFFF}; uint16_t handle_{0xFFFF};

View File

@@ -4,6 +4,7 @@ from esphome import automation, pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import i2c from esphome.components import i2c
from esphome.components.esp32 import add_idf_component from esphome.components.esp32 import add_idf_component
from esphome.components.psram import DOMAIN as psram_domain
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_BRIGHTNESS, CONF_BRIGHTNESS,
@@ -26,10 +27,9 @@ import esphome.final_validate as fv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
AUTO_LOAD = ["camera"]
DEPENDENCIES = ["esp32"] DEPENDENCIES = ["esp32"]
AUTO_LOAD = ["camera", "psram"]
esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera") esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera")
ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.EntityBase) ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.EntityBase)
ESP32CameraImageData = esp32_camera_ns.struct("CameraImageData") ESP32CameraImageData = esp32_camera_ns.struct("CameraImageData")
@@ -163,6 +163,14 @@ CONF_ON_IMAGE = "on_image"
camera_range_param = cv.int_range(min=-2, max=2) camera_range_param = cv.int_range(min=-2, max=2)
def validate_fb_location_(value):
validator = cv.enum(ENUM_FB_LOCATION, upper=True)
if value.lower() == psram_domain:
validator = cv.All(validator, cv.requires_component(psram_domain))
return validator(value)
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
cv.ENTITY_BASE_SCHEMA.extend( cv.ENTITY_BASE_SCHEMA.extend(
{ {
@@ -236,9 +244,9 @@ CONFIG_SCHEMA = cv.All(
cv.framerate, cv.Range(min=0, max=1) cv.framerate, cv.Range(min=0, max=1)
), ),
cv.Optional(CONF_FRAME_BUFFER_COUNT, default=1): cv.int_range(min=1, max=2), cv.Optional(CONF_FRAME_BUFFER_COUNT, default=1): cv.int_range(min=1, max=2),
cv.Optional(CONF_FRAME_BUFFER_LOCATION, default="PSRAM"): cv.enum( cv.Optional(
ENUM_FB_LOCATION, upper=True CONF_FRAME_BUFFER_LOCATION, default="PSRAM"
), ): validate_fb_location_,
cv.Optional(CONF_ON_STREAM_START): automation.validate_automation( cv.Optional(CONF_ON_STREAM_START): automation.validate_automation(
{ {
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(

View File

@@ -0,0 +1,78 @@
import hashlib
from typing import Any
import esphome.codegen as cg
from esphome.components import esp32, update
import esphome.config_validation as cv
from esphome.const import CONF_PATH, CONF_RAW_DATA_ID
from esphome.core import CORE, HexInt
CODEOWNERS = ["@swoboda1337"]
AUTO_LOAD = ["sha256", "watchdog"]
DEPENDENCIES = ["esp32_hosted"]
CONF_SHA256 = "sha256"
esp32_hosted_ns = cg.esphome_ns.namespace("esp32_hosted")
Esp32HostedUpdate = esp32_hosted_ns.class_(
"Esp32HostedUpdate", update.UpdateEntity, cg.Component
)
def _validate_sha256(value: Any) -> str:
value = cv.string_strict(value)
if len(value) != 64:
raise cv.Invalid("SHA256 must be 64 hexadecimal characters")
try:
bytes.fromhex(value)
except ValueError as e:
raise cv.Invalid(f"SHA256 must be valid hexadecimal: {e}") from e
return value
CONFIG_SCHEMA = cv.All(
update.update_schema(Esp32HostedUpdate, device_class="firmware").extend(
{
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
cv.Required(CONF_PATH): cv.file_,
cv.Required(CONF_SHA256): _validate_sha256,
}
),
esp32.only_on_variant(
supported=[
esp32.const.VARIANT_ESP32H2,
esp32.const.VARIANT_ESP32P4,
]
),
)
def _validate_firmware(config: dict[str, Any]) -> None:
path = CORE.relative_config_path(config[CONF_PATH])
with open(path, "rb") as f:
firmware_data = f.read()
calculated = hashlib.sha256(firmware_data).hexdigest()
expected = config[CONF_SHA256].lower()
if calculated != expected:
raise cv.Invalid(
f"SHA256 mismatch for {config[CONF_PATH]}: expected {expected}, got {calculated}"
)
FINAL_VALIDATE_SCHEMA = _validate_firmware
async def to_code(config: dict[str, Any]) -> None:
var = await update.new_update(config)
path = config[CONF_PATH]
with open(CORE.relative_config_path(path), "rb") as f:
firmware_data = f.read()
rhs = [HexInt(x) for x in firmware_data]
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
sha256_bytes = bytes.fromhex(config[CONF_SHA256])
cg.add(var.set_firmware_sha256([HexInt(b) for b in sha256_bytes]))
cg.add(var.set_firmware_data(prog_arr))
cg.add(var.set_firmware_size(len(firmware_data)))
await cg.register_component(var, config)

View File

@@ -0,0 +1,164 @@
#if defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4)
#include "esp32_hosted_update.h"
#include "esphome/components/watchdog/watchdog.h"
#include "esphome/components/sha256/sha256.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
#include <esp_image_format.h>
#include <esp_app_desc.h>
#include <esp_hosted.h>
extern "C" {
#include <esp_hosted_ota.h>
}
namespace esphome::esp32_hosted {
static const char *const TAG = "esp32_hosted.update";
// older coprocessor firmware versions have a 1500-byte limit per RPC call
constexpr size_t CHUNK_SIZE = 1500;
void Esp32HostedUpdate::setup() {
this->update_info_.title = "ESP32 Hosted Coprocessor";
// get coprocessor version
esp_hosted_coprocessor_fwver_t ver_info;
if (esp_hosted_get_coprocessor_fwversion(&ver_info) == ESP_OK) {
this->update_info_.current_version = str_sprintf("%d.%d.%d", ver_info.major1, ver_info.minor1, ver_info.patch1);
} else {
this->update_info_.current_version = "unknown";
}
ESP_LOGD(TAG, "Coprocessor version: %s", this->update_info_.current_version.c_str());
// get image version
const int app_desc_offset = sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t);
if (this->firmware_size_ >= app_desc_offset + sizeof(esp_app_desc_t)) {
esp_app_desc_t *app_desc = (esp_app_desc_t *) (this->firmware_data_ + app_desc_offset);
if (app_desc->magic_word == ESP_APP_DESC_MAGIC_WORD) {
ESP_LOGD(TAG, "Firmware version: %s", app_desc->version);
ESP_LOGD(TAG, "Project name: %s", app_desc->project_name);
ESP_LOGD(TAG, "Build date: %s", app_desc->date);
ESP_LOGD(TAG, "Build time: %s", app_desc->time);
ESP_LOGD(TAG, "IDF version: %s", app_desc->idf_ver);
this->update_info_.latest_version = app_desc->version;
if (this->update_info_.latest_version != this->update_info_.current_version) {
this->state_ = update::UPDATE_STATE_AVAILABLE;
} else {
this->state_ = update::UPDATE_STATE_NO_UPDATE;
}
} else {
ESP_LOGW(TAG, "Invalid app description magic word: 0x%08x (expected 0x%08x)", app_desc->magic_word,
ESP_APP_DESC_MAGIC_WORD);
this->state_ = update::UPDATE_STATE_NO_UPDATE;
}
} else {
ESP_LOGW(TAG, "Firmware too small to contain app description");
this->state_ = update::UPDATE_STATE_NO_UPDATE;
}
// publish state
this->status_clear_error();
this->publish_state();
}
void Esp32HostedUpdate::dump_config() {
ESP_LOGCONFIG(TAG,
"ESP32 Hosted Update:\n"
" Current Version: %s\n"
" Latest Version: %s\n"
" Latest Size: %zu bytes",
this->update_info_.current_version.c_str(), this->update_info_.latest_version.c_str(),
this->firmware_size_);
}
void Esp32HostedUpdate::perform(bool force) {
if (this->state_ != update::UPDATE_STATE_AVAILABLE && !force) {
ESP_LOGW(TAG, "Update not available");
return;
}
if (this->firmware_data_ == nullptr || this->firmware_size_ == 0) {
ESP_LOGE(TAG, "No firmware data available");
return;
}
sha256::SHA256 hasher;
hasher.init();
hasher.add(this->firmware_data_, this->firmware_size_);
hasher.calculate();
if (!hasher.equals_bytes(this->firmware_sha256_.data())) {
this->status_set_error("SHA256 verification failed");
this->publish_state();
return;
}
ESP_LOGI(TAG, "Starting OTA update (%zu bytes)", this->firmware_size_);
watchdog::WatchdogManager watchdog(20000);
update::UpdateState prev_state = this->state_;
this->state_ = update::UPDATE_STATE_INSTALLING;
this->update_info_.has_progress = false;
this->publish_state();
esp_err_t err = esp_hosted_slave_ota_begin(); // NOLINT
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to begin OTA: %s", esp_err_to_name(err));
this->state_ = prev_state;
this->status_set_error("Failed to begin OTA");
this->publish_state();
return;
}
uint8_t chunk[CHUNK_SIZE];
const uint8_t *data_ptr = this->firmware_data_;
size_t remaining = this->firmware_size_;
while (remaining > 0) {
size_t chunk_size = std::min(remaining, static_cast<size_t>(CHUNK_SIZE));
memcpy(chunk, data_ptr, chunk_size);
err = esp_hosted_slave_ota_write(chunk, chunk_size); // NOLINT
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to write OTA data: %s", esp_err_to_name(err));
esp_hosted_slave_ota_end(); // NOLINT
this->state_ = prev_state;
this->status_set_error("Failed to write OTA data");
this->publish_state();
return;
}
data_ptr += chunk_size;
remaining -= chunk_size;
App.feed_wdt();
}
err = esp_hosted_slave_ota_end(); // NOLINT
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to end OTA: %s", esp_err_to_name(err));
this->state_ = prev_state;
this->status_set_error("Failed to end OTA");
this->publish_state();
return;
}
// activate new firmware
err = esp_hosted_slave_ota_activate(); // NOLINT
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to activate OTA: %s", esp_err_to_name(err));
this->state_ = prev_state;
this->status_set_error("Failed to activate OTA");
this->publish_state();
return;
}
// update state
ESP_LOGI(TAG, "OTA update successful");
this->state_ = update::UPDATE_STATE_NO_UPDATE;
this->status_clear_error();
this->publish_state();
// schedule a restart to ensure everything is in sync
ESP_LOGI(TAG, "Restarting in 1 second");
this->set_timeout(1000, []() { App.safe_reboot(); });
}
} // namespace esphome::esp32_hosted
#endif

View File

@@ -0,0 +1,32 @@
#pragma once
#if defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4)
#include "esphome/core/component.h"
#include "esphome/components/update/update_entity.h"
#include <array>
namespace esphome::esp32_hosted {
class Esp32HostedUpdate : public update::UpdateEntity, public Component {
public:
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
void perform(bool force) override;
void check() override {}
void set_firmware_data(const uint8_t *data) { this->firmware_data_ = data; }
void set_firmware_size(size_t size) { this->firmware_size_ = size; }
void set_firmware_sha256(const std::array<uint8_t, 32> &sha256) { this->firmware_sha256_ = sha256; }
protected:
const uint8_t *firmware_data_{nullptr};
size_t firmware_size_{0};
std::array<uint8_t, 32> firmware_sha256_;
};
} // namespace esphome::esp32_hosted
#endif

View File

@@ -270,8 +270,8 @@ void ESP32ImprovComponent::set_error_(improv::Error error) {
} }
} }
void ESP32ImprovComponent::send_response_(std::vector<uint8_t> &response) { void ESP32ImprovComponent::send_response_(std::vector<uint8_t> &&response) {
this->rpc_response_->set_value(ByteBuffer::wrap(response)); this->rpc_response_->set_value(std::move(response));
if (this->state_ != improv::STATE_STOPPED) if (this->state_ != improv::STATE_STOPPED)
this->rpc_response_->notify(); this->rpc_response_->notify();
} }
@@ -409,10 +409,8 @@ void ESP32ImprovComponent::check_wifi_connection_() {
} }
} }
#endif #endif
// Pass to build_rpc_response using vector constructor from iterators to avoid extra copies this->send_response_(improv::build_rpc_response(improv::WIFI_SETTINGS,
std::vector<uint8_t> data = improv::build_rpc_response( std::vector<std::string>(url_strings, url_strings + url_count)));
improv::WIFI_SETTINGS, std::vector<std::string>(url_strings, url_strings + url_count));
this->send_response_(data);
} else if (this->is_active() && this->state_ != improv::STATE_PROVISIONED) { } else if (this->is_active() && this->state_ != improv::STATE_PROVISIONED) {
ESP_LOGD(TAG, "WiFi provisioned externally"); ESP_LOGD(TAG, "WiFi provisioned externally");
} }

View File

@@ -109,7 +109,7 @@ class ESP32ImprovComponent : public Component, public improv_base::ImprovBase {
void set_state_(improv::State state, bool update_advertising = true); void set_state_(improv::State state, bool update_advertising = true);
void set_error_(improv::Error error); void set_error_(improv::Error error);
improv::State get_initial_state_() const; improv::State get_initial_state_() const;
void send_response_(std::vector<uint8_t> &response); void send_response_(std::vector<uint8_t> &&response);
void process_incoming_data_(); void process_incoming_data_();
void on_wifi_connect_timeout_(); void on_wifi_connect_timeout_();
void check_wifi_connection_(); void check_wifi_connection_();

View File

@@ -94,7 +94,7 @@ void ESPHomeOTAComponent::dump_config() {
"Over-The-Air updates:\n" "Over-The-Air updates:\n"
" Address: %s:%u\n" " Address: %s:%u\n"
" Version: %d", " Version: %d",
network::get_use_address().c_str(), this->port_, USE_OTA_VERSION); network::get_use_address(), this->port_, USE_OTA_VERSION);
#ifdef USE_OTA_PASSWORD #ifdef USE_OTA_PASSWORD
if (!this->password_.empty()) { if (!this->password_.empty()) {
ESP_LOGCONFIG(TAG, " Password configured"); ESP_LOGCONFIG(TAG, " Password configured");
@@ -281,19 +281,15 @@ void ESPHomeOTAComponent::handle_data_() {
#endif #endif
// Acknowledge auth OK - 1 byte // Acknowledge auth OK - 1 byte
buf[0] = ota::OTA_RESPONSE_AUTH_OK; this->write_byte_(ota::OTA_RESPONSE_AUTH_OK);
this->writeall_(buf, 1);
// Read size, 4 bytes MSB first // Read size, 4 bytes MSB first
if (!this->readall_(buf, 4)) { if (!this->readall_(buf, 4)) {
this->log_read_error_(LOG_STR("size")); this->log_read_error_(LOG_STR("size"));
goto error; // NOLINT(cppcoreguidelines-avoid-goto) goto error; // NOLINT(cppcoreguidelines-avoid-goto)
} }
ota_size = 0; ota_size = (static_cast<size_t>(buf[0]) << 24) | (static_cast<size_t>(buf[1]) << 16) |
for (uint8_t i = 0; i < 4; i++) { (static_cast<size_t>(buf[2]) << 8) | buf[3];
ota_size <<= 8;
ota_size |= buf[i];
}
ESP_LOGV(TAG, "Size is %u bytes", ota_size); ESP_LOGV(TAG, "Size is %u bytes", ota_size);
// Now that we've passed authentication and are actually // Now that we've passed authentication and are actually
@@ -313,8 +309,7 @@ void ESPHomeOTAComponent::handle_data_() {
update_started = true; update_started = true;
// Acknowledge prepare OK - 1 byte // Acknowledge prepare OK - 1 byte
buf[0] = ota::OTA_RESPONSE_UPDATE_PREPARE_OK; this->write_byte_(ota::OTA_RESPONSE_UPDATE_PREPARE_OK);
this->writeall_(buf, 1);
// Read binary MD5, 32 bytes // Read binary MD5, 32 bytes
if (!this->readall_(buf, 32)) { if (!this->readall_(buf, 32)) {
@@ -326,8 +321,7 @@ void ESPHomeOTAComponent::handle_data_() {
this->backend_->set_update_md5(sbuf); this->backend_->set_update_md5(sbuf);
// Acknowledge MD5 OK - 1 byte // Acknowledge MD5 OK - 1 byte
buf[0] = ota::OTA_RESPONSE_BIN_MD5_OK; this->write_byte_(ota::OTA_RESPONSE_BIN_MD5_OK);
this->writeall_(buf, 1);
while (total < ota_size) { while (total < ota_size) {
// TODO: timeout check // TODO: timeout check
@@ -354,8 +348,7 @@ void ESPHomeOTAComponent::handle_data_() {
total += read; total += read;
#if USE_OTA_VERSION == 2 #if USE_OTA_VERSION == 2
while (size_acknowledged + OTA_BLOCK_SIZE <= total || (total == ota_size && size_acknowledged < ota_size)) { while (size_acknowledged + OTA_BLOCK_SIZE <= total || (total == ota_size && size_acknowledged < ota_size)) {
buf[0] = ota::OTA_RESPONSE_CHUNK_OK; this->write_byte_(ota::OTA_RESPONSE_CHUNK_OK);
this->writeall_(buf, 1);
size_acknowledged += OTA_BLOCK_SIZE; size_acknowledged += OTA_BLOCK_SIZE;
} }
#endif #endif
@@ -374,8 +367,7 @@ void ESPHomeOTAComponent::handle_data_() {
} }
// Acknowledge receive OK - 1 byte // Acknowledge receive OK - 1 byte
buf[0] = ota::OTA_RESPONSE_RECEIVE_OK; this->write_byte_(ota::OTA_RESPONSE_RECEIVE_OK);
this->writeall_(buf, 1);
error_code = this->backend_->end(); error_code = this->backend_->end();
if (error_code != ota::OTA_RESPONSE_OK) { if (error_code != ota::OTA_RESPONSE_OK) {
@@ -384,8 +376,7 @@ void ESPHomeOTAComponent::handle_data_() {
} }
// Acknowledge Update end OK - 1 byte // Acknowledge Update end OK - 1 byte
buf[0] = ota::OTA_RESPONSE_UPDATE_END_OK; this->write_byte_(ota::OTA_RESPONSE_UPDATE_END_OK);
this->writeall_(buf, 1);
// Read ACK // Read ACK
if (!this->readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) { if (!this->readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) {
@@ -404,8 +395,7 @@ void ESPHomeOTAComponent::handle_data_() {
App.safe_reboot(); App.safe_reboot();
error: error:
buf[0] = static_cast<uint8_t>(error_code); this->write_byte_(static_cast<uint8_t>(error_code));
this->writeall_(buf, 1);
this->cleanup_connection_(); this->cleanup_connection_();
if (this->backend_ != nullptr && update_started) { if (this->backend_ != nullptr && update_started) {

View File

@@ -53,6 +53,7 @@ class ESPHomeOTAComponent : public ota::OTAComponent {
#endif // USE_OTA_PASSWORD #endif // USE_OTA_PASSWORD
bool readall_(uint8_t *buf, size_t len); bool readall_(uint8_t *buf, size_t len);
bool writeall_(const uint8_t *buf, size_t len); bool writeall_(const uint8_t *buf, size_t len);
inline bool write_byte_(uint8_t byte) { return this->writeall_(&byte, 1); }
bool try_read_(size_t to_read, const LogString *desc); bool try_read_(size_t to_read, const LogString *desc);
bool try_write_(size_t to_write, const LogString *desc); bool try_write_(size_t to_write, const LogString *desc);

View File

@@ -1,6 +1,6 @@
from esphome import automation, core from esphome import automation, core
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import wifi from esphome.components import socket, wifi
from esphome.components.udp import CONF_ON_RECEIVE from esphome.components.udp import CONF_ON_RECEIVE
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
@@ -17,6 +17,7 @@ from esphome.core import CORE, HexInt
from esphome.types import ConfigType from esphome.types import ConfigType
CODEOWNERS = ["@jesserockz"] CODEOWNERS = ["@jesserockz"]
AUTO_LOAD = ["socket"]
byte_vector = cg.std_vector.template(cg.uint8) byte_vector = cg.std_vector.template(cg.uint8)
peer_address_t = cg.std_ns.class_("array").template(cg.uint8, 6) peer_address_t = cg.std_ns.class_("array").template(cg.uint8, 6)
@@ -120,6 +121,10 @@ async def to_code(config):
if CORE.using_arduino: if CORE.using_arduino:
cg.add_library("WiFi", None) cg.add_library("WiFi", None)
# ESP-NOW uses wake_loop_threadsafe() to wake the main loop from ESP-NOW callbacks
# This enables low-latency event processing instead of waiting for select() timeout
socket.require_wake_loop_threadsafe()
cg.add_define("USE_ESPNOW") cg.add_define("USE_ESPNOW")
if wifi_channel := config.get(CONF_CHANNEL): if wifi_channel := config.get(CONF_CHANNEL):
cg.add(var.set_wifi_channel(wifi_channel)) cg.add(var.set_wifi_channel(wifi_channel))

View File

@@ -4,6 +4,7 @@
#include "espnow_err.h" #include "espnow_err.h"
#include "esphome/core/application.h"
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
@@ -97,6 +98,11 @@ void on_send_report(const uint8_t *mac_addr, esp_now_send_status_t status)
// Push the packet to the queue // Push the packet to the queue
global_esp_now->receive_packet_queue_.push(packet); global_esp_now->receive_packet_queue_.push(packet);
// Push always because we're the only producer and the pool ensures we never exceed queue size // Push always because we're the only producer and the pool ensures we never exceed queue size
// Wake main loop immediately to process ESP-NOW send event instead of waiting for select() timeout
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
App.wake_loop_threadsafe();
#endif
} }
void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int size) { void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int size) {
@@ -114,6 +120,11 @@ void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int
// Push the packet to the queue // Push the packet to the queue
global_esp_now->receive_packet_queue_.push(packet); global_esp_now->receive_packet_queue_.push(packet);
// Push always because we're the only producer and the pool ensures we never exceed queue size // Push always because we're the only producer and the pool ensures we never exceed queue size
// Wake main loop immediately to process ESP-NOW receive event instead of waiting for select() timeout
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
App.wake_loop_threadsafe();
#endif
} }
ESPNowComponent::ESPNowComponent() { global_esp_now = this; } ESPNowComponent::ESPNowComponent() { global_esp_now = this; }

View File

@@ -14,7 +14,7 @@ from esphome.components.esp32.const import (
VARIANT_ESP32S2, VARIANT_ESP32S2,
VARIANT_ESP32S3, VARIANT_ESP32S3,
) )
from esphome.components.network import IPAddress from esphome.components.network import ip_address_literal
from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
@@ -320,11 +320,11 @@ def _final_validate_spi(config):
def manual_ip(config): def manual_ip(config):
return cg.StructInitializer( return cg.StructInitializer(
ManualIP, ManualIP,
("static_ip", IPAddress(str(config[CONF_STATIC_IP]))), ("static_ip", ip_address_literal(config[CONF_STATIC_IP])),
("gateway", IPAddress(str(config[CONF_GATEWAY]))), ("gateway", ip_address_literal(config[CONF_GATEWAY])),
("subnet", IPAddress(str(config[CONF_SUBNET]))), ("subnet", ip_address_literal(config[CONF_SUBNET])),
("dns1", IPAddress(str(config[CONF_DNS1]))), ("dns1", ip_address_literal(config[CONF_DNS1])),
("dns2", IPAddress(str(config[CONF_DNS2]))), ("dns2", ip_address_literal(config[CONF_DNS2])),
) )

View File

@@ -691,9 +691,9 @@ void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_
// set_use_address() is guaranteed to be called during component setup by Python code generation, // set_use_address() is guaranteed to be called during component setup by Python code generation,
// so use_address_ will always be valid when get_use_address() is called - no fallback needed. // so use_address_ will always be valid when get_use_address() is called - no fallback needed.
const std::string &EthernetComponent::get_use_address() const { return this->use_address_; } const char *EthernetComponent::get_use_address() const { return this->use_address_; }
void EthernetComponent::set_use_address(const std::string &use_address) { this->use_address_ = use_address; } void EthernetComponent::set_use_address(const char *use_address) { this->use_address_ = use_address; }
void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) { void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) {
esp_err_t err; esp_err_t err;

View File

@@ -88,8 +88,8 @@ class EthernetComponent : public Component {
network::IPAddresses get_ip_addresses(); network::IPAddresses get_ip_addresses();
network::IPAddress get_dns_address(uint8_t num); network::IPAddress get_dns_address(uint8_t num);
const std::string &get_use_address() const; const char *get_use_address() const;
void set_use_address(const std::string &use_address); void set_use_address(const char *use_address);
void get_eth_mac_address_raw(uint8_t *mac); void get_eth_mac_address_raw(uint8_t *mac);
std::string get_eth_mac_address_pretty(); std::string get_eth_mac_address_pretty();
eth_duplex_t get_duplex_mode(); eth_duplex_t get_duplex_mode();
@@ -114,7 +114,6 @@ class EthernetComponent : public Component {
/// @brief Set arbitratry PHY registers from config. /// @brief Set arbitratry PHY registers from config.
void write_phy_register_(esp_eth_mac_t *mac, PHYRegister register_data); void write_phy_register_(esp_eth_mac_t *mac, PHYRegister register_data);
std::string use_address_;
#ifdef USE_ETHERNET_SPI #ifdef USE_ETHERNET_SPI
uint8_t clk_pin_; uint8_t clk_pin_;
uint8_t miso_pin_; uint8_t miso_pin_;
@@ -158,6 +157,11 @@ class EthernetComponent : public Component {
esp_eth_handle_t eth_handle_; esp_eth_handle_t eth_handle_;
esp_eth_phy_t *phy_{nullptr}; esp_eth_phy_t *phy_{nullptr};
optional<std::array<uint8_t, 6>> fixed_mac_; optional<std::array<uint8_t, 6>> fixed_mac_;
private:
// Stores a pointer to a string literal (static storage duration).
// ONLY set from Python-generated code with string literals - never dynamic strings.
const char *use_address_{""};
}; };
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)

View File

@@ -85,11 +85,6 @@ def event_schema(
return _EVENT_SCHEMA.extend(schema) return _EVENT_SCHEMA.extend(schema)
# Remove before 2025.11.0
EVENT_SCHEMA = event_schema()
EVENT_SCHEMA.add_extra(cv.deprecated_schema_constant("event"))
async def setup_event_core_(var, config, *, event_types: list[str]): async def setup_event_core_(var, config, *, event_types: list[str]):
await setup_entity(var, config, "event") await setup_entity(var, config, "event")

View File

@@ -189,10 +189,6 @@ def fan_schema(
return _FAN_SCHEMA.extend(schema) return _FAN_SCHEMA.extend(schema)
# Remove before 2025.11.0
FAN_SCHEMA = fan_schema(Fan)
FAN_SCHEMA.add_extra(cv.deprecated_schema_constant("fan"))
_PRESET_MODES_SCHEMA = cv.All( _PRESET_MODES_SCHEMA = cv.All(
cv.ensure_list(cv.string_strict), cv.ensure_list(cv.string_strict),
cv.Length(min=1), cv.Length(min=1),

View File

@@ -212,18 +212,19 @@ class FanPresetSetTrigger : public Trigger<std::string> {
public: public:
FanPresetSetTrigger(Fan *state) { FanPresetSetTrigger(Fan *state) {
state->add_on_state_callback([this, state]() { state->add_on_state_callback([this, state]() {
auto preset_mode = state->preset_mode; const auto *preset_mode = state->get_preset_mode();
auto should_trigger = preset_mode != this->last_preset_mode_; auto should_trigger = preset_mode != this->last_preset_mode_;
this->last_preset_mode_ = preset_mode; this->last_preset_mode_ = preset_mode;
if (should_trigger) { if (should_trigger) {
this->trigger(preset_mode); // Trigger with empty string when nullptr to maintain backward compatibility
this->trigger(preset_mode != nullptr ? preset_mode : "");
} }
}); });
this->last_preset_mode_ = state->preset_mode; this->last_preset_mode_ = state->get_preset_mode();
} }
protected: protected:
std::string last_preset_mode_; const char *last_preset_mode_{nullptr};
}; };
} // namespace fan } // namespace fan

View File

@@ -17,6 +17,27 @@ const LogString *fan_direction_to_string(FanDirection direction) {
} }
} }
FanCall &FanCall::set_preset_mode(const std::string &preset_mode) { return this->set_preset_mode(preset_mode.c_str()); }
FanCall &FanCall::set_preset_mode(const char *preset_mode) {
if (preset_mode == nullptr || strlen(preset_mode) == 0) {
this->preset_mode_ = nullptr;
return *this;
}
// Find and validate pointer from traits immediately
auto traits = this->parent_.get_traits();
const char *validated_mode = traits.find_preset_mode(preset_mode);
if (validated_mode != nullptr) {
this->preset_mode_ = validated_mode; // Store pointer from traits
} else {
// Preset mode not found in traits - log warning and don't set
ESP_LOGW(TAG, "%s: Preset mode '%s' not supported", this->parent_.get_name().c_str(), preset_mode);
this->preset_mode_ = nullptr;
}
return *this;
}
void FanCall::perform() { void FanCall::perform() {
ESP_LOGD(TAG, "'%s' - Setting:", this->parent_.get_name().c_str()); ESP_LOGD(TAG, "'%s' - Setting:", this->parent_.get_name().c_str());
this->validate_(); this->validate_();
@@ -32,8 +53,8 @@ void FanCall::perform() {
if (this->direction_.has_value()) { if (this->direction_.has_value()) {
ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(*this->direction_))); ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(*this->direction_)));
} }
if (!this->preset_mode_.empty()) { if (this->has_preset_mode()) {
ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode_.c_str()); ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode_);
} }
this->parent_.control(*this); this->parent_.control(*this);
} }
@@ -46,23 +67,15 @@ void FanCall::validate_() {
// https://developers.home-assistant.io/docs/core/entity/fan/#preset-modes // https://developers.home-assistant.io/docs/core/entity/fan/#preset-modes
// "Manually setting a speed must disable any set preset mode" // "Manually setting a speed must disable any set preset mode"
this->preset_mode_.clear(); this->preset_mode_ = nullptr;
}
if (!this->preset_mode_.empty()) {
const auto &preset_modes = traits.supported_preset_modes();
if (preset_modes.find(this->preset_mode_) == preset_modes.end()) {
ESP_LOGW(TAG, "%s: Preset mode '%s' not supported", this->parent_.get_name().c_str(), this->preset_mode_.c_str());
this->preset_mode_.clear();
}
} }
// when turning on... // when turning on...
if (!this->parent_.state && this->binary_state_.has_value() && if (!this->parent_.state && this->binary_state_.has_value() &&
*this->binary_state_ *this->binary_state_
// ..,and no preset mode will be active... // ..,and no preset mode will be active...
&& this->preset_mode_.empty() && && !this->has_preset_mode() &&
this->parent_.preset_mode.empty() this->parent_.get_preset_mode() == nullptr
// ...and neither current nor new speed is available... // ...and neither current nor new speed is available...
&& traits.supports_speed() && this->parent_.speed == 0 && !this->speed_.has_value()) { && traits.supports_speed() && this->parent_.speed == 0 && !this->speed_.has_value()) {
// ...set speed to 100% // ...set speed to 100%
@@ -92,11 +105,12 @@ FanCall FanRestoreState::to_call(Fan &fan) {
call.set_speed(this->speed); call.set_speed(this->speed);
call.set_direction(this->direction); call.set_direction(this->direction);
if (fan.get_traits().supports_preset_modes()) { auto traits = fan.get_traits();
if (traits.supports_preset_modes()) {
// Use stored preset index to get preset name // Use stored preset index to get preset name
const auto &preset_modes = fan.get_traits().supported_preset_modes(); const auto &preset_modes = traits.supported_preset_modes();
if (this->preset_mode < preset_modes.size()) { if (this->preset_mode < preset_modes.size()) {
call.set_preset_mode(*std::next(preset_modes.begin(), this->preset_mode)); call.set_preset_mode(preset_modes[this->preset_mode]);
} }
} }
return call; return call;
@@ -107,13 +121,15 @@ void FanRestoreState::apply(Fan &fan) {
fan.speed = this->speed; fan.speed = this->speed;
fan.direction = this->direction; fan.direction = this->direction;
if (fan.get_traits().supports_preset_modes()) { auto traits = fan.get_traits();
// Use stored preset index to get preset name if (traits.supports_preset_modes()) {
const auto &preset_modes = fan.get_traits().supported_preset_modes(); // Use stored preset index to get preset name from traits
const auto &preset_modes = traits.supported_preset_modes();
if (this->preset_mode < preset_modes.size()) { if (this->preset_mode < preset_modes.size()) {
fan.preset_mode = *std::next(preset_modes.begin(), this->preset_mode); fan.set_preset_mode_(preset_modes[this->preset_mode]);
} }
} }
fan.publish_state(); fan.publish_state();
} }
@@ -122,6 +138,29 @@ FanCall Fan::turn_off() { return this->make_call().set_state(false); }
FanCall Fan::toggle() { return this->make_call().set_state(!this->state); } FanCall Fan::toggle() { return this->make_call().set_state(!this->state); }
FanCall Fan::make_call() { return FanCall(*this); } FanCall Fan::make_call() { return FanCall(*this); }
const char *Fan::find_preset_mode_(const char *preset_mode) { return this->get_traits().find_preset_mode(preset_mode); }
bool Fan::set_preset_mode_(const char *preset_mode) {
if (preset_mode == nullptr) {
// Treat nullptr as clearing the preset mode
if (this->preset_mode_ == nullptr) {
return false; // No change
}
this->clear_preset_mode_();
return true;
}
const char *validated = this->find_preset_mode_(preset_mode);
if (validated == nullptr || this->preset_mode_ == validated) {
return false; // Preset mode not supported or no change
}
this->preset_mode_ = validated;
return true;
}
bool Fan::set_preset_mode_(const std::string &preset_mode) { return this->set_preset_mode_(preset_mode.c_str()); }
void Fan::clear_preset_mode_() { this->preset_mode_ = nullptr; }
void Fan::add_on_state_callback(std::function<void()> &&callback) { this->state_callback_.add(std::move(callback)); } void Fan::add_on_state_callback(std::function<void()> &&callback) { this->state_callback_.add(std::move(callback)); }
void Fan::publish_state() { void Fan::publish_state() {
auto traits = this->get_traits(); auto traits = this->get_traits();
@@ -137,8 +176,9 @@ void Fan::publish_state() {
if (traits.supports_direction()) { if (traits.supports_direction()) {
ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(this->direction))); ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(this->direction)));
} }
if (traits.supports_preset_modes() && !this->preset_mode.empty()) { const char *preset = this->get_preset_mode();
ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode.c_str()); if (preset != nullptr) {
ESP_LOGD(TAG, " Preset Mode: %s", preset);
} }
this->state_callback_.call(); this->state_callback_.call();
this->save_state_(); this->save_state_();
@@ -182,18 +222,24 @@ void Fan::save_state_() {
return; return;
} }
auto traits = this->get_traits();
FanRestoreState state{}; FanRestoreState state{};
state.state = this->state; state.state = this->state;
state.oscillating = this->oscillating; state.oscillating = this->oscillating;
state.speed = this->speed; state.speed = this->speed;
state.direction = this->direction; state.direction = this->direction;
if (this->get_traits().supports_preset_modes() && !this->preset_mode.empty()) { const char *preset = this->get_preset_mode();
const auto &preset_modes = this->get_traits().supported_preset_modes(); if (preset != nullptr) {
// Store index of current preset mode const auto &preset_modes = traits.supported_preset_modes();
auto preset_iterator = preset_modes.find(this->preset_mode); // Find index of current preset mode (pointer comparison is safe since preset is from traits)
if (preset_iterator != preset_modes.end()) for (size_t i = 0; i < preset_modes.size(); i++) {
state.preset_mode = std::distance(preset_modes.begin(), preset_iterator); if (preset_modes[i] == preset) {
state.preset_mode = i;
break;
}
}
} }
this->rtc_.save(&state); this->rtc_.save(&state);
@@ -216,8 +262,8 @@ void Fan::dump_traits_(const char *tag, const char *prefix) {
} }
if (traits.supports_preset_modes()) { if (traits.supports_preset_modes()) {
ESP_LOGCONFIG(tag, "%s Supported presets:", prefix); ESP_LOGCONFIG(tag, "%s Supported presets:", prefix);
for (const std::string &s : traits.supported_preset_modes()) for (const char *s : traits.supported_preset_modes())
ESP_LOGCONFIG(tag, "%s - %s", prefix, s.c_str()); ESP_LOGCONFIG(tag, "%s - %s", prefix, s);
} }
} }

View File

@@ -60,8 +60,6 @@ class FanCall {
this->speed_ = speed; this->speed_ = speed;
return *this; return *this;
} }
ESPDEPRECATED("set_speed() with string argument is deprecated, use integer argument instead.", "2021.9")
FanCall &set_speed(const char *legacy_speed);
optional<int> get_speed() const { return this->speed_; } optional<int> get_speed() const { return this->speed_; }
FanCall &set_direction(FanDirection direction) { FanCall &set_direction(FanDirection direction) {
this->direction_ = direction; this->direction_ = direction;
@@ -72,11 +70,10 @@ class FanCall {
return *this; return *this;
} }
optional<FanDirection> get_direction() const { return this->direction_; } optional<FanDirection> get_direction() const { return this->direction_; }
FanCall &set_preset_mode(const std::string &preset_mode) { FanCall &set_preset_mode(const std::string &preset_mode);
this->preset_mode_ = preset_mode; FanCall &set_preset_mode(const char *preset_mode);
return *this; const char *get_preset_mode() const { return this->preset_mode_; }
} bool has_preset_mode() const { return this->preset_mode_ != nullptr; }
std::string get_preset_mode() const { return this->preset_mode_; }
void perform(); void perform();
@@ -88,7 +85,7 @@ class FanCall {
optional<bool> oscillating_; optional<bool> oscillating_;
optional<int> speed_; optional<int> speed_;
optional<FanDirection> direction_{}; optional<FanDirection> direction_{};
std::string preset_mode_{}; const char *preset_mode_{nullptr}; // Pointer to string in traits (after validation)
}; };
struct FanRestoreState { struct FanRestoreState {
@@ -114,8 +111,6 @@ class Fan : public EntityBase {
int speed{0}; int speed{0};
/// The current direction of the fan /// The current direction of the fan
FanDirection direction{FanDirection::FORWARD}; FanDirection direction{FanDirection::FORWARD};
// The current preset mode of the fan
std::string preset_mode{};
FanCall turn_on(); FanCall turn_on();
FanCall turn_off(); FanCall turn_off();
@@ -132,8 +127,15 @@ class Fan : public EntityBase {
/// Set the restore mode of this fan. /// Set the restore mode of this fan.
void set_restore_mode(FanRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } void set_restore_mode(FanRestoreMode restore_mode) { this->restore_mode_ = restore_mode; }
/// Get the current preset mode (returns pointer to string stored in traits, or nullptr if not set)
const char *get_preset_mode() const { return this->preset_mode_; }
/// Check if a preset mode is currently active
bool has_preset_mode() const { return this->preset_mode_ != nullptr; }
protected: protected:
friend FanCall; friend FanCall;
friend struct FanRestoreState;
virtual void control(const FanCall &call) = 0; virtual void control(const FanCall &call) = 0;
@@ -142,9 +144,21 @@ class Fan : public EntityBase {
void dump_traits_(const char *tag, const char *prefix); void dump_traits_(const char *tag, const char *prefix);
/// Set the preset mode (finds and stores pointer from traits). Returns true if changed.
bool set_preset_mode_(const char *preset_mode);
/// Set the preset mode (finds and stores pointer from traits). Returns true if changed.
bool set_preset_mode_(const std::string &preset_mode);
/// Clear the preset mode
void clear_preset_mode_();
/// Find and return the matching preset mode pointer from traits, or nullptr if not found.
const char *find_preset_mode_(const char *preset_mode);
CallbackManager<void()> state_callback_{}; CallbackManager<void()> state_callback_{};
ESPPreferenceObject rtc_; ESPPreferenceObject rtc_;
FanRestoreMode restore_mode_; FanRestoreMode restore_mode_;
private:
const char *preset_mode_{nullptr};
}; };
} // namespace fan } // namespace fan

View File

@@ -1,15 +1,10 @@
#include <set>
#include <utility>
#pragma once #pragma once
namespace esphome { #include <cstring>
#include <vector>
#include <initializer_list>
#ifdef USE_API namespace esphome {
namespace api {
class APIConnection;
} // namespace api
#endif
namespace fan { namespace fan {
@@ -36,27 +31,38 @@ class FanTraits {
/// Set whether this fan supports changing direction /// Set whether this fan supports changing direction
void set_direction(bool direction) { this->direction_ = direction; } void set_direction(bool direction) { this->direction_ = direction; }
/// Return the preset modes supported by the fan. /// Return the preset modes supported by the fan.
std::set<std::string> supported_preset_modes() const { return this->preset_modes_; } const std::vector<const char *> &supported_preset_modes() const { return this->preset_modes_; }
/// Set the preset modes supported by the fan. /// Set the preset modes supported by the fan (from initializer list).
void set_supported_preset_modes(const std::set<std::string> &preset_modes) { this->preset_modes_ = preset_modes; } void set_supported_preset_modes(std::initializer_list<const char *> preset_modes) {
this->preset_modes_ = preset_modes;
}
/// Set the preset modes supported by the fan (from vector).
void set_supported_preset_modes(const std::vector<const char *> &preset_modes) { this->preset_modes_ = preset_modes; }
// Deleted overloads to catch incorrect std::string usage at compile time with clear error messages
void set_supported_preset_modes(const std::vector<std::string> &preset_modes) = delete;
void set_supported_preset_modes(std::initializer_list<std::string> preset_modes) = delete;
/// Return if preset modes are supported /// Return if preset modes are supported
bool supports_preset_modes() const { return !this->preset_modes_.empty(); } bool supports_preset_modes() const { return !this->preset_modes_.empty(); }
/// Find and return the matching preset mode pointer from supported modes, or nullptr if not found.
const char *find_preset_mode(const char *preset_mode) const {
if (preset_mode == nullptr)
return nullptr;
for (const char *mode : this->preset_modes_) {
if (strcmp(mode, preset_mode) == 0) {
return mode; // Return pointer from traits
}
}
return nullptr;
}
protected: protected:
#ifdef USE_API
// The API connection is a friend class to access internal methods
friend class api::APIConnection;
// This method returns a reference to the internal preset modes set.
// It is used by the API to avoid copying data when encoding messages.
// Warning: Do not use this method outside of the API connection code.
// It returns a reference to internal data that can be invalidated.
const std::set<std::string> &supported_preset_modes_for_api_() const { return this->preset_modes_; }
#endif
bool oscillation_{false}; bool oscillation_{false};
bool speed_{false}; bool speed_{false};
bool direction_{false}; bool direction_{false};
int speed_count_{}; int speed_count_{};
std::set<std::string> preset_modes_{}; std::vector<const char *> preset_modes_{};
}; };
} // namespace fan } // namespace fan

View File

@@ -1,19 +1,25 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import i2c from esphome.components import i2c
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_VOLTAGE from esphome.const import CONF_ID, CONF_MODEL, CONF_VOLTAGE
CODEOWNERS = ["@jesserockz"] CODEOWNERS = ["@jesserockz", "@sebydocky"]
DEPENDENCIES = ["i2c"] DEPENDENCIES = ["i2c"]
MULTI_CONF = True MULTI_CONF = True
gp8403_ns = cg.esphome_ns.namespace("gp8403") gp8403_ns = cg.esphome_ns.namespace("gp8403")
GP8403 = gp8403_ns.class_("GP8403", cg.Component, i2c.I2CDevice) GP8403Component = gp8403_ns.class_("GP8403Component", cg.Component, i2c.I2CDevice)
GP8403Voltage = gp8403_ns.enum("GP8403Voltage") GP8403Voltage = gp8403_ns.enum("GP8403Voltage")
GP8403Model = gp8403_ns.enum("GP8403Model")
CONF_GP8403_ID = "gp8403_id" CONF_GP8403_ID = "gp8403_id"
MODELS = {
"GP8403": GP8403Model.GP8403,
"GP8413": GP8403Model.GP8413,
}
VOLTAGES = { VOLTAGES = {
"5V": GP8403Voltage.GP8403_VOLTAGE_5V, "5V": GP8403Voltage.GP8403_VOLTAGE_5V,
"10V": GP8403Voltage.GP8403_VOLTAGE_10V, "10V": GP8403Voltage.GP8403_VOLTAGE_10V,
@@ -22,7 +28,8 @@ VOLTAGES = {
CONFIG_SCHEMA = ( CONFIG_SCHEMA = (
cv.Schema( cv.Schema(
{ {
cv.GenerateID(): cv.declare_id(GP8403), cv.GenerateID(): cv.declare_id(GP8403Component),
cv.Optional(CONF_MODEL, default="GP8403"): cv.enum(MODELS, upper=True),
cv.Required(CONF_VOLTAGE): cv.enum(VOLTAGES, upper=True), cv.Required(CONF_VOLTAGE): cv.enum(VOLTAGES, upper=True),
} }
) )
@@ -35,5 +42,5 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
await i2c.register_i2c_device(var, config) await i2c.register_i2c_device(var, config)
cg.add(var.set_model(config[CONF_MODEL]))
cg.add(var.set_voltage(config[CONF_VOLTAGE])) cg.add(var.set_voltage(config[CONF_VOLTAGE]))

View File

@@ -8,16 +8,48 @@ namespace gp8403 {
static const char *const TAG = "gp8403"; static const char *const TAG = "gp8403";
static const uint8_t RANGE_REGISTER = 0x01; static const uint8_t RANGE_REGISTER = 0x01;
static const uint8_t OUTPUT_REGISTER = 0x02;
void GP8403::setup() { this->write_register(RANGE_REGISTER, (uint8_t *) (&this->voltage_), 1); } const LogString *model_to_string(GP8403Model model) {
switch (model) {
case GP8403Model::GP8403:
return LOG_STR("GP8403");
case GP8403Model::GP8413:
return LOG_STR("GP8413");
}
return LOG_STR("Unknown");
};
void GP8403::dump_config() { void GP8403Component::setup() { this->write_register(RANGE_REGISTER, (uint8_t *) (&this->voltage_), 1); }
void GP8403Component::dump_config() {
ESP_LOGCONFIG(TAG, ESP_LOGCONFIG(TAG,
"GP8403:\n" "GP8403:\n"
" Voltage: %dV", " Voltage: %dV\n"
this->voltage_ == GP8403_VOLTAGE_5V ? 5 : 10); " Model: %s",
this->voltage_ == GP8403_VOLTAGE_5V ? 5 : 10, LOG_STR_ARG(model_to_string(this->model_)));
LOG_I2C_DEVICE(this); LOG_I2C_DEVICE(this);
} }
void GP8403Component::write_state(float state, uint8_t channel) {
uint16_t val = 0;
switch (this->model_) {
case GP8403Model::GP8403:
val = ((uint16_t) (4095 * state)) << 4;
break;
case GP8403Model::GP8413:
val = ((uint16_t) (32767 * state)) << 1;
break;
default:
ESP_LOGE(TAG, "Unknown model %s", LOG_STR_ARG(model_to_string(this->model_)));
return;
}
ESP_LOGV(TAG, "Calculated DAC value: %" PRIu16, val);
i2c::ErrorCode err = this->write_register(OUTPUT_REGISTER + (2 * channel), (uint8_t *) &val, 2);
if (err != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Error writing to %s, code %d", LOG_STR_ARG(model_to_string(this->model_)), err);
}
}
} // namespace gp8403 } // namespace gp8403
} // namespace esphome } // namespace esphome

View File

@@ -11,15 +11,24 @@ enum GP8403Voltage {
GP8403_VOLTAGE_10V = 0x11, GP8403_VOLTAGE_10V = 0x11,
}; };
class GP8403 : public Component, public i2c::I2CDevice { enum GP8403Model {
GP8403,
GP8413,
};
class GP8403Component : public Component, public i2c::I2CDevice {
public: public:
void setup() override; void setup() override;
void dump_config() override; void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
void set_model(GP8403Model model) { this->model_ = model; }
void set_voltage(gp8403::GP8403Voltage voltage) { this->voltage_ = voltage; } void set_voltage(gp8403::GP8403Voltage voltage) { this->voltage_ = voltage; }
void write_state(float state, uint8_t channel);
protected: protected:
GP8403Voltage voltage_; GP8403Voltage voltage_;
GP8403Model model_{GP8403Model::GP8403};
}; };
} // namespace gp8403 } // namespace gp8403

View File

@@ -3,7 +3,7 @@ from esphome.components import i2c, output
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_CHANNEL, CONF_ID from esphome.const import CONF_CHANNEL, CONF_ID
from .. import CONF_GP8403_ID, GP8403, gp8403_ns from .. import CONF_GP8403_ID, GP8403Component, gp8403_ns
DEPENDENCIES = ["gp8403"] DEPENDENCIES = ["gp8403"]
@@ -14,7 +14,7 @@ GP8403Output = gp8403_ns.class_(
CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
{ {
cv.GenerateID(): cv.declare_id(GP8403Output), cv.GenerateID(): cv.declare_id(GP8403Output),
cv.GenerateID(CONF_GP8403_ID): cv.use_id(GP8403), cv.GenerateID(CONF_GP8403_ID): cv.use_id(GP8403Component),
cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=1), cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=1),
} }
).extend(cv.COMPONENT_SCHEMA) ).extend(cv.COMPONENT_SCHEMA)

View File

@@ -7,8 +7,6 @@ namespace gp8403 {
static const char *const TAG = "gp8403.output"; static const char *const TAG = "gp8403.output";
static const uint8_t OUTPUT_REGISTER = 0x02;
void GP8403Output::dump_config() { void GP8403Output::dump_config() {
ESP_LOGCONFIG(TAG, ESP_LOGCONFIG(TAG,
"GP8403 Output:\n" "GP8403 Output:\n"
@@ -16,13 +14,7 @@ void GP8403Output::dump_config() {
this->channel_); this->channel_);
} }
void GP8403Output::write_state(float state) { void GP8403Output::write_state(float state) { this->parent_->write_state(state, this->channel_); }
uint16_t value = ((uint16_t) (state * 4095)) << 4;
i2c::ErrorCode err = this->parent_->write_register(OUTPUT_REGISTER + (2 * this->channel_), (uint8_t *) &value, 2);
if (err != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Error writing to GP8403, code %d", err);
}
}
} // namespace gp8403 } // namespace gp8403
} // namespace esphome } // namespace esphome

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