1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-01 23:51:47 +00:00

Compare commits

...

172 Commits

Author SHA1 Message Date
Jesse Hills
a6e7e48b73 10 years 2025-06-26 16:30:18 +12:00
Jesse Hills
f80610d958 365 days 2025-06-26 16:23:10 +12:00
Jesse Hills
1aacf13888 Use shared workflow for locking 2025-06-26 16:19:48 +12:00
J. Nick Koston
23b1e428de Optimize Application class memory layout and reduce loop_interval size (#9208) 2025-06-26 15:35:01 +12:00
J. Nick Koston
f029f4f20e Fix missing protobuf message dump for batched messages with very verbose logging (#9206) 2025-06-26 13:57:41 +12:00
J. Nick Koston
79e3d2b2d7 Optimize API connection memory with tagged pointers (#9203) 2025-06-26 13:55:12 +12:00
J. Nick Koston
c74e5e0f04 Optimize TemplatableValue memory (#9202) 2025-06-26 13:51:51 +12:00
J. Nick Koston
15ef93ccc9 Optimize API connection loop performance (#9184) 2025-06-26 13:47:41 +12:00
J. Nick Koston
e017250445 Reduce logger CPU usage by disabling loop when buffer is empty (#9160)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-06-26 13:44:07 +12:00
J. Nick Koston
17497eec43 Reduce memory required for sensor entities (#9201)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-06-25 18:15:59 -05:00
Clyde Stubbs
6d0c6329ad [lvgl] Allow linear positioning of grid cells (#9196) 2025-06-26 10:45:14 +12:00
Clyde Stubbs
f35be6b5cc [binary_sensor] Add timeout filter (#9198) 2025-06-25 14:09:43 +02:00
DanielV
b18ff48b4a [API] Sub devices and areas (#8544)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2025-06-25 12:03:41 +00:00
Artem Draft
7c28134214 Rename kVARh/VARh to kvarh/varh (#9191) 2025-06-25 22:36:24 +12:00
Rodrigo Martín
16860e8a30 fix(MQTT): Call disconnect callback on DNS error (#9016)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-06-25 22:20:29 +12:00
Jonathan Swoboda
5362d1a89f [esp32_hall] Add dummy component (#9125)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-06-25 21:49:31 +12:00
Keith Burzinski
5531296ee0 [ld2410] Use `App.get_loop_component_start_time()`, shorten log messages (#9194)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2025-06-25 21:48:32 +12:00
Keith Burzinski
47db5e26f3 [ld2420] Shorten log messages + other clean-up (#9200) 2025-06-25 03:16:05 -05:00
Keith Burzinski
cf5197b68a [ld2450] Use `App.get_loop_component_start_time()`, shorten log messages (#9192) 2025-06-25 03:15:50 -05:00
Keith Burzinski
9f831e91b3 [helpers] Add `format_mac_address_pretty` function, migrate components (#9193) 2025-06-25 12:36:33 +12:00
Javier Peletier
2df0ebd895 [modbus_controller] Fix modbus read_lambda precision for non-floats or large integers (#9159) 2025-06-25 11:31:23 +12:00
Jesse Hills
7ad6dab383 [mqtt] Don't wait for connection unless configured to (#8933) 2025-06-24 13:31:38 +12:00
Clyde Stubbs
612c8d5841 [lvgl] Fix dangling pointer issue with qrcode (#9190) 2025-06-24 09:43:40 +10:00
Cody Cutrer
a35e476be5 [opt3001] New component (#6625)
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
2025-06-23 14:31:20 -05:00
Jesse Hills
87a7157fc4 Merge branch 'release' into dev 2025-06-24 07:28:40 +12:00
Jesse Hills
fa34adbf6c Merge pull request #9185 from esphome/bump-2025.6.1
2025.6.1
2025-06-24 07:27:59 +12:00
dependabot[bot]
ac942e0670 Bump aioesphomeapi from 33.1.0 to 33.1.1 (#9187) 2025-06-23 19:58:32 +02:00
Jesse Hills
22e360d479 Bump version to 2025.6.1 2025-06-23 23:32:22 +12:00
myhomeiot
649936200e Restore access to BLEScanResult as get_scan_result (#9148) 2025-06-23 23:32:22 +12:00
rwrozelle
5d6e690c12 Fixes for setup of OpenThread either using TLV or entering Credentials directly (#9157)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-06-23 23:32:22 +12:00
Jesse Hills
2f2ecadae7 [config validation] Add more ip address / network validators (#9181) 2025-06-23 23:32:22 +12:00
J. Nick Koston
6dfb9eba61 Fix missing BLE GAP events causing RSSI sensor and beacon failures (#9138) 2025-06-23 23:32:22 +12:00
Edward Firmo
24587fe875 [nextion] Fix command spacing double timing and response blocking issues (#9134) 2025-06-23 23:32:22 +12:00
J. Nick Koston
a1aebe6a2c Eliminate memory fragmentation with BLE event pool (#9101) 2025-06-23 23:32:22 +12:00
Gustavo Ambrozio
2ad266582f [online_image] Allow suppressing update on url change (#8885) 2025-06-23 20:40:07 +10:00
JonasB2497
1a47164876 Feature fontmetrics (#8978) 2025-06-23 14:47:47 +10:00
myhomeiot
cd22723623 Restore access to BLEScanResult as get_scan_result (#9148) 2025-06-23 15:42:20 +12:00
rwrozelle
aecaffa2f5 Fixes for setup of OpenThread either using TLV or entering Credentials directly (#9157)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-06-23 15:41:29 +12:00
Jesse Hills
87df3596a2 [config validation] Add more ip address / network validators (#9181) 2025-06-23 15:41:06 +12:00
Clyde Stubbs
41c7852128 [lvgl] Use styles instead of object properties for themes (#9116) 2025-06-23 14:25:26 +12:00
Clyde Stubbs
78ec9856fb [lvgl] Add start_value to bar; make values templatable and updateable (#9056) 2025-06-23 14:23:41 +12:00
J. Nick Koston
2a45467bf6 Pre-reserve looping components vector to reduce memory allocations (#9177) 2025-06-23 14:10:09 +12:00
J. Nick Koston
7fc5bfd787 Reduce RAM usage for scheduled tasks (#9180) 2025-06-23 14:09:34 +12:00
J. Nick Koston
04f592ba6d Fix slow noise handshake by reading multiple messages per loop (#9130) 2025-06-23 14:07:53 +12:00
J. Nick Koston
59889a6286 Reduce Logger memory usage by optimizing variable sizes (#9161) 2025-06-23 14:06:02 +12:00
Jesse Hills
dc5cbd4df8 [const] Move `CONF_DEVICES to const.py` (#9179) 2025-06-23 09:54:49 +12:00
Edward Firmo
7ab9083d77 [nextion] Revert to millis() on recv_ret_string_ (#9168) 2025-06-22 20:56:50 +00:00
dependabot[bot]
788803d588 Bump flake8 from 7.2.0 to 7.3.0 (#9172)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-06-22 19:05:54 +00:00
dependabot[bot]
cbfd904b9f Bump aioesphomeapi from 32.2.4 to 33.1.0 (#9173)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-22 11:00:42 +00:00
Jimmy Hedman
c81dbf9d59 Improve on C++17 (#9170) 2025-06-22 12:09:38 +02:00
dependabot[bot]
ac9c608542 Bump esptool from 4.8.1 to 4.9.0 (#9158) 2025-06-21 18:13:07 +02:00
Edward Firmo
a6c20853ca [nextion] Extract common upload_end_ function to shared file (#9155) 2025-06-21 11:26:14 +02:00
Jesse Hills
4ef0264ed3 Clean up RAMAllocators in light related code (#9142) 2025-06-21 17:32:24 +10:00
Clyde Stubbs
169db9cc0a [spi] Enable >6 devices with ESP-IDF (#9128) 2025-06-21 07:55:08 +10:00
RoganDawes
b693b8ccb1 [usb-host] Add support for USB Hubs (#9154) 2025-06-20 22:03:15 +10:00
Keith Burzinski
3e98cceb00 [bh1750] Remove redundant platform name from logging (#9153) 2025-06-20 12:33:46 +02:00
Keith Burzinski
46d962dcf1 [wifi, wifi_info] Tidy up/shorten more log messages (#9151) 2025-06-20 22:02:36 +12:00
Edward Firmo
7dbad42470 [nextion] Cached timing optimization (#9150)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
2025-06-20 07:46:12 +00:00
Edward Firmo
eb97781f68 [nextion] Add command queuing to prevent command loss when spacing is active (#9139)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-06-20 01:38:40 -05:00
Jesse Hills
4d0f8528d2 [esp32_camera] Allow sharing i2c bus (#9137)
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
2025-06-19 01:31:19 -05:00
Jesse Hills
2c17b2bacc [i2c] Make `get_port()` public (#9146) 2025-06-19 05:44:33 +00:00
Jesse Hills
30bea20f7a Clean up RAMAllocators in display related code (#9141) 2025-06-19 05:17:08 +00:00
Jesse Hills
d4cb4ef994 Clean up RAMAllocators in http_request code (#9143) 2025-06-19 03:11:18 +00:00
J. Nick Koston
9c90ca297a Fix missing BLE GAP events causing RSSI sensor and beacon failures (#9138) 2025-06-19 03:03:09 +00:00
Jesse Hills
a9e1a4cef3 Clean up RAMAllocators in audio related code (#9140) 2025-06-19 02:53:54 +00:00
J. Nick Koston
0ce3621ac0 Disable Ethernet loop polling when connected and stable (#9102) 2025-06-19 14:49:31 +12:00
Jesse Hills
d527398dae [i2c] Expose internal i2c bus port number (#9136) 2025-06-18 20:50:47 -05:00
Edward Firmo
2e9ac8945d [nextion] Fix command spacing double timing and response blocking issues (#9134) 2025-06-19 13:41:20 +12:00
J. Nick Koston
40a5638005 Optimize OTA loop to avoid unnecessary stack allocations (#9129) 2025-06-19 13:33:00 +12:00
J. Nick Koston
8ba22183b9 Add enable_loop_soon_any_context() for thread and ISR-safe loop enabling (#9127) 2025-06-19 13:30:41 +12:00
J. Nick Koston
2e11e66db4 Optimize bluetooth_proxy memory usage on ESP32 (#9114)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-06-19 13:11:13 +12:00
J. Nick Koston
eeb0710ad4 Optimize API component memory usage by reordering class members to reduce padding (#9111) 2025-06-19 13:08:25 +12:00
J. Nick Koston
43c677ef37 Optimize API server performance by using cached loop time (#9104) 2025-06-19 12:12:14 +12:00
J. Nick Koston
95544e489d Use smaller atomic types for ESP32 BLE Tracker ring buffer indices (#9106) 2025-06-19 12:10:50 +12:00
J. Nick Koston
a08d021f77 Reduce code duplication in auto-generated API protocol code (#9097) 2025-06-19 12:10:01 +12:00
J. Nick Koston
b7b1d17ecb Remove empty generated protobuf methods (#9098) 2025-06-19 12:06:39 +12:00
Jonathan Swoboda
aa180b9581 Bump ESP32 Arduino version to 3.1.3 (#8604)
Co-authored-by: Kuba Szczodrzyński <kuba@szczodrzynski.pl>
2025-06-19 08:16:25 +12:00
dependabot[bot]
57388254c4 Bump pytest from 8.4.0 to 8.4.1 (#9131)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-18 19:56:26 +00:00
dependabot[bot]
f16f4e2c4c Bump aioesphomeapi from 32.2.3 to 32.2.4 (#9132)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-18 19:55:59 +00:00
dependabot[bot]
89b70e4352 Bump docker/setup-buildx-action from 3.11.0 to 3.11.1 in the docker-actions group (#9133)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-18 21:37:21 +02:00
J. Nick Koston
6667336bd8 Eliminate memory fragmentation with BLE event pool (#9101) 2025-06-18 21:57:49 +12:00
Kuba Szczodrzyński
669ef7a0b1 [web_server] Upgrade ESPAsync libraries (#8867) 2025-06-18 21:51:00 +12:00
Severin von Wnuck-Lipinski
c612985930 Add support for Xiaomi XMWSDJ04MMC (#8591) 2025-06-18 21:49:39 +12:00
J. Nick Koston
2e534ce41e Reduce CPU overhead by allowing components to disable their loop() (#9089) 2025-06-18 21:49:25 +12:00
Jesse Hills
fedb54bb38 Merge branch 'release' into dev 2025-06-18 21:41:59 +12:00
Jesse Hills
68f5144084 Merge pull request #9126 from esphome/bump-2025.6.0
2025.6.0
2025-06-18 21:41:00 +12:00
Jonathan Swoboda
fd3c22945b [i2s_audio] Bump esphome/ESP32-audioI2S to 2.3.0 (#9124) 2025-06-18 04:18:23 +00:00
Jonathan Swoboda
53496a1ecd [heatpumpir] Bump HeatpumpIR to 1.0.35 (#9123) 2025-06-18 04:15:26 +00:00
Michael Hansen
da5cf99549 Add intent progress event to voice assistant enum (#9103) 2025-06-18 15:15:37 +12:00
Jesse Hills
849c858495 Bump version to 2025.6.0 2025-06-18 14:16:24 +12:00
Jesse Hills
808f964841 Merge branch 'beta' into dev 2025-06-18 12:37:57 +12:00
Jesse Hills
16a0f9db97 Merge pull request #9122 from esphome/bump-2025.6.0b3
2025.6.0b3
2025-06-18 12:37:25 +12:00
J. Nick Koston
3bc5db4fd7 Bump ruff in pre-commit to 0.12.0 (#9121) 2025-06-18 10:54:45 +12:00
Jesse Hills
5269523ca1 Bump version to 2025.6.0b3 2025-06-18 10:17:56 +12:00
J. Nick Koston
89267b9e06 Reduce Switch component memory usage by 8 bytes per instance (#9112) 2025-06-18 10:09:11 +12:00
J. Nick Koston
4bc9646e8f Optimize LightState memory layout (#9113) 2025-06-18 10:09:11 +12:00
Clyde Stubbs
fd83628c49 [spi] Cater for non-word-aligned buffers on esp8266 (#9108) 2025-06-18 10:09:11 +12:00
Kevin Ahrendt
62abfbec9e [i2s_audio] Bugfix: crashes when unlocking i2s bus multiple times (#9100) 2025-06-18 10:09:11 +12:00
Keith Burzinski
7cc0008837 [i2s_audio] Add `dump_config` methods, shorten log messages (#9099) 2025-06-18 10:09:11 +12:00
dependabot[bot]
0bf613bd34 Bump ruff from 0.11.13 to 0.12.0 (#9120)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-18 00:08:22 +02:00
Jonathan Swoboda
43ab63455b Pin libretiny to 1.9.1 (#9118) 2025-06-17 22:42:36 +02:00
J. Nick Koston
47e7988c8e Reduce Switch component memory usage by 8 bytes per instance (#9112) 2025-06-17 13:14:03 -05:00
J. Nick Koston
7ed095e635 Optimize LightState memory layout (#9113) 2025-06-17 13:07:45 -05:00
Michael Hansen
cb8b0ec62e Add intent progress event to voice assistant enum (#9103) 2025-06-17 13:05:06 -05:00
J. Nick Koston
bf161f1eaa Resolve esphome::optional vs std::optional ambiguity in code generation (#9119) 2025-06-17 13:04:45 -05:00
Jonathan Swoboda
78c8447d1e [esp32_hall] Remove esp32_hall (#9117) 2025-06-17 15:47:42 +00:00
dependabot[bot]
5ffe50381a Bump docker/setup-buildx-action from 3.10.0 to 3.11.0 in the docker-actions group (#9105)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-17 10:41:54 +02:00
Jonathan Swoboda
b08bd0c24a Bump LibreTiny recommended version to 1.9.1 (#9110) 2025-06-17 04:41:18 +02:00
Clyde Stubbs
738ad8e9d3 [spi] Cater for non-word-aligned buffers on esp8266 (#9108) 2025-06-17 02:30:09 +00:00
Kevin Ahrendt
fa7c42511a [i2s_audio] Bugfix: crashes when unlocking i2s bus multiple times (#9100) 2025-06-17 12:59:07 +12:00
Keith Burzinski
68ef9cb3dc [i2s_audio] Add `dump_config` methods, shorten log messages (#9099) 2025-06-16 07:36:49 +00:00
Jesse Hills
8e176b9c61 Merge branch 'beta' into dev 2025-06-16 17:07:31 +12:00
Jesse Hills
426be153db Merge pull request #9094 from esphome/bump-2025.6.0b2
2025.6.0b2
2025-06-16 17:06:59 +12:00
Jesse Hills
c4f7c2d259 [ruff] Apply various ruff suggestions (#8947) 2025-06-15 22:13:14 -05:00
Jesse Hills
2a81efda0b Remove `std::` prefix as not all platforms have access yet. (#9095) 2025-06-16 12:55:51 +12:00
Jesse Hills
882bfc79c7 Remove `std::` prefix as not all platforms have access yet. (#9095) 2025-06-16 12:55:23 +12:00
dependabot[bot]
6bad276589 Bump aioesphomeapi from 32.2.1 to 32.2.3 (#9091)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-16 11:45:58 +12:00
Jesse Hills
47d8048a62 Bump version to 2025.6.0b2 2025-06-16 10:07:07 +12:00
J. Nick Koston
20d7ba5d7c Reduce Component blocking threshold memory usage by 2 bytes per component (#9081) 2025-06-16 10:07:07 +12:00
J. Nick Koston
e435e72654 Add common base classes for entity protobuf messages to reduce duplicate code (#9090) 2025-06-16 10:07:07 +12:00
J. Nick Koston
497d66f7ec Ensure we can send batches where the first message exceeds MAX_PACKET_SIZE (#9068) 2025-06-16 10:07:07 +12:00
Kevin Ahrendt
242b02a416 [i2s_audio] Check for a nullptr before disabling and deleting channel (#9062) 2025-06-16 10:07:07 +12:00
J. Nick Koston
9644a6bb9c Fix protobuf encoding size mismatch by passing force parameter in encode_string (#9074) 2025-06-16 10:07:07 +12:00
J. Nick Koston
70d66062d6 Make BLE queue lock free (#9088) 2025-06-16 10:07:07 +12:00
J. Nick Koston
39f6f9b0dc Implement a lock free ring buffer for BLEScanResult to avoid drops (#9087) 2025-06-16 10:07:07 +12:00
dhewg
0454dd4e07 [fan] fix initial FanCall to properly set speed (#8277) 2025-06-16 10:07:07 +12:00
J. Nick Koston
6f4e76c8f3 Fix unbound BLE event queue growth and reduce memory usage (#9052) 2025-06-16 10:07:07 +12:00
J. Nick Koston
5cdcf2415d Optimize Application area_ from std::string to const char* (#9085) 2025-06-16 10:07:07 +12:00
J. Nick Koston
1719a2e08b Fix API message encoding to return actual size instead of calculated size (#9073) 2025-06-16 10:07:07 +12:00
J. Nick Koston
5640a9fe73 Optimize memory usage by lazy-allocating raw callbacks in sensors (#9077) 2025-06-16 10:07:07 +12:00
J. Nick Koston
4787e22f61 Reduce entity memory usage by eliminating field shadowing and bit-packing (#9076) 2025-06-16 10:07:01 +12:00
J. Nick Koston
fb12e4e66a Small optimizations to api buffer helper (#9071) 2025-06-16 09:49:45 +12:00
J. Nick Koston
77740a1044 Optimize Component and Application state storage from uint32_t to uint8_t (#9082) 2025-06-16 09:49:45 +12:00
J. Nick Koston
1fdfe7578f Make ParseOnOffState enum uint8_t (#9083) 2025-06-16 09:49:45 +12:00
J. Nick Koston
ebecf7047e Fix captive_portal loading entire web_server (#9066) 2025-06-16 09:49:45 +12:00
Jesse Hills
00e8332bf5 [esp32] Dynamically set default framework based on variant (#9060) 2025-06-16 09:49:45 +12:00
Jesse Hills
5fc1f90822 [prometheus] Remove `cv.only_with_arduino` (#9061) 2025-06-16 09:49:45 +12:00
J. Nick Koston
0a1be3d19c Fix misleading comment in API (#9069) 2025-06-16 09:49:45 +12:00
Nate Clark
40db3146b9 Fix BYPASS_AUTO feature to work with or without an arming delay (#9051) 2025-06-16 09:49:45 +12:00
Edward Firmo
535c495b33 [nextion] Remove upload flags reset from success path to prevent TFT corruption (#9064) 2025-06-16 09:49:45 +12:00
J. Nick Koston
592446e430 Always perform select() when loop duration exceeds interval (#9058) 2025-06-16 09:49:45 +12:00
J. Nick Koston
7a5c9a821a Fix dashboard logging being escaped before parser (#9054) 2025-06-16 09:49:45 +12:00
J. Nick Koston
c17a3b6fcc Reduce Component memory usage by 20 bytes per component (#9080)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-06-16 09:34:37 +12:00
J. Nick Koston
28d11553e0 Reduce Component blocking threshold memory usage by 2 bytes per component (#9081) 2025-06-16 09:33:38 +12:00
J. Nick Koston
1dbebe90ba Add common base classes for entity protobuf messages to reduce duplicate code (#9090) 2025-06-16 09:29:25 +12:00
J. Nick Koston
06810e8e6a Ensure we can send batches where the first message exceeds MAX_PACKET_SIZE (#9068) 2025-06-16 09:22:14 +12:00
Kevin Ahrendt
bd85ba9b6a [i2s_audio] Check for a nullptr before disabling and deleting channel (#9062) 2025-06-16 09:19:50 +12:00
J. Nick Koston
be58cdda3b Fix protobuf encoding size mismatch by passing force parameter in encode_string (#9074) 2025-06-16 09:19:04 +12:00
J. Nick Koston
fcce4a8be6 Make BLE queue lock free (#9088) 2025-06-16 09:16:46 +12:00
J. Nick Koston
61a558a062 Implement a lock free ring buffer for BLEScanResult to avoid drops (#9087) 2025-06-16 08:53:45 +12:00
dhewg
59f69ac5ca [fan] fix initial FanCall to properly set speed (#8277) 2025-06-15 13:16:33 -05:00
dependabot[bot]
f82ac34784 Bump aioesphomeapi from 32.2.1 to 32.2.3 (#9091)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-15 10:42:01 -05:00
J. Nick Koston
07cf6e723b Fix unbound BLE event queue growth and reduce memory usage (#9052) 2025-06-15 04:45:41 +00:00
J. Nick Koston
78e3c6333f Optimize Application area_ from std::string to const char* (#9085) 2025-06-14 22:46:40 -05:00
J. Nick Koston
98e2684107 Fix API message encoding to return actual size instead of calculated size (#9073) 2025-06-15 15:46:02 +12:00
J. Nick Koston
cb019fff9a Optimize memory usage by lazy-allocating raw callbacks in sensors (#9077) 2025-06-15 15:28:15 +12:00
J. Nick Koston
4305c44440 Reduce entity memory usage by eliminating field shadowing and bit-packing (#9076) 2025-06-15 15:21:55 +12:00
J. Nick Koston
a1e4143600 Small optimizations to api buffer helper (#9071) 2025-06-15 14:55:03 +12:00
J. Nick Koston
374c33e8dc Optimize Component and Application state storage from uint32_t to uint8_t (#9082) 2025-06-15 14:48:53 +12:00
J. Nick Koston
dcfe7af9d3 Make ParseOnOffState enum uint8_t (#9083) 2025-06-15 14:44:45 +12:00
Keith Burzinski
049c7e00ca Move some consts to `const.py` (#9084) 2025-06-14 23:23:52 +00:00
Jimmy Hedman
ee37d2f9c8 Build with C++17 (#8603)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-06-14 08:21:39 -05:00
J. Nick Koston
92ea697119 Fix captive_portal loading entire web_server (#9066) 2025-06-14 08:19:41 -05:00
dependabot[bot]
1c488d375f Bump pytest-asyncio from 0.26.0 to 1.0.0 (#9067)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-13 18:40:18 -05:00
Jesse Hills
1a03b4949f [esp32] Dynamically set default framework based on variant (#9060) 2025-06-14 11:17:06 +12:00
Jesse Hills
731b7808cd [prometheus] Remove `cv.only_with_arduino` (#9061) 2025-06-14 11:08:07 +12:00
J. Nick Koston
d9da4cf24d Fix misleading comment in API (#9069) 2025-06-14 09:10:33 +12:00
Nate Clark
666a3ee5e9 Fix BYPASS_AUTO feature to work with or without an arming delay (#9051) 2025-06-13 13:31:00 -05:00
Nico B
02469c2d4c ina219: powerdown the sensor on shutdown (#9053) 2025-06-13 18:17:38 +00:00
Edward Firmo
2a629cae93 [nextion] Remove upload flags reset from success path to prevent TFT corruption (#9064) 2025-06-13 13:39:32 +12:00
dependabot[bot]
1f14c316a3 Bump pytest-cov from 6.1.1 to 6.2.1 (#9063)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-12 18:16:37 -05:00
J. Nick Koston
dac738a916 Always perform select() when loop duration exceeds interval (#9058) 2025-06-12 03:27:10 +00:00
396 changed files with 9578 additions and 3684 deletions

View File

@@ -49,7 +49,7 @@ jobs:
with:
python-version: "3.10"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.10.0
uses: docker/setup-buildx-action@v3.11.1
- name: Set TAG
run: |

View File

@@ -1,28 +1,13 @@
---
name: Lock
name: Lock closed issues and PRs
on:
schedule:
- cron: "30 0 * * *"
- cron: "30 0 * * *" # Run daily at 00:30 UTC
workflow_dispatch:
permissions:
issues: write
pull-requests: write
concurrency:
group: lock
jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v5.0.1
uses: esphome/workflows/.github/workflows/lock.yml@main
with:
pr-inactive-days: "1"
pr-lock-reason: ""
exclude-any-pr-labels: keep-open
issue-inactive-days: "7"
issue-lock-reason: ""
exclude-any-issue-labels: keep-open
since-days: 3650

View File

@@ -99,7 +99,7 @@ jobs:
python-version: "3.10"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.10.0
uses: docker/setup-buildx-action@v3.11.1
- name: Log in to docker hub
uses: docker/login-action@v3.4.0
@@ -178,7 +178,7 @@ jobs:
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.10.0
uses: docker/setup-buildx-action@v3.11.1
- name: Log in to docker hub
if: matrix.registry == 'dockerhub'

View File

@@ -4,7 +4,7 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.11.10
rev: v0.12.0
hooks:
# Run the linter.
- id: ruff
@@ -12,7 +12,7 @@ repos:
# Run the formatter.
- id: ruff-format
- repo: https://github.com/PyCQA/flake8
rev: 7.2.0
rev: 7.3.0
hooks:
- id: flake8
additional_dependencies:

View File

@@ -323,6 +323,7 @@ esphome/components/one_wire/* @ssieb
esphome/components/online_image/* @clydebarrow @guillempages
esphome/components/opentherm/* @olegtarasov
esphome/components/openthread/* @mrene
esphome/components/opt3001/* @ccutrer
esphome/components/ota/* @esphome/core
esphome/components/output/* @esphome/core
esphome/components/packet_transport/* @clydebarrow
@@ -520,6 +521,7 @@ esphome/components/xiaomi_lywsd03mmc/* @ahpohl
esphome/components/xiaomi_mhoc303/* @drug123
esphome/components/xiaomi_mhoc401/* @vevsvevs
esphome/components/xiaomi_rtcgq02lm/* @jesserockz
esphome/components/xiaomi_xmwsdj04mmc/* @medusalix
esphome/components/xl9535/* @mreditor97
esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68
esphome/components/xxtea/* @clydebarrow

View File

@@ -22,6 +22,7 @@ from esphome.cpp_generator import ( # noqa: F401
TemplateArguments,
add,
add_build_flag,
add_build_unflag,
add_define,
add_global,
add_library,
@@ -34,6 +35,7 @@ from esphome.cpp_generator import ( # noqa: F401
process_lambda,
progmem_array,
safe_exp,
set_cpp_standard,
statement,
static_const_array,
templatable,

View File

@@ -193,14 +193,13 @@ void AcDimmer::setup() {
setTimer1Callback(&timer_interrupt);
#endif
#ifdef USE_ESP32
// 80 Divider -> 1 count=1µs
dimmer_timer = timerBegin(0, 80, true);
timerAttachInterrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr, true);
// timer frequency of 1mhz
dimmer_timer = timerBegin(1000000);
timerAttachInterrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr);
// For ESP32, we can't use dynamic interval calculation because the timerX functions
// are not callable from ISR (placed in flash storage).
// Here we just use an interrupt firing every 50 µs.
timerAlarmWrite(dimmer_timer, 50, true);
timerAlarmEnable(dimmer_timer);
timerAlarm(dimmer_timer, 50, true, 0);
#endif
}
void AcDimmer::write_state(float state) {

View File

@@ -14,8 +14,8 @@ from esphome.const import (
CONF_WEB_SERVER,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass
from esphome.cpp_helpers import setup_entity
CODEOWNERS = ["@grahambrown11", "@hwstar"]
IS_PLATFORM_COMPONENT = True
@@ -149,6 +149,9 @@ _ALARM_CONTROL_PANEL_SCHEMA = (
)
_ALARM_CONTROL_PANEL_SCHEMA.add_extra(entity_duplicate_validator("alarm_control_panel"))
def alarm_control_panel_schema(
class_: MockObjClass,
*,
@@ -190,7 +193,7 @@ ALARM_CONTROL_PANEL_CONDITION_SCHEMA = maybe_simple_id(
async def setup_alarm_control_panel_core_(var, config):
await setup_entity(var, config)
await setup_entity(var, config, "alarm_control_panel")
for conf in config.get(CONF_ON_STATE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)

View File

@@ -17,7 +17,11 @@ void Anova::setup() {
this->current_request_ = 0;
}
void Anova::loop() {}
void Anova::loop() {
// Parent BLEClientNode has a loop() method, but this component uses
// polling via update() and BLE callbacks so loop isn't needed
this->disable_loop();
}
void Anova::control(const ClimateCall &call) {
if (call.get_mode().has_value()) {

View File

@@ -188,6 +188,17 @@ message DeviceInfoRequest {
// Empty
}
message AreaInfo {
uint32 area_id = 1;
string name = 2;
}
message DeviceInfo {
uint32 device_id = 1;
string name = 2;
uint32 area_id = 3;
}
message DeviceInfoResponse {
option (id) = 10;
option (source) = SOURCE_SERVER;
@@ -236,6 +247,12 @@ message DeviceInfoResponse {
// Supports receiving and saving api encryption key
bool api_encryption_supported = 19;
repeated DeviceInfo devices = 20;
repeated AreaInfo areas = 21;
// Top-level area info to phase out suggested_area
AreaInfo area = 22;
}
message ListEntitiesRequest {
@@ -266,6 +283,7 @@ enum EntityCategory {
// ==================== BINARY SENSOR ====================
message ListEntitiesBinarySensorResponse {
option (id) = 12;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_BINARY_SENSOR";
@@ -279,9 +297,11 @@ message ListEntitiesBinarySensorResponse {
bool disabled_by_default = 7;
string icon = 8;
EntityCategory entity_category = 9;
uint32 device_id = 10;
}
message BinarySensorStateResponse {
option (id) = 21;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_BINARY_SENSOR";
option (no_delay) = true;
@@ -296,6 +316,7 @@ message BinarySensorStateResponse {
// ==================== COVER ====================
message ListEntitiesCoverResponse {
option (id) = 13;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_COVER";
@@ -312,6 +333,7 @@ message ListEntitiesCoverResponse {
string icon = 10;
EntityCategory entity_category = 11;
bool supports_stop = 12;
uint32 device_id = 13;
}
enum LegacyCoverState {
@@ -325,6 +347,7 @@ enum CoverOperation {
}
message CoverStateResponse {
option (id) = 22;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_COVER";
option (no_delay) = true;
@@ -367,6 +390,7 @@ message CoverCommandRequest {
// ==================== FAN ====================
message ListEntitiesFanResponse {
option (id) = 14;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_FAN";
@@ -383,6 +407,7 @@ message ListEntitiesFanResponse {
string icon = 10;
EntityCategory entity_category = 11;
repeated string supported_preset_modes = 12;
uint32 device_id = 13;
}
enum FanSpeed {
FAN_SPEED_LOW = 0;
@@ -395,6 +420,7 @@ enum FanDirection {
}
message FanStateResponse {
option (id) = 23;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_FAN";
option (no_delay) = true;
@@ -444,6 +470,7 @@ enum ColorMode {
}
message ListEntitiesLightResponse {
option (id) = 15;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_LIGHT";
@@ -464,9 +491,11 @@ message ListEntitiesLightResponse {
bool disabled_by_default = 13;
string icon = 14;
EntityCategory entity_category = 15;
uint32 device_id = 16;
}
message LightStateResponse {
option (id) = 24;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_LIGHT";
option (no_delay) = true;
@@ -536,6 +565,7 @@ enum SensorLastResetType {
message ListEntitiesSensorResponse {
option (id) = 16;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SENSOR";
@@ -554,9 +584,11 @@ message ListEntitiesSensorResponse {
SensorLastResetType legacy_last_reset_type = 11;
bool disabled_by_default = 12;
EntityCategory entity_category = 13;
uint32 device_id = 14;
}
message SensorStateResponse {
option (id) = 25;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SENSOR";
option (no_delay) = true;
@@ -571,6 +603,7 @@ message SensorStateResponse {
// ==================== SWITCH ====================
message ListEntitiesSwitchResponse {
option (id) = 17;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SWITCH";
@@ -584,9 +617,11 @@ message ListEntitiesSwitchResponse {
bool disabled_by_default = 7;
EntityCategory entity_category = 8;
string device_class = 9;
uint32 device_id = 10;
}
message SwitchStateResponse {
option (id) = 26;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SWITCH";
option (no_delay) = true;
@@ -607,6 +642,7 @@ message SwitchCommandRequest {
// ==================== TEXT SENSOR ====================
message ListEntitiesTextSensorResponse {
option (id) = 18;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_TEXT_SENSOR";
@@ -619,9 +655,11 @@ message ListEntitiesTextSensorResponse {
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
string device_class = 8;
uint32 device_id = 9;
}
message TextSensorStateResponse {
option (id) = 27;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_TEXT_SENSOR";
option (no_delay) = true;
@@ -789,6 +827,7 @@ message ExecuteServiceRequest {
// ==================== CAMERA ====================
message ListEntitiesCameraResponse {
option (id) = 43;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_ESP32_CAMERA";
@@ -799,6 +838,7 @@ message ListEntitiesCameraResponse {
bool disabled_by_default = 5;
string icon = 6;
EntityCategory entity_category = 7;
uint32 device_id = 8;
}
message CameraImageResponse {
@@ -869,6 +909,7 @@ enum ClimatePreset {
}
message ListEntitiesClimateResponse {
option (id) = 46;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_CLIMATE";
@@ -900,9 +941,11 @@ message ListEntitiesClimateResponse {
bool supports_target_humidity = 23;
float visual_min_humidity = 24;
float visual_max_humidity = 25;
uint32 device_id = 26;
}
message ClimateStateResponse {
option (id) = 47;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_CLIMATE";
option (no_delay) = true;
@@ -964,6 +1007,7 @@ enum NumberMode {
}
message ListEntitiesNumberResponse {
option (id) = 49;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_NUMBER";
@@ -981,9 +1025,11 @@ message ListEntitiesNumberResponse {
string unit_of_measurement = 11;
NumberMode mode = 12;
string device_class = 13;
uint32 device_id = 14;
}
message NumberStateResponse {
option (id) = 50;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_NUMBER";
option (no_delay) = true;
@@ -1007,6 +1053,7 @@ message NumberCommandRequest {
// ==================== SELECT ====================
message ListEntitiesSelectResponse {
option (id) = 52;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SELECT";
@@ -1019,9 +1066,11 @@ message ListEntitiesSelectResponse {
repeated string options = 6;
bool disabled_by_default = 7;
EntityCategory entity_category = 8;
uint32 device_id = 9;
}
message SelectStateResponse {
option (id) = 53;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SELECT";
option (no_delay) = true;
@@ -1045,6 +1094,7 @@ message SelectCommandRequest {
// ==================== SIREN ====================
message ListEntitiesSirenResponse {
option (id) = 55;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SIREN";
@@ -1059,9 +1109,11 @@ message ListEntitiesSirenResponse {
bool supports_duration = 8;
bool supports_volume = 9;
EntityCategory entity_category = 10;
uint32 device_id = 11;
}
message SirenStateResponse {
option (id) = 56;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SIREN";
option (no_delay) = true;
@@ -1102,6 +1154,7 @@ enum LockCommand {
}
message ListEntitiesLockResponse {
option (id) = 58;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_LOCK";
@@ -1120,9 +1173,11 @@ message ListEntitiesLockResponse {
// Not yet implemented:
string code_format = 11;
uint32 device_id = 12;
}
message LockStateResponse {
option (id) = 59;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_LOCK";
option (no_delay) = true;
@@ -1145,6 +1200,7 @@ message LockCommandRequest {
// ==================== BUTTON ====================
message ListEntitiesButtonResponse {
option (id) = 61;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_BUTTON";
@@ -1157,6 +1213,7 @@ message ListEntitiesButtonResponse {
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
string device_class = 8;
uint32 device_id = 9;
}
message ButtonCommandRequest {
option (id) = 62;
@@ -1196,6 +1253,7 @@ message MediaPlayerSupportedFormat {
}
message ListEntitiesMediaPlayerResponse {
option (id) = 63;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_MEDIA_PLAYER";
@@ -1211,9 +1269,12 @@ message ListEntitiesMediaPlayerResponse {
bool supports_pause = 8;
repeated MediaPlayerSupportedFormat supported_formats = 9;
uint32 device_id = 10;
}
message MediaPlayerStateResponse {
option (id) = 64;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_MEDIA_PLAYER";
option (no_delay) = true;
@@ -1615,6 +1676,7 @@ enum VoiceAssistantEvent {
VOICE_ASSISTANT_STT_VAD_END = 12;
VOICE_ASSISTANT_TTS_STREAM_START = 98;
VOICE_ASSISTANT_TTS_STREAM_END = 99;
VOICE_ASSISTANT_INTENT_PROGRESS = 100;
}
message VoiceAssistantEventData {
@@ -1735,6 +1797,7 @@ enum AlarmControlPanelStateCommand {
message ListEntitiesAlarmControlPanelResponse {
option (id) = 94;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_ALARM_CONTROL_PANEL";
@@ -1748,10 +1811,12 @@ message ListEntitiesAlarmControlPanelResponse {
uint32 supported_features = 8;
bool requires_code = 9;
bool requires_code_to_arm = 10;
uint32 device_id = 11;
}
message AlarmControlPanelStateResponse {
option (id) = 95;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_ALARM_CONTROL_PANEL";
option (no_delay) = true;
@@ -1776,6 +1841,7 @@ enum TextMode {
}
message ListEntitiesTextResponse {
option (id) = 97;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_TEXT";
@@ -1791,9 +1857,11 @@ message ListEntitiesTextResponse {
uint32 max_length = 9;
string pattern = 10;
TextMode mode = 11;
uint32 device_id = 12;
}
message TextStateResponse {
option (id) = 98;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_TEXT";
option (no_delay) = true;
@@ -1818,6 +1886,7 @@ message TextCommandRequest {
// ==================== DATETIME DATE ====================
message ListEntitiesDateResponse {
option (id) = 100;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_DATETIME_DATE";
@@ -1829,9 +1898,11 @@ message ListEntitiesDateResponse {
string icon = 5;
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
uint32 device_id = 8;
}
message DateStateResponse {
option (id) = 101;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_DATETIME_DATE";
option (no_delay) = true;
@@ -1859,6 +1930,7 @@ message DateCommandRequest {
// ==================== DATETIME TIME ====================
message ListEntitiesTimeResponse {
option (id) = 103;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_DATETIME_TIME";
@@ -1870,9 +1942,11 @@ message ListEntitiesTimeResponse {
string icon = 5;
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
uint32 device_id = 8;
}
message TimeStateResponse {
option (id) = 104;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_DATETIME_TIME";
option (no_delay) = true;
@@ -1900,6 +1974,7 @@ message TimeCommandRequest {
// ==================== EVENT ====================
message ListEntitiesEventResponse {
option (id) = 107;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_EVENT";
@@ -1914,9 +1989,11 @@ message ListEntitiesEventResponse {
string device_class = 8;
repeated string event_types = 9;
uint32 device_id = 10;
}
message EventResponse {
option (id) = 108;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_EVENT";
@@ -1927,6 +2004,7 @@ message EventResponse {
// ==================== VALVE ====================
message ListEntitiesValveResponse {
option (id) = 109;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_VALVE";
@@ -1943,6 +2021,7 @@ message ListEntitiesValveResponse {
bool assumed_state = 9;
bool supports_position = 10;
bool supports_stop = 11;
uint32 device_id = 12;
}
enum ValveOperation {
@@ -1952,6 +2031,7 @@ enum ValveOperation {
}
message ValveStateResponse {
option (id) = 110;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_VALVE";
option (no_delay) = true;
@@ -1976,6 +2056,7 @@ message ValveCommandRequest {
// ==================== DATETIME DATETIME ====================
message ListEntitiesDateTimeResponse {
option (id) = 112;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_DATETIME_DATETIME";
@@ -1987,9 +2068,11 @@ message ListEntitiesDateTimeResponse {
string icon = 5;
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
uint32 device_id = 8;
}
message DateTimeStateResponse {
option (id) = 113;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_DATETIME_DATETIME";
option (no_delay) = true;
@@ -2013,6 +2096,7 @@ message DateTimeCommandRequest {
// ==================== UPDATE ====================
message ListEntitiesUpdateResponse {
option (id) = 116;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_UPDATE";
@@ -2025,9 +2109,11 @@ message ListEntitiesUpdateResponse {
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
string device_class = 8;
uint32 device_id = 9;
}
message UpdateStateResponse {
option (id) = 117;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_UPDATE";
option (no_delay) = true;

View File

@@ -28,8 +28,19 @@
namespace esphome {
namespace api {
// Read a maximum of 5 messages per loop iteration to prevent starving other components.
// This is a balance between API responsiveness and allowing other components to run.
// Since each message could contain multiple protobuf messages when using packet batching,
// this limits the number of messages processed, not the number of TCP packets.
static constexpr uint8_t MAX_MESSAGES_PER_LOOP = 5;
static constexpr uint8_t MAX_PING_RETRIES = 60;
static constexpr uint16_t PING_RETRY_INTERVAL = 1000;
static constexpr uint32_t KEEPALIVE_DISCONNECT_TIMEOUT = (KEEPALIVE_TIMEOUT_MS * 5) / 2;
static const char *const TAG = "api.connection";
#ifdef USE_ESP32_CAMERA
static const int ESP32_CAMERA_STOP_STREAM = 5000;
#endif
APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent)
: parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) {
@@ -61,8 +72,8 @@ void APIConnection::start() {
APIError err = this->helper_->init();
if (err != APIError::OK) {
on_fatal_error();
ESP_LOGW(TAG, "%s: Helper init failed: %s errno=%d", this->client_combined_info_.c_str(), api_error_to_str(err),
errno);
ESP_LOGW(TAG, "%s: Helper init failed: %s errno=%d", this->get_client_combined_info().c_str(),
api_error_to_str(err), errno);
return;
}
this->client_info_ = helper_->getpeername();
@@ -84,16 +95,6 @@ APIConnection::~APIConnection() {
}
void APIConnection::loop() {
if (this->remove_)
return;
if (!network::is_connected()) {
// when network is disconnected force disconnect immediately
// don't wait for timeout
this->on_fatal_error();
ESP_LOGW(TAG, "%s: Network unavailable; disconnecting", this->client_combined_info_.c_str());
return;
}
if (this->next_close_) {
// requested a disconnect
this->helper_->close();
@@ -104,30 +105,34 @@ void APIConnection::loop() {
APIError err = this->helper_->loop();
if (err != APIError::OK) {
on_fatal_error();
ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->client_combined_info_.c_str(),
ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->get_client_combined_info().c_str(),
api_error_to_str(err), errno);
return;
}
const uint32_t now = App.get_loop_component_start_time();
// Check if socket has data ready before attempting to read
if (this->helper_->is_socket_ready()) {
// Read up to MAX_MESSAGES_PER_LOOP messages per loop to improve throughput
for (uint8_t message_count = 0; message_count < MAX_MESSAGES_PER_LOOP; message_count++) {
ReadPacketBuffer buffer;
err = this->helper_->read_packet(&buffer);
if (err == APIError::WOULD_BLOCK) {
// pass
// No more data available
break;
} else if (err != APIError::OK) {
on_fatal_error();
if (err == APIError::SOCKET_READ_FAILED && errno == ECONNRESET) {
ESP_LOGW(TAG, "%s: Connection reset", this->client_combined_info_.c_str());
ESP_LOGW(TAG, "%s: Connection reset", this->get_client_combined_info().c_str());
} else if (err == APIError::CONNECTION_CLOSED) {
ESP_LOGW(TAG, "%s: Connection closed", this->client_combined_info_.c_str());
ESP_LOGW(TAG, "%s: Connection closed", this->get_client_combined_info().c_str());
} else {
ESP_LOGW(TAG, "%s: Reading failed: %s errno=%d", this->client_combined_info_.c_str(), api_error_to_str(err),
errno);
ESP_LOGW(TAG, "%s: Reading failed: %s errno=%d", this->get_client_combined_info().c_str(),
api_error_to_str(err), errno);
}
return;
} else {
this->last_traffic_ = App.get_loop_component_start_time();
this->last_traffic_ = now;
// read a packet
if (buffer.data_len > 0) {
this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]);
@@ -138,42 +143,39 @@ void APIConnection::loop() {
return;
}
}
}
// Process deferred batch if scheduled
if (this->deferred_batch_.batch_scheduled &&
App.get_loop_component_start_time() - this->deferred_batch_.batch_start_time >= this->get_batch_delay_ms_()) {
now - this->deferred_batch_.batch_start_time >= this->get_batch_delay_ms_()) {
this->process_batch_();
}
if (!this->list_entities_iterator_.completed())
if (!this->list_entities_iterator_.completed()) {
this->list_entities_iterator_.advance();
if (!this->initial_state_iterator_.completed() && this->list_entities_iterator_.completed())
} else if (!this->initial_state_iterator_.completed()) {
this->initial_state_iterator_.advance();
}
static uint8_t max_ping_retries = 60;
static uint16_t ping_retry_interval = 1000;
const uint32_t now = App.get_loop_component_start_time();
if (this->sent_ping_) {
// Disconnect if not responded within 2.5*keepalive
if (now - this->last_traffic_ > (KEEPALIVE_TIMEOUT_MS * 5) / 2) {
if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) {
on_fatal_error();
ESP_LOGW(TAG, "%s is unresponsive; disconnecting", this->client_combined_info_.c_str());
ESP_LOGW(TAG, "%s is unresponsive; disconnecting", this->get_client_combined_info().c_str());
}
} else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS && now > this->next_ping_retry_) {
ESP_LOGVV(TAG, "Sending keepalive PING");
this->sent_ping_ = this->send_message(PingRequest());
if (!this->sent_ping_) {
this->next_ping_retry_ = now + ping_retry_interval;
this->next_ping_retry_ = now + PING_RETRY_INTERVAL;
this->ping_retries_++;
std::string warn_str = str_sprintf("%s: Sending keepalive failed %u time(s);",
this->client_combined_info_.c_str(), this->ping_retries_);
if (this->ping_retries_ >= max_ping_retries) {
if (this->ping_retries_ >= MAX_PING_RETRIES) {
on_fatal_error();
ESP_LOGE(TAG, "%s disconnecting", warn_str.c_str());
ESP_LOGE(TAG, "%s: Ping failed %u times", this->get_client_combined_info().c_str(), this->ping_retries_);
} else if (this->ping_retries_ >= 10) {
ESP_LOGW(TAG, "%s retrying in %u ms", warn_str.c_str(), ping_retry_interval);
ESP_LOGW(TAG, "%s: Ping retry %u", this->get_client_combined_info().c_str(), this->ping_retries_);
} else {
ESP_LOGD(TAG, "%s retrying in %u ms", warn_str.c_str(), ping_retry_interval);
ESP_LOGD(TAG, "%s: Ping retry %u", this->get_client_combined_info().c_str(), this->ping_retries_);
}
}
}
@@ -197,22 +199,20 @@ void APIConnection::loop() {
// bool done = 3;
buffer.encode_bool(3, done);
bool success = this->send_buffer(buffer, 44);
bool success = this->send_buffer(buffer, CameraImageResponse::MESSAGE_TYPE);
if (success) {
this->image_reader_.consume_data(to_send);
}
if (success && done) {
if (done) {
this->image_reader_.return_image();
}
}
}
#endif
if (state_subs_at_ != -1) {
if (state_subs_at_ >= 0) {
const auto &subs = this->parent_->get_state_subs();
if (state_subs_at_ >= (int) subs.size()) {
state_subs_at_ = -1;
} else {
if (state_subs_at_ < static_cast<int>(subs.size())) {
auto &it = subs[state_subs_at_];
SubscribeHomeAssistantStateResponse resp;
resp.entity_id = it.entity_id;
@@ -221,6 +221,8 @@ void APIConnection::loop() {
if (this->send_message(resp)) {
state_subs_at_++;
}
} else {
state_subs_at_ = -1;
}
}
}
@@ -233,7 +235,7 @@ DisconnectResponse APIConnection::disconnect(const DisconnectRequest &msg) {
// remote initiated disconnect_client
// don't close yet, we still need to send the disconnect response
// close will happen on next loop
ESP_LOGD(TAG, "%s disconnected", this->client_combined_info_.c_str());
ESP_LOGD(TAG, "%s disconnected", this->get_client_combined_info().c_str());
this->next_close_ = true;
DisconnectResponse resp;
return resp;
@@ -248,25 +250,46 @@ void APIConnection::on_disconnect_response(const DisconnectResponse &value) {
uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint16_t message_type, APIConnection *conn,
uint32_t remaining_size, bool is_single) {
// Calculate size
uint32_t size = 0;
msg.calculate_size(size);
uint32_t calculated_size = 0;
msg.calculate_size(calculated_size);
// Cache frame sizes to avoid repeated virtual calls
const uint8_t header_padding = conn->helper_->frame_header_padding();
const uint8_t footer_size = conn->helper_->frame_footer_size();
// Calculate total size with padding for buffer allocation
uint16_t total_size =
static_cast<uint16_t>(size) + conn->helper_->frame_header_padding() + conn->helper_->frame_footer_size();
size_t total_calculated_size = calculated_size + header_padding + footer_size;
// Check if it fits
if (total_size > remaining_size) {
if (total_calculated_size > remaining_size) {
return 0; // Doesn't fit
}
// Allocate exact buffer space needed (just the payload, not the overhead)
ProtoWriteBuffer buffer =
is_single ? conn->allocate_single_message_buffer(size) : conn->allocate_batch_message_buffer(size);
// Allocate buffer space - pass payload size, allocation functions add header/footer space
ProtoWriteBuffer buffer = is_single ? conn->allocate_single_message_buffer(calculated_size)
: conn->allocate_batch_message_buffer(calculated_size);
// Get buffer size after allocation (which includes header padding)
std::vector<uint8_t> &shared_buf = conn->parent_->get_shared_buffer_ref();
size_t size_before_encode = shared_buf.size();
// Encode directly into buffer
msg.encode(buffer);
return total_size;
#ifdef HAS_PROTO_MESSAGE_DUMP
// Log the message for VV debugging
conn->log_send_message_(msg.message_name(), msg.dump());
#endif
// Calculate actual encoded size (not including header that was already added)
size_t actual_payload_size = shared_buf.size() - size_before_encode;
// Return actual total size (header + actual payload + footer)
size_t actual_total_size = header_padding + actual_payload_size + footer_size;
// Verify that calculate_size() returned the correct value
assert(calculated_size == actual_payload_size);
return static_cast<uint16_t>(actual_total_size);
}
#ifdef USE_BINARY_SENSOR
@@ -285,7 +308,7 @@ uint16_t APIConnection::try_send_binary_sensor_state(EntityBase *entity, APIConn
BinarySensorStateResponse resp;
resp.state = binary_sensor->state;
resp.missing_state = !binary_sensor->has_state();
resp.key = binary_sensor->get_object_id_hash();
fill_entity_state_base(binary_sensor, resp);
return encode_message_to_buffer(resp, BinarySensorStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
}
@@ -319,7 +342,7 @@ uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection *
if (traits.get_supports_tilt())
msg.tilt = cover->tilt;
msg.current_operation = static_cast<enums::CoverOperation>(cover->current_operation);
msg.key = cover->get_object_id_hash();
fill_entity_state_base(cover, msg);
return encode_message_to_buffer(msg, CoverStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
}
uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@@ -387,7 +410,7 @@ uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *co
msg.direction = static_cast<enums::FanDirection>(fan->direction);
if (traits.supports_preset_modes())
msg.preset_mode = fan->preset_mode;
msg.key = fan->get_object_id_hash();
fill_entity_state_base(fan, msg);
return encode_message_to_buffer(msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
}
uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@@ -454,7 +477,7 @@ uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *
resp.warm_white = values.get_warm_white();
if (light->supports_effects())
resp.effect = light->get_effect_name();
resp.key = light->get_object_id_hash();
fill_entity_state_base(light, resp);
return encode_message_to_buffer(resp, LightStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
}
uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@@ -536,7 +559,7 @@ uint16_t APIConnection::try_send_sensor_state(EntityBase *entity, APIConnection
SensorStateResponse resp;
resp.state = sensor->state;
resp.missing_state = !sensor->has_state();
resp.key = sensor->get_object_id_hash();
fill_entity_state_base(sensor, resp);
return encode_message_to_buffer(resp, SensorStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
}
@@ -570,7 +593,7 @@ uint16_t APIConnection::try_send_switch_state(EntityBase *entity, APIConnection
auto *a_switch = static_cast<switch_::Switch *>(entity);
SwitchStateResponse resp;
resp.state = a_switch->state;
resp.key = a_switch->get_object_id_hash();
fill_entity_state_base(a_switch, resp);
return encode_message_to_buffer(resp, SwitchStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
}
@@ -613,7 +636,7 @@ uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnec
TextSensorStateResponse resp;
resp.state = text_sensor->state;
resp.missing_state = !text_sensor->has_state();
resp.key = text_sensor->get_object_id_hash();
fill_entity_state_base(text_sensor, resp);
return encode_message_to_buffer(resp, TextSensorStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
}
uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@@ -637,7 +660,7 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection
bool is_single) {
auto *climate = static_cast<climate::Climate *>(entity);
ClimateStateResponse resp;
resp.key = climate->get_object_id_hash();
fill_entity_state_base(climate, resp);
auto traits = climate->get_traits();
resp.mode = static_cast<enums::ClimateMode>(climate->mode);
resp.action = static_cast<enums::ClimateAction>(climate->action);
@@ -746,7 +769,7 @@ uint16_t APIConnection::try_send_number_state(EntityBase *entity, APIConnection
NumberStateResponse resp;
resp.state = number->state;
resp.missing_state = !number->has_state();
resp.key = number->get_object_id_hash();
fill_entity_state_base(number, resp);
return encode_message_to_buffer(resp, NumberStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
}
@@ -787,7 +810,7 @@ uint16_t APIConnection::try_send_date_state(EntityBase *entity, APIConnection *c
resp.year = date->year;
resp.month = date->month;
resp.day = date->day;
resp.key = date->get_object_id_hash();
fill_entity_state_base(date, resp);
return encode_message_to_buffer(resp, DateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
}
void APIConnection::send_date_info(datetime::DateEntity *date) {
@@ -824,7 +847,7 @@ uint16_t APIConnection::try_send_time_state(EntityBase *entity, APIConnection *c
resp.hour = time->hour;
resp.minute = time->minute;
resp.second = time->second;
resp.key = time->get_object_id_hash();
fill_entity_state_base(time, resp);
return encode_message_to_buffer(resp, TimeStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
}
void APIConnection::send_time_info(datetime::TimeEntity *time) {
@@ -863,7 +886,7 @@ uint16_t APIConnection::try_send_datetime_state(EntityBase *entity, APIConnectio
ESPTime state = datetime->state_as_esptime();
resp.epoch_seconds = state.timestamp;
}
resp.key = datetime->get_object_id_hash();
fill_entity_state_base(datetime, resp);
return encode_message_to_buffer(resp, DateTimeStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
}
void APIConnection::send_datetime_info(datetime::DateTimeEntity *datetime) {
@@ -902,7 +925,7 @@ uint16_t APIConnection::try_send_text_state(EntityBase *entity, APIConnection *c
TextStateResponse resp;
resp.state = text->state;
resp.missing_state = !text->has_state();
resp.key = text->get_object_id_hash();
fill_entity_state_base(text, resp);
return encode_message_to_buffer(resp, TextStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
}
@@ -943,7 +966,7 @@ uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection
SelectStateResponse resp;
resp.state = select->state;
resp.missing_state = !select->has_state();
resp.key = select->get_object_id_hash();
fill_entity_state_base(select, resp);
return encode_message_to_buffer(resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
}
@@ -1003,7 +1026,7 @@ uint16_t APIConnection::try_send_lock_state(EntityBase *entity, APIConnection *c
auto *a_lock = static_cast<lock::Lock *>(entity);
LockStateResponse resp;
resp.state = static_cast<enums::LockState>(a_lock->state);
resp.key = a_lock->get_object_id_hash();
fill_entity_state_base(a_lock, resp);
return encode_message_to_buffer(resp, LockStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
}
@@ -1047,7 +1070,7 @@ uint16_t APIConnection::try_send_valve_state(EntityBase *entity, APIConnection *
ValveStateResponse resp;
resp.position = valve->position;
resp.current_operation = static_cast<enums::ValveOperation>(valve->current_operation);
resp.key = valve->get_object_id_hash();
fill_entity_state_base(valve, resp);
return encode_message_to_buffer(resp, ValveStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
}
void APIConnection::send_valve_info(valve::Valve *valve) {
@@ -1095,7 +1118,7 @@ uint16_t APIConnection::try_send_media_player_state(EntityBase *entity, APIConne
resp.state = static_cast<enums::MediaPlayerState>(report_state);
resp.volume = media_player->volume;
resp.muted = media_player->is_muted();
resp.key = media_player->get_object_id_hash();
fill_entity_state_base(media_player, resp);
return encode_message_to_buffer(resp, MediaPlayerStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
}
void APIConnection::send_media_player_info(media_player::MediaPlayer *media_player) {
@@ -1359,7 +1382,7 @@ uint16_t APIConnection::try_send_alarm_control_panel_state(EntityBase *entity, A
auto *a_alarm_control_panel = static_cast<alarm_control_panel::AlarmControlPanel *>(entity);
AlarmControlPanelStateResponse resp;
resp.state = static_cast<enums::AlarmControlPanelState>(a_alarm_control_panel->get_state());
resp.key = a_alarm_control_panel->get_object_id_hash();
fill_entity_state_base(a_alarm_control_panel, resp);
return encode_message_to_buffer(resp, AlarmControlPanelStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
}
void APIConnection::send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) {
@@ -1414,7 +1437,7 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe
#ifdef USE_EVENT
void APIConnection::send_event(event::Event *event, const std::string &event_type) {
this->schedule_message_(event, MessageCreator(event_type, EventResponse::MESSAGE_TYPE), EventResponse::MESSAGE_TYPE);
this->schedule_message_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE);
}
void APIConnection::send_event_info(event::Event *event) {
this->schedule_message_(event, &APIConnection::try_send_event_info, ListEntitiesEventResponse::MESSAGE_TYPE);
@@ -1423,7 +1446,7 @@ uint16_t APIConnection::try_send_event_response(event::Event *event, const std::
uint32_t remaining_size, bool is_single) {
EventResponse resp;
resp.event_type = event_type;
resp.key = event->get_object_id_hash();
fill_entity_state_base(event, resp);
return encode_message_to_buffer(resp, EventResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
}
@@ -1461,7 +1484,7 @@ uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection
resp.release_summary = update->update_info.summary;
resp.release_url = update->update_info.release_url;
}
resp.key = update->get_object_id_hash();
fill_entity_state_base(update, resp);
return encode_message_to_buffer(resp, UpdateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
}
void APIConnection::send_update_info(update::UpdateEntity *update) {
@@ -1522,14 +1545,13 @@ bool APIConnection::try_send_log_message(int level, const char *tag, const char
buffer.encode_string(3, line, line_length); // string message = 3
// SubscribeLogsResponse - 29
return this->send_buffer(buffer, 29);
return this->send_buffer(buffer, SubscribeLogsResponse::MESSAGE_TYPE);
}
HelloResponse APIConnection::hello(const HelloRequest &msg) {
this->client_info_ = msg.client_info;
this->client_peername_ = this->helper_->getpeername();
this->client_combined_info_ = this->client_info_ + " (" + this->client_peername_ + ")";
this->helper_->set_log_info(this->client_combined_info_);
this->helper_->set_log_info(this->get_client_combined_info());
this->client_api_version_major_ = msg.api_version_major;
this->client_api_version_minor_ = msg.api_version_minor;
ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->client_info_.c_str(),
@@ -1551,7 +1573,7 @@ ConnectResponse APIConnection::connect(const ConnectRequest &msg) {
// bool invalid_password = 1;
resp.invalid_password = !correct;
if (correct) {
ESP_LOGD(TAG, "%s connected", this->client_combined_info_.c_str());
ESP_LOGD(TAG, "%s connected", this->get_client_combined_info().c_str());
this->connection_state_ = ConnectionState::AUTHENTICATED;
this->parent_->get_client_connected_trigger()->trigger(this->client_info_, this->client_peername_);
#ifdef USE_HOMEASSISTANT_TIME
@@ -1604,6 +1626,23 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) {
#endif
#ifdef USE_API_NOISE
resp.api_encryption_supported = true;
#endif
#ifdef USE_DEVICES
for (auto const &device : App.get_devices()) {
DeviceInfo device_info;
device_info.device_id = device->get_device_id();
device_info.name = device->get_name();
device_info.area_id = device->get_area_id();
resp.devices.push_back(device_info);
}
#endif
#ifdef USE_AREAS
for (auto const &area : App.get_areas()) {
AreaInfo area_info;
area_info.area_id = area->get_area_id();
area_info.name = area->get_name();
resp.areas.push_back(area_info);
}
#endif
return resp;
}
@@ -1657,7 +1696,7 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) {
APIError err = this->helper_->loop();
if (err != APIError::OK) {
on_fatal_error();
ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->client_combined_info_.c_str(),
ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->get_client_combined_info().c_str(),
api_error_to_str(err), errno);
return false;
}
@@ -1669,7 +1708,7 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) {
return false;
}
bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint16_t message_type) {
if (!this->try_to_clear_buffer(message_type != 29)) { // SubscribeLogsResponse
if (!this->try_to_clear_buffer(message_type != SubscribeLogsResponse::MESSAGE_TYPE)) { // SubscribeLogsResponse
return false;
}
@@ -1679,10 +1718,10 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint16_t message_type)
if (err != APIError::OK) {
on_fatal_error();
if (err == APIError::SOCKET_WRITE_FAILED && errno == ECONNRESET) {
ESP_LOGW(TAG, "%s: Connection reset", this->client_combined_info_.c_str());
ESP_LOGW(TAG, "%s: Connection reset", this->get_client_combined_info().c_str());
} else {
ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", this->client_combined_info_.c_str(), api_error_to_str(err),
errno);
ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", this->get_client_combined_info().c_str(),
api_error_to_str(err), errno);
}
return false;
}
@@ -1691,11 +1730,11 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint16_t message_type)
}
void APIConnection::on_unauthenticated_access() {
this->on_fatal_error();
ESP_LOGD(TAG, "%s requested access without authentication", this->client_combined_info_.c_str());
ESP_LOGD(TAG, "%s requested access without authentication", this->get_client_combined_info().c_str());
}
void APIConnection::on_no_setup_connection() {
this->on_fatal_error();
ESP_LOGD(TAG, "%s requested access without full connection", this->client_combined_info_.c_str());
ESP_LOGD(TAG, "%s requested access without full connection", this->get_client_combined_info().c_str());
}
void APIConnection::on_fatal_error() {
this->helper_->close();
@@ -1753,7 +1792,8 @@ void APIConnection::process_batch_() {
const auto &item = this->deferred_batch_.items[0];
// Let the creator calculate size and encode if it fits
uint16_t payload_size = item.creator(item.entity, this, std::numeric_limits<uint16_t>::max(), true);
uint16_t payload_size =
item.creator(item.entity, this, std::numeric_limits<uint16_t>::max(), true, item.message_type);
if (payload_size > 0 &&
this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, item.message_type)) {
@@ -1791,7 +1831,7 @@ void APIConnection::process_batch_() {
this->batch_first_message_ = true;
size_t items_processed = 0;
uint32_t remaining_size = MAX_PACKET_SIZE;
uint16_t remaining_size = std::numeric_limits<uint16_t>::max();
// Track where each message's header padding begins in the buffer
// For plaintext: this is where the 6-byte header padding starts
@@ -1803,7 +1843,7 @@ void APIConnection::process_batch_() {
for (const auto &item : this->deferred_batch_.items) {
// Try to encode message
// The creator will calculate overhead to determine if the message fits
uint16_t payload_size = item.creator(item.entity, this, remaining_size, false);
uint16_t payload_size = item.creator(item.entity, this, remaining_size, false, item.message_type);
if (payload_size == 0) {
// Message won't fit, stop processing
@@ -1816,11 +1856,15 @@ void APIConnection::process_batch_() {
packet_info.emplace_back(item.message_type, current_offset, proto_payload_size);
// Update tracking variables
items_processed++;
// After first message, set remaining size to MAX_PACKET_SIZE to avoid fragmentation
if (items_processed == 1) {
remaining_size = MAX_PACKET_SIZE;
}
remaining_size -= payload_size;
// Calculate where the next message's header padding will start
// Current buffer size + footer space (that prepare_message_buffer will add for this message)
current_offset = this->parent_->get_shared_buffer_ref().size() + footer_size;
items_processed++;
}
if (items_processed == 0) {
@@ -1840,10 +1884,10 @@ void APIConnection::process_batch_() {
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
on_fatal_error();
if (err == APIError::SOCKET_WRITE_FAILED && errno == ECONNRESET) {
ESP_LOGW(TAG, "%s: Connection reset during batch write", this->client_combined_info_.c_str());
ESP_LOGW(TAG, "%s: Connection reset during batch write", this->get_client_combined_info().c_str());
} else {
ESP_LOGW(TAG, "%s: Batch write failed %s errno=%d", this->client_combined_info_.c_str(), api_error_to_str(err),
errno);
ESP_LOGW(TAG, "%s: Batch write failed %s errno=%d", this->get_client_combined_info().c_str(),
api_error_to_str(err), errno);
}
}
@@ -1862,22 +1906,24 @@ void APIConnection::process_batch_() {
}
uint16_t APIConnection::MessageCreator::operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) const {
switch (message_type_) {
case 0: // Function pointer
return data_.ptr(entity, conn, remaining_size, is_single);
bool is_single, uint16_t message_type) const {
if (has_tagged_string_ptr_()) {
// Handle string-based messages
switch (message_type) {
#ifdef USE_EVENT
case EventResponse::MESSAGE_TYPE: {
auto *e = static_cast<event::Event *>(entity);
return APIConnection::try_send_event_response(e, *data_.string_ptr, conn, remaining_size, is_single);
return APIConnection::try_send_event_response(e, *get_string_ptr_(), conn, remaining_size, is_single);
}
#endif
default:
// Should not happen, return 0 to indicate no message
return 0;
}
} else {
// Function pointer case
return data_.ptr(entity, conn, remaining_size, is_single);
}
}
uint16_t APIConnection::try_send_list_info_done(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,

View File

@@ -240,8 +240,8 @@ class APIConnection : public APIServerConnection {
// - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext)
// - Footer: space for MAC (16 bytes for Noise, 0 for Plaintext)
shared_buf.reserve(reserve_size + header_padding + this->helper_->frame_footer_size());
// Insert header padding bytes so message encoding starts at the correct position
shared_buf.insert(shared_buf.begin(), header_padding, 0);
// Resize to add header padding so message encoding starts at the correct position
shared_buf.resize(header_padding);
return {&shared_buf};
}
@@ -249,47 +249,47 @@ class APIConnection : public APIServerConnection {
ProtoWriteBuffer prepare_message_buffer(uint16_t message_size, bool is_first_message) {
// Get reference to shared buffer (it maintains state between batch messages)
std::vector<uint8_t> &shared_buf = this->parent_->get_shared_buffer_ref();
size_t current_size = shared_buf.size();
if (is_first_message) {
// For first message, initialize buffer with header padding
uint8_t header_padding = this->helper_->frame_header_padding();
shared_buf.clear();
shared_buf.reserve(message_size + header_padding);
shared_buf.resize(header_padding);
// Fill header padding with zeros
std::fill(shared_buf.begin(), shared_buf.end(), 0);
} else {
// For subsequent messages, add footer space for previous message and header for this message
uint8_t footer_size = this->helper_->frame_footer_size();
uint8_t header_padding = this->helper_->frame_header_padding();
// Reserve additional space for everything
shared_buf.reserve(current_size + footer_size + header_padding + message_size);
// Single resize to add both footer and header padding
size_t new_size = current_size + footer_size + header_padding;
shared_buf.resize(new_size);
// Fill the newly added bytes with zeros (footer + header padding)
std::fill(shared_buf.begin() + current_size, shared_buf.end(), 0);
}
size_t current_size = shared_buf.size();
// Calculate padding to add:
// - First message: just header padding
// - Subsequent messages: footer for previous message + header padding for this message
size_t padding_to_add = is_first_message
? this->helper_->frame_header_padding()
: this->helper_->frame_header_padding() + this->helper_->frame_footer_size();
// Reserve space for padding + message
shared_buf.reserve(current_size + padding_to_add + message_size);
// Resize to add the padding bytes
shared_buf.resize(current_size + padding_to_add);
return {&shared_buf};
}
bool try_to_clear_buffer(bool log_out_of_space);
bool send_buffer(ProtoWriteBuffer buffer, uint16_t message_type) override;
std::string get_client_combined_info() const { return this->client_combined_info_; }
std::string get_client_combined_info() const {
if (this->client_info_ == this->client_peername_) {
// Before Hello message, both are the same (just IP:port)
return this->client_info_;
}
return this->client_info_ + " (" + this->client_peername_ + ")";
}
// Buffer allocator methods for batch processing
ProtoWriteBuffer allocate_single_message_buffer(uint16_t size);
ProtoWriteBuffer allocate_batch_message_buffer(uint16_t size);
protected:
// Helper function to fill common entity fields
template<typename ResponseT> static void fill_entity_info_base(esphome::EntityBase *entity, ResponseT &response) {
// Helper function to fill common entity info fields
static void fill_entity_info_base(esphome::EntityBase *entity, InfoResponseProtoMessage &response) {
// Set common fields that are shared by all entity types
response.key = entity->get_object_id_hash();
response.object_id = entity->get_object_id();
@@ -301,6 +301,14 @@ class APIConnection : public APIServerConnection {
response.icon = entity->get_icon();
response.disabled_by_default = entity->is_disabled_by_default();
response.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category());
#ifdef USE_DEVICES
response.device_id = entity->get_device_id();
#endif
}
// Helper function to fill common entity state fields
static void fill_entity_state_base(esphome::EntityBase *entity, StateResponseProtoMessage &response) {
response.key = entity->get_object_id_hash();
}
// Non-template helper to encode any ProtoMessage
@@ -433,90 +441,99 @@ class APIConnection : public APIServerConnection {
// Helper function to get estimated message size for buffer pre-allocation
static uint16_t get_estimated_message_size(uint16_t message_type);
enum class ConnectionState {
// Pointers first (4 bytes each, naturally aligned)
std::unique_ptr<APIFrameHelper> helper_;
APIServer *parent_;
// 4-byte aligned types
uint32_t last_traffic_;
uint32_t next_ping_retry_{0};
int state_subs_at_ = -1;
// Strings (12 bytes each on 32-bit)
std::string client_info_;
std::string client_peername_;
// 2-byte aligned types
uint16_t client_api_version_major_{0};
uint16_t client_api_version_minor_{0};
// Group all 1-byte types together to minimize padding
enum class ConnectionState : uint8_t {
WAITING_FOR_HELLO,
CONNECTED,
AUTHENTICATED,
} connection_state_{ConnectionState::WAITING_FOR_HELLO};
uint8_t log_subscription_{ESPHOME_LOG_LEVEL_NONE};
bool remove_{false};
bool state_subscription_{false};
bool sent_ping_{false};
bool service_call_subscription_{false};
bool next_close_ = false;
uint8_t ping_retries_{0};
// 8 bytes used, no padding needed
std::unique_ptr<APIFrameHelper> helper_;
std::string client_info_;
std::string client_peername_;
std::string client_combined_info_;
uint32_t client_api_version_major_{0};
uint32_t client_api_version_minor_{0};
// Larger objects at the end
InitialStateIterator initial_state_iterator_;
ListEntitiesIterator list_entities_iterator_;
#ifdef USE_ESP32_CAMERA
esp32_camera::CameraImageReader image_reader_;
#endif
bool state_subscription_{false};
int log_subscription_{ESPHOME_LOG_LEVEL_NONE};
uint32_t last_traffic_;
uint32_t next_ping_retry_{0};
uint8_t ping_retries_{0};
bool sent_ping_{false};
bool service_call_subscription_{false};
bool next_close_ = false;
APIServer *parent_;
InitialStateIterator initial_state_iterator_;
ListEntitiesIterator list_entities_iterator_;
int state_subs_at_ = -1;
// Function pointer type for message encoding
using MessageCreatorPtr = uint16_t (*)(EntityBase *, APIConnection *, uint32_t remaining_size, bool is_single);
// Optimized MessageCreator class using union dispatch
// Optimized MessageCreator class using tagged pointer
class MessageCreator {
// Ensure pointer alignment allows LSB tagging
static_assert(alignof(std::string *) > 1, "String pointer alignment must be > 1 for LSB tagging");
public:
// Constructor for function pointer (message_type = 0)
MessageCreator(MessageCreatorPtr ptr) : message_type_(0) { data_.ptr = ptr; }
// Constructor for function pointer
MessageCreator(MessageCreatorPtr ptr) {
// Function pointers are always aligned, so LSB is 0
data_.ptr = ptr;
}
// Constructor for string state capture
MessageCreator(const std::string &value, uint16_t msg_type) : message_type_(msg_type) {
data_.string_ptr = new std::string(value);
explicit MessageCreator(const std::string &str_value) {
// Allocate string and tag the pointer
auto *str = new std::string(str_value);
// Set LSB to 1 to indicate string pointer
data_.tagged = reinterpret_cast<uintptr_t>(str) | 1;
}
// Destructor
~MessageCreator() {
// Clean up string data for string-based message types
if (uses_string_data_()) {
delete data_.string_ptr;
if (has_tagged_string_ptr_()) {
delete get_string_ptr_();
}
}
// Copy constructor
MessageCreator(const MessageCreator &other) : message_type_(other.message_type_) {
if (message_type_ == 0) {
data_.ptr = other.data_.ptr;
} else if (uses_string_data_()) {
data_.string_ptr = new std::string(*other.data_.string_ptr);
MessageCreator(const MessageCreator &other) {
if (other.has_tagged_string_ptr_()) {
auto *str = new std::string(*other.get_string_ptr_());
data_.tagged = reinterpret_cast<uintptr_t>(str) | 1;
} else {
data_ = other.data_; // For POD types
data_ = other.data_;
}
}
// Move constructor
MessageCreator(MessageCreator &&other) noexcept : data_(other.data_), message_type_(other.message_type_) {
other.message_type_ = 0; // Reset other to function pointer type
other.data_.ptr = nullptr;
}
MessageCreator(MessageCreator &&other) noexcept : data_(other.data_) { other.data_.ptr = nullptr; }
// Assignment operators (needed for batch deduplication)
MessageCreator &operator=(const MessageCreator &other) {
if (this != &other) {
// Clean up current string data if needed
if (uses_string_data_()) {
delete data_.string_ptr;
if (has_tagged_string_ptr_()) {
delete get_string_ptr_();
}
// Copy new data
message_type_ = other.message_type_;
if (other.message_type_ == 0) {
data_.ptr = other.data_.ptr;
} else if (other.uses_string_data_()) {
data_.string_ptr = new std::string(*other.data_.string_ptr);
if (other.has_tagged_string_ptr_()) {
auto *str = new std::string(*other.get_string_ptr_());
data_.tagged = reinterpret_cast<uintptr_t>(str) | 1;
} else {
data_ = other.data_;
}
@@ -527,30 +544,35 @@ class APIConnection : public APIServerConnection {
MessageCreator &operator=(MessageCreator &&other) noexcept {
if (this != &other) {
// Clean up current string data if needed
if (uses_string_data_()) {
delete data_.string_ptr;
if (has_tagged_string_ptr_()) {
delete get_string_ptr_();
}
// Move data
message_type_ = other.message_type_;
data_ = other.data_;
// Reset other to safe state
other.message_type_ = 0;
other.data_.ptr = nullptr;
}
return *this;
}
// Call operator
uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) const;
// Call operator - now accepts message_type as parameter
uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single,
uint16_t message_type) const;
private:
// Helper to check if this message type uses heap-allocated strings
bool uses_string_data_() const { return message_type_ == EventResponse::MESSAGE_TYPE; }
union CreatorData {
MessageCreatorPtr ptr; // 8 bytes
std::string *string_ptr; // 8 bytes
} data_; // 8 bytes
uint16_t message_type_; // 2 bytes (0 = function ptr, >0 = state capture)
// Check if this contains a string pointer
bool has_tagged_string_ptr_() const { return (data_.tagged & 1) != 0; }
// Get the actual string pointer (clears the tag bit)
std::string *get_string_ptr_() const {
// NOLINTNEXTLINE(performance-no-int-to-ptr)
return reinterpret_cast<std::string *>(data_.tagged & ~uintptr_t(1));
}
union {
MessageCreatorPtr ptr;
uintptr_t tagged;
} data_; // 4 bytes on 32-bit
};
// Generic batching mechanism for both state updates and entity info

View File

@@ -66,6 +66,17 @@ const char *api_error_to_str(APIError err) {
return "UNKNOWN";
}
// Default implementation for loop - handles sending buffered data
APIError APIFrameHelper::loop() {
if (!this->tx_buf_.empty()) {
APIError err = try_send_tx_buf_();
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
return err;
}
}
return APIError::OK; // Convert WOULD_BLOCK to OK to avoid connection termination
}
// Helper method to buffer data from IOVs
void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len) {
SendBuffer buffer;
@@ -274,17 +285,21 @@ APIError APINoiseFrameHelper::init() {
}
/// Run through handshake messages (if in that phase)
APIError APINoiseFrameHelper::loop() {
// During handshake phase, process as many actions as possible until we can't progress
// socket_->ready() stays true until next main loop, but state_action() will return
// WOULD_BLOCK when no more data is available to read
while (state_ != State::DATA && this->socket_->ready()) {
APIError err = state_action_();
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
return err;
}
if (!this->tx_buf_.empty()) {
err = try_send_tx_buf_();
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
return err;
if (err == APIError::WOULD_BLOCK) {
break;
}
}
return APIError::OK; // Convert WOULD_BLOCK to OK to avoid connection termination
// Use base class implementation for buffer sending
return APIFrameHelper::loop();
}
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
@@ -330,17 +345,15 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
return APIError::WOULD_BLOCK;
}
if (rx_header_buf_[0] != 0x01) {
state_ = State::FAILED;
HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]);
return APIError::BAD_INDICATOR;
}
// header reading done
}
// read body
uint8_t indicator = rx_header_buf_[0];
if (indicator != 0x01) {
state_ = State::FAILED;
HELPER_LOG("Bad indicator byte %u", indicator);
return APIError::BAD_INDICATOR;
}
uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2];
if (state_ != State::DATA && msg_size > 128) {
@@ -586,10 +599,6 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
return APIError::BAD_DATA_PACKET;
}
// uint16_t type;
// uint16_t data_len;
// uint8_t *data;
// uint8_t *padding; zero or more bytes to fill up the rest of the packet
uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1];
uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3];
if (data_len > msg_size - 4) {
@@ -822,18 +831,12 @@ APIError APIPlaintextFrameHelper::init() {
state_ = State::DATA;
return APIError::OK;
}
/// Not used for plaintext
APIError APIPlaintextFrameHelper::loop() {
if (state_ != State::DATA) {
return APIError::BAD_STATE;
}
if (!this->tx_buf_.empty()) {
APIError err = try_send_tx_buf_();
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
return err;
}
}
return APIError::OK; // Convert WOULD_BLOCK to OK to avoid connection termination
// Use base class implementation for buffer sending
return APIFrameHelper::loop();
}
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter

View File

@@ -38,7 +38,7 @@ struct PacketInfo {
: message_type(type), offset(off), payload_size(size), padding(0) {}
};
enum class APIError : int {
enum class APIError : uint16_t {
OK = 0,
WOULD_BLOCK = 1001,
BAD_HANDSHAKE_PACKET_LEN = 1002,
@@ -74,7 +74,7 @@ class APIFrameHelper {
}
virtual ~APIFrameHelper() = default;
virtual APIError init() = 0;
virtual APIError loop() = 0;
virtual APIError loop();
virtual APIError read_packet(ReadPacketBuffer *buffer) = 0;
bool can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); }
std::string getpeername() { return socket_->getpeername(); }
@@ -125,38 +125,6 @@ class APIFrameHelper {
const uint8_t *current_data() const { return data.data() + offset; }
};
// Queue of data buffers to be sent
std::deque<SendBuffer> tx_buf_;
// Common state enum for all frame helpers
// Note: Not all states are used by all implementations
// - INITIALIZE: Used by both Noise and Plaintext
// - CLIENT_HELLO, SERVER_HELLO, HANDSHAKE: Only used by Noise protocol
// - DATA: Used by both Noise and Plaintext
// - CLOSED: Used by both Noise and Plaintext
// - FAILED: Used by both Noise and Plaintext
// - EXPLICIT_REJECT: Only used by Noise protocol
enum class State {
INITIALIZE = 1,
CLIENT_HELLO = 2, // Noise only
SERVER_HELLO = 3, // Noise only
HANDSHAKE = 4, // Noise only
DATA = 5,
CLOSED = 6,
FAILED = 7,
EXPLICIT_REJECT = 8, // Noise only
};
// Current state of the frame helper
State state_{State::INITIALIZE};
// Helper name for logging
std::string info_;
// Socket for communication
socket::Socket *socket_{nullptr};
std::unique_ptr<socket::Socket> socket_owned_;
// Common implementation for writing raw data to socket
APIError write_raw_(const struct iovec *iov, int iovcnt);
@@ -169,15 +137,41 @@ class APIFrameHelper {
APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf,
const std::string &info, StateEnum &state, StateEnum failed_state);
// Pointers first (4 bytes each)
socket::Socket *socket_{nullptr};
std::unique_ptr<socket::Socket> socket_owned_;
// Common state enum for all frame helpers
// Note: Not all states are used by all implementations
// - INITIALIZE: Used by both Noise and Plaintext
// - CLIENT_HELLO, SERVER_HELLO, HANDSHAKE: Only used by Noise protocol
// - DATA: Used by both Noise and Plaintext
// - CLOSED: Used by both Noise and Plaintext
// - FAILED: Used by both Noise and Plaintext
// - EXPLICIT_REJECT: Only used by Noise protocol
enum class State : uint8_t {
INITIALIZE = 1,
CLIENT_HELLO = 2, // Noise only
SERVER_HELLO = 3, // Noise only
HANDSHAKE = 4, // Noise only
DATA = 5,
CLOSED = 6,
FAILED = 7,
EXPLICIT_REJECT = 8, // Noise only
};
// Containers (size varies, but typically 12+ bytes on 32-bit)
std::deque<SendBuffer> tx_buf_;
std::string info_;
std::vector<struct iovec> reusable_iovs_;
std::vector<uint8_t> rx_buf_;
// Group smaller types together
uint16_t rx_buf_len_ = 0;
State state_{State::INITIALIZE};
uint8_t frame_header_padding_{0};
uint8_t frame_footer_size_{0};
// Reusable IOV array for write_protobuf_packets to avoid repeated allocations
std::vector<struct iovec> reusable_iovs_;
// Receive buffer for reading frame data
std::vector<uint8_t> rx_buf_;
uint16_t rx_buf_len_ = 0;
// 5 bytes total, 3 bytes padding
// Common initialization for both plaintext and noise protocols
APIError init_common_();
@@ -213,19 +207,28 @@ class APINoiseFrameHelper : public APIFrameHelper {
APIError init_handshake_();
APIError check_handshake_finished_();
void send_explicit_handshake_reject_(const std::string &reason);
// Pointers first (4 bytes each)
NoiseHandshakeState *handshake_{nullptr};
NoiseCipherState *send_cipher_{nullptr};
NoiseCipherState *recv_cipher_{nullptr};
// Shared pointer (8 bytes on 32-bit = 4 bytes control block pointer + 4 bytes object pointer)
std::shared_ptr<APINoiseContext> ctx_;
// Vector (12 bytes on 32-bit)
std::vector<uint8_t> prologue_;
// NoiseProtocolId (size depends on implementation)
NoiseProtocolId nid_;
// Group small types together
// Fixed-size header buffer for noise protocol:
// 1 byte for indicator + 2 bytes for message size (16-bit value, not varint)
// Note: Maximum message size is UINT16_MAX (65535), with a limit of 128 bytes during handshake phase
uint8_t rx_header_buf_[3];
uint8_t rx_header_buf_len_ = 0;
std::vector<uint8_t> prologue_;
std::shared_ptr<APINoiseContext> ctx_;
NoiseHandshakeState *handshake_{nullptr};
NoiseCipherState *send_cipher_{nullptr};
NoiseCipherState *recv_cipher_{nullptr};
NoiseProtocolId nid_;
// 4 bytes total, no padding
};
#endif // USE_API_NOISE
@@ -252,6 +255,12 @@ class APIPlaintextFrameHelper : public APIFrameHelper {
protected:
APIError try_read_frame_(ParsedFrame *frame);
// Group 2-byte aligned types
uint16_t rx_header_parsed_type_ = 0;
uint16_t rx_header_parsed_len_ = 0;
// Group 1-byte types together
// Fixed-size header buffer for plaintext protocol:
// We now store the indicator byte + the two varints.
// To match noise protocol's maximum message size (UINT16_MAX = 65535), we need:
@@ -263,8 +272,7 @@ class APIPlaintextFrameHelper : public APIFrameHelper {
uint8_t rx_header_buf_[6]; // 1 byte indicator + 5 bytes for varints (3 for size + 2 for type)
uint8_t rx_header_buf_pos_ = 0;
bool rx_header_parsed_ = false;
uint16_t rx_header_parsed_type_ = 0;
uint16_t rx_header_parsed_len_ = 0;
// 8 bytes total, no padding needed
};
#endif

View File

@@ -21,4 +21,5 @@ extend google.protobuf.MessageOptions {
optional string ifdef = 1038;
optional bool log = 1039 [default=true];
optional bool no_delay = 1040 [default=false];
optional string base_class = 1041;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -620,545 +620,301 @@ void APIServerConnection::on_ping_request(const PingRequest &msg) {
}
}
void APIServerConnection::on_device_info_request(const DeviceInfoRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (this->check_connection_setup_()) {
DeviceInfoResponse ret = this->device_info(msg);
if (!this->send_message(ret)) {
this->on_fatal_error();
}
}
}
void APIServerConnection::on_list_entities_request(const ListEntitiesRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
if (this->check_authenticated_()) {
this->list_entities(msg);
}
}
void APIServerConnection::on_subscribe_states_request(const SubscribeStatesRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
if (this->check_authenticated_()) {
this->subscribe_states(msg);
}
}
void APIServerConnection::on_subscribe_logs_request(const SubscribeLogsRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
if (this->check_authenticated_()) {
this->subscribe_logs(msg);
}
}
void APIServerConnection::on_subscribe_homeassistant_services_request(
const SubscribeHomeassistantServicesRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
if (this->check_authenticated_()) {
this->subscribe_homeassistant_services(msg);
}
}
void APIServerConnection::on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
if (this->check_authenticated_()) {
this->subscribe_home_assistant_states(msg);
}
void APIServerConnection::on_get_time_request(const GetTimeRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
void APIServerConnection::on_get_time_request(const GetTimeRequest &msg) {
if (this->check_connection_setup_()) {
GetTimeResponse ret = this->get_time(msg);
if (!this->send_message(ret)) {
this->on_fatal_error();
}
}
}
void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
if (this->check_authenticated_()) {
this->execute_service(msg);
}
}
#ifdef USE_API_NOISE
void APIServerConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
if (this->check_authenticated_()) {
NoiseEncryptionSetKeyResponse ret = this->noise_encryption_set_key(msg);
if (!this->send_message(ret)) {
this->on_fatal_error();
}
}
}
#endif
#ifdef USE_BUTTON
void APIServerConnection::on_button_command_request(const ButtonCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
if (this->check_authenticated_()) {
this->button_command(msg);
}
}
#endif
#ifdef USE_ESP32_CAMERA
void APIServerConnection::on_camera_image_request(const CameraImageRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
if (this->check_authenticated_()) {
this->camera_image(msg);
}
}
#endif
#ifdef USE_CLIMATE
void APIServerConnection::on_climate_command_request(const ClimateCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
if (this->check_authenticated_()) {
this->climate_command(msg);
}
}
#endif
#ifdef USE_COVER
void APIServerConnection::on_cover_command_request(const CoverCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
if (this->check_authenticated_()) {
this->cover_command(msg);
}
}
#endif
#ifdef USE_DATETIME_DATE
void APIServerConnection::on_date_command_request(const DateCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
if (this->check_authenticated_()) {
this->date_command(msg);
}
}
#endif
#ifdef USE_DATETIME_DATETIME
void APIServerConnection::on_date_time_command_request(const DateTimeCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
if (this->check_authenticated_()) {
this->datetime_command(msg);
}
}
#endif
#ifdef USE_FAN
void APIServerConnection::on_fan_command_request(const FanCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
if (this->check_authenticated_()) {
this->fan_command(msg);
}
}
#endif
#ifdef USE_LIGHT
void APIServerConnection::on_light_command_request(const LightCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
if (this->check_authenticated_()) {
this->light_command(msg);
}
}
#endif
#ifdef USE_LOCK
void APIServerConnection::on_lock_command_request(const LockCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
if (this->check_authenticated_()) {
this->lock_command(msg);
}
}
#endif
#ifdef USE_MEDIA_PLAYER
void APIServerConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
if (this->check_authenticated_()) {
this->media_player_command(msg);
}
}
#endif
#ifdef USE_NUMBER
void APIServerConnection::on_number_command_request(const NumberCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
if (this->check_authenticated_()) {
this->number_command(msg);
}
}
#endif
#ifdef USE_SELECT
void APIServerConnection::on_select_command_request(const SelectCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
if (this->check_authenticated_()) {
this->select_command(msg);
}
}
#endif
#ifdef USE_SIREN
void APIServerConnection::on_siren_command_request(const SirenCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
if (this->check_authenticated_()) {
this->siren_command(msg);
}
}
#endif
#ifdef USE_SWITCH
void APIServerConnection::on_switch_command_request(const SwitchCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
if (this->check_authenticated_()) {
this->switch_command(msg);
}
}
#endif
#ifdef USE_TEXT
void APIServerConnection::on_text_command_request(const TextCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
if (this->check_authenticated_()) {
this->text_command(msg);
}
}
#endif
#ifdef USE_DATETIME_TIME
void APIServerConnection::on_time_command_request(const TimeCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
if (this->check_authenticated_()) {
this->time_command(msg);
}
}
#endif
#ifdef USE_UPDATE
void APIServerConnection::on_update_command_request(const UpdateCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
if (this->check_authenticated_()) {
this->update_command(msg);
}
}
#endif
#ifdef USE_VALVE
void APIServerConnection::on_valve_command_request(const ValveCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
if (this->check_authenticated_()) {
this->valve_command(msg);
}
}
#endif
#ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_subscribe_bluetooth_le_advertisements_request(
const SubscribeBluetoothLEAdvertisementsRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
if (this->check_authenticated_()) {
this->subscribe_bluetooth_le_advertisements(msg);
}
}
#endif
#ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_bluetooth_device_request(const BluetoothDeviceRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
if (this->check_authenticated_()) {
this->bluetooth_device_request(msg);
}
}
#endif
#ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
if (this->check_authenticated_()) {
this->bluetooth_gatt_get_services(msg);
}
}
#endif
#ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
if (this->check_authenticated_()) {
this->bluetooth_gatt_read(msg);
}
}
#endif
#ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
if (this->check_authenticated_()) {
this->bluetooth_gatt_write(msg);
}
}
#endif
#ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
if (this->check_authenticated_()) {
this->bluetooth_gatt_read_descriptor(msg);
}
}
#endif
#ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
if (this->check_authenticated_()) {
this->bluetooth_gatt_write_descriptor(msg);
}
}
#endif
#ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
if (this->check_authenticated_()) {
this->bluetooth_gatt_notify(msg);
}
}
#endif
#ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_subscribe_bluetooth_connections_free_request(
const SubscribeBluetoothConnectionsFreeRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
if (this->check_authenticated_()) {
BluetoothConnectionsFreeResponse ret = this->subscribe_bluetooth_connections_free(msg);
if (!this->send_message(ret)) {
this->on_fatal_error();
}
}
}
#endif
#ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_unsubscribe_bluetooth_le_advertisements_request(
const UnsubscribeBluetoothLEAdvertisementsRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
if (this->check_authenticated_()) {
this->unsubscribe_bluetooth_le_advertisements(msg);
}
}
#endif
#ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
if (this->check_authenticated_()) {
this->bluetooth_scanner_set_mode(msg);
}
}
#endif
#ifdef USE_VOICE_ASSISTANT
void APIServerConnection::on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
if (this->check_authenticated_()) {
this->subscribe_voice_assistant(msg);
}
}
#endif
#ifdef USE_VOICE_ASSISTANT
void APIServerConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
if (this->check_authenticated_()) {
VoiceAssistantConfigurationResponse ret = this->voice_assistant_get_configuration(msg);
if (!this->send_message(ret)) {
this->on_fatal_error();
}
}
}
#endif
#ifdef USE_VOICE_ASSISTANT
void APIServerConnection::on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
if (this->check_authenticated_()) {
this->voice_assistant_set_configuration(msg);
}
}
#endif
#ifdef USE_ALARM_CONTROL_PANEL
void APIServerConnection::on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
if (this->check_authenticated_()) {
this->alarm_control_panel_command(msg);
}
}
#endif
} // namespace api

View File

@@ -19,7 +19,7 @@ class APIServerConnectionBase : public ProtoService {
template<typename T> bool send_message(const T &msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_send_message_(T::message_name(), msg.dump());
this->log_send_message_(msg.message_name(), msg.dump());
#endif
return this->send_message_(msg, T::MESSAGE_TYPE);
}

View File

@@ -47,6 +47,11 @@ void APIServer::setup() {
}
#endif
// Schedule reboot if no clients connect within timeout
if (this->reboot_timeout_ != 0) {
this->schedule_reboot_timeout_();
}
this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections
if (this->socket_ == nullptr) {
ESP_LOGW(TAG, "Could not create socket");
@@ -106,8 +111,6 @@ void APIServer::setup() {
}
#endif
this->last_connected_ = millis();
#ifdef USE_ESP32_CAMERA
if (esp32_camera::global_esp32_camera != nullptr && !esp32_camera::global_esp32_camera->is_internal()) {
esp32_camera::global_esp32_camera->add_image_callback(
@@ -121,6 +124,16 @@ void APIServer::setup() {
#endif
}
void APIServer::schedule_reboot_timeout_() {
this->status_set_warning();
this->set_timeout("api_reboot", this->reboot_timeout_, []() {
if (!global_api_server->is_connected()) {
ESP_LOGE(TAG, "No clients; rebooting");
App.reboot();
}
});
}
void APIServer::loop() {
// Accept new clients only if the socket exists and has incoming connections
if (this->socket_ && this->socket_->ready()) {
@@ -130,51 +143,61 @@ void APIServer::loop() {
auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len);
if (!sock)
break;
ESP_LOGD(TAG, "Accepted %s", sock->getpeername().c_str());
ESP_LOGD(TAG, "Accept %s", sock->getpeername().c_str());
auto *conn = new APIConnection(std::move(sock), this);
this->clients_.emplace_back(conn);
conn->start();
// Clear warning status and cancel reboot when first client connects
if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) {
this->status_clear_warning();
this->cancel_timeout("api_reboot");
}
}
}
if (this->clients_.empty()) {
return;
}
// Process clients and remove disconnected ones in a single pass
if (!this->clients_.empty()) {
// Check network connectivity once for all clients
if (!network::is_connected()) {
// Network is down - disconnect all clients
for (auto &client : this->clients_) {
client->on_fatal_error();
ESP_LOGW(TAG, "%s: Network down; disconnect", client->get_client_combined_info().c_str());
}
// Continue to process and clean up the clients below
}
size_t client_index = 0;
while (client_index < this->clients_.size()) {
auto &client = this->clients_[client_index];
if (client->remove_) {
// Handle disconnection
if (!client->remove_) {
// Common case: process active client
client->loop();
client_index++;
continue;
}
// Rare case: handle disconnection
this->client_disconnected_trigger_->trigger(client->client_info_, client->client_peername_);
ESP_LOGV(TAG, "Removing connection to %s", client->client_info_.c_str());
ESP_LOGV(TAG, "Remove connection %s", client->client_info_.c_str());
// Swap with the last element and pop (avoids expensive vector shifts)
if (client_index < this->clients_.size() - 1) {
std::swap(this->clients_[client_index], this->clients_.back());
}
this->clients_.pop_back();
// Don't increment client_index since we need to process the swapped element
} else {
// Process active client
client->loop();
client_index++; // Move to next client
}
}
}
if (this->reboot_timeout_ != 0) {
const uint32_t now = millis();
if (!this->is_connected()) {
if (now - this->last_connected_ > this->reboot_timeout_) {
ESP_LOGE(TAG, "No client connected; rebooting");
App.reboot();
}
this->status_set_warning();
} else {
this->last_connected_ = now;
this->status_clear_warning();
// Schedule reboot when last client disconnects
if (this->clients_.empty() && this->reboot_timeout_ != 0) {
this->schedule_reboot_timeout_();
}
// Don't increment client_index since we need to process the swapped element
}
}

View File

@@ -142,19 +142,27 @@ class APIServer : public Component, public Controller {
}
protected:
bool shutting_down_ = false;
void schedule_reboot_timeout_();
// Pointers and pointer-like types first (4 bytes each)
std::unique_ptr<socket::Socket> socket_ = nullptr;
uint16_t port_{6053};
Trigger<std::string, std::string> *client_connected_trigger_ = new Trigger<std::string, std::string>();
Trigger<std::string, std::string> *client_disconnected_trigger_ = new Trigger<std::string, std::string>();
// 4-byte aligned types
uint32_t reboot_timeout_{300000};
uint32_t batch_delay_{100};
uint32_t last_connected_{0};
// Vectors and strings (12 bytes each on 32-bit)
std::vector<std::unique_ptr<APIConnection>> clients_;
std::string password_;
std::vector<uint8_t> shared_write_buffer_; // Shared proto write buffer for all connections
std::vector<HomeAssistantStateSubscription> state_subs_;
std::vector<UserServiceDescriptor *> user_services_;
Trigger<std::string, std::string> *client_connected_trigger_ = new Trigger<std::string, std::string>();
Trigger<std::string, std::string> *client_disconnected_trigger_ = new Trigger<std::string, std::string>();
// Group smaller types together
uint16_t port_{6053};
bool shutting_down_ = false;
// 3 bytes used, 1 byte padding
#ifdef USE_API_NOISE
std::shared_ptr<APINoiseContext> noise_ctx_ = std::make_shared<APINoiseContext>();

View File

@@ -216,7 +216,7 @@ class ProtoWriteBuffer {
this->buffer_->insert(this->buffer_->end(), data, data + len);
}
void encode_string(uint32_t field_id, const std::string &value, bool force = false) {
this->encode_string(field_id, value.data(), value.size());
this->encode_string(field_id, value.data(), value.size(), force);
}
void encode_bytes(uint32_t field_id, const uint8_t *data, size_t len, bool force = false) {
this->encode_string(field_id, reinterpret_cast<const char *>(data), len, force);
@@ -327,12 +327,15 @@ class ProtoWriteBuffer {
class ProtoMessage {
public:
virtual ~ProtoMessage() = default;
virtual void encode(ProtoWriteBuffer buffer) const = 0;
// Default implementation for messages with no fields
virtual void encode(ProtoWriteBuffer buffer) const {}
void decode(const uint8_t *buffer, size_t length);
virtual void calculate_size(uint32_t &total_size) const = 0;
// Default implementation for messages with no fields
virtual void calculate_size(uint32_t &total_size) const {}
#ifdef HAS_PROTO_MESSAGE_DUMP
std::string dump() const;
virtual void dump_to(std::string &out) const = 0;
virtual const char *message_name() const { return "unknown"; }
#endif
protected:
@@ -377,6 +380,26 @@ class ProtoService {
// Send the buffer
return this->send_buffer(buffer, message_type);
}
// Authentication helper methods
bool check_connection_setup_() {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return false;
}
return true;
}
bool check_authenticated_() {
if (!this->check_connection_setup_()) {
return false;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return false;
}
return true;
}
};
} // namespace api

View File

@@ -21,8 +21,8 @@ CONFIG_SCHEMA = cv.All(
@coroutine_with_priority(200.0)
async def to_code(config):
if CORE.is_esp32 or CORE.is_libretiny:
# https://github.com/esphome/AsyncTCP/blob/master/library.json
cg.add_library("esphome/AsyncTCP-esphome", "2.1.4")
# https://github.com/ESP32Async/AsyncTCP
cg.add_library("ESP32Async/AsyncTCP", "3.4.4")
elif CORE.is_esp8266:
# https://github.com/esphome/ESPAsyncTCP
cg.add_library("esphome/ESPAsyncTCP-esphome", "2.0.0")
# https://github.com/ESP32Async/ESPAsyncTCP
cg.add_library("ESP32Async/ESPAsyncTCP", "2.0.0")

View File

@@ -86,7 +86,7 @@ bool AudioTransferBuffer::reallocate(size_t new_buffer_size) {
bool AudioTransferBuffer::allocate_buffer_(size_t buffer_size) {
this->buffer_size_ = buffer_size;
RAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
RAMAllocator<uint8_t> allocator;
this->buffer_ = allocator.allocate(this->buffer_size_);
if (this->buffer_ == nullptr) {
@@ -101,7 +101,7 @@ bool AudioTransferBuffer::allocate_buffer_(size_t buffer_size) {
void AudioTransferBuffer::deallocate_buffer_() {
if (this->buffer_ != nullptr) {
RAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
RAMAllocator<uint8_t> allocator;
allocator.deallocate(this->buffer_, this->buffer_size_);
this->buffer_ = nullptr;
this->data_start_ = nullptr;

View File

@@ -480,7 +480,11 @@ void BedJetHub::set_clock(uint8_t hour, uint8_t minute) {
/* Internal */
void BedJetHub::loop() {}
void BedJetHub::loop() {
// Parent BLEClientNode has a loop() method, but this component uses
// polling via update() and BLE callbacks so loop isn't needed
this->disable_loop();
}
void BedJetHub::update() { this->dispatch_status_(); }
void BedJetHub::dump_config() {

View File

@@ -83,7 +83,11 @@ void BedJetClimate::reset_state_() {
this->publish_state();
}
void BedJetClimate::loop() {}
void BedJetClimate::loop() {
// This component is controlled via the parent BedJetHub
// Empty loop not needed, disable to save CPU cycles
this->disable_loop();
}
void BedJetClimate::control(const ClimateCall &call) {
ESP_LOGD(TAG, "Received BedJetClimate::control");

View File

@@ -7,11 +7,13 @@
extern "C" {
#include "rtos_pub.h"
#include "spi.h"
// rtos_pub.h must be included before the rest of the includes
#include "arm_arch.h"
#include "general_dma_pub.h"
#include "gpio_pub.h"
#include "icu_pub.h"
#include "spi.h"
#undef SPI_DAT
#undef SPI_BASE
};
@@ -124,7 +126,7 @@ void BekenSPILEDStripLightOutput::setup() {
size_t buffer_size = this->get_buffer_size_();
size_t dma_buffer_size = (buffer_size * 8) + (2 * 64);
ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
RAMAllocator<uint8_t> allocator;
this->buf_ = allocator.allocate(buffer_size);
if (this->buf_ == nullptr) {
ESP_LOGE(TAG, "Cannot allocate LED buffer!");

View File

@@ -50,7 +50,7 @@ void BH1750Sensor::read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function<
// turn on (after one-shot sensor automatically powers down)
uint8_t turn_on = BH1750_COMMAND_POWER_ON;
if (this->write(&turn_on, 1) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Turning on BH1750 failed");
ESP_LOGW(TAG, "Power on failed");
f(NAN);
return;
}
@@ -60,7 +60,7 @@ void BH1750Sensor::read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function<
uint8_t mtreg_hi = BH1750_COMMAND_MT_REG_HI | ((mtreg >> 5) & 0b111);
uint8_t mtreg_lo = BH1750_COMMAND_MT_REG_LO | ((mtreg >> 0) & 0b11111);
if (this->write(&mtreg_hi, 1) != i2c::ERROR_OK || this->write(&mtreg_lo, 1) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Setting measurement time for BH1750 failed");
ESP_LOGW(TAG, "Set measurement time failed");
active_mtreg_ = 0;
f(NAN);
return;
@@ -88,7 +88,7 @@ void BH1750Sensor::read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function<
return;
}
if (this->write(&cmd, 1) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Starting measurement for BH1750 failed");
ESP_LOGW(TAG, "Start measurement failed");
f(NAN);
return;
}
@@ -99,7 +99,7 @@ void BH1750Sensor::read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function<
this->set_timeout("read", meas_time, [this, mode, mtreg, f]() {
uint16_t raw_value;
if (this->read(reinterpret_cast<uint8_t *>(&raw_value), 2) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Reading BH1750 data failed");
ESP_LOGW(TAG, "Read data failed");
f(NAN);
return;
}
@@ -156,7 +156,7 @@ void BH1750Sensor::update() {
this->publish_state(NAN);
return;
}
ESP_LOGD(TAG, "'%s': Got illuminance=%.1flx", this->get_name().c_str(), val);
ESP_LOGD(TAG, "'%s': Illuminance=%.1flx", this->get_name().c_str(), val);
this->status_clear_warning();
this->publish_state(val);
});

View File

@@ -60,8 +60,8 @@ from esphome.const import (
DEVICE_CLASS_WINDOW,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass
from esphome.cpp_helpers import setup_entity
from esphome.util import Registry
CODEOWNERS = ["@esphome/core"]
@@ -148,6 +148,7 @@ BinarySensorCondition = binary_sensor_ns.class_("BinarySensorCondition", Conditi
# Filters
Filter = binary_sensor_ns.class_("Filter")
TimeoutFilter = binary_sensor_ns.class_("TimeoutFilter", Filter, cg.Component)
DelayedOnOffFilter = binary_sensor_ns.class_("DelayedOnOffFilter", Filter, cg.Component)
DelayedOnFilter = binary_sensor_ns.class_("DelayedOnFilter", Filter, cg.Component)
DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter, cg.Component)
@@ -171,6 +172,19 @@ async def invert_filter_to_code(config, filter_id):
return cg.new_Pvariable(filter_id)
@register_filter(
"timeout",
TimeoutFilter,
cv.templatable(cv.positive_time_period_milliseconds),
)
async def timeout_filter_to_code(config, filter_id):
var = cg.new_Pvariable(filter_id)
await cg.register_component(var, {})
template_ = await cg.templatable(config, [], cg.uint32)
cg.add(var.set_timeout_value(template_))
return var
@register_filter(
"delayed_on_off",
DelayedOnOffFilter,
@@ -491,6 +505,9 @@ _BINARY_SENSOR_SCHEMA = (
)
_BINARY_SENSOR_SCHEMA.add_extra(entity_duplicate_validator("binary_sensor"))
def binary_sensor_schema(
class_: MockObjClass = cv.UNDEFINED,
*,
@@ -521,7 +538,7 @@ BINARY_SENSOR_SCHEMA.add_extra(cv.deprecated_schema_constant("binary_sensor"))
async def setup_binary_sensor_core_(var, config):
await setup_entity(var, config)
await setup_entity(var, config, "binary_sensor")
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
cg.add(var.set_device_class(device_class))

View File

@@ -25,6 +25,12 @@ void Filter::input(bool value) {
}
}
void TimeoutFilter::input(bool value) {
this->set_timeout("timeout", this->timeout_delay_.value(), [this]() { this->parent_->invalidate_state(); });
// we do not de-dup here otherwise changes from invalid to valid state will not be output
this->output(value);
}
optional<bool> DelayedOnOffFilter::new_value(bool value) {
if (value) {
this->set_timeout("ON_OFF", this->on_delay_.value(), [this]() { this->output(true); });

View File

@@ -16,7 +16,7 @@ class Filter {
public:
virtual optional<bool> new_value(bool value) = 0;
void input(bool value);
virtual void input(bool value);
void output(bool value);
@@ -28,6 +28,16 @@ class Filter {
Deduplicator<bool> dedup_;
};
class TimeoutFilter : public Filter, public Component {
public:
optional<bool> new_value(bool value) override { return value; }
void input(bool value) override;
template<typename T> void set_timeout_value(T timeout) { this->timeout_delay_ = timeout; }
protected:
TemplatableValue<uint32_t> timeout_delay_{};
};
class DelayedOnOffFilter : public Filter, public Component {
public:
optional<bool> new_value(bool value) override;

View File

@@ -11,7 +11,11 @@ namespace ble_client {
static const char *const TAG = "ble_rssi_sensor";
void BLEClientRSSISensor::loop() {}
void BLEClientRSSISensor::loop() {
// Parent BLEClientNode has a loop() method, but this component uses
// polling via update() and BLE GAP callbacks so loop isn't needed
this->disable_loop();
}
void BLEClientRSSISensor::dump_config() {
LOG_SENSOR("", "BLE Client RSSI Sensor", this);

View File

@@ -11,7 +11,11 @@ namespace ble_client {
static const char *const TAG = "ble_sensor";
void BLESensor::loop() {}
void BLESensor::loop() {
// Parent BLEClientNode has a loop() method, but this component uses
// polling via update() and BLE callbacks so loop isn't needed
this->disable_loop();
}
void BLESensor::dump_config() {
LOG_SENSOR("", "BLE Sensor", this);

View File

@@ -14,7 +14,11 @@ static const char *const TAG = "ble_text_sensor";
static const std::string EMPTY = "";
void BLETextSensor::loop() {}
void BLETextSensor::loop() {
// Parent BLEClientNode has a loop() method, but this component uses
// polling via update() and BLE callbacks so loop isn't needed
this->disable_loop();
}
void BLETextSensor::dump_config() {
LOG_TEXT_SENSOR("", "BLE Text Sensor", this);

View File

@@ -26,10 +26,17 @@ class BluetoothConnection : public esp32_ble_client::BLEClientBase {
protected:
friend class BluetoothProxy;
bool seen_mtu_or_services_{false};
int16_t send_service_{-2};
// Memory optimized layout for 32-bit systems
// Group 1: Pointers (4 bytes each, naturally aligned)
BluetoothProxy *proxy_;
// Group 2: 2-byte types
int16_t send_service_{-2}; // Needs to handle negative values and service count
// Group 3: 1-byte types
bool seen_mtu_or_services_{false};
// 1 byte used, 1 byte padding
};
} // namespace bluetooth_proxy

View File

@@ -58,7 +58,7 @@ static std::vector<api::BluetoothLERawAdvertisement> &get_batch_buffer() {
return batch_buffer;
}
bool BluetoothProxy::parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) {
bool BluetoothProxy::parse_devices(const esp32_ble::BLEScanResult *scan_results, size_t count) {
if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || !this->raw_advertisements_)
return false;
@@ -73,7 +73,7 @@ bool BluetoothProxy::parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_p
// Add new advertisements to the batch buffer
for (size_t i = 0; i < count; i++) {
auto &result = advertisements[i];
auto &result = scan_results[i];
uint8_t length = result.adv_data_len + result.scan_rsp_len;
batch_buffer.emplace_back();

View File

@@ -52,7 +52,7 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
public:
BluetoothProxy();
bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
bool parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) override;
bool parse_devices(const esp32_ble::BLEScanResult *scan_results, size_t count) override;
void dump_config() override;
void setup() override;
void loop() override;
@@ -134,11 +134,17 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
BluetoothConnection *get_connection_(uint64_t address, bool reserve);
bool active_;
std::vector<BluetoothConnection *> connections_{};
// Memory optimized layout for 32-bit systems
// Group 1: Pointers (4 bytes each, naturally aligned)
api::APIConnection *api_connection_{nullptr};
// Group 2: Container types (typically 12 bytes on 32-bit)
std::vector<BluetoothConnection *> connections_{};
// Group 3: 1-byte types grouped together
bool active_;
bool raw_advertisements_{false};
// 2 bytes used, 2 bytes padding
};
extern BluetoothProxy *global_bluetooth_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)

View File

@@ -93,9 +93,8 @@ void BME280Component::setup() {
// Mark as not failed before initializing. Some devices will turn off sensors to save on batteries
// and when they come back on, the COMPONENT_STATE_FAILED bit must be unset on the component.
if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) {
this->component_state_ &= ~COMPONENT_STATE_MASK;
this->component_state_ |= COMPONENT_STATE_CONSTRUCTION;
if (this->is_failed()) {
this->reset_to_construction_state();
}
if (!this->read_byte(BME280_REGISTER_CHIPID, &chip_id)) {

View File

@@ -12,8 +12,8 @@ from esphome.const import (
CONF_OVERSAMPLING,
CONF_PRESSURE,
CONF_TEMPERATURE,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
ICON_GAS_CYLINDER,
STATE_CLASS_MEASUREMENT,

View File

@@ -18,8 +18,8 @@ from esphome.const import (
DEVICE_CLASS_UPDATE,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass
from esphome.cpp_helpers import setup_entity
CODEOWNERS = ["@esphome/core"]
IS_PLATFORM_COMPONENT = True
@@ -61,6 +61,9 @@ _BUTTON_SCHEMA = (
)
_BUTTON_SCHEMA.add_extra(entity_duplicate_validator("button"))
def button_schema(
class_: MockObjClass,
*,
@@ -87,7 +90,7 @@ BUTTON_SCHEMA.add_extra(cv.deprecated_schema_constant("button"))
async def setup_button_core_(var, config):
await setup_entity(var, config)
await setup_entity(var, config, "button")
for conf in config.get(CONF_ON_PRESS, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)

View File

@@ -1,4 +1,5 @@
import re
from esphome import automation
import esphome.codegen as cg
import esphome.config_validation as cv

View File

@@ -41,6 +41,7 @@ async def to_code(config):
if CORE.using_arduino:
if CORE.is_esp32:
cg.add_library("ESP32 Async UDP", None)
cg.add_library("DNSServer", None)
cg.add_library("WiFi", None)
if CORE.is_esp8266:

View File

@@ -37,7 +37,12 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
request->redirect("/?save");
}
void CaptivePortal::setup() {}
void CaptivePortal::setup() {
#ifndef USE_ARDUINO
// No DNS server needed for non-Arduino frameworks
this->disable_loop();
#endif
}
void CaptivePortal::start() {
this->base_->init();
if (!this->initialized_) {
@@ -50,6 +55,8 @@ void CaptivePortal::start() {
this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError);
network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip();
this->dns_server_->start(53, "*", ip);
// Re-enable loop() when DNS server is started
this->enable_loop();
#endif
this->base_->get_server()->onNotFound([this](AsyncWebServerRequest *req) {
@@ -68,7 +75,11 @@ void CaptivePortal::start() {
void CaptivePortal::handleRequest(AsyncWebServerRequest *req) {
if (req->url() == "/") {
#ifndef USE_ESP8266
auto *response = req->beginResponse(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ));
#else
auto *response = req->beginResponse_P(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ));
#endif
response->addHeader("Content-Encoding", "gzip");
req->send(response);
return;

View File

@@ -21,8 +21,11 @@ class CaptivePortal : public AsyncWebHandler, public Component {
void dump_config() override;
#ifdef USE_ARDUINO
void loop() override {
if (this->dns_server_ != nullptr)
if (this->dns_server_ != nullptr) {
this->dns_server_->processNextRequest();
} else {
this->disable_loop();
}
}
#endif
float get_setup_priority() const override;
@@ -37,7 +40,7 @@ class CaptivePortal : public AsyncWebHandler, public Component {
#endif
}
bool canHandle(AsyncWebServerRequest *request) override {
bool canHandle(AsyncWebServerRequest *request) const override {
if (!this->active_)
return false;

View File

@@ -48,8 +48,8 @@ from esphome.const import (
CONF_WEB_SERVER,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass
from esphome.cpp_helpers import setup_entity
IS_PLATFORM_COMPONENT = True
@@ -247,6 +247,9 @@ _CLIMATE_SCHEMA = (
)
_CLIMATE_SCHEMA.add_extra(entity_duplicate_validator("climate"))
def climate_schema(
class_: MockObjClass,
*,
@@ -273,7 +276,7 @@ CLIMATE_SCHEMA.add_extra(cv.deprecated_schema_constant("climate"))
async def setup_climate_core_(var, config):
await setup_entity(var, config)
await setup_entity(var, config, "climate")
visual = config[CONF_VISUAL]
if (min_temp := visual.get(CONF_MIN_TEMPERATURE)) is not None:

View File

@@ -1,10 +1,10 @@
"""CM1106 Sensor component for ESPHome."""
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components import sensor, uart
import esphome.config_validation as cv
from esphome.const import (
CONF_CO2,
CONF_ID,

View File

@@ -33,8 +33,8 @@ from esphome.const import (
DEVICE_CLASS_WINDOW,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass
from esphome.cpp_helpers import setup_entity
IS_PLATFORM_COMPONENT = True
@@ -126,6 +126,9 @@ _COVER_SCHEMA = (
)
_COVER_SCHEMA.add_extra(entity_duplicate_validator("cover"))
def cover_schema(
class_: MockObjClass,
*,
@@ -154,7 +157,7 @@ COVER_SCHEMA.add_extra(cv.deprecated_schema_constant("cover"))
async def setup_cover_core_(var, config):
await setup_entity(var, config)
await setup_entity(var, config, "cover")
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
cg.add(var.set_device_class(device_class))

View File

@@ -22,8 +22,8 @@ from esphome.const import (
CONF_YEAR,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass
from esphome.cpp_helpers import setup_entity
CODEOWNERS = ["@rfdarter", "@jesserockz"]
@@ -84,6 +84,8 @@ _DATETIME_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
).add_extra(_validate_time_present)
_DATETIME_SCHEMA.add_extra(entity_duplicate_validator("datetime"))
def date_schema(class_: MockObjClass) -> cv.Schema:
schema = cv.Schema(
@@ -133,7 +135,7 @@ def datetime_schema(class_: MockObjClass) -> cv.Schema:
async def setup_datetime_core_(var, config):
await setup_entity(var, config)
await setup_entity(var, config, "datetime")
if (mqtt_id := config.get(CONF_MQTT_ID)) is not None:
mqtt_ = cg.new_Pvariable(mqtt_id, var)

View File

@@ -11,25 +11,25 @@ static const char *const TAG = "datetime.date_entity";
void DateEntity::publish_state() {
if (this->year_ == 0 || this->month_ == 0 || this->day_ == 0) {
this->has_state_ = false;
this->set_has_state(false);
return;
}
if (this->year_ < 1970 || this->year_ > 3000) {
this->has_state_ = false;
this->set_has_state(false);
ESP_LOGE(TAG, "Year must be between 1970 and 3000");
return;
}
if (this->month_ < 1 || this->month_ > 12) {
this->has_state_ = false;
this->set_has_state(false);
ESP_LOGE(TAG, "Month must be between 1 and 12");
return;
}
if (this->day_ > days_in_month(this->month_, this->year_)) {
this->has_state_ = false;
this->set_has_state(false);
ESP_LOGE(TAG, "Day must be between 1 and %d for month %d", days_in_month(this->month_, this->year_), this->month_);
return;
}
this->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_);
this->state_callback_.call();
}

View File

@@ -13,9 +13,6 @@ namespace datetime {
class DateTimeBase : public EntityBase {
public:
/// Return whether this Datetime has gotten a full state yet.
bool has_state() const { return this->has_state_; }
virtual ESPTime state_as_esptime() const = 0;
void add_on_state_callback(std::function<void()> &&callback) { this->state_callback_.add(std::move(callback)); }
@@ -31,8 +28,6 @@ class DateTimeBase : public EntityBase {
#ifdef USE_TIME
time::RealTimeClock *rtc_;
#endif
bool has_state_{false};
};
#ifdef USE_TIME

View File

@@ -11,40 +11,40 @@ static const char *const TAG = "datetime.datetime_entity";
void DateTimeEntity::publish_state() {
if (this->year_ == 0 || this->month_ == 0 || this->day_ == 0) {
this->has_state_ = false;
this->set_has_state(false);
return;
}
if (this->year_ < 1970 || this->year_ > 3000) {
this->has_state_ = false;
this->set_has_state(false);
ESP_LOGE(TAG, "Year must be between 1970 and 3000");
return;
}
if (this->month_ < 1 || this->month_ > 12) {
this->has_state_ = false;
this->set_has_state(false);
ESP_LOGE(TAG, "Month must be between 1 and 12");
return;
}
if (this->day_ > days_in_month(this->month_, this->year_)) {
this->has_state_ = false;
this->set_has_state(false);
ESP_LOGE(TAG, "Day must be between 1 and %d for month %d", days_in_month(this->month_, this->year_), this->month_);
return;
}
if (this->hour_ > 23) {
this->has_state_ = false;
this->set_has_state(false);
ESP_LOGE(TAG, "Hour must be between 0 and 23");
return;
}
if (this->minute_ > 59) {
this->has_state_ = false;
this->set_has_state(false);
ESP_LOGE(TAG, "Minute must be between 0 and 59");
return;
}
if (this->second_ > 59) {
this->has_state_ = false;
this->set_has_state(false);
ESP_LOGE(TAG, "Second must be between 0 and 59");
return;
}
this->has_state_ = true;
this->set_has_state(true);
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->state_callback_.call();

View File

@@ -11,21 +11,21 @@ static const char *const TAG = "datetime.time_entity";
void TimeEntity::publish_state() {
if (this->hour_ > 23) {
this->has_state_ = false;
this->set_has_state(false);
ESP_LOGE(TAG, "Hour must be between 0 and 23");
return;
}
if (this->minute_ > 59) {
this->has_state_ = false;
this->set_has_state(false);
ESP_LOGE(TAG, "Minute must be between 0 and 59");
return;
}
if (this->second_ > 59) {
this->has_state_ = false;
this->set_has_state(false);
ESP_LOGE(TAG, "Second must be between 0 and 59");
return;
}
this->has_state_ = true;
this->set_has_state(true);
ESP_LOGD(TAG, "'%s': Sending time %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_,
this->second_);
this->state_callback_.call();

View File

@@ -455,7 +455,7 @@ CONFIG_SCHEMA = cv.Schema(
CONF_NAME: "Demo Plain Sensor",
},
{
CONF_NAME: "Demo Temperature Sensor",
CONF_NAME: "Demo Temperature Sensor 1",
CONF_UNIT_OF_MEASUREMENT: UNIT_CELSIUS,
CONF_ICON: ICON_THERMOMETER,
CONF_ACCURACY_DECIMALS: 1,
@@ -463,7 +463,7 @@ CONFIG_SCHEMA = cv.Schema(
CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
{
CONF_NAME: "Demo Temperature Sensor",
CONF_NAME: "Demo Temperature Sensor 2",
CONF_UNIT_OF_MEASUREMENT: UNIT_CELSIUS,
CONF_ICON: ICON_THERMOMETER,
CONF_ACCURACY_DECIMALS: 1,

View File

@@ -11,7 +11,7 @@ namespace display {
static const char *const TAG = "display";
void DisplayBuffer::init_internal_(uint32_t buffer_length) {
ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
RAMAllocator<uint8_t> allocator;
this->buffer_ = allocator.allocate(buffer_length);
if (this->buffer_ == nullptr) {
ESP_LOGE(TAG, "Could not allocate buffer for display!");

View File

@@ -94,6 +94,13 @@ COMPILER_OPTIMIZATIONS = {
"SIZE": "CONFIG_COMPILER_OPTIMIZATION_SIZE",
}
ARDUINO_ALLOWED_VARIANTS = [
VARIANT_ESP32,
VARIANT_ESP32C3,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
]
def get_cpu_frequencies(*frequencies):
return [str(x) + "MHZ" for x in frequencies]
@@ -125,6 +132,8 @@ def set_core_data(config):
choices = CPU_FREQUENCIES[variant]
if "160MHZ" in choices:
cpu_frequency = "160MHZ"
elif "360MHZ" in choices:
cpu_frequency = "360MHZ"
else:
cpu_frequency = choices[-1]
config[CONF_CPU_FREQUENCY] = cpu_frequency
@@ -143,12 +152,17 @@ def set_core_data(config):
CORE.data[KEY_ESP32][KEY_COMPONENTS] = {}
elif conf[CONF_TYPE] == FRAMEWORK_ARDUINO:
CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "arduino"
if variant not in ARDUINO_ALLOWED_VARIANTS:
raise cv.Invalid(
f"ESPHome does not support using the Arduino framework for the {variant}. Please use the ESP-IDF framework instead.",
path=[CONF_FRAMEWORK, CONF_TYPE],
)
CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse(
config[CONF_FRAMEWORK][CONF_VERSION]
)
CORE.data[KEY_ESP32][KEY_BOARD] = config[CONF_BOARD]
CORE.data[KEY_ESP32][KEY_VARIANT] = config[CONF_VARIANT]
CORE.data[KEY_ESP32][KEY_VARIANT] = variant
CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES] = {}
return config
@@ -277,11 +291,8 @@ def add_extra_build_file(filename: str, path: str) -> bool:
def _format_framework_arduino_version(ver: cv.Version) -> str:
# format the given arduino (https://github.com/espressif/arduino-esp32/releases) version to
# a PIO platformio/framework-arduinoespressif32 value
# List of package versions: https://api.registry.platformio.org/v3/packages/platformio/tool/framework-arduinoespressif32
if ver <= cv.Version(1, 0, 3):
return f"~2.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
return f"~3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
# a PIO pioarduino/framework-arduinoespressif32 value
return f"pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/{str(ver)}/esp32-{str(ver)}.zip"
def _format_framework_espidf_version(
@@ -305,12 +316,10 @@ def _format_framework_espidf_version(
# The default/recommended arduino framework version
# - https://github.com/espressif/arduino-esp32/releases
# - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-arduinoespressif32
RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(2, 0, 5)
# The platformio/espressif32 version to use for arduino frameworks
# - https://github.com/platformio/platform-espressif32/releases
# - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32
ARDUINO_PLATFORM_VERSION = cv.Version(5, 4, 0)
RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 1, 3)
# The platform-espressif32 version to use for arduino frameworks
# - https://github.com/pioarduino/platform-espressif32/releases
ARDUINO_PLATFORM_VERSION = cv.Version(53, 3, 13)
# The default/recommended esp-idf framework version
# - https://github.com/espressif/esp-idf/releases
@@ -353,8 +362,8 @@ SUPPORTED_PIOARDUINO_ESP_IDF_5X = [
def _arduino_check_versions(value):
value = value.copy()
lookups = {
"dev": (cv.Version(2, 1, 0), "https://github.com/espressif/arduino-esp32.git"),
"latest": (cv.Version(2, 0, 9), None),
"dev": (cv.Version(3, 1, 3), "https://github.com/espressif/arduino-esp32.git"),
"latest": (cv.Version(3, 1, 3), None),
"recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None),
}
@@ -376,6 +385,10 @@ def _arduino_check_versions(value):
CONF_PLATFORM_VERSION, _parse_platform_version(str(ARDUINO_PLATFORM_VERSION))
)
if value[CONF_SOURCE].startswith("http"):
# prefix is necessary or platformio will complain with a cryptic error
value[CONF_SOURCE] = f"framework-arduinoespressif32@{value[CONF_SOURCE]}"
if version != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION:
_LOGGER.warning(
"The selected Arduino framework version is not the recommended one. "
@@ -618,6 +631,21 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All(
)
def _set_default_framework(config):
if CONF_FRAMEWORK not in config:
config = config.copy()
variant = config[CONF_VARIANT]
if variant in ARDUINO_ALLOWED_VARIANTS:
config[CONF_FRAMEWORK] = ARDUINO_FRAMEWORK_SCHEMA({})
config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ARDUINO
else:
config[CONF_FRAMEWORK] = ESP_IDF_FRAMEWORK_SCHEMA({})
config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ESP_IDF
return config
FRAMEWORK_ESP_IDF = "esp-idf"
FRAMEWORK_ARDUINO = "arduino"
FRAMEWORK_SCHEMA = cv.typed_schema(
@@ -627,7 +655,6 @@ FRAMEWORK_SCHEMA = cv.typed_schema(
},
lower=True,
space="-",
default_type=FRAMEWORK_ARDUINO,
)
@@ -654,10 +681,11 @@ CONFIG_SCHEMA = cv.All(
),
cv.Optional(CONF_PARTITIONS): cv.file_,
cv.Optional(CONF_VARIANT): cv.one_of(*VARIANTS, upper=True),
cv.Optional(CONF_FRAMEWORK, default={}): FRAMEWORK_SCHEMA,
cv.Optional(CONF_FRAMEWORK): FRAMEWORK_SCHEMA,
}
),
_detect_variant,
_set_default_framework,
set_core_data,
)
@@ -668,6 +696,7 @@ FINAL_VALIDATE_SCHEMA = cv.Schema(final_validate)
async def to_code(config):
cg.add_platformio_option("board", config[CONF_BOARD])
cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE])
cg.set_cpp_standard("gnu++17")
cg.add_build_flag("-DUSE_ESP32")
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{config[CONF_VARIANT]}")
@@ -801,10 +830,7 @@ async def to_code(config):
cg.add_platformio_option("framework", "arduino")
cg.add_build_flag("-DUSE_ARDUINO")
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO")
cg.add_platformio_option(
"platform_packages",
[f"platformio/framework-arduinoespressif32@{conf[CONF_SOURCE]}"],
)
cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]])
if CONF_PARTITIONS in config:
cg.add_platformio_option("board_build.partitions", config[CONF_PARTITIONS])

View File

@@ -1,8 +1,10 @@
#ifdef USE_ESP32
#include "ble.h"
#include "ble_event_pool.h"
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <esp_bt.h>
@@ -23,9 +25,6 @@ namespace esp32_ble {
static const char *const TAG = "esp32_ble";
static RAMAllocator<BLEEvent> EVENT_ALLOCATOR( // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
RAMAllocator<BLEEvent>::ALLOW_FAILURE | RAMAllocator<BLEEvent>::ALLOC_INTERNAL);
void ESP32BLE::setup() {
global_ble = this;
ESP_LOGCONFIG(TAG, "Running setup");
@@ -304,82 +303,191 @@ void ESP32BLE::loop() {
BLEEvent *ble_event = this->ble_events_.pop();
while (ble_event != nullptr) {
switch (ble_event->type_) {
case BLEEvent::GATTS:
this->real_gatts_event_handler_(ble_event->event_.gatts.gatts_event, ble_event->event_.gatts.gatts_if,
&ble_event->event_.gatts.gatts_param);
case BLEEvent::GATTS: {
esp_gatts_cb_event_t event = ble_event->event_.gatts.gatts_event;
esp_gatt_if_t gatts_if = ble_event->event_.gatts.gatts_if;
esp_ble_gatts_cb_param_t *param = ble_event->event_.gatts.gatts_param;
ESP_LOGV(TAG, "gatts_event [esp_gatt_if: %d] - %d", gatts_if, event);
for (auto *gatts_handler : this->gatts_event_handlers_) {
gatts_handler->gatts_event_handler(event, gatts_if, param);
}
break;
case BLEEvent::GATTC:
this->real_gattc_event_handler_(ble_event->event_.gattc.gattc_event, ble_event->event_.gattc.gattc_if,
&ble_event->event_.gattc.gattc_param);
}
case BLEEvent::GATTC: {
esp_gattc_cb_event_t event = ble_event->event_.gattc.gattc_event;
esp_gatt_if_t gattc_if = ble_event->event_.gattc.gattc_if;
esp_ble_gattc_cb_param_t *param = ble_event->event_.gattc.gattc_param;
ESP_LOGV(TAG, "gattc_event [esp_gatt_if: %d] - %d", gattc_if, event);
for (auto *gattc_handler : this->gattc_event_handlers_) {
gattc_handler->gattc_event_handler(event, gattc_if, param);
}
break;
case BLEEvent::GAP:
this->real_gap_event_handler_(ble_event->event_.gap.gap_event, &ble_event->event_.gap.gap_param);
}
case BLEEvent::GAP: {
esp_gap_ble_cb_event_t gap_event = ble_event->event_.gap.gap_event;
switch (gap_event) {
case ESP_GAP_BLE_SCAN_RESULT_EVT:
// Use the new scan event handler - no memcpy!
for (auto *scan_handler : this->gap_scan_event_handlers_) {
scan_handler->gap_scan_event_handler(ble_event->scan_result());
}
break;
// Scan complete events
case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
// All three scan complete events have the same structure with just status
// The scan_complete struct matches ESP-IDF's layout exactly, so this reinterpret_cast is safe
// This is verified at compile-time by static_assert checks in ble_event.h
// The struct already contains our copy of the status (copied in BLEEvent constructor)
ESP_LOGV(TAG, "gap_event_handler - %d", gap_event);
for (auto *gap_handler : this->gap_event_handlers_) {
gap_handler->gap_event_handler(
gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.scan_complete));
}
break;
// Advertising complete events
case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT:
case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT:
case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
// All advertising complete events have the same structure with just status
ESP_LOGV(TAG, "gap_event_handler - %d", gap_event);
for (auto *gap_handler : this->gap_event_handlers_) {
gap_handler->gap_event_handler(
gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.adv_complete));
}
break;
// RSSI complete event
case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT:
ESP_LOGV(TAG, "gap_event_handler - %d", gap_event);
for (auto *gap_handler : this->gap_event_handlers_) {
gap_handler->gap_event_handler(
gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.read_rssi_complete));
}
break;
// Security events
case ESP_GAP_BLE_AUTH_CMPL_EVT:
case ESP_GAP_BLE_SEC_REQ_EVT:
case ESP_GAP_BLE_PASSKEY_NOTIF_EVT:
case ESP_GAP_BLE_PASSKEY_REQ_EVT:
case ESP_GAP_BLE_NC_REQ_EVT:
ESP_LOGV(TAG, "gap_event_handler - %d", gap_event);
for (auto *gap_handler : this->gap_event_handlers_) {
gap_handler->gap_event_handler(
gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.security));
}
break;
default:
// Unknown/unhandled event
ESP_LOGW(TAG, "Unhandled GAP event type in loop: %d", gap_event);
break;
}
break;
}
default:
break;
}
ble_event->~BLEEvent();
EVENT_ALLOCATOR.deallocate(ble_event, 1);
// Return the event to the pool
this->ble_event_pool_.release(ble_event);
ble_event = this->ble_events_.pop();
}
if (this->advertising_ != nullptr) {
this->advertising_->loop();
}
// Log dropped events periodically
uint16_t dropped = this->ble_events_.get_and_reset_dropped_count();
if (dropped > 0) {
ESP_LOGW(TAG, "Dropped %u BLE events due to buffer overflow", dropped);
}
}
void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1);
if (new_event == nullptr) {
// Memory too fragmented to allocate new event. Can only drop it until memory comes back
// Helper function to load new event data based on type
void load_ble_event(BLEEvent *event, esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
event->load_gap_event(e, p);
}
void load_ble_event(BLEEvent *event, esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) {
event->load_gattc_event(e, i, p);
}
void load_ble_event(BLEEvent *event, esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) {
event->load_gatts_event(e, i, p);
}
template<typename... Args> void enqueue_ble_event(Args... args) {
// Allocate an event from the pool
BLEEvent *event = global_ble->ble_event_pool_.allocate();
if (event == nullptr) {
// No events available - queue is full or we're out of memory
global_ble->ble_events_.increment_dropped_count();
return;
}
new (new_event) BLEEvent(event, param);
global_ble->ble_events_.push(new_event);
} // NOLINT(clang-analyzer-unix.Malloc)
void ESP32BLE::real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
ESP_LOGV(TAG, "(BLE) gap_event_handler - %d", event);
for (auto *gap_handler : this->gap_event_handlers_) {
gap_handler->gap_event_handler(event, param);
// Load new event data (replaces previous event)
load_ble_event(event, args...);
// Push the event to the queue
global_ble->ble_events_.push(event);
// Push always succeeds because we're the only producer and the pool ensures we never exceed queue size
}
// Explicit template instantiations for the friend function
template void enqueue_ble_event(esp_gap_ble_cb_event_t, esp_ble_gap_cb_param_t *);
template void enqueue_ble_event(esp_gatts_cb_event_t, esp_gatt_if_t, esp_ble_gatts_cb_param_t *);
template void enqueue_ble_event(esp_gattc_cb_event_t, esp_gatt_if_t, esp_ble_gattc_cb_param_t *);
void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
switch (event) {
// Queue GAP events that components need to handle
// Scanning events - used by esp32_ble_tracker
case ESP_GAP_BLE_SCAN_RESULT_EVT:
case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
// Advertising events - used by esp32_ble_beacon and esp32_ble server
case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT:
case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT:
case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
// Connection events - used by ble_client
case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT:
// Security events - used by ble_client and bluetooth_proxy
case ESP_GAP_BLE_AUTH_CMPL_EVT:
case ESP_GAP_BLE_SEC_REQ_EVT:
case ESP_GAP_BLE_PASSKEY_NOTIF_EVT:
case ESP_GAP_BLE_PASSKEY_REQ_EVT:
case ESP_GAP_BLE_NC_REQ_EVT:
enqueue_ble_event(event, param);
return;
// Ignore these GAP events as they are not relevant for our use case
case ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT:
case ESP_GAP_BLE_SET_PKT_LENGTH_COMPLETE_EVT:
return;
default:
break;
}
ESP_LOGW(TAG, "Ignoring unexpected GAP event type: %d", event);
}
void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
esp_ble_gatts_cb_param_t *param) {
BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1);
if (new_event == nullptr) {
// Memory too fragmented to allocate new event. Can only drop it until memory comes back
return;
}
new (new_event) BLEEvent(event, gatts_if, param);
global_ble->ble_events_.push(new_event);
} // NOLINT(clang-analyzer-unix.Malloc)
void ESP32BLE::real_gatts_event_handler_(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
esp_ble_gatts_cb_param_t *param) {
ESP_LOGV(TAG, "(BLE) gatts_event [esp_gatt_if: %d] - %d", gatts_if, event);
for (auto *gatts_handler : this->gatts_event_handlers_) {
gatts_handler->gatts_event_handler(event, gatts_if, param);
}
enqueue_ble_event(event, gatts_if, param);
}
void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) {
BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1);
if (new_event == nullptr) {
// Memory too fragmented to allocate new event. Can only drop it until memory comes back
return;
}
new (new_event) BLEEvent(event, gattc_if, param);
global_ble->ble_events_.push(new_event);
} // NOLINT(clang-analyzer-unix.Malloc)
void ESP32BLE::real_gattc_event_handler_(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) {
ESP_LOGV(TAG, "(BLE) gattc_event [esp_gatt_if: %d] - %d", gattc_if, event);
for (auto *gattc_handler : this->gattc_event_handlers_) {
gattc_handler->gattc_event_handler(event, gattc_if, param);
}
enqueue_ble_event(event, gattc_if, param);
}
float ESP32BLE::get_setup_priority() const { return setup_priority::BLUETOOTH; }
@@ -409,13 +517,12 @@ void ESP32BLE::dump_config() {
break;
}
ESP_LOGCONFIG(TAG,
"ESP32 BLE:\n"
" MAC address: %02X:%02X:%02X:%02X:%02X:%02X\n"
"BLE:\n"
" MAC address: %s\n"
" IO Capability: %s",
mac_address[0], mac_address[1], mac_address[2], mac_address[3], mac_address[4], mac_address[5],
io_capability_s);
format_mac_address_pretty(mac_address).c_str(), io_capability_s);
} else {
ESP_LOGCONFIG(TAG, "ESP32 BLE: bluetooth stack is not enabled");
ESP_LOGCONFIG(TAG, "Bluetooth stack is not enabled");
}
}

View File

@@ -2,6 +2,7 @@
#include "ble_advertising.h"
#include "ble_uuid.h"
#include "ble_scan_result.h"
#include <functional>
@@ -11,6 +12,7 @@
#include "esphome/core/helpers.h"
#include "ble_event.h"
#include "ble_event_pool.h"
#include "queue.h"
#ifdef USE_ESP32
@@ -22,6 +24,16 @@
namespace esphome {
namespace esp32_ble {
// Maximum number of BLE scan results to buffer
#ifdef USE_PSRAM
static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 32;
#else
static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 20;
#endif
// Maximum size of the BLE event queue - must be power of 2 for lock-free queue
static constexpr size_t MAX_BLE_QUEUE_SIZE = 64;
uint64_t ble_addr_to_uint64(const esp_bd_addr_t address);
// NOLINTNEXTLINE(modernize-use-using)
@@ -57,6 +69,11 @@ class GAPEventHandler {
virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) = 0;
};
class GAPScanEventHandler {
public:
virtual void gap_scan_event_handler(const BLEScanResult &scan_result) = 0;
};
class GATTcEventHandler {
public:
virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
@@ -101,6 +118,9 @@ class ESP32BLE : public Component {
void advertising_register_raw_advertisement_callback(std::function<void(bool)> &&callback);
void register_gap_event_handler(GAPEventHandler *handler) { this->gap_event_handlers_.push_back(handler); }
void register_gap_scan_event_handler(GAPScanEventHandler *handler) {
this->gap_scan_event_handlers_.push_back(handler);
}
void register_gattc_event_handler(GATTcEventHandler *handler) { this->gattc_event_handlers_.push_back(handler); }
void register_gatts_event_handler(GATTsEventHandler *handler) { this->gatts_event_handlers_.push_back(handler); }
void register_ble_status_event_handler(BLEStatusEventHandler *handler) {
@@ -113,22 +133,23 @@ class ESP32BLE : public Component {
static void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param);
static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param);
void real_gatts_event_handler_(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param);
void real_gattc_event_handler_(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param);
void real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param);
bool ble_setup_();
bool ble_dismantle_();
bool ble_pre_setup_();
void advertising_init_();
private:
template<typename... Args> friend void enqueue_ble_event(Args... args);
std::vector<GAPEventHandler *> gap_event_handlers_;
std::vector<GAPScanEventHandler *> gap_scan_event_handlers_;
std::vector<GATTcEventHandler *> gattc_event_handlers_;
std::vector<GATTsEventHandler *> gatts_event_handlers_;
std::vector<BLEStatusEventHandler *> ble_status_event_handlers_;
BLEComponentState state_{BLE_COMPONENT_STATE_OFF};
Queue<BLEEvent> ble_events_;
LockFreeQueue<BLEEvent, MAX_BLE_QUEUE_SIZE> ble_events_;
BLEEventPool<MAX_BLE_QUEUE_SIZE> ble_event_pool_;
BLEAdvertising *advertising_{};
esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE};
uint32_t advertising_cycle_time_{};

View File

@@ -2,92 +2,399 @@
#ifdef USE_ESP32
#include <cstddef> // for offsetof
#include <vector>
#include <esp_gap_ble_api.h>
#include <esp_gattc_api.h>
#include <esp_gatts_api.h>
#include "ble_scan_result.h"
namespace esphome {
namespace esp32_ble {
// Compile-time verification that ESP-IDF scan complete events only contain a status field
// This ensures our reinterpret_cast in ble.cpp is safe
static_assert(sizeof(esp_ble_gap_cb_param_t::ble_scan_param_cmpl_evt_param) == sizeof(esp_bt_status_t),
"ESP-IDF scan_param_cmpl structure has unexpected size");
static_assert(sizeof(esp_ble_gap_cb_param_t::ble_scan_start_cmpl_evt_param) == sizeof(esp_bt_status_t),
"ESP-IDF scan_start_cmpl structure has unexpected size");
static_assert(sizeof(esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param) == sizeof(esp_bt_status_t),
"ESP-IDF scan_stop_cmpl structure has unexpected size");
// Verify the status field is at offset 0 (first member)
static_assert(offsetof(esp_ble_gap_cb_param_t, scan_param_cmpl.status) == 0,
"status must be first member of scan_param_cmpl");
static_assert(offsetof(esp_ble_gap_cb_param_t, scan_start_cmpl.status) == 0,
"status must be first member of scan_start_cmpl");
static_assert(offsetof(esp_ble_gap_cb_param_t, scan_stop_cmpl.status) == 0,
"status must be first member of scan_stop_cmpl");
// Compile-time verification for advertising complete events
static_assert(sizeof(esp_ble_gap_cb_param_t::ble_adv_data_cmpl_evt_param) == sizeof(esp_bt_status_t),
"ESP-IDF adv_data_cmpl structure has unexpected size");
static_assert(sizeof(esp_ble_gap_cb_param_t::ble_scan_rsp_data_cmpl_evt_param) == sizeof(esp_bt_status_t),
"ESP-IDF scan_rsp_data_cmpl structure has unexpected size");
static_assert(sizeof(esp_ble_gap_cb_param_t::ble_adv_data_raw_cmpl_evt_param) == sizeof(esp_bt_status_t),
"ESP-IDF adv_data_raw_cmpl structure has unexpected size");
static_assert(sizeof(esp_ble_gap_cb_param_t::ble_adv_start_cmpl_evt_param) == sizeof(esp_bt_status_t),
"ESP-IDF adv_start_cmpl structure has unexpected size");
static_assert(sizeof(esp_ble_gap_cb_param_t::ble_adv_stop_cmpl_evt_param) == sizeof(esp_bt_status_t),
"ESP-IDF adv_stop_cmpl structure has unexpected size");
// Verify the status field is at offset 0 for advertising events
static_assert(offsetof(esp_ble_gap_cb_param_t, adv_data_cmpl.status) == 0,
"status must be first member of adv_data_cmpl");
static_assert(offsetof(esp_ble_gap_cb_param_t, scan_rsp_data_cmpl.status) == 0,
"status must be first member of scan_rsp_data_cmpl");
static_assert(offsetof(esp_ble_gap_cb_param_t, adv_data_raw_cmpl.status) == 0,
"status must be first member of adv_data_raw_cmpl");
static_assert(offsetof(esp_ble_gap_cb_param_t, adv_start_cmpl.status) == 0,
"status must be first member of adv_start_cmpl");
static_assert(offsetof(esp_ble_gap_cb_param_t, adv_stop_cmpl.status) == 0,
"status must be first member of adv_stop_cmpl");
// Compile-time verification for RSSI complete event structure
static_assert(offsetof(esp_ble_gap_cb_param_t, read_rssi_cmpl.status) == 0,
"status must be first member of read_rssi_cmpl");
static_assert(offsetof(esp_ble_gap_cb_param_t, read_rssi_cmpl.rssi) == sizeof(esp_bt_status_t),
"rssi must immediately follow status in read_rssi_cmpl");
static_assert(offsetof(esp_ble_gap_cb_param_t, read_rssi_cmpl.remote_addr) == sizeof(esp_bt_status_t) + sizeof(int8_t),
"remote_addr must follow rssi in read_rssi_cmpl");
// Received GAP, GATTC and GATTS events are only queued, and get processed in the main loop().
// This class stores each event in a single type.
// This class stores each event with minimal memory usage.
// GAP events (99% of traffic) don't have the vector overhead.
// GATTC/GATTS events use heap allocation for their param and data.
//
// Event flow:
// 1. ESP-IDF BLE stack calls our static handlers in the BLE task context
// 2. The handlers create a BLEEvent instance, copying only the data we need
// 3. The event is pushed to a thread-safe queue
// 4. In the main loop(), events are popped from the queue and processed
// 5. The event destructor cleans up any external allocations
//
// Thread safety:
// - GAP events: We copy only the fields we need directly into the union
// - GATTC/GATTS events: We heap-allocate and copy the entire param struct, ensuring
// the data remains valid even after the BLE callback returns. The original
// param pointer from ESP-IDF is only valid during the callback.
//
// CRITICAL DESIGN NOTE:
// The heap allocations for GATTC/GATTS events are REQUIRED for memory safety.
// DO NOT attempt to optimize by removing these allocations or storing pointers
// to the original ESP-IDF data. The ESP-IDF callback data has a different lifetime
// than our event processing, and accessing it after the callback returns would
// result in use-after-free bugs and crashes.
class BLEEvent {
public:
BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
this->event_.gap.gap_event = e;
memcpy(&this->event_.gap.gap_param, p, sizeof(esp_ble_gap_cb_param_t));
this->type_ = GAP;
};
BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) {
this->event_.gattc.gattc_event = e;
this->event_.gattc.gattc_if = i;
memcpy(&this->event_.gattc.gattc_param, p, sizeof(esp_ble_gattc_cb_param_t));
// Need to also make a copy of relevant event data.
switch (e) {
case ESP_GATTC_NOTIFY_EVT:
this->data.assign(p->notify.value, p->notify.value + p->notify.value_len);
this->event_.gattc.gattc_param.notify.value = this->data.data();
break;
case ESP_GATTC_READ_CHAR_EVT:
case ESP_GATTC_READ_DESCR_EVT:
this->data.assign(p->read.value, p->read.value + p->read.value_len);
this->event_.gattc.gattc_param.read.value = this->data.data();
break;
default:
break;
}
this->type_ = GATTC;
};
BLEEvent(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) {
this->event_.gatts.gatts_event = e;
this->event_.gatts.gatts_if = i;
memcpy(&this->event_.gatts.gatts_param, p, sizeof(esp_ble_gatts_cb_param_t));
// Need to also make a copy of relevant event data.
switch (e) {
case ESP_GATTS_WRITE_EVT:
this->data.assign(p->write.value, p->write.value + p->write.len);
this->event_.gatts.gatts_param.write.value = this->data.data();
break;
default:
break;
}
this->type_ = GATTS;
};
union {
// NOLINTNEXTLINE(readability-identifier-naming)
struct gap_event {
esp_gap_ble_cb_event_t gap_event;
esp_ble_gap_cb_param_t gap_param;
} gap;
// NOLINTNEXTLINE(readability-identifier-naming)
struct gattc_event {
esp_gattc_cb_event_t gattc_event;
esp_gatt_if_t gattc_if;
esp_ble_gattc_cb_param_t gattc_param;
} gattc;
// NOLINTNEXTLINE(readability-identifier-naming)
struct gatts_event {
esp_gatts_cb_event_t gatts_event;
esp_gatt_if_t gatts_if;
esp_ble_gatts_cb_param_t gatts_param;
} gatts;
} event_;
std::vector<uint8_t> data{};
// NOLINTNEXTLINE(readability-identifier-naming)
enum ble_event_t : uint8_t {
GAP,
GATTC,
GATTS,
} type_;
};
// Type definitions for cleaner method signatures
struct StatusOnlyData {
esp_bt_status_t status;
};
struct RSSICompleteData {
esp_bt_status_t status;
int8_t rssi;
esp_bd_addr_t remote_addr;
};
// Constructor for GAP events - no external allocations needed
BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
this->type_ = GAP;
this->init_gap_data_(e, p);
}
// Constructor for GATTC events - uses heap allocation
// IMPORTANT: The heap allocation is REQUIRED and must not be removed as an optimization.
// The param pointer from ESP-IDF is only valid during the callback execution.
// Since BLE events are processed asynchronously in the main loop, we must create
// our own copy to ensure the data remains valid until the event is processed.
BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) {
this->type_ = GATTC;
this->init_gattc_data_(e, i, p);
}
// Constructor for GATTS events - uses heap allocation
// IMPORTANT: The heap allocation is REQUIRED and must not be removed as an optimization.
// The param pointer from ESP-IDF is only valid during the callback execution.
// Since BLE events are processed asynchronously in the main loop, we must create
// our own copy to ensure the data remains valid until the event is processed.
BLEEvent(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) {
this->type_ = GATTS;
this->init_gatts_data_(e, i, p);
}
// Destructor to clean up heap allocations
~BLEEvent() { this->cleanup_heap_data(); }
// Default constructor for pre-allocation in pool
BLEEvent() : type_(GAP) {}
// Clean up any heap-allocated data
void cleanup_heap_data() {
if (this->type_ == GAP) {
return;
}
if (this->type_ == GATTC) {
delete this->event_.gattc.gattc_param;
delete this->event_.gattc.data;
this->event_.gattc.gattc_param = nullptr;
this->event_.gattc.data = nullptr;
return;
}
if (this->type_ == GATTS) {
delete this->event_.gatts.gatts_param;
delete this->event_.gatts.data;
this->event_.gatts.gatts_param = nullptr;
this->event_.gatts.data = nullptr;
}
}
// Load new event data for reuse (replaces previous event data)
void load_gap_event(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
this->cleanup_heap_data();
this->type_ = GAP;
this->init_gap_data_(e, p);
}
void load_gattc_event(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) {
this->cleanup_heap_data();
this->type_ = GATTC;
this->init_gattc_data_(e, i, p);
}
void load_gatts_event(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) {
this->cleanup_heap_data();
this->type_ = GATTS;
this->init_gatts_data_(e, i, p);
}
// Disable copy to prevent double-delete
BLEEvent(const BLEEvent &) = delete;
BLEEvent &operator=(const BLEEvent &) = delete;
union {
// NOLINTNEXTLINE(readability-identifier-naming)
struct gap_event {
esp_gap_ble_cb_event_t gap_event;
union {
BLEScanResult scan_result; // 73 bytes - Used by: esp32_ble_tracker
// This matches ESP-IDF's scan complete event structures
// All three (scan_param_cmpl, scan_start_cmpl, scan_stop_cmpl) have identical layout
// Used by: esp32_ble_tracker
StatusOnlyData scan_complete; // 1 byte
// Advertising complete events all have same structure
// Used by: esp32_ble_beacon, esp32_ble server components
// ADV_DATA_SET, SCAN_RSP_DATA_SET, ADV_DATA_RAW_SET, ADV_START, ADV_STOP
StatusOnlyData adv_complete; // 1 byte
// RSSI complete event
// Used by: ble_client (ble_rssi_sensor component)
RSSICompleteData read_rssi_complete; // 8 bytes
// Security events - we store the full security union
// Used by: ble_client (automation), bluetooth_proxy, esp32_ble_client
esp_ble_sec_t security; // Variable size, but fits within scan_result size
};
} gap; // 80 bytes total
// NOLINTNEXTLINE(readability-identifier-naming)
struct gattc_event {
esp_gattc_cb_event_t gattc_event;
esp_gatt_if_t gattc_if;
esp_ble_gattc_cb_param_t *gattc_param; // Heap-allocated
std::vector<uint8_t> *data; // Heap-allocated
} gattc; // 16 bytes (pointers only)
// NOLINTNEXTLINE(readability-identifier-naming)
struct gatts_event {
esp_gatts_cb_event_t gatts_event;
esp_gatt_if_t gatts_if;
esp_ble_gatts_cb_param_t *gatts_param; // Heap-allocated
std::vector<uint8_t> *data; // Heap-allocated
} gatts; // 16 bytes (pointers only)
} event_; // 80 bytes
ble_event_t type_;
// Helper methods to access event data
ble_event_t type() const { return type_; }
esp_gap_ble_cb_event_t gap_event_type() const { return event_.gap.gap_event; }
const BLEScanResult &scan_result() const { return event_.gap.scan_result; }
esp_bt_status_t scan_complete_status() const { return event_.gap.scan_complete.status; }
esp_bt_status_t adv_complete_status() const { return event_.gap.adv_complete.status; }
const RSSICompleteData &read_rssi_complete() const { return event_.gap.read_rssi_complete; }
const esp_ble_sec_t &security() const { return event_.gap.security; }
private:
// Initialize GAP event data
void init_gap_data_(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
this->event_.gap.gap_event = e;
if (p == nullptr) {
return; // Invalid event, but we can't log in header file
}
// Copy data based on event type
switch (e) {
case ESP_GAP_BLE_SCAN_RESULT_EVT:
memcpy(this->event_.gap.scan_result.bda, p->scan_rst.bda, sizeof(esp_bd_addr_t));
this->event_.gap.scan_result.ble_addr_type = p->scan_rst.ble_addr_type;
this->event_.gap.scan_result.rssi = p->scan_rst.rssi;
this->event_.gap.scan_result.adv_data_len = p->scan_rst.adv_data_len;
this->event_.gap.scan_result.scan_rsp_len = p->scan_rst.scan_rsp_len;
this->event_.gap.scan_result.search_evt = p->scan_rst.search_evt;
memcpy(this->event_.gap.scan_result.ble_adv, p->scan_rst.ble_adv,
ESP_BLE_ADV_DATA_LEN_MAX + ESP_BLE_SCAN_RSP_DATA_LEN_MAX);
break;
case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
this->event_.gap.scan_complete.status = p->scan_param_cmpl.status;
break;
case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
this->event_.gap.scan_complete.status = p->scan_start_cmpl.status;
break;
case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
this->event_.gap.scan_complete.status = p->scan_stop_cmpl.status;
break;
// Advertising complete events - all have same structure with just status
// Used by: esp32_ble_beacon, esp32_ble server components
case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT:
this->event_.gap.adv_complete.status = p->adv_data_cmpl.status;
break;
case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT:
this->event_.gap.adv_complete.status = p->scan_rsp_data_cmpl.status;
break;
case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: // Used by: esp32_ble_beacon
this->event_.gap.adv_complete.status = p->adv_data_raw_cmpl.status;
break;
case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: // Used by: esp32_ble_beacon
this->event_.gap.adv_complete.status = p->adv_start_cmpl.status;
break;
case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: // Used by: esp32_ble_beacon
this->event_.gap.adv_complete.status = p->adv_stop_cmpl.status;
break;
// RSSI complete event
// Used by: ble_client (ble_rssi_sensor)
case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT:
this->event_.gap.read_rssi_complete.status = p->read_rssi_cmpl.status;
this->event_.gap.read_rssi_complete.rssi = p->read_rssi_cmpl.rssi;
memcpy(this->event_.gap.read_rssi_complete.remote_addr, p->read_rssi_cmpl.remote_addr, sizeof(esp_bd_addr_t));
break;
// Security events - copy the entire security union
// Used by: ble_client, bluetooth_proxy, esp32_ble_client
case ESP_GAP_BLE_AUTH_CMPL_EVT: // Used by: bluetooth_proxy, esp32_ble_client
case ESP_GAP_BLE_SEC_REQ_EVT: // Used by: esp32_ble_client
case ESP_GAP_BLE_PASSKEY_NOTIF_EVT: // Used by: ble_client automation
case ESP_GAP_BLE_PASSKEY_REQ_EVT: // Used by: ble_client automation
case ESP_GAP_BLE_NC_REQ_EVT: // Used by: ble_client automation
memcpy(&this->event_.gap.security, &p->ble_security, sizeof(esp_ble_sec_t));
break;
default:
// We only store data for GAP events that components currently use
// Unknown events still get queued and logged in ble.cpp:375 as
// "Unhandled GAP event type in loop" - this helps identify new events
// that components might need in the future
break;
}
}
// Initialize GATTC event data
void init_gattc_data_(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) {
this->event_.gattc.gattc_event = e;
this->event_.gattc.gattc_if = i;
if (p == nullptr) {
this->event_.gattc.gattc_param = nullptr;
this->event_.gattc.data = nullptr;
return; // Invalid event, but we can't log in header file
}
// Heap-allocate param and data
// Heap allocation is used because GATTC/GATTS events are rare (<1% of events)
// while GAP events (99%) are stored inline to minimize memory usage
// IMPORTANT: This heap allocation provides clear ownership semantics:
// - The BLEEvent owns the allocated memory for its lifetime
// - The data remains valid from the BLE callback context until processed in the main loop
// - Without this copy, we'd have use-after-free bugs as ESP-IDF reuses the callback memory
this->event_.gattc.gattc_param = new esp_ble_gattc_cb_param_t(*p);
// Copy data for events that need it
// The param struct contains pointers (e.g., notify.value) that point to temporary buffers.
// We must copy this data to ensure it remains valid when the event is processed later.
switch (e) {
case ESP_GATTC_NOTIFY_EVT:
this->event_.gattc.data = new std::vector<uint8_t>(p->notify.value, p->notify.value + p->notify.value_len);
this->event_.gattc.gattc_param->notify.value = this->event_.gattc.data->data();
break;
case ESP_GATTC_READ_CHAR_EVT:
case ESP_GATTC_READ_DESCR_EVT:
this->event_.gattc.data = new std::vector<uint8_t>(p->read.value, p->read.value + p->read.value_len);
this->event_.gattc.gattc_param->read.value = this->event_.gattc.data->data();
break;
default:
this->event_.gattc.data = nullptr;
break;
}
}
// Initialize GATTS event data
void init_gatts_data_(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) {
this->event_.gatts.gatts_event = e;
this->event_.gatts.gatts_if = i;
if (p == nullptr) {
this->event_.gatts.gatts_param = nullptr;
this->event_.gatts.data = nullptr;
return; // Invalid event, but we can't log in header file
}
// Heap-allocate param and data
// Heap allocation is used because GATTC/GATTS events are rare (<1% of events)
// while GAP events (99%) are stored inline to minimize memory usage
// IMPORTANT: This heap allocation provides clear ownership semantics:
// - The BLEEvent owns the allocated memory for its lifetime
// - The data remains valid from the BLE callback context until processed in the main loop
// - Without this copy, we'd have use-after-free bugs as ESP-IDF reuses the callback memory
this->event_.gatts.gatts_param = new esp_ble_gatts_cb_param_t(*p);
// Copy data for events that need it
// The param struct contains pointers (e.g., write.value) that point to temporary buffers.
// We must copy this data to ensure it remains valid when the event is processed later.
switch (e) {
case ESP_GATTS_WRITE_EVT:
this->event_.gatts.data = new std::vector<uint8_t>(p->write.value, p->write.value + p->write.len);
this->event_.gatts.gatts_param->write.value = this->event_.gatts.data->data();
break;
default:
this->event_.gatts.data = nullptr;
break;
}
}
};
// Verify the gap_event struct hasn't grown beyond expected size
// The gap member in the union should be 80 bytes (including the gap_event enum)
static_assert(sizeof(decltype(((BLEEvent *) nullptr)->event_.gap)) <= 80, "gap_event struct has grown beyond 80 bytes");
// Verify esp_ble_sec_t fits within our union
static_assert(sizeof(esp_ble_sec_t) <= 73, "esp_ble_sec_t is larger than BLEScanResult");
// BLEEvent total size: 84 bytes (80 byte union + 1 byte type + 3 bytes padding)
} // namespace esp32_ble
} // namespace esphome

View File

@@ -0,0 +1,72 @@
#pragma once
#ifdef USE_ESP32
#include <atomic>
#include <cstddef>
#include "ble_event.h"
#include "queue.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace esp32_ble {
// BLE Event Pool - On-demand pool of BLEEvent objects to avoid heap fragmentation
// Events are allocated on first use and reused thereafter, growing to peak usage
template<uint8_t SIZE> class BLEEventPool {
public:
BLEEventPool() : total_created_(0) {}
~BLEEventPool() {
// Clean up any remaining events in the free list
BLEEvent *event;
while ((event = this->free_list_.pop()) != nullptr) {
delete event;
}
}
// Allocate an event from the pool
// Returns nullptr if pool is full
BLEEvent *allocate() {
// Try to get from free list first
BLEEvent *event = this->free_list_.pop();
if (event != nullptr)
return event;
// Need to create a new event
if (this->total_created_ >= SIZE) {
// Pool is at capacity
return nullptr;
}
// Use internal RAM for better performance
RAMAllocator<BLEEvent> allocator(RAMAllocator<BLEEvent>::ALLOC_INTERNAL);
event = allocator.allocate(1);
if (event == nullptr) {
// Memory allocation failed
return nullptr;
}
// Placement new to construct the object
new (event) BLEEvent();
this->total_created_++;
return event;
}
// Return an event to the pool for reuse
void release(BLEEvent *event) {
if (event != nullptr) {
this->free_list_.push(event);
}
}
private:
LockFreeQueue<BLEEvent, SIZE> free_list_; // Free events ready for reuse
uint8_t total_created_; // Total events created (high water mark)
};
} // namespace esp32_ble
} // namespace esphome
#endif

View File

@@ -0,0 +1,24 @@
#pragma once
#ifdef USE_ESP32
#include <esp_gap_ble_api.h>
namespace esphome {
namespace esp32_ble {
// Structure for BLE scan results - only fields we actually use
struct __attribute__((packed)) BLEScanResult {
esp_bd_addr_t bda;
uint8_t ble_addr_type;
int8_t rssi;
uint8_t ble_adv[ESP_BLE_ADV_DATA_LEN_MAX + ESP_BLE_SCAN_RSP_DATA_LEN_MAX];
uint8_t adv_data_len;
uint8_t scan_rsp_len;
uint8_t search_evt;
}; // ~73 bytes vs ~400 bytes for full esp_ble_gap_cb_param_t
} // namespace esp32_ble
} // namespace esphome
#endif

View File

@@ -2,52 +2,81 @@
#ifdef USE_ESP32
#include <mutex>
#include <queue>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <atomic>
#include <cstddef>
/*
* BLE events come in from a separate Task (thread) in the ESP32 stack. Rather
* than trying to deal with various locking strategies, all incoming GAP and GATT
* events will simply be placed on a semaphore guarded queue. The next time the
* component runs loop(), these events are popped off the queue and handed at
* this safer time.
* than using mutex-based locking, this lock-free queue allows the BLE
* task to enqueue events without blocking. The main loop() then processes
* these events at a safer time.
*
* This is a Single-Producer Single-Consumer (SPSC) lock-free ring buffer.
* The BLE task is the only producer, and the main loop() is the only consumer.
*/
namespace esphome {
namespace esp32_ble {
template<class T> class Queue {
template<class T, uint8_t SIZE> class LockFreeQueue {
public:
Queue() { m_ = xSemaphoreCreateMutex(); }
LockFreeQueue() : head_(0), tail_(0), dropped_count_(0) {}
void push(T *element) {
bool push(T *element) {
if (element == nullptr)
return;
// It is not called from main loop. Thus it won't block main thread.
xSemaphoreTake(m_, portMAX_DELAY);
q_.push(element);
xSemaphoreGive(m_);
return false;
uint8_t current_tail = tail_.load(std::memory_order_relaxed);
uint8_t next_tail = (current_tail + 1) % SIZE;
if (next_tail == head_.load(std::memory_order_acquire)) {
// Buffer full
dropped_count_.fetch_add(1, std::memory_order_relaxed);
return false;
}
buffer_[current_tail] = element;
tail_.store(next_tail, std::memory_order_release);
return true;
}
T *pop() {
T *element = nullptr;
uint8_t current_head = head_.load(std::memory_order_relaxed);
if (xSemaphoreTake(m_, 5L / portTICK_PERIOD_MS)) {
if (!q_.empty()) {
element = q_.front();
q_.pop();
}
xSemaphoreGive(m_);
if (current_head == tail_.load(std::memory_order_acquire)) {
return nullptr; // Empty
}
T *element = buffer_[current_head];
head_.store((current_head + 1) % SIZE, std::memory_order_release);
return element;
}
size_t size() const {
uint8_t tail = tail_.load(std::memory_order_acquire);
uint8_t head = head_.load(std::memory_order_acquire);
return (tail - head + SIZE) % SIZE;
}
uint16_t get_and_reset_dropped_count() { return dropped_count_.exchange(0, std::memory_order_relaxed); }
void increment_dropped_count() { dropped_count_.fetch_add(1, std::memory_order_relaxed); }
bool empty() const { return head_.load(std::memory_order_acquire) == tail_.load(std::memory_order_acquire); }
bool full() const {
uint8_t next_tail = (tail_.load(std::memory_order_relaxed) + 1) % SIZE;
return next_tail == head_.load(std::memory_order_acquire);
}
protected:
std::queue<T *> q_;
SemaphoreHandle_t m_;
T *buffer_[SIZE];
// Atomic: written by producer (push/increment), read+reset by consumer (get_and_reset)
std::atomic<uint16_t> dropped_count_; // 65535 max - more than enough for drop tracking
// Atomic: written by consumer (pop), read by producer (push) to check if full
std::atomic<uint8_t> head_;
// Atomic: written by producer (push), read by consumer (pop) to check if empty
std::atomic<uint8_t> tail_;
};
} // namespace esp32_ble

View File

@@ -22,6 +22,16 @@ void BLEClientBase::setup() {
this->connection_index_ = connection_index++;
}
void BLEClientBase::set_state(espbt::ClientState st) {
ESP_LOGV(TAG, "[%d] [%s] Set state %d", this->connection_index_, this->address_str_.c_str(), (int) st);
ESPBTClient::set_state(st);
if (st == espbt::ClientState::READY_TO_CONNECT) {
// Enable loop when we need to connect
this->enable_loop();
}
}
void BLEClientBase::loop() {
if (!esp32_ble::global_ble->is_active()) {
this->set_state(espbt::ClientState::INIT);
@@ -37,9 +47,14 @@ void BLEClientBase::loop() {
}
// READY_TO_CONNECT means we have discovered the device
// and the scanner has been stopped by the tracker.
if (this->state_ == espbt::ClientState::READY_TO_CONNECT) {
else if (this->state_ == espbt::ClientState::READY_TO_CONNECT) {
this->connect();
}
// If its idle, we can disable the loop as set_state
// will enable it again when we need to connect.
else if (this->state_ == espbt::ClientState::IDLE) {
this->disable_loop();
}
}
float BLEClientBase::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; }

View File

@@ -93,22 +93,37 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
bool check_addr(esp_bd_addr_t &addr) { return memcmp(addr, this->remote_bda_, sizeof(esp_bd_addr_t)) == 0; }
void set_state(espbt::ClientState st) override;
protected:
int gattc_if_;
esp_bd_addr_t remote_bda_;
esp_ble_addr_type_t remote_addr_type_{BLE_ADDR_TYPE_PUBLIC};
uint16_t conn_id_{UNSET_CONN_ID};
// Memory optimized layout for 32-bit systems
// Group 1: 8-byte types
uint64_t address_{0};
bool auto_connect_{false};
// Group 2: Container types (grouped for memory optimization)
std::string address_str_{};
uint8_t connection_index_;
int16_t service_count_{0};
uint16_t mtu_{23};
bool paired_{false};
espbt::ConnectionType connection_type_{espbt::ConnectionType::V1};
std::vector<BLEService *> services_;
// Group 3: 4-byte types
int gattc_if_;
esp_gatt_status_t status_{ESP_GATT_OK};
// Group 4: Arrays (6 bytes)
esp_bd_addr_t remote_bda_;
// Group 5: 2-byte types
uint16_t conn_id_{UNSET_CONN_ID};
uint16_t mtu_{23};
// Group 6: 1-byte types and small enums
esp_ble_addr_type_t remote_addr_type_{BLE_ADDR_TYPE_PUBLIC};
espbt::ConnectionType connection_type_{espbt::ConnectionType::V1};
uint8_t connection_index_;
uint8_t service_count_{0}; // ESP32 has max handles < 255, typical devices have < 50 services
bool auto_connect_{false};
bool paired_{false};
// 6 bytes used, 2 bytes padding
void log_event_(const char *name);
};

View File

@@ -268,6 +268,7 @@ async def to_code(config):
parent = await cg.get_variable(config[esp32_ble.CONF_BLE_ID])
cg.add(parent.register_gap_event_handler(var))
cg.add(parent.register_gap_scan_event_handler(var))
cg.add(parent.register_gattc_event_handler(var))
cg.add(parent.register_ble_status_event_handler(var))
cg.add(var.set_parent(parent))

View File

@@ -50,17 +50,15 @@ void ESP32BLETracker::setup() {
ESP_LOGE(TAG, "BLE Tracker was marked failed by ESP32BLE");
return;
}
ExternalRAMAllocator<esp_ble_gap_cb_param_t::ble_scan_result_evt_param> allocator(
ExternalRAMAllocator<esp_ble_gap_cb_param_t::ble_scan_result_evt_param>::ALLOW_FAILURE);
this->scan_result_buffer_ = allocator.allocate(ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE);
RAMAllocator<BLEScanResult> allocator;
this->scan_ring_buffer_ = allocator.allocate(SCAN_RESULT_BUFFER_SIZE);
if (this->scan_result_buffer_ == nullptr) {
ESP_LOGE(TAG, "Could not allocate buffer for BLE Tracker!");
if (this->scan_ring_buffer_ == nullptr) {
ESP_LOGE(TAG, "Could not allocate ring buffer for BLE Tracker!");
this->mark_failed();
}
global_esp32_ble_tracker = this;
this->scan_result_lock_ = xSemaphoreCreateMutex();
#ifdef USE_OTA
ota::get_global_ota_callback()->add_on_state_callback(
@@ -120,27 +118,31 @@ void ESP32BLETracker::loop() {
}
bool promote_to_connecting = discovered && !searching && !connecting;
if (this->scanner_state_ == ScannerState::RUNNING &&
this->scan_result_index_ && // if it looks like we have a scan result we will take the lock
xSemaphoreTake(this->scan_result_lock_, 0)) {
uint32_t index = this->scan_result_index_;
if (index >= ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) {
ESP_LOGW(TAG, "Too many BLE events to process. Some devices may not show up.");
}
// Process scan results from lock-free SPSC ring buffer
// Consumer side: This runs in the main loop thread
if (this->scanner_state_ == ScannerState::RUNNING) {
// Load our own index with relaxed ordering (we're the only writer)
uint8_t read_idx = this->ring_read_index_.load(std::memory_order_relaxed);
// Load producer's index with acquire to see their latest writes
uint8_t write_idx = this->ring_write_index_.load(std::memory_order_acquire);
while (read_idx != write_idx) {
// Process one result at a time directly from ring buffer
BLEScanResult &scan_result = this->scan_ring_buffer_[read_idx];
if (this->raw_advertisements_) {
for (auto *listener : this->listeners_) {
listener->parse_devices(this->scan_result_buffer_, this->scan_result_index_);
listener->parse_devices(&scan_result, 1);
}
for (auto *client : this->clients_) {
client->parse_devices(this->scan_result_buffer_, this->scan_result_index_);
client->parse_devices(&scan_result, 1);
}
}
if (this->parse_advertisements_) {
for (size_t i = 0; i < index; i++) {
ESPBTDevice device;
device.parse_scan_rst(this->scan_result_buffer_[i]);
device.parse_scan_rst(scan_result);
bool found = false;
for (auto *listener : this->listeners_) {
@@ -161,9 +163,19 @@ void ESP32BLETracker::loop() {
this->print_bt_device_info(device);
}
}
// Move to next entry in ring buffer
read_idx = (read_idx + 1) % SCAN_RESULT_BUFFER_SIZE;
// Store with release to ensure reads complete before index update
this->ring_read_index_.store(read_idx, std::memory_order_release);
}
// Log dropped results periodically
size_t dropped = this->scan_results_dropped_.exchange(0, std::memory_order_relaxed);
if (dropped > 0) {
ESP_LOGW(TAG, "Dropped %zu BLE scan results due to buffer overflow", dropped);
}
this->scan_result_index_ = 0;
xSemaphoreGive(this->scan_result_lock_);
}
if (this->scanner_state_ == ScannerState::STOPPED) {
this->end_of_scan_(); // Change state to IDLE
@@ -370,9 +382,6 @@ void ESP32BLETracker::recalculate_advertisement_parser_types() {
void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
switch (event) {
case ESP_GAP_BLE_SCAN_RESULT_EVT:
this->gap_scan_result_(param->scan_rst);
break;
case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
this->gap_scan_set_param_complete_(param->scan_param_cmpl);
break;
@@ -385,11 +394,57 @@ void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_ga
default:
break;
}
// Forward all events to clients (scan results are handled separately via gap_scan_event_handler)
for (auto *client : this->clients_) {
client->gap_event_handler(event, param);
}
}
void ESP32BLETracker::gap_scan_event_handler(const BLEScanResult &scan_result) {
ESP_LOGV(TAG, "gap_scan_result - event %d", scan_result.search_evt);
if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) {
// Lock-free SPSC ring buffer write (Producer side)
// This runs in the ESP-IDF Bluetooth stack callback thread
// IMPORTANT: Only this thread writes to ring_write_index_
// Load our own index with relaxed ordering (we're the only writer)
uint8_t write_idx = this->ring_write_index_.load(std::memory_order_relaxed);
uint8_t next_write_idx = (write_idx + 1) % SCAN_RESULT_BUFFER_SIZE;
// Load consumer's index with acquire to see their latest updates
uint8_t read_idx = this->ring_read_index_.load(std::memory_order_acquire);
// Check if buffer is full
if (next_write_idx != read_idx) {
// Write to ring buffer
this->scan_ring_buffer_[write_idx] = scan_result;
// Store with release to ensure the write is visible before index update
this->ring_write_index_.store(next_write_idx, std::memory_order_release);
} else {
// Buffer full, track dropped results
this->scan_results_dropped_.fetch_add(1, std::memory_order_relaxed);
}
} else if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_CMPL_EVT) {
// Scan finished on its own
if (this->scanner_state_ != ScannerState::RUNNING) {
if (this->scanner_state_ == ScannerState::STOPPING) {
ESP_LOGE(TAG, "Scan was not running when scan completed.");
} else if (this->scanner_state_ == ScannerState::STARTING) {
ESP_LOGE(TAG, "Scan was not started when scan completed.");
} else if (this->scanner_state_ == ScannerState::FAILED) {
ESP_LOGE(TAG, "Scan was in failed state when scan completed.");
} else if (this->scanner_state_ == ScannerState::IDLE) {
ESP_LOGE(TAG, "Scan was idle when scan completed.");
} else if (this->scanner_state_ == ScannerState::STOPPED) {
ESP_LOGE(TAG, "Scan was stopped when scan completed.");
}
}
this->set_scanner_state_(ScannerState::STOPPED);
}
}
void ESP32BLETracker::gap_scan_set_param_complete_(const esp_ble_gap_cb_param_t::ble_scan_param_cmpl_evt_param &param) {
ESP_LOGV(TAG, "gap_scan_set_param_complete - status %d", param.status);
if (param.status == ESP_BT_STATUS_DONE) {
@@ -444,34 +499,6 @@ void ESP32BLETracker::gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_
this->set_scanner_state_(ScannerState::STOPPED);
}
void ESP32BLETracker::gap_scan_result_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param &param) {
ESP_LOGV(TAG, "gap_scan_result - event %d", param.search_evt);
if (param.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) {
if (xSemaphoreTake(this->scan_result_lock_, 0)) {
if (this->scan_result_index_ < ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) {
this->scan_result_buffer_[this->scan_result_index_++] = param;
}
xSemaphoreGive(this->scan_result_lock_);
}
} else if (param.search_evt == ESP_GAP_SEARCH_INQ_CMPL_EVT) {
// Scan finished on its own
if (this->scanner_state_ != ScannerState::RUNNING) {
if (this->scanner_state_ == ScannerState::STOPPING) {
ESP_LOGE(TAG, "Scan was not running when scan completed.");
} else if (this->scanner_state_ == ScannerState::STARTING) {
ESP_LOGE(TAG, "Scan was not started when scan completed.");
} else if (this->scanner_state_ == ScannerState::FAILED) {
ESP_LOGE(TAG, "Scan was in failed state when scan completed.");
} else if (this->scanner_state_ == ScannerState::IDLE) {
ESP_LOGE(TAG, "Scan was idle when scan completed.");
} else if (this->scanner_state_ == ScannerState::STOPPED) {
ESP_LOGE(TAG, "Scan was stopped when scan completed.");
}
}
this->set_scanner_state_(ScannerState::STOPPED);
}
}
void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) {
for (auto *client : this->clients_) {
@@ -494,13 +521,16 @@ optional<ESPBLEiBeacon> ESPBLEiBeacon::from_manufacturer_data(const ServiceData
return ESPBLEiBeacon(data.data.data());
}
void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param &param) {
this->scan_result_ = param;
void ESPBTDevice::parse_scan_rst(const BLEScanResult &scan_result) {
this->scan_result_ = &scan_result;
for (uint8_t i = 0; i < ESP_BD_ADDR_LEN; i++)
this->address_[i] = param.bda[i];
this->address_type_ = param.ble_addr_type;
this->rssi_ = param.rssi;
this->parse_adv_(param);
this->address_[i] = scan_result.bda[i];
this->address_type_ = static_cast<esp_ble_addr_type_t>(scan_result.ble_addr_type);
this->rssi_ = scan_result.rssi;
// Parse advertisement data directly
uint8_t total_len = scan_result.adv_data_len + scan_result.scan_rsp_len;
this->parse_adv_(scan_result.ble_adv, total_len);
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
ESP_LOGVV(TAG, "Parse Result:");
@@ -558,13 +588,13 @@ void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_e
ESP_LOGVV(TAG, " Data: %s", format_hex_pretty(data.data).c_str());
}
ESP_LOGVV(TAG, " Adv data: %s", format_hex_pretty(param.ble_adv, param.adv_data_len + param.scan_rsp_len).c_str());
ESP_LOGVV(TAG, " Adv data: %s",
format_hex_pretty(scan_result.ble_adv, scan_result.adv_data_len + scan_result.scan_rsp_len).c_str());
#endif
}
void ESPBTDevice::parse_adv_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param &param) {
void ESPBTDevice::parse_adv_(const uint8_t *payload, uint8_t len) {
size_t offset = 0;
const uint8_t *payload = param.ble_adv;
uint8_t len = param.adv_data_len + param.scan_rsp_len;
while (offset + 2 < len) {
const uint8_t field_length = payload[offset++]; // First byte is length of adv record

View File

@@ -6,6 +6,7 @@
#include "esphome/core/helpers.h"
#include <array>
#include <atomic>
#include <string>
#include <vector>
@@ -62,7 +63,7 @@ class ESPBLEiBeacon {
class ESPBTDevice {
public:
void parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param &param);
void parse_scan_rst(const BLEScanResult &scan_result);
std::string address_str() const;
@@ -84,7 +85,8 @@ class ESPBTDevice {
const std::vector<ServiceData> &get_service_datas() const { return service_datas_; }
const esp_ble_gap_cb_param_t::ble_scan_result_evt_param &get_scan_result() const { return scan_result_; }
// Exposed through a function for use in lambdas
const BLEScanResult &get_scan_result() const { return *scan_result_; }
bool resolve_irk(const uint8_t *irk) const;
@@ -98,7 +100,7 @@ class ESPBTDevice {
}
protected:
void parse_adv_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param &param);
void parse_adv_(const uint8_t *payload, uint8_t len);
esp_bd_addr_t address_{
0,
@@ -112,7 +114,7 @@ class ESPBTDevice {
std::vector<ESPBTUUID> service_uuids_{};
std::vector<ServiceData> manufacturer_datas_{};
std::vector<ServiceData> service_datas_{};
esp_ble_gap_cb_param_t::ble_scan_result_evt_param scan_result_{};
const BLEScanResult *scan_result_{nullptr};
};
class ESP32BLETracker;
@@ -121,9 +123,7 @@ class ESPBTDeviceListener {
public:
virtual void on_scan_end() {}
virtual bool parse_device(const ESPBTDevice &device) = 0;
virtual bool parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) {
return false;
};
virtual bool parse_devices(const BLEScanResult *scan_results, size_t count) { return false; };
virtual AdvertisementParserType get_advertisement_parser_type() {
return AdvertisementParserType::PARSED_ADVERTISEMENTS;
};
@@ -133,7 +133,7 @@ class ESPBTDeviceListener {
ESP32BLETracker *parent_{nullptr};
};
enum class ClientState {
enum class ClientState : uint8_t {
// Connection is allocated
INIT,
// Client is disconnecting
@@ -169,7 +169,7 @@ enum class ScannerState {
STOPPED,
};
enum class ConnectionType {
enum class ConnectionType : uint8_t {
// The default connection type, we hold all the services in ram
// for the duration of the connection.
V1,
@@ -197,19 +197,24 @@ class ESPBTClient : public ESPBTDeviceListener {
}
}
ClientState state() const { return state_; }
int app_id;
// Memory optimized layout
uint8_t app_id; // App IDs are small integers assigned sequentially
protected:
// Group 1: 1-byte types
ClientState state_{ClientState::INIT};
// want_disconnect_ is set to true when a disconnect is requested
// while the client is connecting. This is used to disconnect the
// client as soon as we get the connection id (conn_id_) from the
// ESP_GATTC_OPEN_EVT event.
bool want_disconnect_{false};
// 2 bytes used, 2 bytes padding
};
class ESP32BLETracker : public Component,
public GAPEventHandler,
public GAPScanEventHandler,
public GATTcEventHandler,
public BLEStatusEventHandler,
public Parented<ESP32BLE> {
@@ -240,6 +245,7 @@ class ESP32BLETracker : public Component,
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override;
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override;
void gap_scan_event_handler(const BLEScanResult &scan_result) override;
void ble_before_disabled_event_handler() override;
void add_scanner_state_callback(std::function<void(ScannerState)> &&callback) {
@@ -264,7 +270,7 @@ class ESP32BLETracker : public Component,
/// Called to set the scanner state. Will also call callbacks to let listeners know when state is changed.
void set_scanner_state_(ScannerState state);
int app_id_{0};
uint8_t app_id_{0};
/// Vector of addresses that have already been printed in print_bt_device_info
std::vector<uint64_t> already_discovered_;
@@ -285,14 +291,16 @@ class ESP32BLETracker : public Component,
bool ble_was_disabled_{true};
bool raw_advertisements_{false};
bool parse_advertisements_{false};
SemaphoreHandle_t scan_result_lock_;
size_t scan_result_index_{0};
#ifdef USE_PSRAM
const static u_int8_t SCAN_RESULT_BUFFER_SIZE = 32;
#else
const static u_int8_t SCAN_RESULT_BUFFER_SIZE = 20;
#endif // USE_PSRAM
esp_ble_gap_cb_param_t::ble_scan_result_evt_param *scan_result_buffer_;
// Lock-free Single-Producer Single-Consumer (SPSC) ring buffer for scan results
// Producer: ESP-IDF Bluetooth stack callback (gap_scan_event_handler)
// Consumer: ESPHome main loop (loop() method)
// This design ensures zero blocking in the BT callback and prevents scan result loss
BLEScanResult *scan_ring_buffer_;
std::atomic<uint8_t> ring_write_index_{0}; // Written only by BT callback (producer)
std::atomic<uint8_t> ring_read_index_{0}; // Written only by main loop (consumer)
std::atomic<uint16_t> scan_results_dropped_{0}; // Tracks buffer overflow events
esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS};
esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS};
int connecting_{0};

View File

@@ -1,5 +1,6 @@
from esphome import automation, pins
import esphome.codegen as cg
from esphome.components import i2c
from esphome.components.esp32 import add_idf_component
import esphome.config_validation as cv
from esphome.const import (
@@ -7,6 +8,7 @@ from esphome.const import (
CONF_CONTRAST,
CONF_DATA_PINS,
CONF_FREQUENCY,
CONF_I2C_ID,
CONF_ID,
CONF_PIN,
CONF_RESET_PIN,
@@ -17,7 +19,7 @@ from esphome.const import (
CONF_VSYNC_PIN,
)
from esphome.core import CORE
from esphome.cpp_helpers import setup_entity
from esphome.core.entity_helpers import setup_entity
DEPENDENCIES = ["esp32"]
@@ -149,7 +151,8 @@ CONF_ON_IMAGE = "on_image"
camera_range_param = cv.int_range(min=-2, max=2)
CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
CONFIG_SCHEMA = cv.All(
cv.ENTITY_BASE_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(ESP32Camera),
# pin assignment
@@ -167,12 +170,16 @@ CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
),
}
),
cv.Required(CONF_I2C_PINS): cv.Schema(
cv.Optional(CONF_I2C_PINS): cv.Schema(
{
cv.Required(CONF_SDA): pins.internal_gpio_output_pin_number,
cv.Required(CONF_SCL): pins.internal_gpio_output_pin_number,
}
),
cv.Optional(CONF_I2C_ID): cv.Any(
cv.use_id(i2c.InternalI2CBus),
msg="I2C bus must be an internal ESP32 I2C bus",
),
cv.Optional(CONF_RESET_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_POWER_DOWN_PIN): pins.internal_gpio_output_pin_number,
# image
@@ -204,7 +211,9 @@ CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
ENUM_GAIN_CEILING, upper=True
),
# white balance
cv.Optional(CONF_WB_MODE, default="AUTO"): cv.enum(ENUM_WB_MODE, upper=True),
cv.Optional(CONF_WB_MODE, default="AUTO"): cv.enum(
ENUM_WB_MODE, upper=True
),
# test pattern
cv.Optional(CONF_TEST_PATTERN, default=False): cv.boolean,
# framerates
@@ -231,11 +240,15 @@ CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
),
cv.Optional(CONF_ON_IMAGE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESP32CameraImageTrigger),
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
ESP32CameraImageTrigger
),
}
),
}
).extend(cv.COMPONENT_SCHEMA)
).extend(cv.COMPONENT_SCHEMA),
cv.has_exactly_one_key(CONF_I2C_PINS, CONF_I2C_ID),
)
SETTERS = {
# pin assignment
@@ -271,7 +284,7 @@ SETTERS = {
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await setup_entity(var, config)
await setup_entity(var, config, "camera")
await cg.register_component(var, config)
for key, setter in SETTERS.items():
@@ -280,6 +293,10 @@ async def to_code(config):
extclk = config[CONF_EXTERNAL_CLOCK]
cg.add(var.set_external_clock(extclk[CONF_PIN], extclk[CONF_FREQUENCY]))
if i2c_id := config.get(CONF_I2C_ID):
i2c_hub = await cg.get_variable(i2c_id)
cg.add(var.set_i2c_id(i2c_hub))
else:
i2c_pins = config[CONF_I2C_PINS]
cg.add(var.set_i2c_pins(i2c_pins[CONF_SDA], i2c_pins[CONF_SCL]))
cg.add(var.set_max_update_interval(1000 / config[CONF_MAX_FRAMERATE]))

View File

@@ -1,9 +1,9 @@
#ifdef USE_ESP32
#include "esp32_camera.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include <freertos/task.h>
@@ -16,6 +16,12 @@ static const char *const TAG = "esp32_camera";
void ESP32Camera::setup() {
global_esp32_camera = this;
#ifdef USE_I2C
if (this->i2c_bus_ != nullptr) {
this->config_.sccb_i2c_port = this->i2c_bus_->get_port();
}
#endif
/* initialize time to now */
this->last_update_ = millis();
@@ -57,7 +63,7 @@ void ESP32Camera::dump_config() {
" External Clock: Pin:%d Frequency:%u\n"
" I2C Pins: SDA:%d SCL:%d\n"
" Reset Pin: %d",
this->name_.c_str(), YESNO(this->internal_), conf.pin_d0, conf.pin_d1, conf.pin_d2, conf.pin_d3,
this->name_.c_str(), YESNO(this->is_internal()), conf.pin_d0, conf.pin_d1, conf.pin_d2, conf.pin_d3,
conf.pin_d4, conf.pin_d5, conf.pin_d6, conf.pin_d7, conf.pin_vsync, conf.pin_href, conf.pin_pclk,
conf.pin_xclk, conf.xclk_freq_hz, conf.pin_sccb_sda, conf.pin_sccb_scl, conf.pin_reset);
switch (this->config_.frame_size) {
@@ -246,6 +252,13 @@ void ESP32Camera::set_i2c_pins(uint8_t sda, uint8_t scl) {
this->config_.pin_sccb_sda = sda;
this->config_.pin_sccb_scl = scl;
}
#ifdef USE_I2C
void ESP32Camera::set_i2c_id(i2c::InternalI2CBus *i2c_bus) {
this->i2c_bus_ = i2c_bus;
this->config_.pin_sccb_sda = -1;
this->config_.pin_sccb_scl = -1;
}
#endif // USE_I2C
void ESP32Camera::set_reset_pin(uint8_t pin) { this->config_.pin_reset = pin; }
void ESP32Camera::set_power_down_pin(uint8_t pin) { this->config_.pin_pwdn = pin; }

View File

@@ -2,13 +2,17 @@
#ifdef USE_ESP32
#include <esp_camera.h>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/helpers.h"
#include <esp_camera.h>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#ifdef USE_I2C
#include "esphome/components/i2c/i2c_bus.h"
#endif // USE_I2C
namespace esphome {
namespace esp32_camera {
@@ -118,6 +122,9 @@ class ESP32Camera : public EntityBase, public Component {
void set_pixel_clock_pin(uint8_t pin);
void set_external_clock(uint8_t pin, uint32_t frequency);
void set_i2c_pins(uint8_t sda, uint8_t scl);
#ifdef USE_I2C
void set_i2c_id(i2c::InternalI2CBus *i2c_bus);
#endif // USE_I2C
void set_reset_pin(uint8_t pin);
void set_power_down_pin(uint8_t pin);
/* -- image */
@@ -210,6 +217,9 @@ class ESP32Camera : public EntityBase, public Component {
uint32_t last_idle_request_{0};
uint32_t last_update_{0};
#ifdef USE_I2C
i2c::InternalI2CBus *i2c_bus_{nullptr};
#endif // USE_I2C
};
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)

View File

@@ -3,7 +3,7 @@ import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_MODE, CONF_PORT
CODEOWNERS = ["@ayufan"]
DEPENDENCIES = ["esp32_camera"]
DEPENDENCIES = ["esp32_camera", "network"]
MULTI_CONF = True
esp32_camera_web_server_ns = cg.esphome_ns.namespace("esp32_camera_web_server")

View File

@@ -1,25 +0,0 @@
#ifdef USE_ESP32
#include "esp32_hall.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include <driver/adc.h>
namespace esphome {
namespace esp32_hall {
static const char *const TAG = "esp32_hall";
void ESP32HallSensor::update() {
adc1_config_width(ADC_WIDTH_BIT_12);
int value_int = hall_sensor_read();
float value = (value_int / 4095.0f) * 10000.0f;
ESP_LOGD(TAG, "'%s': Got reading %.0f µT", this->name_.c_str(), value);
this->publish_state(value);
}
std::string ESP32HallSensor::unique_id() { return get_mac_address() + "-hall"; }
void ESP32HallSensor::dump_config() { LOG_SENSOR("", "ESP32 Hall Sensor", this); }
} // namespace esp32_hall
} // namespace esphome
#endif

View File

@@ -1,23 +0,0 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#ifdef USE_ESP32
namespace esphome {
namespace esp32_hall {
class ESP32HallSensor : public sensor::Sensor, public PollingComponent {
public:
void dump_config() override;
void update() override;
std::string unique_id() override;
};
} // namespace esp32_hall
} // namespace esphome
#endif

View File

@@ -1,24 +1,5 @@
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import ICON_MAGNET, STATE_CLASS_MEASUREMENT, UNIT_MICROTESLA
DEPENDENCIES = ["esp32"]
esp32_hall_ns = cg.esphome_ns.namespace("esp32_hall")
ESP32HallSensor = esp32_hall_ns.class_(
"ESP32HallSensor", sensor.Sensor, cg.PollingComponent
CONFIG_SCHEMA = cv.invalid(
"The esp32_hall component has been removed as of ESPHome 2025.7.0. See https://github.com/esphome/esphome/pull/9117 for details."
)
CONFIG_SCHEMA = sensor.sensor_schema(
ESP32HallSensor,
unit_of_measurement=UNIT_MICROTESLA,
icon=ICON_MAGNET,
accuracy_decimals=1,
state_class=STATE_CLASS_MEASUREMENT,
).extend(cv.polling_component_schema("60s"))
async def to_code(config):
var = await sensor.new_sensor(config)
await cg.register_component(var, config)

View File

@@ -168,6 +168,8 @@ void ESP32ImprovComponent::loop() {
case improv::STATE_PROVISIONED: {
this->incoming_data_.clear();
this->set_status_indicator_state_(false);
// Provisioning complete, no further loop execution needed
this->disable_loop();
break;
}
}
@@ -254,6 +256,7 @@ void ESP32ImprovComponent::start() {
ESP_LOGD(TAG, "Setting Improv to start");
this->should_start_ = true;
this->enable_loop();
}
void ESP32ImprovComponent::stop() {

View File

@@ -1,48 +1,8 @@
import esphome.codegen as cg
from esphome.components import esp32
import esphome.config_validation as cv
from esphome.const import KEY_CORE, KEY_FRAMEWORK_VERSION
from esphome.core import CORE
CODEOWNERS = ["@jesserockz"]
RMT_TX_CHANNELS = {
esp32.const.VARIANT_ESP32: [0, 1, 2, 3, 4, 5, 6, 7],
esp32.const.VARIANT_ESP32S2: [0, 1, 2, 3],
esp32.const.VARIANT_ESP32S3: [0, 1, 2, 3],
esp32.const.VARIANT_ESP32C3: [0, 1],
esp32.const.VARIANT_ESP32C6: [0, 1],
esp32.const.VARIANT_ESP32H2: [0, 1],
}
RMT_RX_CHANNELS = {
esp32.const.VARIANT_ESP32: [0, 1, 2, 3, 4, 5, 6, 7],
esp32.const.VARIANT_ESP32S2: [0, 1, 2, 3],
esp32.const.VARIANT_ESP32S3: [4, 5, 6, 7],
esp32.const.VARIANT_ESP32C3: [2, 3],
esp32.const.VARIANT_ESP32C6: [2, 3],
esp32.const.VARIANT_ESP32H2: [2, 3],
}
rmt_channel_t = cg.global_ns.enum("rmt_channel_t")
RMT_CHANNEL_ENUMS = {
0: rmt_channel_t.RMT_CHANNEL_0,
1: rmt_channel_t.RMT_CHANNEL_1,
2: rmt_channel_t.RMT_CHANNEL_2,
3: rmt_channel_t.RMT_CHANNEL_3,
4: rmt_channel_t.RMT_CHANNEL_4,
5: rmt_channel_t.RMT_CHANNEL_5,
6: rmt_channel_t.RMT_CHANNEL_6,
7: rmt_channel_t.RMT_CHANNEL_7,
}
def use_new_rmt_driver():
framework_version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
if CORE.using_esp_idf and framework_version >= cv.Version(5, 0, 0):
return True
return False
def validate_clock_resolution():
def _validator(value):
@@ -60,21 +20,3 @@ def validate_clock_resolution():
return value
return _validator
def validate_rmt_channel(*, tx: bool):
rmt_channels = RMT_TX_CHANNELS if tx else RMT_RX_CHANNELS
def _validator(value):
cv.only_on_esp32(value)
value = cv.int_(value)
variant = esp32.get_esp32_variant()
if variant not in rmt_channels:
raise cv.Invalid(f"ESP32 variant {variant} does not support RMT.")
if value not in rmt_channels[variant]:
raise cv.Invalid(
f"RMT channel {value} does not support {'transmitting' if tx else 'receiving'} for ESP32 variant {variant}."
)
return cv.enum(RMT_CHANNEL_ENUMS)(value)
return _validator

View File

@@ -42,7 +42,6 @@ void ESP32RMTLEDStripLightOutput::setup() {
return;
}
#if ESP_IDF_VERSION_MAJOR >= 5
RAMAllocator<rmt_symbol_word_t> rmt_allocator(this->use_psram_ ? 0 : RAMAllocator<rmt_symbol_word_t>::ALLOC_INTERNAL);
// 8 bits per byte, 1 rmt_symbol_word_t per bit + 1 rmt_symbol_word_t for reset
@@ -79,36 +78,6 @@ void ESP32RMTLEDStripLightOutput::setup() {
this->mark_failed();
return;
}
#else
RAMAllocator<rmt_item32_t> rmt_allocator(this->use_psram_ ? 0 : RAMAllocator<rmt_item32_t>::ALLOC_INTERNAL);
// 8 bits per byte, 1 rmt_item32_t per bit + 1 rmt_item32_t for reset
this->rmt_buf_ = rmt_allocator.allocate(buffer_size * 8 + 1);
rmt_config_t config;
memset(&config, 0, sizeof(config));
config.channel = this->channel_;
config.rmt_mode = RMT_MODE_TX;
config.gpio_num = gpio_num_t(this->pin_);
config.mem_block_num = 1;
config.clk_div = RMT_CLK_DIV;
config.tx_config.loop_en = false;
config.tx_config.carrier_level = RMT_CARRIER_LEVEL_LOW;
config.tx_config.carrier_en = false;
config.tx_config.idle_level = RMT_IDLE_LEVEL_LOW;
config.tx_config.idle_output_en = true;
if (rmt_config(&config) != ESP_OK) {
ESP_LOGE(TAG, "Cannot initialize RMT!");
this->mark_failed();
return;
}
if (rmt_driver_install(config.channel, 0, 0) != ESP_OK) {
ESP_LOGE(TAG, "Cannot install RMT driver!");
this->mark_failed();
return;
}
#endif
}
void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high,
@@ -145,11 +114,7 @@ void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) {
ESP_LOGVV(TAG, "Writing RGB values to bus");
#if ESP_IDF_VERSION_MAJOR >= 5
esp_err_t error = rmt_tx_wait_all_done(this->channel_, 1000);
#else
esp_err_t error = rmt_wait_tx_done(this->channel_, pdMS_TO_TICKS(1000));
#endif
if (error != ESP_OK) {
ESP_LOGE(TAG, "RMT TX timeout");
this->status_set_warning();
@@ -162,11 +127,7 @@ void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) {
size_t size = 0;
size_t len = 0;
uint8_t *psrc = this->buf_;
#if ESP_IDF_VERSION_MAJOR >= 5
rmt_symbol_word_t *pdest = this->rmt_buf_;
#else
rmt_item32_t *pdest = this->rmt_buf_;
#endif
while (size < buffer_size) {
uint8_t b = *psrc;
for (int i = 0; i < 8; i++) {
@@ -184,15 +145,11 @@ void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) {
len++;
}
#if ESP_IDF_VERSION_MAJOR >= 5
rmt_transmit_config_t config;
memset(&config, 0, sizeof(config));
config.loop_count = 0;
config.flags.eot_level = 0;
error = rmt_transmit(this->channel_, this->encoder_, this->rmt_buf_, len * sizeof(rmt_symbol_word_t), &config);
#else
error = rmt_write_items(this->channel_, this->rmt_buf_, len, false);
#endif
if (error != ESP_OK) {
ESP_LOGE(TAG, "RMT TX error");
this->status_set_warning();
@@ -251,11 +208,7 @@ void ESP32RMTLEDStripLightOutput::dump_config() {
"ESP32 RMT LED Strip:\n"
" Pin: %u",
this->pin_);
#if ESP_IDF_VERSION_MAJOR >= 5
ESP_LOGCONFIG(TAG, " RMT Symbols: %" PRIu32, this->rmt_symbols_);
#else
ESP_LOGCONFIG(TAG, " Channel: %u", this->channel_);
#endif
const char *rgb_order;
switch (this->rgb_order_) {
case ORDER_RGB:

View File

@@ -11,12 +11,7 @@
#include <driver/gpio.h>
#include <esp_err.h>
#include <esp_idf_version.h>
#if ESP_IDF_VERSION_MAJOR >= 5
#include <driver/rmt_tx.h>
#else
#include <driver/rmt.h>
#endif
namespace esphome {
namespace esp32_rmt_led_strip {
@@ -61,11 +56,7 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight {
uint32_t reset_time_high, uint32_t reset_time_low);
void set_rgb_order(RGBOrder rgb_order) { this->rgb_order_ = rgb_order; }
#if ESP_IDF_VERSION_MAJOR >= 5
void set_rmt_symbols(uint32_t rmt_symbols) { this->rmt_symbols_ = rmt_symbols; }
#else
void set_rmt_channel(rmt_channel_t channel) { this->channel_ = channel; }
#endif
void clear_effect_data() override {
for (int i = 0; i < this->size(); i++)
@@ -81,17 +72,11 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight {
uint8_t *buf_{nullptr};
uint8_t *effect_data_{nullptr};
#if ESP_IDF_VERSION_MAJOR >= 5
rmt_channel_handle_t channel_{nullptr};
rmt_encoder_handle_t encoder_{nullptr};
rmt_symbol_word_t *rmt_buf_{nullptr};
rmt_symbol_word_t bit0_, bit1_, reset_;
uint32_t rmt_symbols_{48};
#else
rmt_item32_t *rmt_buf_{nullptr};
rmt_item32_t bit0_, bit1_, reset_;
rmt_channel_t channel_{RMT_CHANNEL_0};
#endif
uint8_t pin_;
uint16_t num_leds_;

View File

@@ -3,7 +3,7 @@ import logging
from esphome import pins
import esphome.codegen as cg
from esphome.components import esp32, esp32_rmt, light
from esphome.components import esp32, light
import esphome.config_validation as cv
from esphome.const import (
CONF_CHIPSET,
@@ -13,11 +13,9 @@ from esphome.const import (
CONF_OUTPUT_ID,
CONF_PIN,
CONF_RGB_ORDER,
CONF_RMT_CHANNEL,
CONF_RMT_SYMBOLS,
CONF_USE_DMA,
)
from esphome.core import CORE
_LOGGER = logging.getLogger(__name__)
@@ -69,53 +67,6 @@ CONF_RESET_HIGH = "reset_high"
CONF_RESET_LOW = "reset_low"
class OptionalForIDF5(cv.SplitDefault):
@property
def default(self):
if not esp32_rmt.use_new_rmt_driver():
return cv.UNDEFINED
return super().default
@default.setter
def default(self, value):
# Ignore default set from vol.Optional
pass
def only_with_new_rmt_driver(obj):
if not esp32_rmt.use_new_rmt_driver():
raise cv.Invalid(
"This feature is only available for the IDF framework version 5."
)
return obj
def not_with_new_rmt_driver(obj):
if esp32_rmt.use_new_rmt_driver():
raise cv.Invalid(
"This feature is not available for the IDF framework version 5."
)
return obj
def final_validation(config):
if not esp32_rmt.use_new_rmt_driver():
if CONF_RMT_CHANNEL not in config:
if CORE.using_esp_idf:
raise cv.Invalid(
"rmt_channel is a required option for IDF version < 5."
)
raise cv.Invalid(
"rmt_channel is a required option for the Arduino framework."
)
_LOGGER.warning(
"RMT_LED_STRIP support for IDF version < 5 is deprecated and will be removed soon."
)
FINAL_VALIDATE_SCHEMA = final_validation
CONFIG_SCHEMA = cv.All(
light.ADDRESSABLE_LIGHT_SCHEMA.extend(
{
@@ -123,20 +74,17 @@ CONFIG_SCHEMA = cv.All(
cv.Required(CONF_PIN): pins.internal_gpio_output_pin_number,
cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int,
cv.Required(CONF_RGB_ORDER): cv.enum(RGB_ORDERS, upper=True),
cv.Optional(CONF_RMT_CHANNEL): cv.All(
not_with_new_rmt_driver, esp32_rmt.validate_rmt_channel(tx=True)
),
OptionalForIDF5(
cv.SplitDefault(
CONF_RMT_SYMBOLS,
esp32_idf=192,
esp32_s2_idf=192,
esp32_s3_idf=192,
esp32_p4_idf=192,
esp32_c3_idf=96,
esp32_c5_idf=96,
esp32_c6_idf=96,
esp32_h2_idf=96,
): cv.All(only_with_new_rmt_driver, cv.int_range(min=2)),
esp32=192,
esp32_s2=192,
esp32_s3=192,
esp32_p4=192,
esp32_c3=96,
esp32_c5=96,
esp32_c6=96,
esp32_h2=96,
): cv.int_range(min=2),
cv.Optional(CONF_MAX_REFRESH_RATE): cv.positive_time_period_microseconds,
cv.Optional(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True),
cv.Optional(CONF_IS_RGBW, default=False): cv.boolean,
@@ -145,7 +93,6 @@ CONFIG_SCHEMA = cv.All(
esp32.only_on_variant(
supported=[esp32.const.VARIANT_ESP32S3, esp32.const.VARIANT_ESP32P4]
),
cv.only_with_esp_idf,
cv.boolean,
),
cv.Optional(CONF_USE_PSRAM, default=True): cv.boolean,
@@ -218,15 +165,6 @@ async def to_code(config):
cg.add(var.set_is_rgbw(config[CONF_IS_RGBW]))
cg.add(var.set_is_wrgb(config[CONF_IS_WRGB]))
cg.add(var.set_use_psram(config[CONF_USE_PSRAM]))
if esp32_rmt.use_new_rmt_driver():
cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS]))
if CONF_USE_DMA in config:
cg.add(var.set_use_dma(config[CONF_USE_DMA]))
else:
rmt_channel_t = cg.global_ns.enum("rmt_channel_t")
cg.add(
var.set_rmt_channel(
getattr(rmt_channel_t, f"RMT_CHANNEL_{config[CONF_RMT_CHANNEL]}")
)
)

View File

@@ -183,6 +183,7 @@ async def to_code(config):
cg.add_platformio_option("board", config[CONF_BOARD])
cg.add_build_flag("-DUSE_ESP8266")
cg.set_cpp_standard("gnu++17")
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
cg.add_define("ESPHOME_VARIANT", "ESP8266")

View File

@@ -26,19 +26,19 @@ void ESPHomeOTAComponent::setup() {
ota::register_ota_platform(this);
#endif
server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections
if (server_ == nullptr) {
this->server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections
if (this->server_ == nullptr) {
ESP_LOGW(TAG, "Could not create socket");
this->mark_failed();
return;
}
int enable = 1;
int err = server_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
int err = this->server_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
if (err != 0) {
ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err);
// we can still continue
}
err = server_->setblocking(false);
err = this->server_->setblocking(false);
if (err != 0) {
ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err);
this->mark_failed();
@@ -54,14 +54,14 @@ void ESPHomeOTAComponent::setup() {
return;
}
err = server_->bind((struct sockaddr *) &server, sizeof(server));
err = this->server_->bind((struct sockaddr *) &server, sizeof(server));
if (err != 0) {
ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno);
this->mark_failed();
return;
}
err = server_->listen(4);
err = this->server_->listen(4);
if (err != 0) {
ESP_LOGW(TAG, "Socket unable to listen: errno %d", errno);
this->mark_failed();
@@ -82,7 +82,14 @@ void ESPHomeOTAComponent::dump_config() {
#endif
}
void ESPHomeOTAComponent::loop() { this->handle_(); }
void ESPHomeOTAComponent::loop() {
// Skip handle_() call if no client connected and no incoming connections
// This optimization reduces idle loop overhead when OTA is not active
// Note: No need to check server_ for null as the component is marked failed in setup() if server_ creation fails
if (this->client_ != nullptr || this->server_->ready()) {
this->handle_();
}
}
static const uint8_t FEATURE_SUPPORTS_COMPRESSION = 0x01;
@@ -101,23 +108,21 @@ void ESPHomeOTAComponent::handle_() {
size_t size_acknowledged = 0;
#endif
if (client_ == nullptr) {
// Check if the server socket is ready before accepting
if (this->server_->ready()) {
if (this->client_ == nullptr) {
// We already checked server_->ready() in loop(), so we can accept directly
struct sockaddr_storage source_addr;
socklen_t addr_len = sizeof(source_addr);
client_ = server_->accept((struct sockaddr *) &source_addr, &addr_len);
}
}
if (client_ == nullptr)
this->client_ = this->server_->accept((struct sockaddr *) &source_addr, &addr_len);
if (this->client_ == nullptr)
return;
}
int enable = 1;
int err = client_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int));
int err = this->client_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int));
if (err != 0) {
ESP_LOGW(TAG, "Socket could not enable TCP nodelay, errno %d", errno);
client_->close();
client_ = nullptr;
this->client_->close();
this->client_ = nullptr;
return;
}

View File

@@ -106,7 +106,7 @@ void EthernetComponent::setup() {
.post_cb = nullptr,
};
#if USE_ESP_IDF && (ESP_IDF_VERSION_MAJOR >= 5)
#if ESP_IDF_VERSION_MAJOR >= 5
eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(host, &devcfg);
#else
spi_device_handle_t spi_handle = nullptr;
@@ -274,6 +274,9 @@ void EthernetComponent::loop() {
ESP_LOGW(TAG, "Connection lost; reconnecting");
this->state_ = EthernetComponentState::CONNECTING;
this->start_connect_();
} else {
// When connected and stable, disable the loop to save CPU cycles
this->disable_loop();
}
break;
}
@@ -397,11 +400,13 @@ void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base
case ETHERNET_EVENT_START:
event_name = "ETH started";
global_eth_component->started_ = true;
global_eth_component->enable_loop_soon_any_context();
break;
case ETHERNET_EVENT_STOP:
event_name = "ETH stopped";
global_eth_component->started_ = false;
global_eth_component->connected_ = false;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
break;
case ETHERNET_EVENT_CONNECTED:
event_name = "ETH connected";
@@ -409,6 +414,7 @@ void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base
case ETHERNET_EVENT_DISCONNECTED:
event_name = "ETH disconnected";
global_eth_component->connected_ = false;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
break;
default:
return;
@@ -425,8 +431,10 @@ void EthernetComponent::got_ip_event_handler(void *arg, esp_event_base_t event_b
global_eth_component->got_ipv4_address_ = true;
#if USE_NETWORK_IPV6 && (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0)
global_eth_component->connected_ = global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
#else
global_eth_component->connected_ = true;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
#endif /* USE_NETWORK_IPV6 */
}
@@ -439,8 +447,10 @@ void EthernetComponent::got_ip6_event_handler(void *arg, esp_event_base_t event_
#if (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0)
global_eth_component->connected_ =
global_eth_component->got_ipv4_address_ && (global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT);
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
#else
global_eth_component->connected_ = global_eth_component->got_ipv4_address_;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
#endif
}
#endif /* USE_NETWORK_IPV6 */
@@ -620,6 +630,7 @@ bool EthernetComponent::powerdown() {
}
this->connected_ = false;
this->started_ = false;
// No need to enable_loop() here as this is only called during shutdown/reboot
if (this->phy_->pwrctl(this->phy_, false) != ESP_OK) {
ESP_LOGE(TAG, "Error powering down ethernet PHY");
return false;

View File

@@ -18,8 +18,8 @@ from esphome.const import (
DEVICE_CLASS_MOTION,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass
from esphome.cpp_helpers import setup_entity
CODEOWNERS = ["@nohat"]
IS_PLATFORM_COMPONENT = True
@@ -59,6 +59,9 @@ _EVENT_SCHEMA = (
)
_EVENT_SCHEMA.add_extra(entity_duplicate_validator("event"))
def event_schema(
class_: MockObjClass = cv.UNDEFINED,
*,
@@ -88,7 +91,7 @@ EVENT_SCHEMA.add_extra(cv.deprecated_schema_constant("event"))
async def setup_event_core_(var, config, *, event_types: list[str]):
await setup_entity(var, config)
await setup_entity(var, config, "event")
for conf in config.get(CONF_ON_EVENT, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)

View File

@@ -32,7 +32,7 @@ from esphome.const import (
CONF_WEB_SERVER,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.cpp_helpers import setup_entity
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
IS_PLATFORM_COMPONENT = True
@@ -161,6 +161,9 @@ _FAN_SCHEMA = (
)
_FAN_SCHEMA.add_extra(entity_duplicate_validator("fan"))
def fan_schema(
class_: cg.Pvariable,
*,
@@ -225,7 +228,7 @@ def validate_preset_modes(value):
async def setup_fan_core_(var, config):
await setup_entity(var, config)
await setup_entity(var, config, "fan")
cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE]))

View File

@@ -41,39 +41,48 @@ void FanCall::perform() {
void FanCall::validate_() {
auto traits = this->parent_.get_traits();
if (this->speed_.has_value())
if (this->speed_.has_value()) {
this->speed_ = clamp(*this->speed_, 1, traits.supported_speed_count());
if (this->binary_state_.has_value() && *this->binary_state_) {
// when turning on, if neither current nor new speed available, set speed to 100%
if (traits.supports_speed() && !this->parent_.state && this->parent_.speed == 0 && !this->speed_.has_value()) {
this->speed_ = traits.supported_speed_count();
}
}
if (this->oscillating_.has_value() && !traits.supports_oscillation()) {
ESP_LOGW(TAG, "'%s' - This fan does not support oscillation!", this->parent_.get_name().c_str());
this->oscillating_.reset();
}
if (this->speed_.has_value() && !traits.supports_speed()) {
ESP_LOGW(TAG, "'%s' - This fan does not support speeds!", this->parent_.get_name().c_str());
this->speed_.reset();
}
if (this->direction_.has_value() && !traits.supports_direction()) {
ESP_LOGW(TAG, "'%s' - This fan does not support directions!", this->parent_.get_name().c_str());
this->direction_.reset();
// https://developers.home-assistant.io/docs/core/entity/fan/#preset-modes
// "Manually setting a speed must disable any set preset mode"
this->preset_mode_.clear();
}
if (!this->preset_mode_.empty()) {
const auto &preset_modes = traits.supported_preset_modes();
if (preset_modes.find(this->preset_mode_) == preset_modes.end()) {
ESP_LOGW(TAG, "'%s' - This fan does not support preset mode '%s'!", this->parent_.get_name().c_str(),
this->preset_mode_.c_str());
ESP_LOGW(TAG, "%s: Preset mode '%s' not supported", this->parent_.get_name().c_str(), this->preset_mode_.c_str());
this->preset_mode_.clear();
}
}
// when turning on...
if (!this->parent_.state && this->binary_state_.has_value() &&
*this->binary_state_
// ..,and no preset mode will be active...
&& this->preset_mode_.empty() &&
this->parent_.preset_mode.empty()
// ...and neither current nor new speed is available...
&& traits.supports_speed() && this->parent_.speed == 0 && !this->speed_.has_value()) {
// ...set speed to 100%
this->speed_ = traits.supported_speed_count();
}
if (this->oscillating_.has_value() && !traits.supports_oscillation()) {
ESP_LOGW(TAG, "%s: Oscillation not supported", this->parent_.get_name().c_str());
this->oscillating_.reset();
}
if (this->speed_.has_value() && !traits.supports_speed()) {
ESP_LOGW(TAG, "%s: Speed control not supported", this->parent_.get_name().c_str());
this->speed_.reset();
}
if (this->direction_.has_value() && !traits.supports_direction()) {
ESP_LOGW(TAG, "%s: Direction control not supported", this->parent_.get_name().c_str());
this->direction_.reset();
}
}
FanCall FanRestoreState::to_call(Fan &fan) {

View File

@@ -1,6 +1,7 @@
from collections.abc import MutableMapping
import functools
import hashlib
from itertools import accumulate
import logging
import os
from pathlib import Path
@@ -468,8 +469,9 @@ class EFont:
class GlyphInfo:
def __init__(self, data_len, advance, offset_x, offset_y, width, height):
self.data_len = data_len
def __init__(self, glyph, data, advance, offset_x, offset_y, width, height):
self.glyph = glyph
self.bitmap_data = data
self.advance = advance
self.offset_x = offset_x
self.offset_y = offset_y
@@ -477,6 +479,62 @@ class GlyphInfo:
self.height = height
def glyph_to_glyphinfo(glyph, font, size, bpp):
scale = 256 // (1 << bpp)
if not font.is_scalable:
sizes = [pt_to_px(x.size) for x in font.available_sizes]
if size in sizes:
font.select_size(sizes.index(size))
else:
font.set_pixel_sizes(size, 0)
flags = FT_LOAD_RENDER
if bpp != 1:
flags |= FT_LOAD_NO_BITMAP
else:
flags |= FT_LOAD_TARGET_MONO
font.load_char(glyph, flags)
width = font.glyph.bitmap.width
height = font.glyph.bitmap.rows
buffer = font.glyph.bitmap.buffer
pitch = font.glyph.bitmap.pitch
glyph_data = [0] * ((height * width * bpp + 7) // 8)
src_mode = font.glyph.bitmap.pixel_mode
pos = 0
for y in range(height):
for x in range(width):
if src_mode == ft_pixel_mode_mono:
pixel = (
(1 << bpp) - 1
if buffer[y * pitch + x // 8] & (1 << (7 - x % 8))
else 0
)
else:
pixel = buffer[y * pitch + x] // scale
for bit_num in range(bpp):
if pixel & (1 << (bpp - bit_num - 1)):
glyph_data[pos // 8] |= 0x80 >> (pos % 8)
pos += 1
ascender = pt_to_px(font.size.ascender)
if ascender == 0:
if not font.is_scalable:
ascender = size
else:
_LOGGER.error(
"Unable to determine ascender of font %s %s",
font.family_name,
font.style_name,
)
return GlyphInfo(
glyph,
glyph_data,
pt_to_px(font.glyph.metrics.horiAdvance),
font.glyph.bitmap_left,
ascender - font.glyph.bitmap_top,
width,
height,
)
async def to_code(config):
"""
Collect all glyph codepoints, construct a map from a codepoint to a font file.
@@ -506,98 +564,47 @@ async def to_code(config):
codepoints = list(point_set)
codepoints.sort(key=functools.cmp_to_key(glyph_comparator))
glyph_args = {}
data = []
bpp = config[CONF_BPP]
scale = 256 // (1 << bpp)
size = config[CONF_SIZE]
# create the data array for all glyphs
for codepoint in codepoints:
font = point_font_map[codepoint]
if not font.is_scalable:
sizes = [pt_to_px(x.size) for x in font.available_sizes]
if size in sizes:
font.select_size(sizes.index(size))
else:
font.set_pixel_sizes(size, 0)
flags = FT_LOAD_RENDER
if bpp != 1:
flags |= FT_LOAD_NO_BITMAP
else:
flags |= FT_LOAD_TARGET_MONO
font.load_char(codepoint, flags)
width = font.glyph.bitmap.width
height = font.glyph.bitmap.rows
buffer = font.glyph.bitmap.buffer
pitch = font.glyph.bitmap.pitch
glyph_data = [0] * ((height * width * bpp + 7) // 8)
src_mode = font.glyph.bitmap.pixel_mode
pos = 0
for y in range(height):
for x in range(width):
if src_mode == ft_pixel_mode_mono:
pixel = (
(1 << bpp) - 1
if buffer[y * pitch + x // 8] & (1 << (7 - x % 8))
else 0
)
else:
pixel = buffer[y * pitch + x] // scale
for bit_num in range(bpp):
if pixel & (1 << (bpp - bit_num - 1)):
glyph_data[pos // 8] |= 0x80 >> (pos % 8)
pos += 1
ascender = pt_to_px(font.size.ascender)
if ascender == 0:
if not font.is_scalable:
ascender = size
else:
_LOGGER.error(
"Unable to determine ascender of font %s", config[CONF_FILE]
)
glyph_args[codepoint] = GlyphInfo(
len(data),
pt_to_px(font.glyph.metrics.horiAdvance),
font.glyph.bitmap_left,
ascender - font.glyph.bitmap_top,
width,
height,
)
data += glyph_data
rhs = [HexInt(x) for x in data]
glyph_args = [
glyph_to_glyphinfo(x, point_font_map[x], size, bpp) for x in codepoints
]
rhs = [HexInt(x) for x in flatten([x.bitmap_data for x in glyph_args])]
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
# Create the glyph table that points to data in the above array.
glyph_initializer = []
for codepoint in codepoints:
glyph_initializer.append(
glyph_initializer = [
cg.StructInitializer(
GlyphData,
(
"a_char",
cg.RawExpression(
f"(const uint8_t *){cpp_string_escape(codepoint)}"
),
cg.RawExpression(f"(const uint8_t *){cpp_string_escape(x.glyph)}"),
),
(
"data",
cg.RawExpression(
f"{str(prog_arr)} + {str(glyph_args[codepoint].data_len)}"
cg.RawExpression(f"{str(prog_arr)} + {str(y - len(x.bitmap_data))}"),
),
),
("advance", glyph_args[codepoint].advance),
("offset_x", glyph_args[codepoint].offset_x),
("offset_y", glyph_args[codepoint].offset_y),
("width", glyph_args[codepoint].width),
("height", glyph_args[codepoint].height),
("advance", x.advance),
("offset_x", x.offset_x),
("offset_y", x.offset_y),
("width", x.width),
("height", x.height),
)
for (x, y) in zip(
glyph_args, list(accumulate([len(x.bitmap_data) for x in glyph_args]))
)
]
glyphs = cg.static_const_array(config[CONF_RAW_GLYPH_ID], glyph_initializer)
font_height = pt_to_px(base_font.size.height)
ascender = pt_to_px(base_font.size.ascender)
descender = abs(pt_to_px(base_font.size.descender))
g = glyph_to_glyphinfo("x", base_font, size, bpp)
xheight = g.height if len(g.bitmap_data) > 1 else 0
g = glyph_to_glyphinfo("X", base_font, size, bpp)
capheight = g.height if len(g.bitmap_data) > 1 else 0
if font_height == 0:
if not base_font.is_scalable:
font_height = size
@@ -610,5 +617,8 @@ async def to_code(config):
len(glyph_initializer),
ascender,
font_height,
descender,
xheight,
capheight,
bpp,
)

View File

@@ -45,8 +45,15 @@ void Glyph::scan_area(int *x1, int *y1, int *width, int *height) const {
*height = this->glyph_data_->height;
}
Font::Font(const GlyphData *data, int data_nr, int baseline, int height, uint8_t bpp)
: baseline_(baseline), height_(height), bpp_(bpp) {
Font::Font(const GlyphData *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight,
uint8_t bpp)
: baseline_(baseline),
height_(height),
descender_(descender),
linegap_(height - baseline - descender),
xheight_(xheight),
capheight_(capheight),
bpp_(bpp) {
glyphs_.reserve(data_nr);
for (int i = 0; i < data_nr; ++i)
glyphs_.emplace_back(&data[i]);

View File

@@ -50,11 +50,17 @@ class Font
public:
/** Construct the font with the given glyphs.
*
* @param glyphs A vector of glyphs, must be sorted lexicographically.
* @param data A vector of glyphs, must be sorted lexicographically.
* @param data_nr The number of glyphs in data.
* @param baseline The y-offset from the top of the text to the baseline.
* @param bottom The y-offset from the top of the text to the bottom (i.e. height).
* @param height The y-offset from the top of the text to the bottom.
* @param descender The y-offset from the baseline to the lowest stroke in the font (e.g. from letters like g or p).
* @param xheight The height of lowercase letters, usually measured at the "x" glyph.
* @param capheight The height of capital letters, usually measured at the "X" glyph.
* @param bpp The bits per pixel used for this font. Used to read data out of the glyph bitmaps.
*/
Font(const GlyphData *data, int data_nr, int baseline, int height, uint8_t bpp = 1);
Font(const GlyphData *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight,
uint8_t bpp = 1);
int match_next_glyph(const uint8_t *str, int *match_length);
@@ -65,14 +71,23 @@ class Font
#endif
inline int get_baseline() { return this->baseline_; }
inline int get_height() { return this->height_; }
inline int get_ascender() { return this->baseline_; }
inline int get_descender() { return this->descender_; }
inline int get_linegap() { return this->linegap_; }
inline int get_xheight() { return this->xheight_; }
inline int get_capheight() { return this->capheight_; }
inline int get_bpp() { return this->bpp_; }
const std::vector<Glyph, ExternalRAMAllocator<Glyph>> &get_glyphs() const { return glyphs_; }
const std::vector<Glyph, RAMAllocator<Glyph>> &get_glyphs() const { return glyphs_; }
protected:
std::vector<Glyph, ExternalRAMAllocator<Glyph>> glyphs_;
std::vector<Glyph, RAMAllocator<Glyph>> glyphs_;
int baseline_;
int height_;
int descender_;
int linegap_;
int xheight_;
int capheight_;
uint8_t bpp_; // bits per pixel
};

View File

@@ -125,6 +125,6 @@ async def to_code(config):
cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE]))
cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE]))
cg.add_library("tonia/HeatpumpIR", "1.0.32")
cg.add_library("tonia/HeatpumpIR", "1.0.35")
if CORE.is_libretiny:
CORE.add_platformio_option("lib_ignore", "IRremoteESP8266")

View File

@@ -41,6 +41,6 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
cg.add_build_flag("-DUSE_HOST")
cg.add_define("USE_ESPHOME_HOST_MAC_ADDRESS", config[CONF_MAC_ADDRESS].parts)
cg.add_build_flag("-std=c++17")
cg.add_build_flag("-std=gnu++17")
cg.add_define("ESPHOME_BOARD", "host")
cg.add_platformio_option("platform", "platformio/native")

View File

@@ -175,7 +175,7 @@ async def to_code(config):
not config.get(CONF_VERIFY_SSL),
)
else:
cg.add_library("WiFiClientSecure", None)
cg.add_library("NetworkClientSecure", None)
cg.add_library("HTTPClient", None)
if CORE.is_esp8266:
cg.add_library("ESP8266HTTPClient", None)

View File

@@ -239,7 +239,7 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
std::string response_body;
if (this->capture_response_.value(x...)) {
ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
RAMAllocator<uint8_t> allocator;
uint8_t *buf = allocator.allocate(max_length);
if (buf != nullptr) {
size_t read_index = 0;

View File

@@ -6,6 +6,7 @@
#if defined(USE_ESP32) || defined(USE_RP2040)
#include <HTTPClient.h>
#include <WiFiClient.h>
#endif
#ifdef USE_ESP8266
#include <ESP8266HTTPClient.h>

View File

@@ -54,7 +54,7 @@ void HttpRequestUpdate::update_task(void *params) {
UPDATE_RETURN;
}
ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
RAMAllocator<uint8_t> allocator;
uint8_t *data = allocator.allocate(container->content_length);
if (data == nullptr) {
std::string msg = str_sprintf("Failed to allocate %d bytes for manifest", container->content_length);

View File

@@ -22,8 +22,9 @@ import esphome.final_validate as fv
CODEOWNERS = ["@esphome/core"]
i2c_ns = cg.esphome_ns.namespace("i2c")
I2CBus = i2c_ns.class_("I2CBus")
ArduinoI2CBus = i2c_ns.class_("ArduinoI2CBus", I2CBus, cg.Component)
IDFI2CBus = i2c_ns.class_("IDFI2CBus", I2CBus, cg.Component)
InternalI2CBus = i2c_ns.class_("InternalI2CBus", I2CBus)
ArduinoI2CBus = i2c_ns.class_("ArduinoI2CBus", InternalI2CBus, cg.Component)
IDFI2CBus = i2c_ns.class_("IDFI2CBus", InternalI2CBus, cg.Component)
I2CDevice = i2c_ns.class_("I2CDevice")
@@ -71,6 +72,7 @@ CONFIG_SCHEMA = cv.All(
@coroutine_with_priority(1.0)
async def to_code(config):
cg.add_global(i2c_ns.using)
cg.add_define("USE_I2C")
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)

View File

@@ -1,6 +1,6 @@
#pragma once
#include <cstdint>
#include <cstddef>
#include <cstdint>
#include <utility>
#include <vector>
@@ -108,5 +108,12 @@ class I2CBus {
bool scan_{false}; ///< Should we scan ? Can be set in the yaml
};
class InternalI2CBus : public I2CBus {
public:
/// @brief Returns the I2C port number.
/// @return the port number of the internal I2C bus
virtual int get_port() const = 0;
};
} // namespace i2c
} // namespace esphome

View File

@@ -1,11 +1,11 @@
#ifdef USE_ARDUINO
#include "i2c_bus_arduino.h"
#include <Arduino.h>
#include <cstring>
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <Arduino.h>
#include <cstring>
namespace esphome {
namespace i2c {
@@ -23,6 +23,7 @@ void ArduinoI2CBus::setup() {
} else {
wire_ = new TwoWire(next_bus_num); // NOLINT(cppcoreguidelines-owning-memory)
}
this->port_ = next_bus_num;
next_bus_num++;
#elif defined(USE_ESP8266)
wire_ = new TwoWire(); // NOLINT(cppcoreguidelines-owning-memory)
@@ -125,7 +126,7 @@ ErrorCode ArduinoI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt)
size_t to_request = 0;
for (size_t i = 0; i < cnt; i++)
to_request += buffers[i].len;
size_t ret = wire_->requestFrom((int) address, (int) to_request, 1);
size_t ret = wire_->requestFrom(address, to_request, true);
if (ret != to_request) {
ESP_LOGVV(TAG, "RX %u from %02X failed with error %u", to_request, address, ret);
return ERROR_TIMEOUT;

View File

@@ -2,9 +2,9 @@
#ifdef USE_ARDUINO
#include "i2c_bus.h"
#include "esphome/core/component.h"
#include <Wire.h>
#include "esphome/core/component.h"
#include "i2c_bus.h"
namespace esphome {
namespace i2c {
@@ -15,7 +15,7 @@ enum RecoveryCode {
RECOVERY_COMPLETED,
};
class ArduinoI2CBus : public I2CBus, public Component {
class ArduinoI2CBus : public InternalI2CBus, public Component {
public:
void setup() override;
void dump_config() override;
@@ -29,12 +29,15 @@ class ArduinoI2CBus : public I2CBus, public Component {
void set_frequency(uint32_t frequency) { frequency_ = frequency; }
void set_timeout(uint32_t timeout) { timeout_ = timeout; }
int get_port() const override { return this->port_; }
private:
void recover_();
void set_pins_and_clock_();
RecoveryCode recovery_result_;
protected:
int8_t port_{-1};
TwoWire *wire_;
uint8_t sda_pin_;
uint8_t scl_pin_;

View File

@@ -2,9 +2,9 @@
#ifdef USE_ESP_IDF
#include "i2c_bus.h"
#include "esphome/core/component.h"
#include <driver/i2c.h>
#include "esphome/core/component.h"
#include "i2c_bus.h"
namespace esphome {
namespace i2c {
@@ -15,7 +15,7 @@ enum RecoveryCode {
RECOVERY_COMPLETED,
};
class IDFI2CBus : public I2CBus, public Component {
class IDFI2CBus : public InternalI2CBus, public Component {
public:
void setup() override;
void dump_config() override;
@@ -31,6 +31,8 @@ class IDFI2CBus : public I2CBus, public Component {
void set_frequency(uint32_t frequency) { frequency_ = frequency; }
void set_timeout(uint32_t timeout) { timeout_ = timeout; }
int get_port() const override { return static_cast<int>(this->port_); }
private:
void recover_();
RecoveryCode recovery_result_;

View File

@@ -9,7 +9,7 @@ namespace i2s_audio {
static const char *const TAG = "i2s_audio";
#if defined(USE_ESP_IDF) && (ESP_IDF_VERSION_MAJOR >= 5)
#if ESP_IDF_VERSION_MAJOR >= 5
static const uint8_t I2S_NUM_MAX = SOC_I2S_NUM; // because IDF 5+ took this away :(
#endif
@@ -18,7 +18,7 @@ void I2SAudioComponent::setup() {
static i2s_port_t next_port_num = I2S_NUM_0;
if (next_port_num >= I2S_NUM_MAX) {
ESP_LOGE(TAG, "Too many I2S Audio components");
ESP_LOGE(TAG, "Too many components");
this->mark_failed();
return;
}

View File

@@ -114,7 +114,7 @@ async def to_code(config):
cg.add(var.set_external_dac_channels(2 if config[CONF_MODE] == "stereo" else 1))
cg.add(var.set_i2s_comm_fmt_lsb(config[CONF_I2S_COMM_FMT] == "lsb"))
cg.add_library("WiFiClientSecure", None)
cg.add_library("NetworkClientSecure", None)
cg.add_library("HTTPClient", None)
cg.add_library("esphome/ESP32-audioI2S", "2.2.0")
cg.add_library("esphome/ESP32-audioI2S", "2.3.0")
cg.add_build_flag("-DAUDIO_NO_SD_FS")

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