1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-12 12:55:46 +00:00

Compare commits

..

175 Commits

Author SHA1 Message Date
J. Nick Koston
d648b3f462 propsals 2025-11-07 22:19:51 -06:00
J. Nick Koston
05d7410afa propsals 2025-11-07 22:05:29 -06:00
J. Nick Koston
32797534a7 propsals 2025-11-07 22:04:58 -06:00
J. Nick Koston
b264c6caac cleanup defines 2025-11-07 18:16:22 -06:00
J. Nick Koston
e3fb074a60 preen 2025-11-07 17:14:50 -06:00
J. Nick Koston
6e7f66d393 missing registry 2025-11-07 16:40:36 -06:00
J. Nick Koston
ac85949f17 cleanups 2025-11-07 16:38:32 -06:00
J. Nick Koston
0962024d99 cleanups 2025-11-07 16:35:24 -06:00
J. Nick Koston
327543303c cleanups 2025-11-07 16:34:37 -06:00
J. Nick Koston
8229e3a471 cleanups 2025-11-07 16:33:01 -06:00
J. Nick Koston
1b6471f4b0 cleanups 2025-11-07 16:30:38 -06:00
J. Nick Koston
c87d07ba70 fixes 2025-11-07 16:15:07 -06:00
J. Nick Koston
fc8dc33023 fixes 2025-11-07 16:13:59 -06:00
J. Nick Koston
c0e4f415f1 Revert "no ifdefs needed on forward decs"
This reverts commit 871c5ddb4e.
2025-11-07 16:10:56 -06:00
J. Nick Koston
871c5ddb4e no ifdefs needed on forward decs 2025-11-07 16:07:54 -06:00
J. Nick Koston
6ef2763cab controller registry 2025-11-07 16:01:45 -06:00
J. Nick Koston
929279dc23 controller registry 2025-11-07 15:55:22 -06:00
J. Nick Koston
6fa0f1e290 controller registry 2025-11-07 15:51:13 -06:00
J. Nick Koston
51eb8ea1d0 controller registry 2025-11-07 15:48:02 -06:00
J. Nick Koston
cbdd663fbf Merge remote-tracking branch 'upstream/dev' into controller_registry 2025-11-07 15:46:57 -06:00
J. Nick Koston
c77bb3b269 [event] Store event types in flash memory (#11767) 2025-11-07 15:46:16 -06:00
J. Nick Koston
f1009a7468 tweak 2025-11-07 15:44:17 -06:00
J. Nick Koston
295fe8da04 controller registry phase1/2 2025-11-07 15:32:46 -06:00
dependabot[bot]
79d1a558af Bump ruff from 0.14.3 to 0.14.4 (#11768)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-11-07 20:12:15 +00:00
J. Nick Koston
a5bf55b6ac [ci] Fix component batching for beta/release branches (3-4 → 40 per batch) (#11759) 2025-11-07 20:19:45 +13:00
J. Nick Koston
85d2565f25 [tests] Fix determine_jobs tests failing when target branch is beta (#11758) 2025-11-07 20:18:43 +13:00
J. Nick Koston
4f08f0750a [ai_instructions] Add public API and breaking changes guidelines (#11756) 2025-11-06 22:34:53 -06:00
J. Nick Koston
3c41e080c5 [core] Use ESPDEPRECATED macro for deprecation warnings (#11755) 2025-11-07 03:37:02 +00:00
J. Nick Koston
7c30d57391 [wifi] Refactor AP selection to use index instead of copy (saves 88 bytes) (#11749) 2025-11-06 21:26:53 -06:00
J. Nick Koston
182e106bfa [wifi] Guard AP-related members with USE_WIFI_AP to save RAM (#11753) 2025-11-07 15:44:40 +13:00
J. Nick Koston
d0b399d771 [ci] Reduce release time by removing 468 redundant ESP32-C3 IDF tests (#11737) 2025-11-07 15:44:01 +13:00
philippderdiedas
5d20e3a3b4 Add MCP3221 i2c A-D-Converter (#7764) 2025-11-07 14:25:14 +13:00
Kevin Ahrendt
ba5fa7c10a [psram] Add option to disable ignore not found sdkconfig setting (#11411)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-11-07 14:22:50 +13:00
J. Nick Koston
5cdb891b58 [socket] Deduplicate IP formatting in LWIP raw TCP implementation (#11747) 2025-11-07 14:21:58 +13:00
rwrozelle
26607713bb [openthread] add poll period for mtd devices (#11374)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-11-06 16:57:31 +13:00
Szewcson
895d76ca03 [gdk101] Fix fw version reporting (#11029)
Signed-off-by: szewcu <szewcson@gmail.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-11-05 22:19:29 -05:00
J. Nick Koston
74187845b7 [select] Convert remaining components to use index-based control() (#11693)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-11-06 15:55:26 +13:00
J. Nick Koston
822eacfd77 [core] Fix wait_until and for_condition timing regression in automation chains (#11716) 2025-11-06 15:49:24 +13:00
Clyde Stubbs
ab5d8f67ae [core] Add helper functions for clamp_at_... (#10387) 2025-11-06 15:48:02 +13:00
J. Nick Koston
83f30a64ed [api] Store YAML service names in flash instead of heap (#11744) 2025-11-06 15:31:59 +13:00
J. Nick Koston
5eea7bdb44 Update AI instructions with C++ style guidelines from developers docs (#11743) 2025-11-06 14:45:48 +13:00
J. Nick Koston
bdfd88441a [ci] Skip memory impact analysis when more than 40 components changed (#11741) 2025-11-05 19:31:23 -06:00
Clyde Stubbs
20b6e0d5c2 [lvgl] Allow text substitution for NaN (#11712) 2025-11-06 10:37:38 +11:00
J. Nick Koston
ce5e608863 [ci] Skip memory impact analysis for release and beta branches (#11740) 2025-11-05 14:32:45 -06:00
J. Nick Koston
aa5795c019 [tests] Fix ID collision between bl0940 and nau7802 component tests (#11739) 2025-11-05 13:17:34 -06:00
J. Nick Koston
00c0854323 [core] Deprecate get_icon(), get_device_class(), get_unit_of_measurement() and fix remaining non-MQTT usages (#11732) 2025-11-05 12:50:35 -06:00
J. Nick Koston
be006ecadd [mdns] Eliminate redundant hostname copy to save heap memory (#11734) 2025-11-05 18:31:19 +00:00
J. Nick Koston
b08419fa47 [mqtt] Use StringRef to avoid string copies in discovery (#11731) 2025-11-06 07:30:45 +13:00
J. Nick Koston
d36ef050a9 [template] Mark all component classes as final (#11733) 2025-11-06 07:15:50 +13:00
J. Nick Koston
df53ff7afe [scheduler] Extract helper functions to improve code readability (#11730) 2025-11-06 07:13:12 +13:00
J. Nick Koston
b7838671ae [ld2420] Eliminate substr() allocation in firmware version parsing (#11724) 2025-11-05 10:57:20 -06:00
J. Nick Koston
479f8dd85c [rtttl] Reduce flash usage by eliminating substr() allocations (#11722) 2025-11-05 09:17:28 -06:00
J. Nick Koston
6e2dbbf636 [voice_assistant] Eliminate substr() allocations in text truncation (#11725) 2025-11-05 09:15:05 -06:00
J. Nick Koston
6b522dfee6 [wifi_info] Reduce heap usage by up to 1.7KB in scan_results sensor (#11723) 2025-11-05 09:14:21 -06:00
J. Nick Koston
32975c9d8b [select][lvgl] Fix FixedVector size() returning 0 when using operator[] after init() (#11721) 2025-11-05 01:49:27 +00:00
J. Nick Koston
1446e7174a [core] Reduce action framework argument copies by 83% (#11704)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-11-05 01:23:24 +00:00
Gnuspice
64f8963566 [const] Move CONF_ENABLED to const.py (#11719) 2025-11-05 12:46:06 +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
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
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
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
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
1076 changed files with 12630 additions and 6015 deletions

View File

@@ -51,7 +51,79 @@ This document provides essential context for AI models interacting with this pro
* **Naming Conventions:** * **Naming Conventions:**
* **Python:** Follows PEP 8. Use clear, descriptive names following snake_case. * **Python:** Follows PEP 8. Use clear, descriptive names following snake_case.
* **C++:** Follows the Google C++ Style Guide. * **C++:** Follows the Google C++ Style Guide with these specifics (following clang-tidy conventions):
- Function, method, and variable names: `lower_snake_case`
- Class/struct/enum names: `UpperCamelCase`
- Top-level constants (global/namespace scope): `UPPER_SNAKE_CASE`
- Function-local constants: `lower_snake_case`
- Protected/private fields: `lower_snake_case_with_trailing_underscore_`
- Favor descriptive names over abbreviations
* **C++ Field Visibility:**
* **Prefer `protected`:** Use `protected` for most class fields to enable extensibility and testing. Fields should be `lower_snake_case_with_trailing_underscore_`.
* **Use `private` for safety-critical cases:** Use `private` visibility when direct field access could introduce bugs or violate invariants:
1. **Pointer lifetime issues:** When setters validate and store pointers from known lists to prevent dangling references.
```cpp
// Helper to find matching string in vector and return its pointer
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;
}
class ClimateDevice {
public:
void set_custom_fan_modes(std::initializer_list<const char *> modes) {
this->custom_fan_modes_ = modes;
this->active_custom_fan_mode_ = nullptr; // Reset when modes change
}
bool set_custom_fan_mode(const char *mode) {
// Find mode in supported list and store that pointer (not the input pointer)
const char *validated_mode = vector_find(this->custom_fan_modes_, mode);
if (validated_mode != nullptr) {
this->active_custom_fan_mode_ = validated_mode;
return true;
}
return false;
}
private:
std::vector<const char *> custom_fan_modes_; // Pointers to string literals in flash
const char *active_custom_fan_mode_{nullptr}; // Must point to entry in custom_fan_modes_
};
```
2. **Invariant coupling:** When multiple fields must remain synchronized to prevent buffer overflows or data corruption.
```cpp
class Buffer {
public:
void resize(size_t new_size) {
auto new_data = std::make_unique<uint8_t[]>(new_size);
if (this->data_) {
std::memcpy(new_data.get(), this->data_.get(), std::min(this->size_, new_size));
}
this->data_ = std::move(new_data);
this->size_ = new_size; // Must stay in sync with data_
}
private:
std::unique_ptr<uint8_t[]> data_;
size_t size_{0}; // Must match allocated size of data_
};
```
3. **Resource management:** When setters perform cleanup or registration operations that derived classes might skip.
* **Provide `protected` accessor methods:** When derived classes need controlled access to `private` members.
* **C++ Preprocessor Directives:**
* **Avoid `#define` for constants:** Using `#define` for constants is discouraged and should be replaced with `const` variables or enums.
* **Use `#define` only for:**
- Conditional compilation (`#ifdef`, `#ifndef`)
- Compile-time sizes calculated during Python code generation (e.g., configuring `std::array` or `StaticVector` dimensions via `cg.add_define()`)
* **C++ Additional Conventions:**
* **Member access:** Prefix all class member access with `this->` (e.g., `this->value_` not `value_`)
* **Indentation:** Use spaces (two per indentation level), not tabs
* **Type aliases:** Prefer `using type_t = int;` over `typedef int type_t;`
* **Line length:** Wrap lines at no more than 120 characters
* **Component Structure:** * **Component Structure:**
* **Standard Files:** * **Standard Files:**
@@ -368,3 +440,45 @@ This document provides essential context for AI models interacting with this pro
* **Python:** When adding a new Python dependency, add it to the appropriate `requirements*.txt` file and `pyproject.toml`. * **Python:** When adding a new Python dependency, add it to the appropriate `requirements*.txt` file and `pyproject.toml`.
* **C++ / PlatformIO:** When adding a new C++ dependency, add it to `platformio.ini` and use `cg.add_library`. * **C++ / PlatformIO:** When adding a new C++ dependency, add it to `platformio.ini` and use `cg.add_library`.
* **Build Flags:** Use `cg.add_build_flag(...)` to add compiler flags. * **Build Flags:** Use `cg.add_build_flag(...)` to add compiler flags.
## 8. Public API and Breaking Changes
* **Public C++ API:**
* **Components**: Only documented features at [esphome.io](https://esphome.io) are public API. Undocumented `public` members are internal.
* **Core/Base Classes** (`esphome/core/`, `Component`, `Sensor`, etc.): All `public` members are public API.
* **Components with Global Accessors** (`global_api_server`, etc.): All `public` members are public API (except config setters).
* **Public Python API:**
* All documented configuration options at [esphome.io](https://esphome.io) are public API.
* Python code in `esphome/core/` actively used by existing core components is considered stable API.
* Other Python code is internal unless explicitly documented for external component use.
* **Breaking Changes Policy:**
* Aim for **6-month deprecation window** when possible
* Clean breaks allowed for: signature changes, deep refactorings, resource constraints
* Must document migration path in PR description (generates release notes)
* Blog post required for core/base class changes or significant architectural changes
* Full details: https://developers.esphome.io/contributing/code/#public-api-and-breaking-changes
* **Breaking Change Checklist:**
- [ ] Clear justification (RAM/flash savings, architectural improvement)
- [ ] Explored non-breaking alternatives
- [ ] Added deprecation warnings if possible (use `ESPDEPRECATED` macro for C++)
- [ ] Documented migration path in PR description with before/after examples
- [ ] Updated all internal usage and esphome-docs
- [ ] Tested backward compatibility during deprecation period
* **Deprecation Pattern (C++):**
```cpp
// Remove before 2026.6.0
ESPDEPRECATED("Use new_method() instead. Removed in 2026.6.0", "2025.12.0")
void old_method() { this->new_method(); }
```
* **Deprecation Pattern (Python):**
```python
# Remove before 2026.6.0
if CONF_OLD_KEY in config:
_LOGGER.warning(f"'{CONF_OLD_KEY}' deprecated, use '{CONF_NEW_KEY}'. Removed in 2026.6.0")
config[CONF_NEW_KEY] = config.pop(CONF_OLD_KEY) # Auto-migrate
```

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.4
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
@@ -289,6 +290,7 @@ esphome/components/mcp23x17_base/* @jesserockz
esphome/components/mcp23xxx_base/* @jesserockz esphome/components/mcp23xxx_base/* @jesserockz
esphome/components/mcp2515/* @danielschramm @mvturnho esphome/components/mcp2515/* @danielschramm @mvturnho
esphome/components/mcp3204/* @rsumner esphome/components/mcp3204/* @rsumner
esphome/components/mcp3221/* @philippderdiedas
esphome/components/mcp4461/* @p1ngb4ck esphome/components/mcp4461/* @p1ngb4ck
esphome/components/mcp4728/* @berfenger esphome/components/mcp4728/* @berfenger
esphome/components/mcp47a1/* @jesserockz esphome/components/mcp47a1/* @jesserockz
@@ -479,6 +481,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

@@ -0,0 +1,309 @@
# Sensor Callback Optimization - Zero-Cost Implementation
## The Perfect Optimization
By storing the partition count **in the Sensor class** alongside existing small fields, we achieve a **zero-cost optimization** with only wins and no losses!
## Implementation Design
### Key Insight: Reuse Available Padding
Sensor already has grouped small fields with 1 byte of available space:
```cpp
class Sensor {
protected:
// Existing small members grouped together
int8_t accuracy_decimals_{-1}; // 1 byte
StateClass state_class_{STATE_CLASS_NONE}; // 1 byte (uint8_t enum)
struct SensorFlags {
uint8_t has_accuracy_override : 1;
uint8_t has_state_class_override : 1;
uint8_t force_update : 1;
uint8_t reserved : 5;
} sensor_flags_{}; // 1 byte
uint8_t filtered_count_{0}; // 1 byte ← NEW! Perfect fit!
// Total: 4 bytes (naturally aligned, no padding waste)
};
```
### Callbacks Structure (Heap-Allocated)
```cpp
class Sensor {
protected:
std::unique_ptr<std::vector<std::function<void(float)>>> callbacks_;
// Partition layout: [filtered_0, ..., filtered_n-1, raw_0, ..., raw_m-1]
// ^ ^
// 0 filtered_count_
};
```
### Core Methods
```cpp
void Sensor::add_on_state_callback(std::function<void(float)> &&callback) {
if (!this->callbacks_) {
this->callbacks_ = std::make_unique<std::vector<std::function<void(float)>>>();
}
// Add to filtered section: append + swap into position
this->callbacks_->push_back(std::move(callback));
if (this->filtered_count_ < this->callbacks_->size() - 1) {
std::swap((*this->callbacks_)[this->filtered_count_],
(*this->callbacks_)[this->callbacks_->size() - 1]);
}
this->filtered_count_++;
}
void Sensor::add_on_raw_state_callback(std::function<void(float)> &&callback) {
if (!this->callbacks_) {
this->callbacks_ = std::make_unique<std::vector<std::function<void(float)>>>();
}
// Add to raw section: just append (already at end)
this->callbacks_->push_back(std::move(callback));
}
void Sensor::publish_state(float state) {
this->raw_state = state;
// Call raw callbacks (before filters)
if (this->callbacks_) {
for (size_t i = this->filtered_count_; i < this->callbacks_->size(); i++) {
(*this->callbacks_)[i](state);
}
}
ESP_LOGV(TAG, "'%s': Received new state %f", this->name_.c_str(), state);
// ... apply filters ...
}
void Sensor::internal_send_state_to_frontend(float state) {
this->set_has_state(true);
this->state = state;
ESP_LOGD(TAG, "'%s': Sending state %.5f %s with %d decimals of accuracy",
this->get_name().c_str(), state, this->get_unit_of_measurement_ref().c_str(),
this->get_accuracy_decimals());
// Call filtered callbacks (after filters)
if (this->callbacks_) {
for (size_t i = 0; i < this->filtered_count_; i++) {
(*this->callbacks_)[i](state);
}
}
#if defined(USE_SENSOR) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_sensor_update(this);
#endif
}
```
## Memory Analysis (ESP32 32-bit)
### Current Implementation
```cpp
std::unique_ptr<CallbackManager<void(float)>> raw_callback_; // 4 bytes
CallbackManager<void(float)> callback_; // 12 bytes
```
### Partitioned Implementation
```cpp
std::unique_ptr<std::vector<std::function<void(float)>>> callbacks_; // 4 bytes
uint8_t filtered_count_{0}; // 0 bytes (uses existing padding slot)
```
## Memory Comparison
| Scenario | Current | Partitioned | Savings |
|----------|---------|-------------|---------|
| **No callbacks** | 16 bytes | 4 bytes | **+12 bytes** ✅ |
| **1 filtered (MQTT)** | 32 bytes | 32 bytes | **±0 bytes** ✅ |
| **1 raw only** | 44 bytes | 32 bytes | **+12 bytes** ✅ |
| **1 raw + 1 filtered** | 60 bytes | 48 bytes | **+12 bytes** ✅ |
| **2 filtered** | 48 bytes | 48 bytes | **±0 bytes** ✅ |
### Detailed Breakdown
**No callbacks:**
- Current: 4 (raw ptr) + 12 (callback_ vec) = 16 bytes
- Partitioned: 4 (callbacks_ ptr) + 0 (count uses existing padding) = **4 bytes**
- **Saves: 12 bytes** ✅
**1 filtered callback (MQTT):**
- Current: 4 + 12 + 16 (function) = 32 bytes
- Partitioned: 4 (ptr) + 12 (vector on heap) + 16 (function) = **32 bytes**
- **Saves: 0 bytes** (ZERO COST!) ✅
**1 raw + 1 filtered:**
- Current: 4 + 12 + 12 (raw vec on heap) + 16 + 16 = 60 bytes
- Partitioned: 4 + 12 + 16 + 16 = **48 bytes**
- **Saves: 12 bytes** ✅
## Real-World Impact
### Typical IoT Device (15 sensors)
**API-only (no MQTT, no automations):**
- Current: 15 × 16 = 240 bytes
- Optimized: 15 × 4 = 60 bytes
- **Saves: 180 bytes** ✅
**With MQTT on all sensors:**
- Current: 15 × 32 = 480 bytes
- Optimized: 15 × 32 = 480 bytes
- **Saves: 0 bytes** (ZERO COST!) ✅
**Mixed (10 API-only + 5 MQTT):**
- Current: (10 × 16) + (5 × 32) = 320 bytes
- Optimized: (10 × 4) + (5 × 32) = 200 bytes
- **Saves: 120 bytes** ✅
### Large Dashboard (50 sensors)
**API-only:**
- Current: 50 × 16 = 800 bytes
- Optimized: 50 × 4 = 200 bytes
- **Saves: 600 bytes** ✅
**With MQTT on 20 sensors:**
- Current: (30 × 16) + (20 × 32) = 1,120 bytes
- Optimized: (30 × 4) + (20 × 32) = 760 bytes
- **Saves: 360 bytes** ✅
## Performance Characteristics
### Time Complexity
- `add_on_state_callback()`: **O(1)** - append + swap
- `add_on_raw_state_callback()`: **O(1)** - append
- `publish_state()` (call raw): **O(m)** - iterate raw section
- `internal_send_state_to_frontend()` (call filtered): **O(n)** - iterate filtered section
### Hot Path Performance
**Before:**
```cpp
if (this->raw_callback_) {
this->raw_callback_->call(state); // Separate container
}
// ...
this->callback_.call(state); // Separate container
```
**After:**
```cpp
// Call raw callbacks
if (this->callbacks_) {
for (size_t i = filtered_count_; i < callbacks_->size(); i++) {
(*callbacks_)[i](state);
}
}
// ...
// Call filtered callbacks
if (this->callbacks_) {
for (size_t i = 0; i < filtered_count_; i++) {
(*callbacks_)[i](state);
}
}
```
**Performance impact:**
- ✅ Better cache locality (single vector instead of two containers)
- ✅ No branching inside loops (vs checking callback types)
- ✅ Tight loops for typical 0-2 callbacks case
- ⚠️ One extra nullptr check (negligible, likely free with branch prediction)
## Advantages
### Memory
1.**12 bytes saved** per sensor without callbacks (most common after Controller Registry)
2.**ZERO cost** for MQTT-enabled sensors (32 → 32 bytes)
3.**12 bytes saved** for sensors with both raw + filtered callbacks
4.**No padding waste** (reuses existing padding slot in Sensor class)
### Architecture
1.**Cleaner:** ONE vector instead of TWO separate CallbackManager instances
2.**Simpler:** Partitioned vector is more elegant than dual containers
3.**Better cache locality:** Callbacks stored contiguously
4.**O(1) insertion:** Both add operations use append (+ optional swap)
### Code Quality
1.**No new fields in hot path:** filtered_count_ reuses padding
2.**No branching in iteration:** Direct range iteration
3.**Order preservation not needed:** Callbacks are independent
## Implementation Files
### Modified Files
- `esphome/components/sensor/sensor.h`
- `esphome/components/sensor/sensor.cpp`
### Changes Required
1. Replace callback storage with partitioned vector
2. Update `add_on_state_callback()` to use swap-based insertion
3. Update `add_on_raw_state_callback()` to append
4. Update `publish_state()` to iterate raw section
5. Update `internal_send_state_to_frontend()` to iterate filtered section
6. Add `filtered_count_` field (uses existing padding)
## TextSensor Implementation
TextSensor can use the **exact same pattern**:
```cpp
class TextSensor {
protected:
std::unique_ptr<std::vector<std::function<void(std::string)>>> callbacks_;
uint8_t filtered_count_{0}; // Store in class (check for available padding)
};
```
Same benefits apply!
## Migration Risk Assessment
### Low Risk
- ✅ No API changes (public methods unchanged)
- ✅ Callback behavior identical (same execution order within each type)
- ✅ Only internal implementation changes
- ✅ Well-tested pattern (partitioned vectors common in CS)
### Testing Strategy
1. Unit tests: Verify callback execution order preserved
2. Integration tests: Test with MQTT, automations, copy components
3. Memory benchmarks: Confirm actual RAM savings on real devices
4. Regression tests: Ensure no behavior changes for existing configs
## Recommendation
**IMPLEMENT IMMEDIATELY**
This optimization has:
-**Zero cost** for MQTT users (32 → 32 bytes)
-**12-byte savings** for API-only sensors (most common)
-**12-byte savings** for sensors with automations
-**Better architecture** (one container vs two)
-**No downsides** whatsoever
**Expected savings for typical device: 150-600 bytes**
This is a **pure win** optimization with no trade-offs!
## Implementation Priority
### Phase 1: Sensor ⭐⭐⭐ (HIGHEST PRIORITY)
- Most common entity type
- Biggest impact
- Zero cost even for MQTT users
- **Start here!**
### Phase 2: TextSensor ⭐⭐
- Second most common entity with raw callbacks
- Same pattern as Sensor
### Phase 3: Other entities (simple lazy vector) ⭐
- BinarySensor, Switch, etc. don't have raw callbacks
- Can use simpler lazy-allocated vector
- Still save 12 bytes when no callbacks

View File

@@ -0,0 +1,845 @@
# CallbackManager Optimization Plan
**Note:** ESPHome uses C++20 (gnu++20), so implementations leverage modern C++ features:
- **Concepts** for type constraints and better error messages
- **Designated initializers** for cleaner struct initialization
- **consteval** for compile-time validation
- **Requires clauses** for inline constraints
## Current State
### Memory Profile (ESP32 - 32-bit)
```cpp
sizeof(std::function<void(T)>): 32 bytes
sizeof(void*): 4 bytes
sizeof(function pointer): 4 bytes
```
### Current Implementation
```cpp
template<typename... Ts> class CallbackManager<void(Ts...)> {
public:
void add(std::function<void(Ts...)> &&callback) {
this->callbacks_.push_back(std::move(callback));
}
void call(Ts... args) {
for (auto &cb : this->callbacks_)
cb(args...);
}
size_t size() const { return this->callbacks_.size(); }
protected:
std::vector<std::function<void(Ts...)>> callbacks_;
};
```
### Memory Cost Per Instance
- **Per callback:** 32 bytes (std::function storage)
- **Vector reallocation code:** ~132 bytes (`_M_realloc_append` template instantiation)
- **Example (1 callback):** 32 + 132 = 164 bytes
### Codebase Usage
- **Total CallbackManager instances:** ~67 files
- **Estimated total callbacks:** 100-150 across all components
- **Examples:**
- `sensor.h`: `CallbackManager<void(float)>` - multiple callbacks per sensor
- `esp32_ble_tracker.h`: `CallbackManager<void(ScannerState)>` - 1 callback (bluetooth_proxy)
- `esp32_improv.h`: `CallbackManager<void(State, Error)>` - up to 5 callbacks (automation triggers)
- `climate.h`: `CallbackManager<void()>` - multiple callbacks for state/control
### Current Usage Pattern
All callbacks currently use lambda captures:
```cpp
// bluetooth_proxy.cpp
parent_->add_scanner_state_callback([this](ScannerState state) {
if (this->api_connection_ != nullptr) {
this->send_bluetooth_scanner_state_(state);
}
});
// sensor.cpp (via automation)
sensor->add_on_state_callback([this](float state) {
this->trigger(state);
});
```
---
## Optimization Options
### Option 1: Function Pointer + Context (Recommended)
**C++20 Implementation (Type-Safe with Concepts):**
```cpp
#include <concepts>
#include <type_traits>
// Concept to validate callback signature
template<typename F, typename Context, typename... Ts>
concept CallbackFunction = requires(F func, Context* ctx, Ts... args) {
{ func(ctx, args...) } -> std::same_as<void>;
};
template<typename... Ts>
class CallbackManager<void(Ts...)> {
private:
struct Callback {
void (*invoker)(void*, Ts...); // 4 bytes - type-erased invoker
void* context; // 4 bytes - captured context
// Total: 8 bytes
};
// Type-safe invoker template - knows real context type
template<typename Context>
static void invoke(void* ctx, Ts... args) {
auto typed_func = reinterpret_cast<void(*)(Context*, Ts...)>(
*static_cast<void**>(ctx)
);
auto typed_ctx = static_cast<Context*>(
*reinterpret_cast<void**>(static_cast<char*>(ctx) + sizeof(void*))
);
typed_func(typed_ctx, args...);
}
std::vector<Callback> callbacks_;
public:
// Type-safe registration with concept constraint
template<typename Context>
requires CallbackFunction<void(*)(Context*, Ts...), Context, Ts...>
void add(void (*func)(Context*, Ts...), Context* context) {
// Use designated initializers (C++20)
callbacks_.push_back({
.invoker = [](void* storage, Ts... args) {
// Extract function pointer and context from packed storage
void* func_and_ctx[2];
std::memcpy(func_and_ctx, storage, sizeof(func_and_ctx));
auto typed_func = reinterpret_cast<void(*)(Context*, Ts...)>(func_and_ctx[0]);
auto typed_ctx = static_cast<Context*>(func_and_ctx[1]);
typed_func(typed_ctx, args...);
},
.context = nullptr // Will store packed data
});
// Pack function pointer and context into the callback storage
void* func_and_ctx[2] = { reinterpret_cast<void*>(func), context };
std::memcpy(&callbacks_.back(), func_and_ctx, sizeof(func_and_ctx));
}
void call(Ts... args) {
for (auto& cb : callbacks_) {
cb.invoker(&cb, args...);
}
}
constexpr size_t size() const { return callbacks_.size(); }
};
```
**Cleaner C++20 Implementation (12 bytes, simpler):**
```cpp
template<typename... Ts>
class CallbackManager<void(Ts...)> {
private:
struct Callback {
void (*invoker)(void*, void*, Ts...); // 4 bytes - generic invoker
void* func_ptr; // 4 bytes - actual function
void* context; // 4 bytes - context
// Total: 12 bytes (still 20 bytes saved vs std::function!)
};
template<typename Context>
static consteval auto make_invoker() {
return +[](void* func, void* ctx, Ts... args) {
auto typed_func = reinterpret_cast<void(*)(Context*, Ts...)>(func);
typed_func(static_cast<Context*>(ctx), args...);
};
}
std::vector<Callback> callbacks_;
public:
// C++20 concepts for type safety
template<typename Context>
requires std::invocable<void(*)(Context*, Ts...), Context*, Ts...>
void add(void (*func)(Context*, Ts...), Context* context) {
// C++20 designated initializers
callbacks_.push_back({
.invoker = make_invoker<Context>(),
.func_ptr = reinterpret_cast<void*>(func),
.context = context
});
}
void call(Ts... args) {
for (auto& cb : callbacks_) {
cb.invoker(cb.func_ptr, cb.context, args...);
}
}
constexpr size_t size() const { return callbacks_.size(); }
constexpr bool empty() const { return callbacks_.empty(); }
};
```
**Most Efficient C++20 Implementation (8 bytes):**
```cpp
template<typename... Ts>
class CallbackManager<void(Ts...)> {
private:
struct Callback {
void (*invoker)(void*, Ts...); // 4 bytes
void* context; // 4 bytes
// Total: 8 bytes - maximum savings!
};
// C++20: consteval ensures compile-time evaluation
template<typename Context>
static consteval auto make_invoker() {
// The + forces decay to function pointer
return +[](void* ctx, Ts... args) {
// Unpack the storage struct
struct Storage {
void (*func)(Context*, Ts...);
Context* context;
};
auto* storage = static_cast<Storage*>(ctx);
storage->func(storage->context, args...);
};
}
std::vector<Callback> callbacks_;
public:
template<typename Context>
requires std::invocable<void(*)(Context*, Ts...), Context*, Ts...>
void add(void (*func)(Context*, Ts...), Context* context) {
// Allocate storage for function + context
struct Storage {
void (*func)(Context*, Ts...);
Context* context;
};
auto* storage = new Storage{func, context};
callbacks_.push_back({
.invoker = make_invoker<Context>(),
.context = storage
});
}
~CallbackManager() {
// Clean up storage
for (auto& cb : callbacks_) {
delete static_cast<void*>(cb.context);
}
}
void call(Ts... args) {
for (auto& cb : callbacks_) {
cb.invoker(cb.context, args...);
}
}
constexpr size_t size() const { return callbacks_.size(); }
};
```
**Simplest C++20 Implementation (Recommended):**
```cpp
template<typename... Ts>
class CallbackManager<void(Ts...)> {
private:
struct Callback {
void (*invoker)(void*, void*, Ts...); // 4 bytes
void* func_ptr; // 4 bytes
void* context; // 4 bytes
// Total: 12 bytes
};
template<typename Context>
static void invoke(void* func, void* ctx, Ts... args) {
reinterpret_cast<void(*)(Context*, Ts...)>(func)(static_cast<Context*>(ctx), args...);
}
std::vector<Callback> callbacks_;
public:
template<typename Context>
requires std::invocable<void(*)(Context*, Ts...), Context*, Ts...>
void add(void (*func)(Context*, Ts...), Context* context) {
callbacks_.push_back({
.invoker = &invoke<Context>,
.func_ptr = reinterpret_cast<void*>(func),
.context = context
});
}
void call(Ts... args) {
for (auto& cb : callbacks_) {
cb.invoker(cb.func_ptr, cb.context, args...);
}
}
constexpr size_t size() const { return callbacks_.size(); }
};
```
**C++20 Benefits:**
-**Concepts** provide clear compile errors
-**Designated initializers** make code more readable
-**consteval** ensures compile-time evaluation
-**constexpr** improvements allow more compile-time validation
-**Requires clauses** document constraints inline
**Usage Changes:**
```cpp
// OLD (lambda):
parent_->add_scanner_state_callback([this](ScannerState state) {
if (this->api_connection_ != nullptr) {
this->send_bluetooth_scanner_state_(state);
}
});
// NEW (static function + context):
static void scanner_state_callback(BluetoothProxy* proxy, ScannerState state) {
if (proxy->api_connection_ != nullptr) {
proxy->send_bluetooth_scanner_state_(state);
}
}
// Registration
parent_->add_scanner_state_callback(scanner_state_callback, this);
```
**Savings:**
- **Per callback:** 24 bytes (32 → 8) or 20 bytes (32 → 12 for simpler version)
- **RAM saved (100-150 callbacks):** 2.4 - 3.6 KB
- **Flash saved:** ~5-10 KB (eliminates std::function template instantiations)
**Pros:**
- ✅ Maximum memory savings (75% reduction)
- ✅ Type-safe at registration time
- ✅ No virtual function overhead
- ✅ Works with all capture patterns
- ✅ Simple implementation
**Cons:**
- ❌ Requires converting lambdas to static functions
- ❌ Changes API for all 67 CallbackManager users
- ❌ More verbose at call site
---
### Option 2: Member Function Pointers
**Implementation:**
```cpp
template<typename... Ts>
class CallbackManager<void(Ts...)> {
private:
struct Callback {
void (*invoker)(void*, Ts...); // 4 bytes
void* obj; // 4 bytes
// Total: 8 bytes
};
template<typename T, void (T::*Method)(Ts...)>
static void invoke_member(void* obj, Ts... args) {
(static_cast<T*>(obj)->*Method)(args...);
}
std::vector<Callback> callbacks_;
public:
// Register a member function
template<typename T, void (T::*Method)(Ts...)>
void add(T* obj) {
callbacks_.push_back({
&invoke_member<T, Method>,
obj
});
}
void call(Ts... args) {
for (auto& cb : callbacks_) {
cb.invoker(cb.obj, args...);
}
}
size_t size() const { return callbacks_.size(); }
};
```
**Usage Changes:**
```cpp
// Add a method to BluetoothProxy
void BluetoothProxy::on_scanner_state_changed(ScannerState state) {
if (this->api_connection_ != nullptr) {
this->send_bluetooth_scanner_state_(state);
}
}
// Register it
parent_->add_scanner_state_callback<BluetoothProxy,
&BluetoothProxy::on_scanner_state_changed>(this);
```
**Savings:**
- **Per callback:** 24 bytes (32 → 8)
- **RAM saved:** 2.4 - 3.6 KB
- **Flash saved:** ~5-10 KB
**Pros:**
- ✅ Same memory savings as Option 1
- ✅ Most type-safe (member function pointers)
- ✅ No static functions needed
- ✅ Clean separation of callback logic
**Cons:**
- ❌ Verbose syntax at registration: `add<Type, &Type::method>(this)`
- ❌ Requires adding methods to classes
- ❌ Can't capture additional state beyond `this`
- ❌ Template parameters at call site are ugly
---
### Option 3: Hybrid (Backward Compatible)
**Implementation:**
```cpp
template<typename... Ts>
class CallbackManager<void(Ts...)> {
private:
struct Callback {
void (*invoker)(void*, Ts...); // 4 bytes
void* data; // 4 bytes
bool is_std_function; // 1 byte + 3 padding = 4 bytes
// Total: 12 bytes
};
std::vector<Callback> callbacks_;
public:
// Optimized: function pointer + context
template<typename Context>
void add(void (*func)(Context*, Ts...), Context* context) {
callbacks_.push_back({
[](void* ctx, Ts... args) {
auto cb = static_cast<Callback*>(ctx);
auto typed_func = reinterpret_cast<void(*)(Context*, Ts...)>(cb->data);
auto typed_ctx = static_cast<Context*>(*reinterpret_cast<void**>(
static_cast<char*>(cb) + offsetof(Callback, data)
));
typed_func(typed_ctx, args...);
},
reinterpret_cast<void*>(func),
false
});
}
// Legacy: std::function support (for gradual migration)
void add(std::function<void(Ts...)>&& func) {
auto* stored = new std::function<void(Ts...)>(std::move(func));
callbacks_.push_back({
[](void* ctx, Ts... args) {
(*static_cast<std::function<void(Ts...)>*>(ctx))(args...);
},
stored,
true
});
}
~CallbackManager() {
for (auto& cb : callbacks_) {
if (cb.is_std_function) {
delete static_cast<std::function<void(Ts...)>*>(cb.data);
}
}
}
void call(Ts... args) {
for (auto& cb : callbacks_) {
cb.invoker(&cb, args...);
}
}
size_t size() const { return callbacks_.size(); }
};
```
**Usage:**
```cpp
// NEW (optimized):
parent_->add_scanner_state_callback(scanner_state_callback, this);
// OLD (still works - gradual migration):
parent_->add_scanner_state_callback([this](ScannerState state) {
// ... lambda still works
});
```
**Savings:**
- **Per optimized callback:** 20 bytes (32 → 12)
- **Per legacy callback:** 0 bytes (still uses std::function)
- **Allows gradual migration**
**Pros:**
- ✅ Backward compatible
- ✅ Gradual migration path
- ✅ Mix optimized and legacy in same codebase
- ✅ No breaking changes
**Cons:**
- ❌ More complex implementation
- ❌ Need to track which callbacks need cleanup
- ❌ Extra bool field (padding makes it 12 bytes instead of 8)
- ❌ std::function still compiled in
---
### Option 4: FixedVector (Keep std::function, Optimize Vector)
**Implementation:**
```cpp
template<typename... Ts>
class CallbackManager<void(Ts...)> {
public:
void add(std::function<void(Ts...)> &&callback) {
if (this->callbacks_.empty()) {
// Most CallbackManagers have 1-5 callbacks
this->callbacks_.init(5);
}
this->callbacks_.push_back(std::move(callback));
}
void call(Ts... args) {
for (auto &cb : this->callbacks_)
cb(args...);
}
size_t size() const { return this->callbacks_.size(); }
protected:
FixedVector<std::function<void(Ts...)>> callbacks_; // Changed from std::vector
};
```
**Savings:**
- **Per callback:** 0 bytes (still 32 bytes)
- **Per instance:** ~132 bytes (eliminates `_M_realloc_append`)
- **Flash saved:** ~5-10 KB (one less vector template instantiation per type)
- **Total:** ~132 bytes × ~20 unique callback types = ~2.6 KB
**Pros:**
- ✅ No API changes
- ✅ Drop-in replacement
- ✅ Eliminates vector reallocation machinery
- ✅ Zero migration cost
**Cons:**
- ❌ No per-callback savings
- ❌ std::function still 32 bytes each
- ❌ Must guess max size at runtime
- ❌ Can still overflow if guess is wrong
---
### Option 5: Template Parameter for Storage (Advanced)
**Implementation:**
```cpp
enum class CallbackStorage {
FUNCTION, // Use std::function (default, most flexible)
FUNCTION_PTR // Use function pointer + context (optimal)
};
template<typename... Ts, CallbackStorage Storage = CallbackStorage::FUNCTION>
class CallbackManager<void(Ts...)> {
// Specialize implementation based on Storage parameter
};
// Default: std::function (backward compatible)
template<typename... Ts>
class CallbackManager<void(Ts...), CallbackStorage::FUNCTION> {
protected:
std::vector<std::function<void(Ts...)>> callbacks_;
// ... current implementation
};
// Optimized: function pointer + context
template<typename... Ts>
class CallbackManager<void(Ts...), CallbackStorage::FUNCTION_PTR> {
private:
struct Callback {
void (*func)(void*, Ts...);
void* context;
};
std::vector<Callback> callbacks_;
// ... Option 1 implementation
};
```
**Usage:**
```cpp
// Old components (no changes):
CallbackManager<void(float)> callback_; // Uses std::function by default
// Optimized components:
CallbackManager<void(ScannerState), CallbackStorage::FUNCTION_PTR> scanner_state_callbacks_;
```
**Savings:**
- **Opt-in per component**
- **Same as Option 1 for optimized components**
**Pros:**
- ✅ Gradual migration
- ✅ No breaking changes
- ✅ Explicit opt-in per component
- ✅ Clear which components are optimized
**Cons:**
- ❌ Complex template metaprogramming
- ❌ Two implementations to maintain
- ❌ Template parameter pollution
- ❌ Harder to understand codebase
---
## Comparison Matrix
| Option | Per-Callback Savings | Flash Savings | API Changes | Complexity | Migration Cost |
|--------|---------------------|---------------|-------------|------------|----------------|
| **1. Function Ptr + Context** | **24 bytes** (75%) | **~10 KB** | Yes | Low | High (67 files) |
| **2. Member Function Ptrs** | **24 bytes** (75%) | **~10 KB** | Yes | Medium | High + class changes |
| **3. Hybrid** | **20 bytes** (opt-in) | **~8 KB** | No | High | Low (gradual) |
| **4. FixedVector** | **0 bytes** | **~3 KB** | No | Low | None |
| **5. Template Parameter** | **24 bytes** (opt-in) | **~10 KB** | Optional | High | Medium |
---
## Migration Effort Estimate
### Option 1 (Function Pointer + Context)
**Files to change:** ~67 files with CallbackManager usage
**Per-file changes:**
1. Convert lambda to static function (5 min)
2. Update registration call (1 min)
3. Test (5 min)
**Estimate:** ~11 min × 67 files = **~12 hours** (assuming some files have multiple callbacks)
**High-impact components to prioritize:**
- `sensor.h` / `sensor.cpp` - many sensor callbacks
- `esp32_ble_tracker.h` - BLE callbacks
- `climate.h` - climate callbacks
- `binary_sensor.h` - binary sensor callbacks
### Option 4 (FixedVector)
**Files to change:** 1 file (`esphome/core/helpers.h`)
**Changes:**
1. Change `std::vector` to `FixedVector` in CallbackManager
2. Initialize with reasonable default size (e.g., 5)
3. Test across codebase
**Estimate:** **~1 hour**
---
## Recommendations
### Immediate Action: Option 4 (FixedVector)
**Why:**
- Zero migration cost
- Immediate ~3 KB flash savings
- No API changes
- Low risk
**Implementation:**
```cpp
template<typename... Ts> class CallbackManager<void(Ts...)> {
public:
void add(std::function<void(Ts...)> &&callback) {
if (this->callbacks_.empty()) {
this->callbacks_.init(8); // Most have < 8 callbacks
}
this->callbacks_.push_back(std::move(callback));
}
// ... rest unchanged
protected:
FixedVector<std::function<void(Ts...)>> callbacks_;
};
```
### Long-term: Option 1 (Function Pointer + Context)
**Why:**
- Maximum savings (2.4-3.6 KB RAM + 10 KB flash)
- Clean, simple implementation
- Type-safe
- Well-tested pattern
**Migration Strategy:**
1. Implement new `CallbackManager` in `helpers.h`
2. Migrate high-impact components first:
- Core components (sensor, binary_sensor, climate)
- BLE components (esp32_ble_tracker, bluetooth_proxy)
- Network components (api, mqtt)
3. Create helper macros to reduce boilerplate
4. Migrate remaining components over 2-3 releases
**Helper Macro Example:**
```cpp
// Define a callback wrapper
#define CALLBACK_WRAPPER(Class, Method, ...) \
static void Method##_callback(Class* self, ##__VA_ARGS__) { \
self->Method(__VA_ARGS__); \
}
// In class:
class BluetoothProxy {
CALLBACK_WRAPPER(BluetoothProxy, on_scanner_state, ScannerState state)
void on_scanner_state(ScannerState state) {
// Implementation
}
void setup() {
parent_->add_scanner_state_callback(on_scanner_state_callback, this);
}
};
```
---
## Testing Plan
### Phase 1: Unit Tests
- Test CallbackManager with various signatures
- Test multiple callbacks (1, 5, 10, 50)
- Test callback removal/cancellation
- Test edge cases (empty, nullptr, etc.)
### Phase 2: Integration Tests
- Create test YAML with heavily-used callbacks
- Run on ESP32, ESP8266, RP2040
- Measure before/after memory usage
- Verify no functional regressions
### Phase 3: Component Tests
- Test high-impact components:
- sensor with multiple state callbacks
- esp32_improv with all automation triggers
- climate with state/control callbacks
- Measure memory with `esphome analyze-memory`
---
## Risk Analysis
### Option 1 Risks
**Risk: Breaking change across 67 files**
- **Mitigation:** Gradual rollout over 2-3 releases
- **Mitigation:** Extensive testing on real hardware
**Risk: Static function verbosity**
- **Mitigation:** Helper macros (see above)
- **Mitigation:** Code generation from Python
**Risk: Missing captures**
- **Mitigation:** Static analysis to find lambda captures
- **Mitigation:** Compile-time errors for incorrect usage
### Option 4 Risks
**Risk: Buffer overflow if size guess is wrong**
- **Mitigation:** Choose conservative default (8)
- **Mitigation:** Add runtime warning on resize
- **Mitigation:** Monitor in CI/testing
**Risk: Still uses std::function (32 bytes each)**
- **Mitigation:** Follow up with Option 1 migration
- **Mitigation:** This is a stepping stone, not final solution
---
## Implementation Timeline
### Week 1: Option 4 (Quick Win)
- Implement FixedVector in CallbackManager
- Test across codebase
- Create PR with memory analysis
- **Expected savings:** ~3 KB flash
### Month 1-2: Option 1 (Core Components)
- Implement function pointer CallbackManager
- Migrate sensor, binary_sensor, climate
- Create helper macros
- **Expected savings:** ~1 KB RAM + 5 KB flash
### Month 3-4: Option 1 (Remaining Components)
- Migrate BLE components
- Migrate network components (api, mqtt)
- Migrate automation components
- **Expected savings:** ~2 KB RAM + 10 KB flash total
### Month 5: Cleanup
- Remove std::function CallbackManager
- Update documentation
- Blog post about optimization
---
## Conclusion
**Recommended Approach:**
1. **Immediate (Week 1):** Implement Option 4 (FixedVector)
- Low risk, zero migration cost
- ~3 KB flash savings
- Sets foundation for Option 1
2. **Short-term (Month 1-2):** Begin Option 1 migration
- Start with high-impact components
- ~1-2 KB RAM + 5 KB flash savings
- Validate approach
3. **Long-term (Month 3-6):** Complete Option 1 migration
- Migrate all components
- ~3-4 KB total RAM + 10 KB flash savings
- Remove std::function variant
**Total Expected Savings:**
- **RAM:** 2.4 - 3.6 KB (75% reduction per callback)
- **Flash:** 8 - 13 KB (vector overhead + template instantiations)
- **Performance:** Slightly faster (no std::function indirection)
This is significant for ESP8266 (80 KB RAM, 1 MB flash) and beneficial for all platforms.

View File

@@ -0,0 +1,75 @@
# Callback Optimization Analysis - Why It Failed
## Goal
Convert stateful lambdas in CallbackManager to stateless function pointers to reduce flash usage.
## Approach Tested
### Attempt 1: Discriminated Union in CallbackManager
**Changed:** `CallbackManager` to use union with discriminator (like `TemplatableValue`)
- Stateless lambdas → function pointer (8 bytes)
- Stateful lambdas → heap-allocated `std::function*` (8 bytes struct + 32 bytes heap)
**Result:**
-**+300 bytes heap usage** (37-38 callbacks × 8 bytes overhead)
- ✅ Flash savings potential: ~200-400 bytes per stateless callback
- **Verdict:** RAM is more precious than flash on ESP8266 - rejected
### Attempt 2: Convert Individual Callbacks to Stateless
**Changed:** API logger callback from `[this]` lambda to static member function
- Used existing `global_api_server` pointer
- Made callback stateless (convertible to function pointer)
**Result:**
```
Removed:
- Lambda _M_invoke: 103 bytes
- Lambda _M_manager: 20 bytes
Added:
- log_callback function: 104 bytes
- Function pointer _M_invoke: 20 bytes
- Function pointer _M_manager: 20 bytes
- Larger setup(): 7 bytes
Net: +32 bytes flash ❌
```
**Why it failed:**
Even though the callback became stateless, `CallbackManager` still uses `std::vector<std::function<void(Ts...)>>`. The function pointer STILL gets wrapped in `std::function`, generating the same template instantiation overhead. We just moved the code from a lambda to a static function.
## Root Cause
The optimization **requires BOTH**:
1. ✅ Stateless callback (function pointer)
2. ❌ Modified `CallbackManager` to store function pointers directly without `std::function` wrapper
Without modifying `CallbackManager`, converting individual callbacks to function pointers provides **no benefit** and actually **increases** code size slightly due to the extra function definition.
## Conclusion
This optimization path is a **dead end** for ESPHome because:
1. **Discriminated union approach**: Increases heap by 300 bytes (unacceptable for ESP8266)
2. **Individual callback conversion**: Increases flash by 32+ bytes (no benefit without CallbackManager changes)
The current `std::vector<std::function<...>>` approach is already optimal for the use case where most callbacks capture state.
## Alternative Approaches Considered
1. **Create separate `StatelessCallbackManager`**: Would require changing all call sites, not worth the complexity
2. **Template parameter to select storage type**: Same issue - requires modifying many components
3. **Hand-pick specific callbacks**: Provides no benefit as shown in Attempt 2
## Recommendation
**Do not pursue this optimization.** The RAM/flash trade-offs are unfavorable for embedded systems where RAM is typically more constrained than flash.
---
**Test Results:**
- Platform: ESP8266-Arduino
- Component: API
- Result: +32 bytes flash (0.01% increase)
- Status: Reverted
🤖 Analysis by Claude Code

View File

@@ -0,0 +1,256 @@
# Callback Optimization Implementation Plan
## Analysis Summary
After Controller Registry (PR #11772), callback infrastructure can be further optimized:
**Current overhead per entity (ESP32 32-bit):**
- No callbacks: 16 bytes (4-byte ptr + 12-byte empty vector)
- With callbacks: 32+ bytes (16 baseline + 16+ per callback)
**Opportunity:** After Controller Registry, most entities have **zero callbacks** (API/WebServer use registry instead). We can save 12 bytes per entity by lazy allocation.
## Entity Types by Callback Needs
### Entities with ONLY filtered callbacks (most)
- Climate, Fan, Light, Cover
- Switch, Lock, Valve
- Number, Select, Text, Button
- AlarmControlPanel, MediaPlayer
- BinarySensor, Event, Update, DateTime
**Optimization:** Simple lazy-allocated vector
### Entities with raw AND filtered callbacks
- **Sensor** - has raw callbacks for automation triggers
- **TextSensor** - has raw callbacks for automation triggers
**Optimization:** Partitioned vector (filtered | raw)
## Proposed Implementations
### Option 1: Simple Lazy Vector (for entities without raw callbacks)
```cpp
class Climate {
protected:
std::unique_ptr<std::vector<std::function<void(Climate&)>>> state_callback_;
};
void Climate::add_on_state_callback(std::function<void(Climate&)> &&callback) {
if (!this->state_callback_) {
this->state_callback_ = std::make_unique<std::vector<std::function<void(Climate&)>>>();
}
this->state_callback_->push_back(std::move(callback));
}
void Climate::publish_state() {
if (this->state_callback_) {
for (auto &cb : *this->state_callback_) {
cb(*this);
}
}
}
```
**Memory (ESP32):**
- No callbacks: 4 bytes (saves 12 vs current)
- 1 callback: 36 bytes (costs 4 vs current)
- Net: Positive for API-only devices
### Option 2: Partitioned Vector (for Sensor & TextSensor)
```cpp
class Sensor {
protected:
struct Callbacks {
std::vector<std::function<void(float)>> callbacks_;
uint8_t filtered_count_{0}; // Partition point: [filtered | raw]
void add_filtered(std::function<void(float)> &&fn) {
callbacks_.push_back(std::move(fn));
if (filtered_count_ < callbacks_.size() - 1) {
std::swap(callbacks_[filtered_count_], callbacks_[callbacks_.size() - 1]);
}
filtered_count_++;
}
void add_raw(std::function<void(float)> &&fn) {
callbacks_.push_back(std::move(fn)); // Append to raw section
}
void call_filtered(float value) {
for (size_t i = 0; i < filtered_count_; i++) {
callbacks_[i](value);
}
}
void call_raw(float value) {
for (size_t i = filtered_count_; i < callbacks_.size(); i++) {
callbacks_[i](value);
}
}
};
std::unique_ptr<Callbacks> callbacks_;
};
```
**Why partitioned:**
- Maintains separation of raw (pre-filter) vs filtered (post-filter) callbacks
- O(1) insertion via swap (order doesn't matter)
- No branching in hot path
- Saves 12 bytes when no callbacks
## Memory Impact Analysis
### Scenario 1: API-only device (10 sensors, no MQTT, no automations)
**Current:** 10 × 16 = 160 bytes
**Optimized:** 10 × 4 = 40 bytes
**Saves: 120 bytes**
### Scenario 2: MQTT-enabled device (10 sensors with MQTT)
**Current:** 10 × 32 = 320 bytes
**Optimized:** 10 × 36 = 360 bytes
**Costs: 40 bytes** ⚠️
### Scenario 3: Mixed device (5 API-only + 5 MQTT)
**Current:** (5 × 16) + (5 × 32) = 240 bytes
**Optimized:** (5 × 4) + (5 × 36) = 200 bytes
**Saves: 40 bytes**
### Scenario 4: Sensor with automation (1 raw + 1 filtered)
**Current:** 16 + 12 + 16 + 16 = 60 bytes
**Optimized:** 4 + 16 + 32 = 52 bytes
**Saves: 8 bytes**
## Implementation Strategy
### Phase 1: Simple Entities (high impact, low complexity)
1. **Climate** (common, no raw callbacks)
2. **Fan** (common, no raw callbacks)
3. **Cover** (common, no raw callbacks)
4. **Switch** (very common, no raw callbacks)
5. **Lock** (no raw callbacks)
**Change:** Replace `CallbackManager<void(...)> callback_` with `std::unique_ptr<std::vector<std::function<...>>>`
### Phase 2: Sensor & TextSensor (more complex)
1. **Sensor** (most common entity, has raw callbacks)
2. **TextSensor** (common, has raw callbacks)
**Change:** Implement partitioned vector approach
### Phase 3: Remaining Entities
- BinarySensor, Number, Select, Text
- Light, Valve, AlarmControlPanel
- MediaPlayer, Button, Event, Update, DateTime
**Change:** Simple lazy vector
## Code Template for Simple Entities
```cpp
// Header (.h)
class EntityType {
public:
void add_on_state_callback(std::function<void(Args...)> &&callback);
protected:
std::unique_ptr<std::vector<std::function<void(Args...)>>> state_callback_;
};
// Implementation (.cpp)
void EntityType::add_on_state_callback(std::function<void(Args...)> &&callback) {
if (!this->state_callback_) {
this->state_callback_ = std::make_unique<std::vector<std::function<void(Args...)>>>();
}
this->state_callback_->push_back(std::move(callback));
}
void EntityType::publish_state(...) {
// ... state update logic ...
if (this->state_callback_) {
for (auto &cb : *this->state_callback_) {
cb(...);
}
}
#ifdef USE_CONTROLLER_REGISTRY
ControllerRegistry::notify_entity_update(this);
#endif
}
```
## Testing Strategy
1. **Unit tests:** Verify callback ordering/execution unchanged
2. **Integration tests:** Test with MQTT, automations, copy components
3. **Memory benchmarks:** Measure actual flash/RAM impact
4. **Compatibility:** Ensure no API breakage
## Expected Results
**For typical ESPHome devices after Controller Registry:**
- Most entities: API/WebServer only (no callbacks)
- Some entities: MQTT (1 callback)
- Few entities: Automations (1-2 callbacks)
**Memory savings:**
- Device with 20 entities, 5 with MQTT: ~180 bytes saved
- Device with 50 entities, 10 with MQTT: ~480 bytes saved
**Trade-off:**
- Entities without callbacks: Save 12 bytes ✅
- Entities with callbacks: Cost 4 bytes ⚠️
- Net benefit: Positive for most devices
## Risks & Mitigation
**Risk 1:** Increased complexity
- **Mitigation:** Start with simple entities first, template for reuse
**Risk 2:** Performance regression
- **Mitigation:** Minimal - just nullptr check (likely free with branch prediction)
**Risk 3:** Edge cases with callback order
- **Mitigation:** Order already undefined within same callback type
## Open Questions
1. Should we template the Callbacks struct for reuse across entity types?
2. Should Phase 1 include a memory benchmark before expanding?
3. Should we make this configurable (compile-time flag)?
## Files Modified
### Phase 1 (Simple Entities)
- `esphome/components/climate/climate.h`
- `esphome/components/climate/climate.cpp`
- `esphome/components/fan/fan.h`
- `esphome/components/fan/fan.cpp`
- `esphome/components/cover/cover.h`
- `esphome/components/cover/cover.cpp`
- (etc. for switch, lock)
### Phase 2 (Partitioned)
- `esphome/components/sensor/sensor.h`
- `esphome/components/sensor/sensor.cpp`
- `esphome/components/text_sensor/text_sensor.h`
- `esphome/components/text_sensor/text_sensor.cpp`
### Phase 3 (Remaining)
- All other entity types
## Conclusion
**Recommendation: Implement in phases**
1. Start with Climate (common entity, simple change)
2. Measure impact on real device
3. If positive, proceed with other simple entities
4. Implement partitioned approach for Sensor/TextSensor
5. Complete remaining entity types
Expected net savings: **50-500 bytes per typical device**, depending on entity count and MQTT usage.

118
callback_usage_analysis.md Normal file
View File

@@ -0,0 +1,118 @@
# add_on_state_callback Usage Analysis
## Summary
After the Controller Registry migration (PR #11772), `add_on_state_callback` is still widely used in the codebase, but for **legitimate reasons** - components that genuinely need per-entity state tracking.
## Usage Breakdown
### 1. **MQTT Components** (~17 uses)
**Purpose:** Per-entity MQTT configuration requires callbacks
- Each MQTT component instance needs to publish to custom topics with custom QoS/retain settings
- Cannot use Controller pattern due to per-entity configuration overhead
- Examples: `mqtt_sensor.cpp`, `mqtt_climate.cpp`, `mqtt_number.cpp`, etc.
```cpp
this->sensor_->add_on_state_callback([this](float state) {
this->publish_state(state);
});
```
### 2. **Copy Components** (~10 uses)
**Purpose:** Mirror state from one entity to another
- Each copy instance tracks a different source entity
- Legitimate use of callbacks for entity-to-entity synchronization
- Examples: `copy_sensor.cpp`, `copy_fan.cpp`, `copy_select.cpp`, etc.
```cpp
source_->add_on_state_callback([this](const std::string &value) {
this->publish_state(value);
});
```
### 3. **Derivative Sensors** (~5-7 uses)
**Purpose:** Compute derived values from source sensors
- **integration_sensor:** Integrates sensor values over time
- **total_daily_energy:** Tracks cumulative energy
- **combination:** Combines multiple sensor values
- **graph:** Samples sensor data for display
- **duty_time:** Tracks on-time duration
- **ntc/absolute_humidity/resistance:** Mathematical transformations
```cpp
this->sensor_->add_on_state_callback([this](float state) {
this->process_sensor_value_(state);
});
```
### 4. **Climate/Cover with Sensors** (~10-15 uses)
**Purpose:** External sensors providing feedback to control loops
- **feedback_cover:** Binary sensors for open/close/obstacle detection
- **bang_bang/pid/thermostat:** External temperature sensors for climate control
- **climate_ir (toshiba/yashima/heatpumpir):** Temperature sensors for IR climate
```cpp
this->sensor_->add_on_state_callback([this](float state) {
this->current_temperature = state;
// Trigger control loop update
});
```
### 5. **Entity Base Classes** (~10-15 definitions)
**Purpose:** Provide the callback interface for all entities
- Not actual usage, just the method definitions
- Examples: `sensor.cpp::add_on_state_callback()`, `climate.cpp::add_on_state_callback()`, etc.
### 6. **Automation Trigger Classes** (~15-20 definitions)
**Purpose:** User-defined YAML automations need callbacks
- Files like `sensor/automation.h`, `climate/automation.h`
- Implement triggers like `on_value:`, `on_state:`
- Cannot be migrated - this is user-facing automation functionality
### 7. **Miscellaneous** (~5-10 uses)
- **voice_assistant/micro_wake_word:** State coordination
- **esp32_improv:** Provisioning state tracking
- **http_request/update:** Update status monitoring
- **switch/binary_sensor:** Cross-component dependencies
- **OTA callbacks:** OTA state monitoring
## Key Insights
### What's NOT Using Callbacks Anymore ✅
**API Server and WebServer** - migrated to Controller Registry
- **Before:** Each entity had 2 callbacks (API + WebServer) = ~32 bytes overhead
- **After:** Zero per-entity overhead = saves ~32 bytes per entity
### What SHOULD Keep Using Callbacks ✅
All the above categories have legitimate reasons:
1. **Per-entity configuration:** MQTT needs custom topics/QoS per entity
2. **Entity-to-entity relationships:** Copy components, derivative sensors
3. **Control loop feedback:** Climate/cover with external sensors
4. **User-defined automations:** YAML triggers configured by users
5. **Component dependencies:** Components that genuinely depend on other entities
## Memory Impact
**Per Sensor (ESP32):**
- Empty callback infrastructure: **~16 bytes** (unique_ptr + empty vector)
- With one callback (e.g., MQTT): **~32 bytes** (16 + std::function)
- With multiple callbacks: **~32 + 16n bytes** (where n = additional callbacks)
**Typical scenarios:**
- Sensor with **only API/WebServer:** ~16 bytes (no callbacks registered)
- Sensor with **MQTT:** ~32 bytes (one callback)
- Sensor with **MQTT + automation:** ~48 bytes (two callbacks)
- Sensor with **copy + total_daily_energy + graph:** ~64 bytes (three callbacks)
## Conclusion
The callback system is still heavily used (~103 occurrences) but for **appropriate reasons**:
- Components with per-entity state/configuration (MQTT, Copy)
- Sensor processing chains (derivatives, transformations)
- Control loops with external feedback (climate, covers)
- User-defined automations (cannot be removed)
The Controller Registry successfully eliminated wasteful callbacks for **stateless global handlers** (API/WebServer), saving ~32 bytes per entity for those use cases.
**No further callback elimination opportunities** exist without fundamentally changing ESPHome's architecture or breaking user-facing features.

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,7 +15,7 @@ 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 ( from esphome.cpp_generator import (
LambdaExpression, LambdaExpression,
MockObj, MockObj,
@@ -182,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
@@ -310,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)
) )

View File

@@ -105,7 +105,7 @@ template<typename... Ts> class AGS10NewI2cAddressAction : public Action<Ts...>,
public: public:
TEMPLATABLE_VALUE(uint8_t, new_address) TEMPLATABLE_VALUE(uint8_t, new_address)
void play(Ts... x) override { this->parent_->new_i2c_address(this->new_address_.value(x...)); } void play(const Ts &...x) override { this->parent_->new_i2c_address(this->new_address_.value(x...)); }
}; };
enum AGS10SetZeroPointActionMode { enum AGS10SetZeroPointActionMode {
@@ -122,7 +122,7 @@ template<typename... Ts> class AGS10SetZeroPointAction : public Action<Ts...>, p
TEMPLATABLE_VALUE(uint16_t, value) TEMPLATABLE_VALUE(uint16_t, value)
TEMPLATABLE_VALUE(AGS10SetZeroPointActionMode, mode) TEMPLATABLE_VALUE(AGS10SetZeroPointActionMode, mode)
void play(Ts... x) override { void play(const Ts &...x) override {
switch (this->mode_.value(x...)) { switch (this->mode_.value(x...)) {
case FACTORY_DEFAULT: case FACTORY_DEFAULT:
this->parent_->set_zero_point_with_factory_defaults(); this->parent_->set_zero_point_with_factory_defaults();

View File

@@ -13,7 +13,7 @@ template<typename... Ts> class SetAutoMuteAction : public Action<Ts...> {
TEMPLATABLE_VALUE(uint8_t, auto_mute_mode) TEMPLATABLE_VALUE(uint8_t, auto_mute_mode)
void play(Ts... x) override { this->aic3204_->set_auto_mute_mode(this->auto_mute_mode_.value(x...)); } void play(const Ts &...x) override { this->aic3204_->set_auto_mute_mode(this->auto_mute_mode_.value(x...)); }
protected: protected:
AIC3204 *aic3204_; AIC3204 *aic3204_;

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

@@ -1,6 +1,8 @@
#include <utility>
#include "alarm_control_panel.h" #include "alarm_control_panel.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include <utility>
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
@@ -34,6 +36,9 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) {
LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state))); LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state)));
this->current_state_ = state; this->current_state_ = state;
this->state_callback_.call(); this->state_callback_.call();
#if defined(USE_ALARM_CONTROL_PANEL) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_alarm_control_panel_update(this);
#endif
if (state == ACP_STATE_TRIGGERED) { if (state == ACP_STATE_TRIGGERED) {
this->triggered_callback_.call(); this->triggered_callback_.call();
} else if (state == ACP_STATE_ARMING) { } else if (state == ACP_STATE_ARMING) {

View File

@@ -89,7 +89,7 @@ template<typename... Ts> class ArmAwayAction : public Action<Ts...> {
TEMPLATABLE_VALUE(std::string, code) TEMPLATABLE_VALUE(std::string, code)
void play(Ts... x) override { void play(const Ts &...x) override {
auto call = this->alarm_control_panel_->make_call(); auto call = this->alarm_control_panel_->make_call();
auto code = this->code_.optional_value(x...); auto code = this->code_.optional_value(x...);
if (code.has_value()) { if (code.has_value()) {
@@ -109,7 +109,7 @@ template<typename... Ts> class ArmHomeAction : public Action<Ts...> {
TEMPLATABLE_VALUE(std::string, code) TEMPLATABLE_VALUE(std::string, code)
void play(Ts... x) override { void play(const Ts &...x) override {
auto call = this->alarm_control_panel_->make_call(); auto call = this->alarm_control_panel_->make_call();
auto code = this->code_.optional_value(x...); auto code = this->code_.optional_value(x...);
if (code.has_value()) { if (code.has_value()) {
@@ -129,7 +129,7 @@ template<typename... Ts> class ArmNightAction : public Action<Ts...> {
TEMPLATABLE_VALUE(std::string, code) TEMPLATABLE_VALUE(std::string, code)
void play(Ts... x) override { void play(const Ts &...x) override {
auto call = this->alarm_control_panel_->make_call(); auto call = this->alarm_control_panel_->make_call();
auto code = this->code_.optional_value(x...); auto code = this->code_.optional_value(x...);
if (code.has_value()) { if (code.has_value()) {
@@ -149,7 +149,7 @@ template<typename... Ts> class DisarmAction : public Action<Ts...> {
TEMPLATABLE_VALUE(std::string, code) TEMPLATABLE_VALUE(std::string, code)
void play(Ts... x) override { this->alarm_control_panel_->disarm(this->code_.optional_value(x...)); } void play(const Ts &...x) override { this->alarm_control_panel_->disarm(this->code_.optional_value(x...)); }
protected: protected:
AlarmControlPanel *alarm_control_panel_; AlarmControlPanel *alarm_control_panel_;
@@ -159,7 +159,7 @@ template<typename... Ts> class PendingAction : public Action<Ts...> {
public: public:
explicit PendingAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} explicit PendingAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {}
void play(Ts... x) override { this->alarm_control_panel_->make_call().pending().perform(); } void play(const Ts &...x) override { this->alarm_control_panel_->make_call().pending().perform(); }
protected: protected:
AlarmControlPanel *alarm_control_panel_; AlarmControlPanel *alarm_control_panel_;
@@ -169,7 +169,7 @@ template<typename... Ts> class TriggeredAction : public Action<Ts...> {
public: public:
explicit TriggeredAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} explicit TriggeredAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {}
void play(Ts... x) override { this->alarm_control_panel_->make_call().triggered().perform(); } void play(const Ts &...x) override { this->alarm_control_panel_->make_call().triggered().perform(); }
protected: protected:
AlarmControlPanel *alarm_control_panel_; AlarmControlPanel *alarm_control_panel_;
@@ -178,7 +178,7 @@ template<typename... Ts> class TriggeredAction : public Action<Ts...> {
template<typename... Ts> class AlarmControlPanelCondition : public Condition<Ts...> { template<typename... Ts> class AlarmControlPanelCondition : public Condition<Ts...> {
public: public:
AlarmControlPanelCondition(AlarmControlPanel *parent) : parent_(parent) {} AlarmControlPanelCondition(AlarmControlPanel *parent) : parent_(parent) {}
bool check(Ts... x) override { bool check(const Ts &...x) override {
return this->parent_->is_state_armed(this->parent_->get_state()) || return this->parent_->is_state_armed(this->parent_->get_state()) ||
this->parent_->get_state() == ACP_STATE_PENDING || this->parent_->get_state() == ACP_STATE_TRIGGERED; this->parent_->get_state() == ACP_STATE_PENDING || this->parent_->get_state() == ACP_STATE_TRIGGERED;
} }

View File

@@ -39,7 +39,7 @@ class Animation : public image::Image {
template<typename... Ts> class AnimationNextFrameAction : public Action<Ts...> { template<typename... Ts> class AnimationNextFrameAction : public Action<Ts...> {
public: public:
AnimationNextFrameAction(Animation *parent) : parent_(parent) {} AnimationNextFrameAction(Animation *parent) : parent_(parent) {}
void play(Ts... x) override { this->parent_->next_frame(); } void play(const Ts &...x) override { this->parent_->next_frame(); }
protected: protected:
Animation *parent_; Animation *parent_;
@@ -48,7 +48,7 @@ template<typename... Ts> class AnimationNextFrameAction : public Action<Ts...> {
template<typename... Ts> class AnimationPrevFrameAction : public Action<Ts...> { template<typename... Ts> class AnimationPrevFrameAction : public Action<Ts...> {
public: public:
AnimationPrevFrameAction(Animation *parent) : parent_(parent) {} AnimationPrevFrameAction(Animation *parent) : parent_(parent) {}
void play(Ts... x) override { this->parent_->prev_frame(); } void play(const Ts &...x) override { this->parent_->prev_frame(); }
protected: protected:
Animation *parent_; Animation *parent_;
@@ -58,7 +58,7 @@ template<typename... Ts> class AnimationSetFrameAction : public Action<Ts...> {
public: public:
AnimationSetFrameAction(Animation *parent) : parent_(parent) {} AnimationSetFrameAction(Animation *parent) : parent_(parent) {}
TEMPLATABLE_VALUE(uint16_t, frame) TEMPLATABLE_VALUE(uint16_t, frame)
void play(Ts... x) override { this->parent_->set_frame(this->frame_.value(x...)); } void play(const Ts &...x) override { this->parent_->set_frame(this->frame_.value(x...)); }
protected: protected:
Animation *parent_; Animation *parent_;

View File

@@ -244,6 +244,9 @@ 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)
# Track controller registration for StaticVector sizing
CORE.register_controller()
cg.add(var.set_port(config[CONF_PORT])) cg.add(var.set_port(config[CONF_PORT]))
if config[CONF_PASSWORD]: if config[CONF_PASSWORD]:
cg.add_define("USE_API_PASSWORD") cg.add_define("USE_API_PASSWORD")

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"];
@@ -2147,7 +2147,7 @@ message ListEntitiesEventResponse {
EntityCategory entity_category = 7; EntityCategory entity_category = 7;
string device_class = 8; string device_class = 8;
repeated string event_types = 9; repeated string event_types = 9 [(container_pointer_no_template) = "FixedVector<const char *>"];
uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"]; uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"];
} }
message EventResponse { message EventResponse {

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);
} }
@@ -1310,8 +1310,7 @@ uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *c
auto *event = static_cast<event::Event *>(entity); auto *event = static_cast<event::Event *>(entity);
ListEntitiesEventResponse msg; ListEntitiesEventResponse msg;
msg.set_device_class(event->get_device_class_ref()); msg.set_device_class(event->get_device_class_ref());
for (const auto &event_type : event->get_event_types()) msg.event_types = &event->get_event_types();
msg.event_types.push_back(event_type);
return fill_and_encode_entity_info(event, msg, ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size, return fill_and_encode_entity_info(event, msg, ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size,
is_single); 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);
@@ -2877,8 +2877,8 @@ void ListEntitiesEventResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_bool(6, this->disabled_by_default); buffer.encode_bool(6, this->disabled_by_default);
buffer.encode_uint32(7, static_cast<uint32_t>(this->entity_category)); buffer.encode_uint32(7, static_cast<uint32_t>(this->entity_category));
buffer.encode_string(8, this->device_class_ref_); buffer.encode_string(8, this->device_class_ref_);
for (auto &it : this->event_types) { for (const char *it : *this->event_types) {
buffer.encode_string(9, it, true); buffer.encode_string(9, it, strlen(it), true);
} }
#ifdef USE_DEVICES #ifdef USE_DEVICES
buffer.encode_uint32(10, this->device_id); buffer.encode_uint32(10, this->device_id);
@@ -2894,9 +2894,9 @@ void ListEntitiesEventResponse::calculate_size(ProtoSize &size) const {
size.add_bool(1, this->disabled_by_default); size.add_bool(1, this->disabled_by_default);
size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); size.add_uint32(1, static_cast<uint32_t>(this->entity_category));
size.add_length(1, this->device_class_ref_.size()); size.add_length(1, this->device_class_ref_.size());
if (!this->event_types.empty()) { if (!this->event_types->empty()) {
for (const auto &it : this->event_types) { for (const char *it : *this->event_types) {
size.add_length_force(1, it.size()); size.add_length_force(1, strlen(it));
} }
} }
#ifdef USE_DEVICES #ifdef USE_DEVICES

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
@@ -2788,7 +2788,7 @@ class ListEntitiesEventResponse final : public InfoResponseProtoMessage {
#endif #endif
StringRef device_class_ref_{}; StringRef device_class_ref_{};
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
std::vector<std::string> event_types{}; const FixedVector<const char *> *event_types{};
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));
@@ -2047,7 +2053,7 @@ void ListEntitiesEventResponse::dump_to(std::string &out) const {
dump_field(out, "disabled_by_default", this->disabled_by_default); dump_field(out, "disabled_by_default", this->disabled_by_default);
dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category)); dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category));
dump_field(out, "device_class", this->device_class_ref_); dump_field(out, "device_class", this->device_class_ref_);
for (const auto &it : this->event_types) { for (const auto &it : *this->event_types) {
dump_field(out, "event_types", it, 4); dump_field(out, "event_types", it, 4);
} }
#ifdef USE_DEVICES #ifdef USE_DEVICES

View File

@@ -5,6 +5,7 @@
#include "esphome/components/network/util.h" #include "esphome/components/network/util.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/util.h" #include "esphome/core/util.h"
@@ -34,7 +35,7 @@ APIServer::APIServer() {
} }
void APIServer::setup() { void APIServer::setup() {
this->setup_controller(); ControllerRegistry::register_controller(this);
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
uint32_t hash = 88491486UL; uint32_t hash = 88491486UL;
@@ -224,7 +225,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()) {
@@ -269,7 +270,7 @@ bool APIServer::check_password(const uint8_t *password_data, size_t password_len
void APIServer::handle_disconnect(APIConnection *conn) {} void APIServer::handle_disconnect(APIConnection *conn) {}
// Macro for entities without extra parameters // Macro for controller update dispatch
#define API_DISPATCH_UPDATE(entity_type, entity_name) \ #define API_DISPATCH_UPDATE(entity_type, entity_name) \
void APIServer::on_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \ void APIServer::on_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \
if (obj->is_internal()) \ if (obj->is_internal()) \
@@ -278,15 +279,6 @@ void APIServer::handle_disconnect(APIConnection *conn) {}
c->send_##entity_name##_state(obj); \ c->send_##entity_name##_state(obj); \
} }
// Macro for entities with extra parameters (but parameters not used in send)
#define API_DISPATCH_UPDATE_IGNORE_PARAMS(entity_type, entity_name, ...) \
void APIServer::on_##entity_name##_update(entity_type *obj, __VA_ARGS__) { /* NOLINT(bugprone-macro-parentheses) */ \
if (obj->is_internal()) \
return; \
for (auto &c : this->clients_) \
c->send_##entity_name##_state(obj); \
}
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
API_DISPATCH_UPDATE(binary_sensor::BinarySensor, binary_sensor) API_DISPATCH_UPDATE(binary_sensor::BinarySensor, binary_sensor)
#endif #endif
@@ -304,15 +296,15 @@ API_DISPATCH_UPDATE(light::LightState, light)
#endif #endif
#ifdef USE_SENSOR #ifdef USE_SENSOR
API_DISPATCH_UPDATE_IGNORE_PARAMS(sensor::Sensor, sensor, float state) API_DISPATCH_UPDATE(sensor::Sensor, sensor)
#endif #endif
#ifdef USE_SWITCH #ifdef USE_SWITCH
API_DISPATCH_UPDATE_IGNORE_PARAMS(switch_::Switch, switch, bool state) API_DISPATCH_UPDATE(switch_::Switch, switch)
#endif #endif
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
API_DISPATCH_UPDATE_IGNORE_PARAMS(text_sensor::TextSensor, text_sensor, const std::string &state) API_DISPATCH_UPDATE(text_sensor::TextSensor, text_sensor)
#endif #endif
#ifdef USE_CLIMATE #ifdef USE_CLIMATE
@@ -320,7 +312,7 @@ API_DISPATCH_UPDATE(climate::Climate, climate)
#endif #endif
#ifdef USE_NUMBER #ifdef USE_NUMBER
API_DISPATCH_UPDATE_IGNORE_PARAMS(number::Number, number, float state) API_DISPATCH_UPDATE(number::Number, number)
#endif #endif
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
@@ -336,11 +328,11 @@ API_DISPATCH_UPDATE(datetime::DateTimeEntity, datetime)
#endif #endif
#ifdef USE_TEXT #ifdef USE_TEXT
API_DISPATCH_UPDATE_IGNORE_PARAMS(text::Text, text, const std::string &state) API_DISPATCH_UPDATE(text::Text, text)
#endif #endif
#ifdef USE_SELECT #ifdef USE_SELECT
API_DISPATCH_UPDATE_IGNORE_PARAMS(select::Select, select, const std::string &state, size_t index) API_DISPATCH_UPDATE(select::Select, select)
#endif #endif
#ifdef USE_LOCK #ifdef USE_LOCK
@@ -356,12 +348,13 @@ API_DISPATCH_UPDATE(media_player::MediaPlayer, media_player)
#endif #endif
#ifdef USE_EVENT #ifdef USE_EVENT
// Event is a special case - it's the only entity that passes extra parameters to the send method // Event is a special case - unlike other entities with simple state fields,
void APIServer::on_event(event::Event *obj, const std::string &event_type) { // events store their state in a member accessed via obj->get_last_event_type()
void APIServer::on_event(event::Event *obj) {
if (obj->is_internal()) if (obj->is_internal())
return; return;
for (auto &c : this->clients_) for (auto &c : this->clients_)
c->send_event(obj, event_type); c->send_event(obj, obj->get_last_event_type());
} }
#endif #endif

View File

@@ -72,19 +72,19 @@ class APIServer : public Component, public Controller {
void on_light_update(light::LightState *obj) override; void on_light_update(light::LightState *obj) override;
#endif #endif
#ifdef USE_SENSOR #ifdef USE_SENSOR
void on_sensor_update(sensor::Sensor *obj, float state) override; void on_sensor_update(sensor::Sensor *obj) override;
#endif #endif
#ifdef USE_SWITCH #ifdef USE_SWITCH
void on_switch_update(switch_::Switch *obj, bool state) override; void on_switch_update(switch_::Switch *obj) override;
#endif #endif
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
void on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state) override; void on_text_sensor_update(text_sensor::TextSensor *obj) override;
#endif #endif
#ifdef USE_CLIMATE #ifdef USE_CLIMATE
void on_climate_update(climate::Climate *obj) override; void on_climate_update(climate::Climate *obj) override;
#endif #endif
#ifdef USE_NUMBER #ifdef USE_NUMBER
void on_number_update(number::Number *obj, float state) override; void on_number_update(number::Number *obj) override;
#endif #endif
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
void on_date_update(datetime::DateEntity *obj) override; void on_date_update(datetime::DateEntity *obj) override;
@@ -96,10 +96,10 @@ class APIServer : public Component, public Controller {
void on_datetime_update(datetime::DateTimeEntity *obj) override; void on_datetime_update(datetime::DateTimeEntity *obj) override;
#endif #endif
#ifdef USE_TEXT #ifdef USE_TEXT
void on_text_update(text::Text *obj, const std::string &state) override; void on_text_update(text::Text *obj) override;
#endif #endif
#ifdef USE_SELECT #ifdef USE_SELECT
void on_select_update(select::Select *obj, const std::string &state, size_t index) override; void on_select_update(select::Select *obj) override;
#endif #endif
#ifdef USE_LOCK #ifdef USE_LOCK
void on_lock_update(lock::Lock *obj) override; void on_lock_update(lock::Lock *obj) override;
@@ -141,7 +141,7 @@ class APIServer : public Component, public Controller {
void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) override; void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) override;
#endif #endif
#ifdef USE_EVENT #ifdef USE_EVENT
void on_event(event::Event *obj, const std::string &event_type) override; void on_event(event::Event *obj) override;
#endif #endif
#ifdef USE_UPDATE #ifdef USE_UPDATE
void on_update(update::UpdateEntity *obj) override; void on_update(update::UpdateEntity *obj) override;
@@ -237,7 +237,7 @@ extern APIServer *global_api_server; // NOLINT(cppcoreguidelines-avoid-non-cons
template<typename... Ts> class APIConnectedCondition : public Condition<Ts...> { template<typename... Ts> class APIConnectedCondition : public Condition<Ts...> {
public: public:
bool check(Ts... x) override { return global_api_server->is_connected(); } bool check(const Ts &...x) override { return global_api_server->is_connected(); }
}; };
} // namespace esphome::api } // namespace esphome::api

View File

@@ -9,11 +9,11 @@
namespace esphome::api { namespace esphome::api {
#ifdef USE_API_SERVICES #ifdef USE_API_SERVICES
template<typename T, typename... Ts> class CustomAPIDeviceService : public UserServiceBase<Ts...> { template<typename T, typename... Ts> class CustomAPIDeviceService : public UserServiceDynamic<Ts...> {
public: public:
CustomAPIDeviceService(const std::string &name, const std::array<std::string, sizeof...(Ts)> &arg_names, T *obj, CustomAPIDeviceService(const std::string &name, const std::array<std::string, sizeof...(Ts)> &arg_names, T *obj,
void (T::*callback)(Ts...)) void (T::*callback)(Ts...))
: UserServiceBase<Ts...>(name, arg_names), obj_(obj), callback_(callback) {} : UserServiceDynamic<Ts...>(name, arg_names), obj_(obj), callback_(callback) {}
protected: protected:
void execute(Ts... x) override { (this->obj_->*this->callback_)(x...); } // NOLINT void execute(Ts... x) override { (this->obj_->*this->callback_)(x...); } // NOLINT

View File

@@ -133,7 +133,7 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
Trigger<std::string, Ts...> *get_error_trigger() const { return this->error_trigger_; } Trigger<std::string, Ts...> *get_error_trigger() const { return this->error_trigger_; }
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES #endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
void play(Ts... x) override { void play(const Ts &...x) override {
HomeassistantActionRequest resp; HomeassistantActionRequest resp;
std::string service_value = this->service_.value(x...); std::string service_value = this->service_.value(x...);
resp.set_service(StringRef(service_value)); resp.set_service(StringRef(service_value));

View File

@@ -23,11 +23,13 @@ template<typename T> T get_execute_arg_value(const ExecuteServiceArgument &arg);
template<typename T> enums::ServiceArgType to_service_arg_type(); template<typename T> enums::ServiceArgType to_service_arg_type();
// Base class for YAML-defined services (most common case)
// Stores only pointers to string literals in flash - no heap allocation
template<typename... Ts> class UserServiceBase : public UserServiceDescriptor { template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
public: public:
UserServiceBase(std::string name, const std::array<std::string, sizeof...(Ts)> &arg_names) UserServiceBase(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
: name_(std::move(name)), arg_names_(arg_names) { : name_(name), arg_names_(arg_names) {
this->key_ = fnv1_hash(this->name_); this->key_ = fnv1_hash(name);
} }
ListEntitiesServicesResponse encode_list_service_response() override { ListEntitiesServicesResponse encode_list_service_response() override {
@@ -47,7 +49,7 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
bool execute_service(const ExecuteServiceRequest &req) override { bool execute_service(const ExecuteServiceRequest &req) override {
if (req.key != this->key_) if (req.key != this->key_)
return false; return false;
if (req.args.size() != this->arg_names_.size()) if (req.args.size() != sizeof...(Ts))
return false; return false;
this->execute_(req.args, typename gens<sizeof...(Ts)>::type()); this->execute_(req.args, typename gens<sizeof...(Ts)>::type());
return true; return true;
@@ -59,14 +61,60 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
this->execute((get_execute_arg_value<Ts>(args[S]))...); this->execute((get_execute_arg_value<Ts>(args[S]))...);
} }
std::string name_; // Pointers to string literals in flash - no heap allocation
const char *name_;
std::array<const char *, sizeof...(Ts)> arg_names_;
uint32_t key_{0}; uint32_t key_{0};
};
// Separate class for custom_api_device services (rare case)
// Stores copies of runtime-generated names
template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor {
public:
UserServiceDynamic(std::string name, const std::array<std::string, sizeof...(Ts)> &arg_names)
: name_(std::move(name)), arg_names_(arg_names) {
this->key_ = fnv1_hash(this->name_.c_str());
}
ListEntitiesServicesResponse encode_list_service_response() override {
ListEntitiesServicesResponse msg;
msg.set_name(StringRef(this->name_));
msg.key = this->key_;
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
msg.args.init(sizeof...(Ts));
for (size_t i = 0; i < sizeof...(Ts); i++) {
auto &arg = msg.args.emplace_back();
arg.type = arg_types[i];
arg.set_name(StringRef(this->arg_names_[i]));
}
return msg;
}
bool execute_service(const ExecuteServiceRequest &req) override {
if (req.key != this->key_)
return false;
if (req.args.size() != sizeof...(Ts))
return false;
this->execute_(req.args, typename gens<sizeof...(Ts)>::type());
return true;
}
protected:
virtual void execute(Ts... x) = 0;
template<typename ArgsContainer, int... S> void execute_(const ArgsContainer &args, seq<S...> type) {
this->execute((get_execute_arg_value<Ts>(args[S]))...);
}
// Heap-allocated strings for runtime-generated names
std::string name_;
std::array<std::string, sizeof...(Ts)> arg_names_; std::array<std::string, sizeof...(Ts)> arg_names_;
uint32_t key_{0};
}; };
template<typename... Ts> class UserServiceTrigger : public UserServiceBase<Ts...>, public Trigger<Ts...> { template<typename... Ts> class UserServiceTrigger : public UserServiceBase<Ts...>, public Trigger<Ts...> {
public: public:
UserServiceTrigger(const std::string &name, const std::array<std::string, sizeof...(Ts)> &arg_names) // Constructor for static names (YAML-defined services - used by code generator)
UserServiceTrigger(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
: UserServiceBase<Ts...>(name, arg_names) {} : UserServiceBase<Ts...>(name, arg_names) {}
protected: protected:

View File

@@ -10,7 +10,7 @@ namespace at581x {
template<typename... Ts> class AT581XResetAction : public Action<Ts...>, public Parented<AT581XComponent> { template<typename... Ts> class AT581XResetAction : public Action<Ts...>, public Parented<AT581XComponent> {
public: public:
void play(Ts... x) { this->parent_->reset_hardware_frontend(); } void play(const Ts &...x) { this->parent_->reset_hardware_frontend(); }
}; };
template<typename... Ts> class AT581XSettingsAction : public Action<Ts...>, public Parented<AT581XComponent> { template<typename... Ts> class AT581XSettingsAction : public Action<Ts...>, public Parented<AT581XComponent> {
@@ -25,7 +25,7 @@ template<typename... Ts> class AT581XSettingsAction : public Action<Ts...>, publ
TEMPLATABLE_VALUE(int, trigger_keep) TEMPLATABLE_VALUE(int, trigger_keep)
TEMPLATABLE_VALUE(int, stage_gain) TEMPLATABLE_VALUE(int, stage_gain)
void play(Ts... x) { void play(const Ts &...x) {
if (this->frequency_.has_value()) { if (this->frequency_.has_value()) {
int v = this->frequency_.value(x...); int v = this->frequency_.value(x...);
this->parent_->set_frequency(v); this->parent_->set_frequency(v);

View File

@@ -13,7 +13,7 @@ template<typename... Ts> class SetMicGainAction : public Action<Ts...> {
TEMPLATABLE_VALUE(float, mic_gain) TEMPLATABLE_VALUE(float, mic_gain)
void play(Ts... x) override { this->audio_adc_->set_mic_gain(this->mic_gain_.value(x...)); } void play(const Ts &...x) override { this->audio_adc_->set_mic_gain(this->mic_gain_.value(x...)); }
protected: protected:
AudioAdc *audio_adc_; AudioAdc *audio_adc_;

View File

@@ -11,7 +11,7 @@ template<typename... Ts> class MuteOffAction : public Action<Ts...> {
public: public:
explicit MuteOffAction(AudioDac *audio_dac) : audio_dac_(audio_dac) {} explicit MuteOffAction(AudioDac *audio_dac) : audio_dac_(audio_dac) {}
void play(Ts... x) override { this->audio_dac_->set_mute_off(); } void play(const Ts &...x) override { this->audio_dac_->set_mute_off(); }
protected: protected:
AudioDac *audio_dac_; AudioDac *audio_dac_;
@@ -21,7 +21,7 @@ template<typename... Ts> class MuteOnAction : public Action<Ts...> {
public: public:
explicit MuteOnAction(AudioDac *audio_dac) : audio_dac_(audio_dac) {} explicit MuteOnAction(AudioDac *audio_dac) : audio_dac_(audio_dac) {}
void play(Ts... x) override { this->audio_dac_->set_mute_on(); } void play(const Ts &...x) override { this->audio_dac_->set_mute_on(); }
protected: protected:
AudioDac *audio_dac_; AudioDac *audio_dac_;
@@ -33,7 +33,7 @@ template<typename... Ts> class SetVolumeAction : public Action<Ts...> {
TEMPLATABLE_VALUE(float, volume) TEMPLATABLE_VALUE(float, volume)
void play(Ts... x) override { this->audio_dac_->set_volume(this->volume_.value(x...)); } void play(const Ts &...x) override { this->audio_dac_->set_volume(this->volume_.value(x...)); }
protected: protected:
AudioDac *audio_dac_; AudioDac *audio_dac_;

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

@@ -548,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

@@ -141,7 +141,7 @@ class StateChangeTrigger : public Trigger<optional<bool>, optional<bool> > {
template<typename... Ts> class BinarySensorCondition : public Condition<Ts...> { template<typename... Ts> class BinarySensorCondition : public Condition<Ts...> {
public: public:
BinarySensorCondition(BinarySensor *parent, bool state) : parent_(parent), state_(state) {} BinarySensorCondition(BinarySensor *parent, bool state) : parent_(parent), state_(state) {}
bool check(Ts... x) override { return this->parent_->state == this->state_; } bool check(const Ts &...x) override { return this->parent_->state == this->state_; }
protected: protected:
BinarySensor *parent_; BinarySensor *parent_;
@@ -153,7 +153,7 @@ template<typename... Ts> class BinarySensorPublishAction : public Action<Ts...>
explicit BinarySensorPublishAction(BinarySensor *sensor) : sensor_(sensor) {} explicit BinarySensorPublishAction(BinarySensor *sensor) : sensor_(sensor) {}
TEMPLATABLE_VALUE(bool, state) TEMPLATABLE_VALUE(bool, state)
void play(Ts... x) override { void play(const Ts &...x) override {
auto val = this->state_.value(x...); auto val = this->state_.value(x...);
this->sensor_->publish_state(val); this->sensor_->publish_state(val);
} }
@@ -166,7 +166,7 @@ template<typename... Ts> class BinarySensorInvalidateAction : public Action<Ts..
public: public:
explicit BinarySensorInvalidateAction(BinarySensor *sensor) : sensor_(sensor) {} explicit BinarySensorInvalidateAction(BinarySensor *sensor) : sensor_(sensor) {}
void play(Ts... x) override { this->sensor_->invalidate_state(); } void play(const Ts &...x) override { this->sensor_->invalidate_state(); }
protected: protected:
BinarySensor *sensor_; BinarySensor *sensor_;

View File

@@ -1,4 +1,6 @@
#include "binary_sensor.h" #include "binary_sensor.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome { namespace esphome {
@@ -37,6 +39,9 @@ void BinarySensor::send_state_internal(bool new_state) {
// Note that set_state_ de-dups and will only trigger callbacks if the state has actually changed // Note that set_state_ de-dups and will only trigger callbacks if the state has actually changed
if (this->set_state_(new_state)) { if (this->set_state_(new_state)) {
ESP_LOGD(TAG, "'%s': New state is %s", this->get_name().c_str(), ONOFF(new_state)); ESP_LOGD(TAG, "'%s': New state is %s", this->get_name().c_str(), ONOFF(new_state));
#if defined(USE_BINARY_SENSOR) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_binary_sensor_update(this);
#endif
} }
} }

View File

@@ -89,7 +89,7 @@ class BL0906 : public PollingComponent, public uart::UARTDevice {
template<typename... Ts> class ResetEnergyAction : public Action<Ts...>, public Parented<BL0906> { template<typename... Ts> class ResetEnergyAction : public Action<Ts...>, public Parented<BL0906> {
public: public:
void play(Ts... x) override { this->parent_->enqueue_action_(&BL0906::reset_energy_); } void play(const Ts &...x) override { this->parent_->enqueue_action_(&BL0906::reset_energy_); }
}; };
} // namespace bl0906 } // namespace bl0906

View File

@@ -123,9 +123,9 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
this->has_simple_value_ = true; this->has_simple_value_ = true;
} }
void play(Ts... x) override {} void play(const Ts &...x) override {}
void play_complex(Ts... x) override { void play_complex(const 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_func(x...); auto value = this->has_simple_value_ ? this->value_.simple : this->value_.template_func(x...);
@@ -229,7 +229,7 @@ template<typename... Ts> class BLEClientPasskeyReplyAction : public Action<Ts...
public: public:
BLEClientPasskeyReplyAction(BLEClient *ble_client) { parent_ = ble_client; } BLEClientPasskeyReplyAction(BLEClient *ble_client) { parent_ = ble_client; }
void play(Ts... x) override { void play(const 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;
@@ -266,7 +266,7 @@ template<typename... Ts> class BLEClientNumericComparisonReplyAction : public Ac
public: public:
BLEClientNumericComparisonReplyAction(BLEClient *ble_client) { parent_ = ble_client; } BLEClientNumericComparisonReplyAction(BLEClient *ble_client) { parent_ = ble_client; }
void play(Ts... x) override { void play(const Ts &...x) override {
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_) {
@@ -299,7 +299,7 @@ template<typename... Ts> class BLEClientRemoveBondAction : public Action<Ts...>
public: public:
BLEClientRemoveBondAction(BLEClient *ble_client) { parent_ = ble_client; } BLEClientRemoveBondAction(BLEClient *ble_client) { parent_ = ble_client; }
void play(Ts... x) override { void play(const Ts &...x) override {
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));
esp_ble_remove_bond_device(remote_bda); esp_ble_remove_bond_device(remote_bda);
@@ -334,9 +334,9 @@ template<typename... Ts> class BLEClientConnectAction : public Action<Ts...>, pu
} }
// not used since we override play_complex_ // not used since we override play_complex_
void play(Ts... x) override {} void play(const Ts &...x) override {}
void play_complex(Ts... x) override { void play_complex(const Ts &...x) override {
// it makes no sense to have multiple instances of this running at the same time. // it makes no sense to have multiple instances of this running at the same time.
// this would occur only if the same automation was re-triggered while still // this would occur only if the same automation was re-triggered while still
// running. So just cancel the second chain if this is detected. // running. So just cancel the second chain if this is detected.
@@ -379,9 +379,9 @@ template<typename... Ts> class BLEClientDisconnectAction : public Action<Ts...>,
} }
// not used since we override play_complex_ // not used since we override play_complex_
void play(Ts... x) override {} void play(const Ts &...x) override {}
void play_complex(Ts... x) override { void play_complex(const Ts &...x) override {
this->num_running_++; this->num_running_++;
if (this->node_state == espbt::ClientState::IDLE) { if (this->node_state == espbt::ClientState::IDLE) {
this->play_next_(x...); this->play_next_(x...);

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

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

@@ -11,7 +11,7 @@ template<typename... Ts> class PressAction : public Action<Ts...> {
public: public:
explicit PressAction(Button *button) : button_(button) {} explicit PressAction(Button *button) : button_(button) {}
void play(Ts... x) override { this->button_->press(); } void play(const Ts &...x) override { this->button_->press(); }
protected: protected:
Button *button_; Button *button_;

View File

@@ -129,7 +129,7 @@ template<typename... Ts> class CanbusSendAction : public Action<Ts...>, public P
this->remote_transmission_request_ = remote_transmission_request; this->remote_transmission_request_ = remote_transmission_request;
} }
void play(Ts... x) override { void play(const Ts &...x) override {
auto can_id = this->can_id_.has_value() ? *this->can_id_ : this->parent_->can_id_; auto can_id = this->can_id_.has_value() ? *this->can_id_ : this->parent_->can_id_;
auto use_extended_id = auto use_extended_id =
this->use_extended_id_.has_value() ? *this->use_extended_id_ : this->parent_->use_extended_id_; this->use_extended_id_.has_value() ? *this->use_extended_id_ : this->parent_->use_extended_id_;

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

@@ -22,7 +22,7 @@ template<typename... Ts> class ControlAction : public Action<Ts...> {
TEMPLATABLE_VALUE(std::string, custom_preset) TEMPLATABLE_VALUE(std::string, custom_preset)
TEMPLATABLE_VALUE(ClimateSwingMode, swing_mode) TEMPLATABLE_VALUE(ClimateSwingMode, swing_mode)
void play(Ts... x) override { void play(const Ts &...x) override {
auto call = this->climate_->make_call(); auto call = this->climate_->make_call();
call.set_mode(this->mode_.optional_value(x...)); call.set_mode(this->mode_.optional_value(x...));
call.set_target_temperature(this->target_temperature_.optional_value(x...)); call.set_target_temperature(this->target_temperature_.optional_value(x...));

View File

@@ -1,4 +1,6 @@
#include "climate.h" #include "climate.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/macros.h" #include "esphome/core/macros.h"
namespace esphome { namespace esphome {
@@ -50,21 +52,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 +98,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 +110,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,25 +186,28 @@ 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 {
ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), fan_mode.c_str());
}
return *this; return *this;
} }
ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), custom_fan_mode);
return *this;
}
ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) { return this->set_fan_mode(fan_mode.c_str()); }
ClimateCall &ClimateCall::set_fan_mode(optional<std::string> fan_mode) { ClimateCall &ClimateCall::set_fan_mode(optional<std::string> fan_mode) {
if (fan_mode.has_value()) { if (fan_mode.has_value()) {
@@ -215,25 +218,28 @@ 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 {
ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), preset.c_str());
}
return *this; return *this;
} }
ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), custom_preset);
return *this;
}
ClimateCall &ClimateCall::set_preset(const std::string &preset) { return this->set_preset(preset.c_str()); }
ClimateCall &ClimateCall::set_preset(optional<std::string> preset) { ClimateCall &ClimateCall::set_preset(optional<std::string> preset) {
if (preset.has_value()) { if (preset.has_value()) {
@@ -287,8 +293,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 +321,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 +386,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 +403,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 +434,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)));
@@ -461,6 +465,9 @@ void Climate::publish_state() {
// Send state to frontend // Send state to frontend
this->state_callback_.call(*this); this->state_callback_.call(*this);
#if defined(USE_CLIMATE) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_climate_update(this);
#endif
// Save state // Save state
this->save_state_(); this->save_state_();
} }
@@ -527,7 +534,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 +542,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 +569,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 +590,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 +742,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 +752,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

@@ -30,7 +30,7 @@ template<typename... Ts> class CM1106CalibrateZeroAction : public Action<Ts...>
public: public:
CM1106CalibrateZeroAction(CM1106Component *cm1106) : cm1106_(cm1106) {} CM1106CalibrateZeroAction(CM1106Component *cm1106) : cm1106_(cm1106) {}
void play(Ts... x) override { this->cm1106_->calibrate_zero(400); } void play(const Ts &...x) override { this->cm1106_->calibrate_zero(400); }
protected: protected:
CM1106Component *cm1106_; CM1106Component *cm1106_;

View File

@@ -8,7 +8,10 @@ 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_IGNORE_NOT_FOUND = "ignore_not_found"
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

@@ -11,7 +11,7 @@ template<typename... Ts> class OpenAction : public Action<Ts...> {
public: public:
explicit OpenAction(Cover *cover) : cover_(cover) {} explicit OpenAction(Cover *cover) : cover_(cover) {}
void play(Ts... x) override { this->cover_->make_call().set_command_open().perform(); } void play(const Ts &...x) override { this->cover_->make_call().set_command_open().perform(); }
protected: protected:
Cover *cover_; Cover *cover_;
@@ -21,7 +21,7 @@ template<typename... Ts> class CloseAction : public Action<Ts...> {
public: public:
explicit CloseAction(Cover *cover) : cover_(cover) {} explicit CloseAction(Cover *cover) : cover_(cover) {}
void play(Ts... x) override { this->cover_->make_call().set_command_close().perform(); } void play(const Ts &...x) override { this->cover_->make_call().set_command_close().perform(); }
protected: protected:
Cover *cover_; Cover *cover_;
@@ -31,7 +31,7 @@ template<typename... Ts> class StopAction : public Action<Ts...> {
public: public:
explicit StopAction(Cover *cover) : cover_(cover) {} explicit StopAction(Cover *cover) : cover_(cover) {}
void play(Ts... x) override { this->cover_->make_call().set_command_stop().perform(); } void play(const Ts &...x) override { this->cover_->make_call().set_command_stop().perform(); }
protected: protected:
Cover *cover_; Cover *cover_;
@@ -41,7 +41,7 @@ template<typename... Ts> class ToggleAction : public Action<Ts...> {
public: public:
explicit ToggleAction(Cover *cover) : cover_(cover) {} explicit ToggleAction(Cover *cover) : cover_(cover) {}
void play(Ts... x) override { this->cover_->make_call().set_command_toggle().perform(); } void play(const Ts &...x) override { this->cover_->make_call().set_command_toggle().perform(); }
protected: protected:
Cover *cover_; Cover *cover_;
@@ -55,7 +55,7 @@ template<typename... Ts> class ControlAction : public Action<Ts...> {
TEMPLATABLE_VALUE(float, position) TEMPLATABLE_VALUE(float, position)
TEMPLATABLE_VALUE(float, tilt) TEMPLATABLE_VALUE(float, tilt)
void play(Ts... x) override { void play(const Ts &...x) override {
auto call = this->cover_->make_call(); auto call = this->cover_->make_call();
if (this->stop_.has_value()) if (this->stop_.has_value())
call.set_stop(this->stop_.value(x...)); call.set_stop(this->stop_.value(x...));
@@ -77,7 +77,7 @@ template<typename... Ts> class CoverPublishAction : public Action<Ts...> {
TEMPLATABLE_VALUE(float, tilt) TEMPLATABLE_VALUE(float, tilt)
TEMPLATABLE_VALUE(CoverOperation, current_operation) TEMPLATABLE_VALUE(CoverOperation, current_operation)
void play(Ts... x) override { void play(const Ts &...x) override {
if (this->position_.has_value()) if (this->position_.has_value())
this->cover_->position = this->position_.value(x...); this->cover_->position = this->position_.value(x...);
if (this->tilt_.has_value()) if (this->tilt_.has_value())
@@ -94,7 +94,7 @@ template<typename... Ts> class CoverPublishAction : public Action<Ts...> {
template<typename... Ts> class CoverIsOpenCondition : public Condition<Ts...> { template<typename... Ts> class CoverIsOpenCondition : public Condition<Ts...> {
public: public:
CoverIsOpenCondition(Cover *cover) : cover_(cover) {} CoverIsOpenCondition(Cover *cover) : cover_(cover) {}
bool check(Ts... x) override { return this->cover_->is_fully_open(); } bool check(const Ts &...x) override { return this->cover_->is_fully_open(); }
protected: protected:
Cover *cover_; Cover *cover_;
@@ -103,7 +103,7 @@ template<typename... Ts> class CoverIsOpenCondition : public Condition<Ts...> {
template<typename... Ts> class CoverIsClosedCondition : public Condition<Ts...> { template<typename... Ts> class CoverIsClosedCondition : public Condition<Ts...> {
public: public:
CoverIsClosedCondition(Cover *cover) : cover_(cover) {} CoverIsClosedCondition(Cover *cover) : cover_(cover) {}
bool check(Ts... x) override { return this->cover_->is_fully_closed(); } bool check(const Ts &...x) override { return this->cover_->is_fully_closed(); }
protected: protected:
Cover *cover_; Cover *cover_;

View File

@@ -1,5 +1,9 @@
#include "cover.h" #include "cover.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include <strings.h> #include <strings.h>
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome { namespace esphome {
@@ -169,6 +173,9 @@ void Cover::publish_state(bool save) {
ESP_LOGD(TAG, " Current Operation: %s", cover_operation_to_str(this->current_operation)); ESP_LOGD(TAG, " Current Operation: %s", cover_operation_to_str(this->current_operation));
this->state_callback_.call(); this->state_callback_.call();
#if defined(USE_COVER) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_cover_update(this);
#endif
if (save) { if (save) {
CoverRestoreState restore{}; CoverRestoreState restore{};

View File

@@ -114,7 +114,7 @@ template<typename... Ts> class CS5460ARestartAction : public Action<Ts...> {
public: public:
CS5460ARestartAction(CS5460AComponent *cs5460a) : cs5460a_(cs5460a) {} CS5460ARestartAction(CS5460AComponent *cs5460a) : cs5460a_(cs5460a) {}
void play(Ts... x) override { cs5460a_->restart(); } void play(const Ts &...x) override { cs5460a_->restart(); }
protected: protected:
CS5460AComponent *cs5460a_; CS5460AComponent *cs5460a_;

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

@@ -1,5 +1,6 @@
#include "date_entity.h" #include "date_entity.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
#include "esphome/core/log.h" #include "esphome/core/log.h"
@@ -32,6 +33,9 @@ void DateEntity::publish_state() {
this->set_has_state(true); this->set_has_state(true);
ESP_LOGD(TAG, "'%s': Sending date %d-%d-%d", this->get_name().c_str(), this->year_, this->month_, this->day_); ESP_LOGD(TAG, "'%s': Sending date %d-%d-%d", this->get_name().c_str(), this->year_, this->month_, this->day_);
this->state_callback_.call(); this->state_callback_.call();
#if defined(USE_DATETIME_DATE) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_date_update(this);
#endif
} }
DateCall DateEntity::make_call() { return DateCall(this); } DateCall DateEntity::make_call() { return DateCall(this); }

View File

@@ -101,7 +101,7 @@ template<typename... Ts> class DateSetAction : public Action<Ts...>, public Pare
public: public:
TEMPLATABLE_VALUE(ESPTime, date) TEMPLATABLE_VALUE(ESPTime, date)
void play(Ts... x) override { void play(const Ts &...x) override {
auto call = this->parent_->make_call(); auto call = this->parent_->make_call();
if (this->date_.has_value()) { if (this->date_.has_value()) {

View File

@@ -1,5 +1,6 @@
#include "datetime_entity.h" #include "datetime_entity.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#ifdef USE_DATETIME_DATETIME #ifdef USE_DATETIME_DATETIME
#include "esphome/core/log.h" #include "esphome/core/log.h"
@@ -48,6 +49,9 @@ void DateTimeEntity::publish_state() {
ESP_LOGD(TAG, "'%s': Sending datetime %04u-%02u-%02u %02d:%02d:%02d", this->get_name().c_str(), this->year_, ESP_LOGD(TAG, "'%s': Sending datetime %04u-%02u-%02u %02d:%02d:%02d", this->get_name().c_str(), this->year_,
this->month_, this->day_, this->hour_, this->minute_, this->second_); this->month_, this->day_, this->hour_, this->minute_, this->second_);
this->state_callback_.call(); this->state_callback_.call();
#if defined(USE_DATETIME_DATETIME) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_datetime_update(this);
#endif
} }
DateTimeCall DateTimeEntity::make_call() { return DateTimeCall(this); } DateTimeCall DateTimeEntity::make_call() { return DateTimeCall(this); }

View File

@@ -124,7 +124,7 @@ template<typename... Ts> class DateTimeSetAction : public Action<Ts...>, public
public: public:
TEMPLATABLE_VALUE(ESPTime, datetime) TEMPLATABLE_VALUE(ESPTime, datetime)
void play(Ts... x) override { void play(const Ts &...x) override {
auto call = this->parent_->make_call(); auto call = this->parent_->make_call();
if (this->datetime_.has_value()) { if (this->datetime_.has_value()) {

View File

@@ -1,5 +1,6 @@
#include "time_entity.h" #include "time_entity.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#ifdef USE_DATETIME_TIME #ifdef USE_DATETIME_TIME
#include "esphome/core/log.h" #include "esphome/core/log.h"
@@ -29,6 +30,9 @@ void TimeEntity::publish_state() {
ESP_LOGD(TAG, "'%s': Sending time %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_, ESP_LOGD(TAG, "'%s': Sending time %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_,
this->second_); this->second_);
this->state_callback_.call(); this->state_callback_.call();
#if defined(USE_DATETIME_TIME) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_time_update(this);
#endif
} }
TimeCall TimeEntity::make_call() { return TimeCall(this); } TimeCall TimeEntity::make_call() { return TimeCall(this); }

View File

@@ -103,7 +103,7 @@ template<typename... Ts> class TimeSetAction : public Action<Ts...>, public Pare
public: public:
TEMPLATABLE_VALUE(ESPTime, time) TEMPLATABLE_VALUE(ESPTime, time)
void play(Ts... x) override { void play(const Ts &...x) override {
auto call = this->parent_->make_call(); auto call = this->parent_->make_call();
if (this->time_.has_value()) { if (this->time_.has_value()) {

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

@@ -148,7 +148,7 @@ template<typename... Ts> class EnterDeepSleepAction : public Action<Ts...> {
void set_time(time::RealTimeClock *time) { this->time_ = time; } void set_time(time::RealTimeClock *time) { this->time_ = time; }
#endif #endif
void play(Ts... x) override { void play(const Ts &...x) override {
if (this->sleep_duration_.has_value()) { if (this->sleep_duration_.has_value()) {
this->deep_sleep_->set_sleep_duration(this->sleep_duration_.value(x...)); this->deep_sleep_->set_sleep_duration(this->sleep_duration_.value(x...));
} }
@@ -207,12 +207,12 @@ template<typename... Ts> class EnterDeepSleepAction : public Action<Ts...> {
template<typename... Ts> class PreventDeepSleepAction : public Action<Ts...>, public Parented<DeepSleepComponent> { template<typename... Ts> class PreventDeepSleepAction : public Action<Ts...>, public Parented<DeepSleepComponent> {
public: public:
void play(Ts... x) override { this->parent_->prevent_deep_sleep(); } void play(const Ts &...x) override { this->parent_->prevent_deep_sleep(); }
}; };
template<typename... Ts> class AllowDeepSleepAction : public Action<Ts...>, public Parented<DeepSleepComponent> { template<typename... Ts> class AllowDeepSleepAction : public Action<Ts...>, public Parented<DeepSleepComponent> {
public: public:
void play(Ts... x) override { this->parent_->allow_deep_sleep(); } void play(const Ts &...x) override { this->parent_->allow_deep_sleep(); }
}; };
} // namespace deep_sleep } // namespace deep_sleep

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

@@ -77,7 +77,7 @@ class DFPlayer : public uart::UARTDevice, public Component {
class ACTION_CLASS : /* NOLINT */ \ class ACTION_CLASS : /* NOLINT */ \
public Action<Ts...>, \ public Action<Ts...>, \
public Parented<DFPlayer> { \ public Parented<DFPlayer> { \
void play(Ts... x) override { this->parent_->ACTION_METHOD(); } \ void play(const Ts &...x) override { this->parent_->ACTION_METHOD(); } \
}; };
DFPLAYER_SIMPLE_ACTION(NextAction, next) DFPLAYER_SIMPLE_ACTION(NextAction, next)
@@ -87,7 +87,7 @@ template<typename... Ts> class PlayMp3Action : public Action<Ts...>, public Pare
public: public:
TEMPLATABLE_VALUE(uint16_t, file) TEMPLATABLE_VALUE(uint16_t, file)
void play(Ts... x) override { void play(const Ts &...x) override {
auto file = this->file_.value(x...); auto file = this->file_.value(x...);
this->parent_->play_mp3(file); this->parent_->play_mp3(file);
} }
@@ -98,7 +98,7 @@ template<typename... Ts> class PlayFileAction : public Action<Ts...>, public Par
TEMPLATABLE_VALUE(uint16_t, file) TEMPLATABLE_VALUE(uint16_t, file)
TEMPLATABLE_VALUE(bool, loop) TEMPLATABLE_VALUE(bool, loop)
void play(Ts... x) override { void play(const Ts &...x) override {
auto file = this->file_.value(x...); auto file = this->file_.value(x...);
auto loop = this->loop_.value(x...); auto loop = this->loop_.value(x...);
if (loop) { if (loop) {
@@ -115,7 +115,7 @@ template<typename... Ts> class PlayFolderAction : public Action<Ts...>, public P
TEMPLATABLE_VALUE(uint16_t, file) TEMPLATABLE_VALUE(uint16_t, file)
TEMPLATABLE_VALUE(bool, loop) TEMPLATABLE_VALUE(bool, loop)
void play(Ts... x) override { void play(const Ts &...x) override {
auto folder = this->folder_.value(x...); auto folder = this->folder_.value(x...);
auto file = this->file_.value(x...); auto file = this->file_.value(x...);
auto loop = this->loop_.value(x...); auto loop = this->loop_.value(x...);
@@ -131,7 +131,7 @@ template<typename... Ts> class SetDeviceAction : public Action<Ts...>, public Pa
public: public:
TEMPLATABLE_VALUE(Device, device) TEMPLATABLE_VALUE(Device, device)
void play(Ts... x) override { void play(const Ts &...x) override {
auto device = this->device_.value(x...); auto device = this->device_.value(x...);
this->parent_->set_device(device); this->parent_->set_device(device);
} }
@@ -141,7 +141,7 @@ template<typename... Ts> class SetVolumeAction : public Action<Ts...>, public Pa
public: public:
TEMPLATABLE_VALUE(uint8_t, volume) TEMPLATABLE_VALUE(uint8_t, volume)
void play(Ts... x) override { void play(const Ts &...x) override {
auto volume = this->volume_.value(x...); auto volume = this->volume_.value(x...);
this->parent_->set_volume(volume); this->parent_->set_volume(volume);
} }
@@ -151,7 +151,7 @@ template<typename... Ts> class SetEqAction : public Action<Ts...>, public Parent
public: public:
TEMPLATABLE_VALUE(EqPreset, eq) TEMPLATABLE_VALUE(EqPreset, eq)
void play(Ts... x) override { void play(const Ts &...x) override {
auto eq = this->eq_.value(x...); auto eq = this->eq_.value(x...);
this->parent_->set_eq(eq); this->parent_->set_eq(eq);
} }
@@ -168,7 +168,7 @@ DFPLAYER_SIMPLE_ACTION(VolumeDownAction, volume_down)
template<typename... Ts> class DFPlayerIsPlayingCondition : public Condition<Ts...>, public Parented<DFPlayer> { template<typename... Ts> class DFPlayerIsPlayingCondition : public Condition<Ts...>, public Parented<DFPlayer> {
public: public:
bool check(Ts... x) override { return this->parent_->is_playing(); } bool check(const Ts &...x) override { return this->parent_->is_playing(); }
}; };
class DFPlayerFinishedPlaybackTrigger : public Trigger<> { class DFPlayerFinishedPlaybackTrigger : public Trigger<> {

View File

@@ -11,7 +11,7 @@ namespace dfrobot_sen0395 {
template<typename... Ts> template<typename... Ts>
class DfrobotSen0395ResetAction : public Action<Ts...>, public Parented<DfrobotSen0395Component> { class DfrobotSen0395ResetAction : public Action<Ts...>, public Parented<DfrobotSen0395Component> {
public: public:
void play(Ts... x) { this->parent_->enqueue(make_unique<ResetSystemCommand>()); } void play(const Ts &...x) { this->parent_->enqueue(make_unique<ResetSystemCommand>()); }
}; };
template<typename... Ts> template<typename... Ts>
@@ -33,7 +33,7 @@ class DfrobotSen0395SettingsAction : public Action<Ts...>, public Parented<Dfrob
TEMPLATABLE_VALUE(float, det_min4) TEMPLATABLE_VALUE(float, det_min4)
TEMPLATABLE_VALUE(float, det_max4) TEMPLATABLE_VALUE(float, det_max4)
void play(Ts... x) { void play(const Ts &...x) {
this->parent_->enqueue(make_unique<PowerCommand>(0)); this->parent_->enqueue(make_unique<PowerCommand>(0));
if (this->factory_reset_.has_value() && this->factory_reset_.value(x...) == true) { if (this->factory_reset_.has_value() && this->factory_reset_.value(x...) == true) {
this->parent_->enqueue(make_unique<FactoryResetCommand>()); this->parent_->enqueue(make_unique<FactoryResetCommand>());

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_;
@@ -709,7 +819,7 @@ template<typename... Ts> class DisplayPageShowAction : public Action<Ts...> {
public: public:
TEMPLATABLE_VALUE(DisplayPage *, page) TEMPLATABLE_VALUE(DisplayPage *, page)
void play(Ts... x) override { void play(const Ts &...x) override {
auto *page = this->page_.value(x...); auto *page = this->page_.value(x...);
if (page != nullptr) { if (page != nullptr) {
page->show(); page->show();
@@ -721,7 +831,7 @@ template<typename... Ts> class DisplayPageShowNextAction : public Action<Ts...>
public: public:
DisplayPageShowNextAction(Display *buffer) : buffer_(buffer) {} DisplayPageShowNextAction(Display *buffer) : buffer_(buffer) {}
void play(Ts... x) override { this->buffer_->show_next_page(); } void play(const Ts &...x) override { this->buffer_->show_next_page(); }
Display *buffer_; Display *buffer_;
}; };
@@ -730,7 +840,7 @@ template<typename... Ts> class DisplayPageShowPrevAction : public Action<Ts...>
public: public:
DisplayPageShowPrevAction(Display *buffer) : buffer_(buffer) {} DisplayPageShowPrevAction(Display *buffer) : buffer_(buffer) {}
void play(Ts... x) override { this->buffer_->show_prev_page(); } void play(const Ts &...x) override { this->buffer_->show_prev_page(); }
Display *buffer_; Display *buffer_;
}; };
@@ -740,7 +850,7 @@ template<typename... Ts> class DisplayIsDisplayingPageCondition : public Conditi
DisplayIsDisplayingPageCondition(Display *parent) : parent_(parent) {} DisplayIsDisplayingPageCondition(Display *parent) : parent_(parent) {}
void set_page(DisplayPage *page) { this->page_ = page; } void set_page(DisplayPage *page) { this->page_ = page; }
bool check(Ts... x) override { return this->parent_->get_active_page() == this->page_; } bool check(const Ts &...x) override { return this->parent_->get_active_page() == this->page_; }
protected: protected:
Display *parent_; Display *parent_;

View File

@@ -10,7 +10,7 @@ template<typename... Ts> class UpAction : public Action<Ts...> {
public: public:
explicit UpAction(DisplayMenuComponent *menu) : menu_(menu) {} explicit UpAction(DisplayMenuComponent *menu) : menu_(menu) {}
void play(Ts... x) override { this->menu_->up(); } void play(const Ts &...x) override { this->menu_->up(); }
protected: protected:
DisplayMenuComponent *menu_; DisplayMenuComponent *menu_;
@@ -20,7 +20,7 @@ template<typename... Ts> class DownAction : public Action<Ts...> {
public: public:
explicit DownAction(DisplayMenuComponent *menu) : menu_(menu) {} explicit DownAction(DisplayMenuComponent *menu) : menu_(menu) {}
void play(Ts... x) override { this->menu_->down(); } void play(const Ts &...x) override { this->menu_->down(); }
protected: protected:
DisplayMenuComponent *menu_; DisplayMenuComponent *menu_;
@@ -30,7 +30,7 @@ template<typename... Ts> class LeftAction : public Action<Ts...> {
public: public:
explicit LeftAction(DisplayMenuComponent *menu) : menu_(menu) {} explicit LeftAction(DisplayMenuComponent *menu) : menu_(menu) {}
void play(Ts... x) override { this->menu_->left(); } void play(const Ts &...x) override { this->menu_->left(); }
protected: protected:
DisplayMenuComponent *menu_; DisplayMenuComponent *menu_;
@@ -40,7 +40,7 @@ template<typename... Ts> class RightAction : public Action<Ts...> {
public: public:
explicit RightAction(DisplayMenuComponent *menu) : menu_(menu) {} explicit RightAction(DisplayMenuComponent *menu) : menu_(menu) {}
void play(Ts... x) override { this->menu_->right(); } void play(const Ts &...x) override { this->menu_->right(); }
protected: protected:
DisplayMenuComponent *menu_; DisplayMenuComponent *menu_;
@@ -50,7 +50,7 @@ template<typename... Ts> class EnterAction : public Action<Ts...> {
public: public:
explicit EnterAction(DisplayMenuComponent *menu) : menu_(menu) {} explicit EnterAction(DisplayMenuComponent *menu) : menu_(menu) {}
void play(Ts... x) override { this->menu_->enter(); } void play(const Ts &...x) override { this->menu_->enter(); }
protected: protected:
DisplayMenuComponent *menu_; DisplayMenuComponent *menu_;
@@ -60,7 +60,7 @@ template<typename... Ts> class ShowAction : public Action<Ts...> {
public: public:
explicit ShowAction(DisplayMenuComponent *menu) : menu_(menu) {} explicit ShowAction(DisplayMenuComponent *menu) : menu_(menu) {}
void play(Ts... x) override { this->menu_->show(); } void play(const Ts &...x) override { this->menu_->show(); }
protected: protected:
DisplayMenuComponent *menu_; DisplayMenuComponent *menu_;
@@ -70,7 +70,7 @@ template<typename... Ts> class HideAction : public Action<Ts...> {
public: public:
explicit HideAction(DisplayMenuComponent *menu) : menu_(menu) {} explicit HideAction(DisplayMenuComponent *menu) : menu_(menu) {}
void play(Ts... x) override { this->menu_->hide(); } void play(const Ts &...x) override { this->menu_->hide(); }
protected: protected:
DisplayMenuComponent *menu_; DisplayMenuComponent *menu_;
@@ -80,7 +80,7 @@ template<typename... Ts> class ShowMainAction : public Action<Ts...> {
public: public:
explicit ShowMainAction(DisplayMenuComponent *menu) : menu_(menu) {} explicit ShowMainAction(DisplayMenuComponent *menu) : menu_(menu) {}
void play(Ts... x) override { this->menu_->show_main(); } void play(const Ts &...x) override { this->menu_->show_main(); }
protected: protected:
DisplayMenuComponent *menu_; DisplayMenuComponent *menu_;
@@ -88,7 +88,7 @@ template<typename... Ts> class ShowMainAction : public Action<Ts...> {
template<typename... Ts> class IsActiveCondition : public Condition<Ts...> { template<typename... Ts> class IsActiveCondition : public Condition<Ts...> {
public: public:
explicit IsActiveCondition(DisplayMenuComponent *menu) : menu_(menu) {} explicit IsActiveCondition(DisplayMenuComponent *menu) : menu_(menu) {}
bool check(Ts... x) override { return this->menu_->is_active(); } bool check(const Ts &...x) override { return this->menu_->is_active(); }
protected: protected:
DisplayMenuComponent *menu_; DisplayMenuComponent *menu_;

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

@@ -59,12 +59,12 @@ class DS1307Component : public time::RealTimeClock, public i2c::I2CDevice {
template<typename... Ts> class WriteAction : public Action<Ts...>, public Parented<DS1307Component> { template<typename... Ts> class WriteAction : public Action<Ts...>, public Parented<DS1307Component> {
public: public:
void play(Ts... x) override { this->parent_->write_time(); } void play(const Ts &...x) override { this->parent_->write_time(); }
}; };
template<typename... Ts> class ReadAction : public Action<Ts...>, public Parented<DS1307Component> { template<typename... Ts> class ReadAction : public Action<Ts...>, public Parented<DS1307Component> {
public: public:
void play(Ts... x) override { this->parent_->read_time(); } void play(const Ts &...x) override { this->parent_->read_time(); }
}; };
} // namespace ds1307 } // namespace ds1307
} // namespace esphome } // namespace esphome

View File

@@ -51,15 +51,15 @@ class DutyTimeSensor : public sensor::Sensor, public PollingComponent {
template<typename... Ts> class BaseAction : public Action<Ts...>, public Parented<DutyTimeSensor> {}; template<typename... Ts> class BaseAction : public Action<Ts...>, public Parented<DutyTimeSensor> {};
template<typename... Ts> class StartAction : public BaseAction<Ts...> { template<typename... Ts> class StartAction : public BaseAction<Ts...> {
void play(Ts... x) override { this->parent_->start(); } void play(const Ts &...x) override { this->parent_->start(); }
}; };
template<typename... Ts> class StopAction : public BaseAction<Ts...> { template<typename... Ts> class StopAction : public BaseAction<Ts...> {
void play(Ts... x) override { this->parent_->stop(); } void play(const Ts &...x) override { this->parent_->stop(); }
}; };
template<typename... Ts> class ResetAction : public BaseAction<Ts...> { template<typename... Ts> class ResetAction : public BaseAction<Ts...> {
void play(Ts... x) override { this->parent_->reset(); } void play(const Ts &...x) override { this->parent_->reset(); }
}; };
template<typename... Ts> class RunningCondition : public Condition<Ts...>, public Parented<DutyTimeSensor> { template<typename... Ts> class RunningCondition : public Condition<Ts...>, public Parented<DutyTimeSensor> {
@@ -67,7 +67,7 @@ template<typename... Ts> class RunningCondition : public Condition<Ts...>, publi
explicit RunningCondition(DutyTimeSensor *parent, bool state) : Parented(parent), state_(state) {} explicit RunningCondition(DutyTimeSensor *parent, bool state) : Parented(parent), state_(state) {}
protected: protected:
bool check(Ts... x) override { return this->parent_->is_running() == this->state_; } bool check(const Ts &...x) override { return this->parent_->is_running() == this->state_; }
bool state_; bool state_;
}; };

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_WIDTH): cv.int_,
cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, cv.Required(CONF_HEIGHT): cv.int_,
cv.Required(CONF_MODEL): cv.one_of(*MODELS, lower=True, space="-"), }
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(
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.positive_time_period_milliseconds,
cv.Range(max=core.TimePeriod(milliseconds=500)), 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 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) {
if (this->state_ == EPaperState::RESET) {
this->reset_pin_->digital_write(false); this->reset_pin_->digital_write(false);
this->disable_loop(); return false;
this->set_timeout(this->reset_duration_, [this] {
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_; auto *sequence = this->init_sequence_;
while (index != sequence_size) { auto length = this->init_sequence_length_;
if (sequence_size - index < 2) { while (index != length) {
if (length - index < 2) {
this->mark_failed("Malformed init sequence"); this->mark_failed("Malformed init sequence");
return; return;
} }
const auto *ptr = sequence + index; const uint8_t cmd = sequence[index++];
const uint8_t length = ptr[1]; if (const uint8_t x = sequence[index++]; x == DELAY_FLAG) {
if (sequence_size - index < length + 2) { ESP_LOGV(TAG, "Delay %dms", cmd);
this->mark_failed("Malformed init sequence"); 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; return;
} }
ESP_LOGV(TAG, "Command %02X, length %d", cmd, num_args);
this->cmd_data(ptr); this->cmd_data(cmd, sequence + index, num_args);
index += length + 2; 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,
CYAN,
SKIP_2,
};
static uint8_t color_to_hex(Color color) {
// --- Step 1: Check for Grayscale (Black or White) ---
// We define "grayscale" as a color where the min and max components
// are close to each other.
unsigned char max_rgb = std::max({color.r, color.g, color.b});
unsigned char min_rgb = std::min({color.r, color.g, color.b});
if ((max_rgb - min_rgb) < GRAY_THRESHOLD) {
// It's a shade of gray. Map to BLACK or WHITE.
// We split the luminance at the halfway point (382 = (255*3)/2)
if ((static_cast<int>(color.r) + color.g + color.b) > 382) {
return WHITE;
} }
} else { return BLACK;
return 0x3; // Red (or Magenta)
} }
} else { // --- Step 2: Check for Primary/Secondary Colors ---
if (color.green > 127) { // If it's not gray, it's a color. We check which components are
if (color.blue > 127) { // "on" (over 128) vs "off". This divides the RGB cube into 8 corners.
return 0x5; // Cyan -> Blue bool r_on = (color.r > 128);
} else { bool g_on = (color.g > 128);
return 0x6; // Green bool b_on = (color.b > 128);
if (r_on && g_on && !b_on) {
return YELLOW;
} }
} else { if (r_on && !g_on && !b_on) {
if (color.blue > 127) { return RED;
return 0x5; // Blue
} else {
return 0x0; // Black
} }
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); // We store 2 pixels per byte
} else { this->buffer_.fill(pixel_color + (pixel_color << 4));
pixel_color = 0x1;
} }
// We store 8 bitset<3> in 3 bytes void EPaperSpectraE6::clear() {
// | byte 1 | byte 2 | byte 3 | // clear buffer to white, just like real paper.
// |aaabbbaa|abbbaaab|bbaaabbb| this->fill(COLOR_ON);
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() {
// 6 colors buffer, 1 pixel = 3 bits, we will store 8 pixels in 24 bits = 3 bytes
return this->get_width_controller() * this->get_height_internal() / 8u * 3u;
} }
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);
if (buf_idx == sizeof bytes_to_send) {
this->start_data_(); this->start_data_();
this->write_array(bytes_to_send, sizeof(bytes_to_send)); this->write_array(bytes_to_send, buf_idx);
this->end_data_(); 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

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