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: with:
python-version: "3.10" python-version: "3.10"
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.10.0 uses: docker/setup-buildx-action@v3.11.1
- name: Set TAG - name: Set TAG
run: | run: |

View File

@@ -1,28 +1,13 @@
--- ---
name: Lock name: Lock closed issues and PRs
on: on:
schedule: schedule:
- cron: "30 0 * * *" - cron: "30 0 * * *" # Run daily at 00:30 UTC
workflow_dispatch: workflow_dispatch:
permissions:
issues: write
pull-requests: write
concurrency:
group: lock
jobs: jobs:
lock: lock:
runs-on: ubuntu-latest uses: esphome/workflows/.github/workflows/lock.yml@main
steps: with:
- uses: dessant/lock-threads@v5.0.1 since-days: 3650
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

View File

@@ -99,7 +99,7 @@ jobs:
python-version: "3.10" python-version: "3.10"
- name: Set up Docker Buildx - 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 - name: Log in to docker hub
uses: docker/login-action@v3.4.0 uses: docker/login-action@v3.4.0
@@ -178,7 +178,7 @@ jobs:
merge-multiple: true merge-multiple: true
- name: Set up Docker Buildx - 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 - name: Log in to docker hub
if: matrix.registry == 'dockerhub' if: matrix.registry == 'dockerhub'

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,8 +14,8 @@ from esphome.const import (
CONF_WEB_SERVER, CONF_WEB_SERVER,
) )
from esphome.core import CORE, coroutine_with_priority 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_generator import MockObjClass
from esphome.cpp_helpers import setup_entity
CODEOWNERS = ["@grahambrown11", "@hwstar"] CODEOWNERS = ["@grahambrown11", "@hwstar"]
IS_PLATFORM_COMPONENT = True 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( def alarm_control_panel_schema(
class_: MockObjClass, class_: MockObjClass,
*, *,
@@ -190,7 +193,7 @@ ALARM_CONTROL_PANEL_CONDITION_SCHEMA = maybe_simple_id(
async def setup_alarm_control_panel_core_(var, config): 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, []): for conf in config.get(CONF_ON_STATE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf) await automation.build_automation(trigger, [], conf)

View File

@@ -17,7 +17,11 @@ void Anova::setup() {
this->current_request_ = 0; 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) { void Anova::control(const ClimateCall &call) {
if (call.get_mode().has_value()) { if (call.get_mode().has_value()) {

View File

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

View File

@@ -28,8 +28,19 @@
namespace esphome { namespace esphome {
namespace api { 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"; static const char *const TAG = "api.connection";
#ifdef USE_ESP32_CAMERA
static const int ESP32_CAMERA_STOP_STREAM = 5000; static const int ESP32_CAMERA_STOP_STREAM = 5000;
#endif
APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent) APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent)
: parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) { : parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) {
@@ -61,8 +72,8 @@ void APIConnection::start() {
APIError err = this->helper_->init(); APIError err = this->helper_->init();
if (err != APIError::OK) { if (err != APIError::OK) {
on_fatal_error(); on_fatal_error();
ESP_LOGW(TAG, "%s: Helper init failed: %s errno=%d", this->client_combined_info_.c_str(), api_error_to_str(err), ESP_LOGW(TAG, "%s: Helper init failed: %s errno=%d", this->get_client_combined_info().c_str(),
errno); api_error_to_str(err), errno);
return; return;
} }
this->client_info_ = helper_->getpeername(); this->client_info_ = helper_->getpeername();
@@ -84,16 +95,6 @@ APIConnection::~APIConnection() {
} }
void APIConnection::loop() { 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_) { if (this->next_close_) {
// requested a disconnect // requested a disconnect
this->helper_->close(); this->helper_->close();
@@ -104,76 +105,77 @@ void APIConnection::loop() {
APIError err = this->helper_->loop(); APIError err = this->helper_->loop();
if (err != APIError::OK) { if (err != APIError::OK) {
on_fatal_error(); 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); api_error_to_str(err), errno);
return; return;
} }
const uint32_t now = App.get_loop_component_start_time();
// Check if socket has data ready before attempting to read // Check if socket has data ready before attempting to read
if (this->helper_->is_socket_ready()) { if (this->helper_->is_socket_ready()) {
ReadPacketBuffer buffer; // Read up to MAX_MESSAGES_PER_LOOP messages per loop to improve throughput
err = this->helper_->read_packet(&buffer); for (uint8_t message_count = 0; message_count < MAX_MESSAGES_PER_LOOP; message_count++) {
if (err == APIError::WOULD_BLOCK) { ReadPacketBuffer buffer;
// pass err = this->helper_->read_packet(&buffer);
} else if (err != APIError::OK) { if (err == APIError::WOULD_BLOCK) {
on_fatal_error(); // No more data available
if (err == APIError::SOCKET_READ_FAILED && errno == ECONNRESET) { break;
ESP_LOGW(TAG, "%s: Connection reset", this->client_combined_info_.c_str()); } else if (err != APIError::OK) {
} else if (err == APIError::CONNECTION_CLOSED) { on_fatal_error();
ESP_LOGW(TAG, "%s: Connection closed", this->client_combined_info_.c_str()); if (err == APIError::SOCKET_READ_FAILED && errno == ECONNRESET) {
} else { ESP_LOGW(TAG, "%s: Connection reset", this->get_client_combined_info().c_str());
ESP_LOGW(TAG, "%s: Reading failed: %s errno=%d", this->client_combined_info_.c_str(), api_error_to_str(err), } else if (err == APIError::CONNECTION_CLOSED) {
errno); ESP_LOGW(TAG, "%s: Connection closed", this->get_client_combined_info().c_str());
} } else {
return; ESP_LOGW(TAG, "%s: Reading failed: %s errno=%d", this->get_client_combined_info().c_str(),
} else { api_error_to_str(err), errno);
this->last_traffic_ = App.get_loop_component_start_time(); }
// read a packet
if (buffer.data_len > 0) {
this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]);
} else {
this->read_message(0, buffer.type, nullptr);
}
if (this->remove_)
return; return;
} else {
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]);
} else {
this->read_message(0, buffer.type, nullptr);
}
if (this->remove_)
return;
}
} }
} }
// Process deferred batch if scheduled // Process deferred batch if scheduled
if (this->deferred_batch_.batch_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_(); this->process_batch_();
} }
if (!this->list_entities_iterator_.completed()) if (!this->list_entities_iterator_.completed()) {
this->list_entities_iterator_.advance(); 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(); 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_) { if (this->sent_ping_) {
// Disconnect if not responded within 2.5*keepalive // 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(); 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_) { } else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS && now > this->next_ping_retry_) {
ESP_LOGVV(TAG, "Sending keepalive PING"); ESP_LOGVV(TAG, "Sending keepalive PING");
this->sent_ping_ = this->send_message(PingRequest()); this->sent_ping_ = this->send_message(PingRequest());
if (!this->sent_ping_) { if (!this->sent_ping_) {
this->next_ping_retry_ = now + ping_retry_interval; this->next_ping_retry_ = now + PING_RETRY_INTERVAL;
this->ping_retries_++; this->ping_retries_++;
std::string warn_str = str_sprintf("%s: Sending keepalive failed %u time(s);", if (this->ping_retries_ >= MAX_PING_RETRIES) {
this->client_combined_info_.c_str(), this->ping_retries_);
if (this->ping_retries_ >= max_ping_retries) {
on_fatal_error(); 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) { } 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 { } 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; // bool done = 3;
buffer.encode_bool(3, done); buffer.encode_bool(3, done);
bool success = this->send_buffer(buffer, 44); bool success = this->send_buffer(buffer, CameraImageResponse::MESSAGE_TYPE);
if (success) { if (success) {
this->image_reader_.consume_data(to_send); this->image_reader_.consume_data(to_send);
} if (done) {
if (success && done) { this->image_reader_.return_image();
this->image_reader_.return_image(); }
} }
} }
#endif #endif
if (state_subs_at_ != -1) { if (state_subs_at_ >= 0) {
const auto &subs = this->parent_->get_state_subs(); const auto &subs = this->parent_->get_state_subs();
if (state_subs_at_ >= (int) subs.size()) { if (state_subs_at_ < static_cast<int>(subs.size())) {
state_subs_at_ = -1;
} else {
auto &it = subs[state_subs_at_]; auto &it = subs[state_subs_at_];
SubscribeHomeAssistantStateResponse resp; SubscribeHomeAssistantStateResponse resp;
resp.entity_id = it.entity_id; resp.entity_id = it.entity_id;
@@ -221,6 +221,8 @@ void APIConnection::loop() {
if (this->send_message(resp)) { if (this->send_message(resp)) {
state_subs_at_++; state_subs_at_++;
} }
} else {
state_subs_at_ = -1;
} }
} }
} }
@@ -233,7 +235,7 @@ DisconnectResponse APIConnection::disconnect(const DisconnectRequest &msg) {
// remote initiated disconnect_client // remote initiated disconnect_client
// don't close yet, we still need to send the disconnect response // don't close yet, we still need to send the disconnect response
// close will happen on next loop // 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; this->next_close_ = true;
DisconnectResponse resp; DisconnectResponse resp;
return 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, uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint16_t message_type, APIConnection *conn,
uint32_t remaining_size, bool is_single) { uint32_t remaining_size, bool is_single) {
// Calculate size // Calculate size
uint32_t size = 0; uint32_t calculated_size = 0;
msg.calculate_size(size); 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 // Calculate total size with padding for buffer allocation
uint16_t total_size = size_t total_calculated_size = calculated_size + header_padding + footer_size;
static_cast<uint16_t>(size) + conn->helper_->frame_header_padding() + conn->helper_->frame_footer_size();
// Check if it fits // Check if it fits
if (total_size > remaining_size) { if (total_calculated_size > remaining_size) {
return 0; // Doesn't fit return 0; // Doesn't fit
} }
// Allocate exact buffer space needed (just the payload, not the overhead) // Allocate buffer space - pass payload size, allocation functions add header/footer space
ProtoWriteBuffer buffer = ProtoWriteBuffer buffer = is_single ? conn->allocate_single_message_buffer(calculated_size)
is_single ? conn->allocate_single_message_buffer(size) : conn->allocate_batch_message_buffer(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 // Encode directly into buffer
msg.encode(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 #ifdef USE_BINARY_SENSOR
@@ -285,7 +308,7 @@ uint16_t APIConnection::try_send_binary_sensor_state(EntityBase *entity, APIConn
BinarySensorStateResponse resp; BinarySensorStateResponse resp;
resp.state = binary_sensor->state; resp.state = binary_sensor->state;
resp.missing_state = !binary_sensor->has_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); 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()) if (traits.get_supports_tilt())
msg.tilt = cover->tilt; msg.tilt = cover->tilt;
msg.current_operation = static_cast<enums::CoverOperation>(cover->current_operation); 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); 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, 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); msg.direction = static_cast<enums::FanDirection>(fan->direction);
if (traits.supports_preset_modes()) if (traits.supports_preset_modes())
msg.preset_mode = fan->preset_mode; 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); 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, 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(); resp.warm_white = values.get_warm_white();
if (light->supports_effects()) if (light->supports_effects())
resp.effect = light->get_effect_name(); 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); 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, 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; SensorStateResponse resp;
resp.state = sensor->state; resp.state = sensor->state;
resp.missing_state = !sensor->has_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); 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); auto *a_switch = static_cast<switch_::Switch *>(entity);
SwitchStateResponse resp; SwitchStateResponse resp;
resp.state = a_switch->state; 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); 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; TextSensorStateResponse resp;
resp.state = text_sensor->state; resp.state = text_sensor->state;
resp.missing_state = !text_sensor->has_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); 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, 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) { bool is_single) {
auto *climate = static_cast<climate::Climate *>(entity); auto *climate = static_cast<climate::Climate *>(entity);
ClimateStateResponse resp; ClimateStateResponse resp;
resp.key = climate->get_object_id_hash(); fill_entity_state_base(climate, resp);
auto traits = climate->get_traits(); auto traits = climate->get_traits();
resp.mode = static_cast<enums::ClimateMode>(climate->mode); resp.mode = static_cast<enums::ClimateMode>(climate->mode);
resp.action = static_cast<enums::ClimateAction>(climate->action); resp.action = static_cast<enums::ClimateAction>(climate->action);
@@ -746,7 +769,7 @@ uint16_t APIConnection::try_send_number_state(EntityBase *entity, APIConnection
NumberStateResponse resp; NumberStateResponse resp;
resp.state = number->state; resp.state = number->state;
resp.missing_state = !number->has_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); 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.year = date->year;
resp.month = date->month; resp.month = date->month;
resp.day = date->day; 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); return encode_message_to_buffer(resp, DateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::send_date_info(datetime::DateEntity *date) { 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.hour = time->hour;
resp.minute = time->minute; resp.minute = time->minute;
resp.second = time->second; 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); return encode_message_to_buffer(resp, TimeStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::send_time_info(datetime::TimeEntity *time) { 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(); ESPTime state = datetime->state_as_esptime();
resp.epoch_seconds = state.timestamp; 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); return encode_message_to_buffer(resp, DateTimeStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::send_datetime_info(datetime::DateTimeEntity *datetime) { 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; TextStateResponse resp;
resp.state = text->state; resp.state = text->state;
resp.missing_state = !text->has_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); 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; SelectStateResponse resp;
resp.state = select->state; resp.state = select->state;
resp.missing_state = !select->has_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); 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); auto *a_lock = static_cast<lock::Lock *>(entity);
LockStateResponse resp; LockStateResponse resp;
resp.state = static_cast<enums::LockState>(a_lock->state); 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); 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; ValveStateResponse resp;
resp.position = valve->position; resp.position = valve->position;
resp.current_operation = static_cast<enums::ValveOperation>(valve->current_operation); 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); return encode_message_to_buffer(resp, ValveStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::send_valve_info(valve::Valve *valve) { 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.state = static_cast<enums::MediaPlayerState>(report_state);
resp.volume = media_player->volume; resp.volume = media_player->volume;
resp.muted = media_player->is_muted(); 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); 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) { 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); auto *a_alarm_control_panel = static_cast<alarm_control_panel::AlarmControlPanel *>(entity);
AlarmControlPanelStateResponse resp; AlarmControlPanelStateResponse resp;
resp.state = static_cast<enums::AlarmControlPanelState>(a_alarm_control_panel->get_state()); 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); 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) { 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 #ifdef USE_EVENT
void APIConnection::send_event(event::Event *event, const std::string &event_type) { 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) { void APIConnection::send_event_info(event::Event *event) {
this->schedule_message_(event, &APIConnection::try_send_event_info, ListEntitiesEventResponse::MESSAGE_TYPE); 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) { uint32_t remaining_size, bool is_single) {
EventResponse resp; EventResponse resp;
resp.event_type = event_type; 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); 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_summary = update->update_info.summary;
resp.release_url = update->update_info.release_url; 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); return encode_message_to_buffer(resp, UpdateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::send_update_info(update::UpdateEntity *update) { 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 buffer.encode_string(3, line, line_length); // string message = 3
// SubscribeLogsResponse - 29 // SubscribeLogsResponse - 29
return this->send_buffer(buffer, 29); return this->send_buffer(buffer, SubscribeLogsResponse::MESSAGE_TYPE);
} }
HelloResponse APIConnection::hello(const HelloRequest &msg) { HelloResponse APIConnection::hello(const HelloRequest &msg) {
this->client_info_ = msg.client_info; this->client_info_ = msg.client_info;
this->client_peername_ = this->helper_->getpeername(); this->client_peername_ = this->helper_->getpeername();
this->client_combined_info_ = this->client_info_ + " (" + this->client_peername_ + ")"; this->helper_->set_log_info(this->get_client_combined_info());
this->helper_->set_log_info(this->client_combined_info_);
this->client_api_version_major_ = msg.api_version_major; this->client_api_version_major_ = msg.api_version_major;
this->client_api_version_minor_ = msg.api_version_minor; 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(), 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; // bool invalid_password = 1;
resp.invalid_password = !correct; resp.invalid_password = !correct;
if (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->connection_state_ = ConnectionState::AUTHENTICATED;
this->parent_->get_client_connected_trigger()->trigger(this->client_info_, this->client_peername_); this->parent_->get_client_connected_trigger()->trigger(this->client_info_, this->client_peername_);
#ifdef USE_HOMEASSISTANT_TIME #ifdef USE_HOMEASSISTANT_TIME
@@ -1604,6 +1626,23 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) {
#endif #endif
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
resp.api_encryption_supported = true; 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 #endif
return resp; return resp;
} }
@@ -1657,7 +1696,7 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) {
APIError err = this->helper_->loop(); APIError err = this->helper_->loop();
if (err != APIError::OK) { if (err != APIError::OK) {
on_fatal_error(); 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); api_error_to_str(err), errno);
return false; return false;
} }
@@ -1669,7 +1708,7 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) {
return false; return false;
} }
bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint16_t message_type) { 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; return false;
} }
@@ -1679,10 +1718,10 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint16_t message_type)
if (err != APIError::OK) { if (err != APIError::OK) {
on_fatal_error(); on_fatal_error();
if (err == APIError::SOCKET_WRITE_FAILED && errno == ECONNRESET) { 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 { } else {
ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", this->client_combined_info_.c_str(), api_error_to_str(err), ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", this->get_client_combined_info().c_str(),
errno); api_error_to_str(err), errno);
} }
return false; return false;
} }
@@ -1691,11 +1730,11 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint16_t message_type)
} }
void APIConnection::on_unauthenticated_access() { void APIConnection::on_unauthenticated_access() {
this->on_fatal_error(); 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() { void APIConnection::on_no_setup_connection() {
this->on_fatal_error(); 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() { void APIConnection::on_fatal_error() {
this->helper_->close(); this->helper_->close();
@@ -1753,7 +1792,8 @@ void APIConnection::process_batch_() {
const auto &item = this->deferred_batch_.items[0]; const auto &item = this->deferred_batch_.items[0];
// Let the creator calculate size and encode if it fits // 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 && if (payload_size > 0 &&
this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, item.message_type)) { 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; this->batch_first_message_ = true;
size_t items_processed = 0; 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 // Track where each message's header padding begins in the buffer
// For plaintext: this is where the 6-byte header padding starts // 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) { for (const auto &item : this->deferred_batch_.items) {
// Try to encode message // Try to encode message
// The creator will calculate overhead to determine if the message fits // 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) { if (payload_size == 0) {
// Message won't fit, stop processing // 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); packet_info.emplace_back(item.message_type, current_offset, proto_payload_size);
// Update tracking variables // 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; remaining_size -= payload_size;
// Calculate where the next message's header padding will start // 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 buffer size + footer space (that prepare_message_buffer will add for this message)
current_offset = this->parent_->get_shared_buffer_ref().size() + footer_size; current_offset = this->parent_->get_shared_buffer_ref().size() + footer_size;
items_processed++;
} }
if (items_processed == 0) { if (items_processed == 0) {
@@ -1840,10 +1884,10 @@ void APIConnection::process_batch_() {
if (err != APIError::OK && err != APIError::WOULD_BLOCK) { if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
on_fatal_error(); on_fatal_error();
if (err == APIError::SOCKET_WRITE_FAILED && errno == ECONNRESET) { 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 { } else {
ESP_LOGW(TAG, "%s: Batch write failed %s errno=%d", this->client_combined_info_.c_str(), api_error_to_str(err), ESP_LOGW(TAG, "%s: Batch write failed %s errno=%d", this->get_client_combined_info().c_str(),
errno); api_error_to_str(err), errno);
} }
} }
@@ -1862,21 +1906,23 @@ void APIConnection::process_batch_() {
} }
uint16_t APIConnection::MessageCreator::operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::MessageCreator::operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) const { bool is_single, uint16_t message_type) const {
switch (message_type_) { if (has_tagged_string_ptr_()) {
case 0: // Function pointer // Handle string-based messages
return data_.ptr(entity, conn, remaining_size, is_single); switch (message_type) {
#ifdef USE_EVENT #ifdef USE_EVENT
case EventResponse::MESSAGE_TYPE: { case EventResponse::MESSAGE_TYPE: {
auto *e = static_cast<event::Event *>(entity); 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 #endif
default:
default: // Should not happen, return 0 to indicate no message
// Should not happen, return 0 to indicate no message return 0;
return 0; }
} else {
// Function pointer case
return data_.ptr(entity, conn, remaining_size, is_single);
} }
} }

View File

@@ -240,8 +240,8 @@ class APIConnection : public APIServerConnection {
// - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext) // - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext)
// - Footer: space for MAC (16 bytes for Noise, 0 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()); shared_buf.reserve(reserve_size + header_padding + this->helper_->frame_footer_size());
// Insert header padding bytes so message encoding starts at the correct position // Resize to add header padding so message encoding starts at the correct position
shared_buf.insert(shared_buf.begin(), header_padding, 0); shared_buf.resize(header_padding);
return {&shared_buf}; return {&shared_buf};
} }
@@ -249,47 +249,47 @@ class APIConnection : public APIServerConnection {
ProtoWriteBuffer prepare_message_buffer(uint16_t message_size, bool is_first_message) { ProtoWriteBuffer prepare_message_buffer(uint16_t message_size, bool is_first_message) {
// Get reference to shared buffer (it maintains state between batch messages) // Get reference to shared buffer (it maintains state between batch messages)
std::vector<uint8_t> &shared_buf = this->parent_->get_shared_buffer_ref(); std::vector<uint8_t> &shared_buf = this->parent_->get_shared_buffer_ref();
size_t current_size = shared_buf.size();
if (is_first_message) { 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.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}; return {&shared_buf};
} }
bool try_to_clear_buffer(bool log_out_of_space); bool try_to_clear_buffer(bool log_out_of_space);
bool send_buffer(ProtoWriteBuffer buffer, uint16_t message_type) override; 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 // Buffer allocator methods for batch processing
ProtoWriteBuffer allocate_single_message_buffer(uint16_t size); ProtoWriteBuffer allocate_single_message_buffer(uint16_t size);
ProtoWriteBuffer allocate_batch_message_buffer(uint16_t size); ProtoWriteBuffer allocate_batch_message_buffer(uint16_t size);
protected: protected:
// Helper function to fill common entity fields // Helper function to fill common entity info fields
template<typename ResponseT> static void fill_entity_info_base(esphome::EntityBase *entity, ResponseT &response) { static void fill_entity_info_base(esphome::EntityBase *entity, InfoResponseProtoMessage &response) {
// Set common fields that are shared by all entity types // Set common fields that are shared by all entity types
response.key = entity->get_object_id_hash(); response.key = entity->get_object_id_hash();
response.object_id = entity->get_object_id(); response.object_id = entity->get_object_id();
@@ -301,6 +301,14 @@ class APIConnection : public APIServerConnection {
response.icon = entity->get_icon(); response.icon = entity->get_icon();
response.disabled_by_default = entity->is_disabled_by_default(); response.disabled_by_default = entity->is_disabled_by_default();
response.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category()); 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 // 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 // Helper function to get estimated message size for buffer pre-allocation
static uint16_t get_estimated_message_size(uint16_t message_type); 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, WAITING_FOR_HELLO,
CONNECTED, CONNECTED,
AUTHENTICATED, AUTHENTICATED,
} connection_state_{ConnectionState::WAITING_FOR_HELLO}; } connection_state_{ConnectionState::WAITING_FOR_HELLO};
uint8_t log_subscription_{ESPHOME_LOG_LEVEL_NONE};
bool remove_{false}; 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_; // Larger objects at the end
InitialStateIterator initial_state_iterator_;
std::string client_info_; ListEntitiesIterator list_entities_iterator_;
std::string client_peername_;
std::string client_combined_info_;
uint32_t client_api_version_major_{0};
uint32_t client_api_version_minor_{0};
#ifdef USE_ESP32_CAMERA #ifdef USE_ESP32_CAMERA
esp32_camera::CameraImageReader image_reader_; esp32_camera::CameraImageReader image_reader_;
#endif #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 // Function pointer type for message encoding
using MessageCreatorPtr = uint16_t (*)(EntityBase *, APIConnection *, uint32_t remaining_size, bool is_single); 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 { class MessageCreator {
// Ensure pointer alignment allows LSB tagging
static_assert(alignof(std::string *) > 1, "String pointer alignment must be > 1 for LSB tagging");
public: public:
// Constructor for function pointer (message_type = 0) // Constructor for function pointer
MessageCreator(MessageCreatorPtr ptr) : message_type_(0) { data_.ptr = ptr; } MessageCreator(MessageCreatorPtr ptr) {
// Function pointers are always aligned, so LSB is 0
data_.ptr = ptr;
}
// Constructor for string state capture // Constructor for string state capture
MessageCreator(const std::string &value, uint16_t msg_type) : message_type_(msg_type) { explicit MessageCreator(const std::string &str_value) {
data_.string_ptr = new std::string(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 // Destructor
~MessageCreator() { ~MessageCreator() {
// Clean up string data for string-based message types if (has_tagged_string_ptr_()) {
if (uses_string_data_()) { delete get_string_ptr_();
delete data_.string_ptr;
} }
} }
// Copy constructor // Copy constructor
MessageCreator(const MessageCreator &other) : message_type_(other.message_type_) { MessageCreator(const MessageCreator &other) {
if (message_type_ == 0) { if (other.has_tagged_string_ptr_()) {
data_.ptr = other.data_.ptr; auto *str = new std::string(*other.get_string_ptr_());
} else if (uses_string_data_()) { data_.tagged = reinterpret_cast<uintptr_t>(str) | 1;
data_.string_ptr = new std::string(*other.data_.string_ptr);
} else { } else {
data_ = other.data_; // For POD types data_ = other.data_;
} }
} }
// Move constructor // Move constructor
MessageCreator(MessageCreator &&other) noexcept : data_(other.data_), message_type_(other.message_type_) { MessageCreator(MessageCreator &&other) noexcept : data_(other.data_) { other.data_.ptr = nullptr; }
other.message_type_ = 0; // Reset other to function pointer type
other.data_.ptr = nullptr;
}
// Assignment operators (needed for batch deduplication) // Assignment operators (needed for batch deduplication)
MessageCreator &operator=(const MessageCreator &other) { MessageCreator &operator=(const MessageCreator &other) {
if (this != &other) { if (this != &other) {
// Clean up current string data if needed // Clean up current string data if needed
if (uses_string_data_()) { if (has_tagged_string_ptr_()) {
delete data_.string_ptr; delete get_string_ptr_();
} }
// Copy new data // Copy new data
message_type_ = other.message_type_; if (other.has_tagged_string_ptr_()) {
if (other.message_type_ == 0) { auto *str = new std::string(*other.get_string_ptr_());
data_.ptr = other.data_.ptr; data_.tagged = reinterpret_cast<uintptr_t>(str) | 1;
} else if (other.uses_string_data_()) {
data_.string_ptr = new std::string(*other.data_.string_ptr);
} else { } else {
data_ = other.data_; data_ = other.data_;
} }
@@ -527,30 +544,35 @@ class APIConnection : public APIServerConnection {
MessageCreator &operator=(MessageCreator &&other) noexcept { MessageCreator &operator=(MessageCreator &&other) noexcept {
if (this != &other) { if (this != &other) {
// Clean up current string data if needed // Clean up current string data if needed
if (uses_string_data_()) { if (has_tagged_string_ptr_()) {
delete data_.string_ptr; delete get_string_ptr_();
} }
// Move data // Move data
message_type_ = other.message_type_;
data_ = other.data_; data_ = other.data_;
// Reset other to safe state // Reset other to safe state
other.message_type_ = 0;
other.data_.ptr = nullptr; other.data_.ptr = nullptr;
} }
return *this; return *this;
} }
// Call operator // Call operator - now accepts message_type as parameter
uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) const; uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single,
uint16_t message_type) const;
private: private:
// Helper to check if this message type uses heap-allocated strings // Check if this contains a string pointer
bool uses_string_data_() const { return message_type_ == EventResponse::MESSAGE_TYPE; } bool has_tagged_string_ptr_() const { return (data_.tagged & 1) != 0; }
union CreatorData {
MessageCreatorPtr ptr; // 8 bytes // Get the actual string pointer (clears the tag bit)
std::string *string_ptr; // 8 bytes std::string *get_string_ptr_() const {
} data_; // 8 bytes // NOLINTNEXTLINE(performance-no-int-to-ptr)
uint16_t message_type_; // 2 bytes (0 = function ptr, >0 = state capture) 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 // 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"; 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 // Helper method to buffer data from IOVs
void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len) { void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len) {
SendBuffer buffer; SendBuffer buffer;
@@ -274,17 +285,21 @@ APIError APINoiseFrameHelper::init() {
} }
/// Run through handshake messages (if in that phase) /// Run through handshake messages (if in that phase)
APIError APINoiseFrameHelper::loop() { APIError APINoiseFrameHelper::loop() {
APIError err = state_action_(); // During handshake phase, process as many actions as possible until we can't progress
if (err != APIError::OK && err != APIError::WOULD_BLOCK) { // socket_->ready() stays true until next main loop, but state_action() will return
return err; // WOULD_BLOCK when no more data is available to read
} while (state_ != State::DATA && this->socket_->ready()) {
if (!this->tx_buf_.empty()) { APIError err = state_action_();
err = try_send_tx_buf_();
if (err != APIError::OK && err != APIError::WOULD_BLOCK) { if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
return err; 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 /** 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; 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 // header reading done
} }
// read body // 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]; uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2];
if (state_ != State::DATA && msg_size > 128) { if (state_ != State::DATA && msg_size > 128) {
@@ -586,10 +599,6 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
return APIError::BAD_DATA_PACKET; 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 type = (((uint16_t) msg_data[0]) << 8) | msg_data[1];
uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3]; uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3];
if (data_len > msg_size - 4) { if (data_len > msg_size - 4) {
@@ -822,18 +831,12 @@ APIError APIPlaintextFrameHelper::init() {
state_ = State::DATA; state_ = State::DATA;
return APIError::OK; return APIError::OK;
} }
/// Not used for plaintext
APIError APIPlaintextFrameHelper::loop() { APIError APIPlaintextFrameHelper::loop() {
if (state_ != State::DATA) { if (state_ != State::DATA) {
return APIError::BAD_STATE; return APIError::BAD_STATE;
} }
if (!this->tx_buf_.empty()) { // Use base class implementation for buffer sending
APIError err = try_send_tx_buf_(); return APIFrameHelper::loop();
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
return err;
}
}
return APIError::OK; // Convert WOULD_BLOCK to OK to avoid connection termination
} }
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter /** 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) {} : message_type(type), offset(off), payload_size(size), padding(0) {}
}; };
enum class APIError : int { enum class APIError : uint16_t {
OK = 0, OK = 0,
WOULD_BLOCK = 1001, WOULD_BLOCK = 1001,
BAD_HANDSHAKE_PACKET_LEN = 1002, BAD_HANDSHAKE_PACKET_LEN = 1002,
@@ -74,7 +74,7 @@ class APIFrameHelper {
} }
virtual ~APIFrameHelper() = default; virtual ~APIFrameHelper() = default;
virtual APIError init() = 0; virtual APIError init() = 0;
virtual APIError loop() = 0; virtual APIError loop();
virtual APIError read_packet(ReadPacketBuffer *buffer) = 0; virtual APIError read_packet(ReadPacketBuffer *buffer) = 0;
bool can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); } bool can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); }
std::string getpeername() { return socket_->getpeername(); } std::string getpeername() { return socket_->getpeername(); }
@@ -125,38 +125,6 @@ class APIFrameHelper {
const uint8_t *current_data() const { return data.data() + offset; } 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 // Common implementation for writing raw data to socket
APIError write_raw_(const struct iovec *iov, int iovcnt); 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, 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); 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_header_padding_{0};
uint8_t frame_footer_size_{0}; uint8_t frame_footer_size_{0};
// 5 bytes total, 3 bytes padding
// 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;
// Common initialization for both plaintext and noise protocols // Common initialization for both plaintext and noise protocols
APIError init_common_(); APIError init_common_();
@@ -213,19 +207,28 @@ class APINoiseFrameHelper : public APIFrameHelper {
APIError init_handshake_(); APIError init_handshake_();
APIError check_handshake_finished_(); APIError check_handshake_finished_();
void send_explicit_handshake_reject_(const std::string &reason); 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: // Fixed-size header buffer for noise protocol:
// 1 byte for indicator + 2 bytes for message size (16-bit value, not varint) // 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 // 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_[3];
uint8_t rx_header_buf_len_ = 0; uint8_t rx_header_buf_len_ = 0;
// 4 bytes total, no padding
std::vector<uint8_t> prologue_;
std::shared_ptr<APINoiseContext> ctx_;
NoiseHandshakeState *handshake_{nullptr};
NoiseCipherState *send_cipher_{nullptr};
NoiseCipherState *recv_cipher_{nullptr};
NoiseProtocolId nid_;
}; };
#endif // USE_API_NOISE #endif // USE_API_NOISE
@@ -252,6 +255,12 @@ class APIPlaintextFrameHelper : public APIFrameHelper {
protected: protected:
APIError try_read_frame_(ParsedFrame *frame); 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: // Fixed-size header buffer for plaintext protocol:
// We now store the indicator byte + the two varints. // We now store the indicator byte + the two varints.
// To match noise protocol's maximum message size (UINT16_MAX = 65535), we need: // 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_[6]; // 1 byte indicator + 5 bytes for varints (3 for size + 2 for type)
uint8_t rx_header_buf_pos_ = 0; uint8_t rx_header_buf_pos_ = 0;
bool rx_header_parsed_ = false; bool rx_header_parsed_ = false;
uint16_t rx_header_parsed_type_ = 0; // 8 bytes total, no padding needed
uint16_t rx_header_parsed_len_ = 0;
}; };
#endif #endif

View File

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

View File

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

View File

@@ -47,6 +47,11 @@ void APIServer::setup() {
} }
#endif #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 this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections
if (this->socket_ == nullptr) { if (this->socket_ == nullptr) {
ESP_LOGW(TAG, "Could not create socket"); ESP_LOGW(TAG, "Could not create socket");
@@ -106,8 +111,6 @@ void APIServer::setup() {
} }
#endif #endif
this->last_connected_ = millis();
#ifdef USE_ESP32_CAMERA #ifdef USE_ESP32_CAMERA
if (esp32_camera::global_esp32_camera != nullptr && !esp32_camera::global_esp32_camera->is_internal()) { if (esp32_camera::global_esp32_camera != nullptr && !esp32_camera::global_esp32_camera->is_internal()) {
esp32_camera::global_esp32_camera->add_image_callback( esp32_camera::global_esp32_camera->add_image_callback(
@@ -121,6 +124,16 @@ void APIServer::setup() {
#endif #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() { void APIServer::loop() {
// Accept new clients only if the socket exists and has incoming connections // Accept new clients only if the socket exists and has incoming connections
if (this->socket_ && this->socket_->ready()) { 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); auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len);
if (!sock) if (!sock)
break; 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); auto *conn = new APIConnection(std::move(sock), this);
this->clients_.emplace_back(conn); this->clients_.emplace_back(conn);
conn->start(); 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 // Process clients and remove disconnected ones in a single pass
if (!this->clients_.empty()) { // Check network connectivity once for all clients
size_t client_index = 0; if (!network::is_connected()) {
while (client_index < this->clients_.size()) { // Network is down - disconnect all clients
auto &client = this->clients_[client_index]; for (auto &client : this->clients_) {
client->on_fatal_error();
if (client->remove_) { ESP_LOGW(TAG, "%s: Network down; disconnect", client->get_client_combined_info().c_str());
// 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());
// 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
}
} }
// Continue to process and clean up the clients below
} }
if (this->reboot_timeout_ != 0) { size_t client_index = 0;
const uint32_t now = millis(); while (client_index < this->clients_.size()) {
if (!this->is_connected()) { auto &client = this->clients_[client_index];
if (now - this->last_connected_ > this->reboot_timeout_) {
ESP_LOGE(TAG, "No client connected; rebooting"); if (!client->remove_) {
App.reboot(); // Common case: process active client
} client->loop();
this->status_set_warning(); client_index++;
} else { continue;
this->last_connected_ = now;
this->status_clear_warning();
} }
// Rare case: handle disconnection
this->client_disconnected_trigger_->trigger(client->client_info_, client->client_peername_);
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();
// 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: 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; 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 reboot_timeout_{300000};
uint32_t batch_delay_{100}; 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::vector<std::unique_ptr<APIConnection>> clients_;
std::string password_; std::string password_;
std::vector<uint8_t> shared_write_buffer_; // Shared proto write buffer for all connections std::vector<uint8_t> shared_write_buffer_; // Shared proto write buffer for all connections
std::vector<HomeAssistantStateSubscription> state_subs_; std::vector<HomeAssistantStateSubscription> state_subs_;
std::vector<UserServiceDescriptor *> user_services_; 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 #ifdef USE_API_NOISE
std::shared_ptr<APINoiseContext> noise_ctx_ = std::make_shared<APINoiseContext>(); 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); this->buffer_->insert(this->buffer_->end(), data, data + len);
} }
void encode_string(uint32_t field_id, const std::string &value, bool force = false) { 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) { 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); this->encode_string(field_id, reinterpret_cast<const char *>(data), len, force);
@@ -327,12 +327,15 @@ class ProtoWriteBuffer {
class ProtoMessage { class ProtoMessage {
public: public:
virtual ~ProtoMessage() = default; 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); 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 #ifdef HAS_PROTO_MESSAGE_DUMP
std::string dump() const; std::string dump() const;
virtual void dump_to(std::string &out) const = 0; virtual void dump_to(std::string &out) const = 0;
virtual const char *message_name() const { return "unknown"; }
#endif #endif
protected: protected:
@@ -377,6 +380,26 @@ class ProtoService {
// Send the buffer // Send the buffer
return this->send_buffer(buffer, message_type); 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 } // namespace api

View File

@@ -21,8 +21,8 @@ CONFIG_SCHEMA = cv.All(
@coroutine_with_priority(200.0) @coroutine_with_priority(200.0)
async def to_code(config): async def to_code(config):
if CORE.is_esp32 or CORE.is_libretiny: if CORE.is_esp32 or CORE.is_libretiny:
# https://github.com/esphome/AsyncTCP/blob/master/library.json # https://github.com/ESP32Async/AsyncTCP
cg.add_library("esphome/AsyncTCP-esphome", "2.1.4") cg.add_library("ESP32Async/AsyncTCP", "3.4.4")
elif CORE.is_esp8266: elif CORE.is_esp8266:
# https://github.com/esphome/ESPAsyncTCP # https://github.com/ESP32Async/ESPAsyncTCP
cg.add_library("esphome/ESPAsyncTCP-esphome", "2.0.0") 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) { bool AudioTransferBuffer::allocate_buffer_(size_t buffer_size) {
this->buffer_size_ = 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_); this->buffer_ = allocator.allocate(this->buffer_size_);
if (this->buffer_ == nullptr) { if (this->buffer_ == nullptr) {
@@ -101,7 +101,7 @@ bool AudioTransferBuffer::allocate_buffer_(size_t buffer_size) {
void AudioTransferBuffer::deallocate_buffer_() { void AudioTransferBuffer::deallocate_buffer_() {
if (this->buffer_ != nullptr) { if (this->buffer_ != nullptr) {
RAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE); RAMAllocator<uint8_t> allocator;
allocator.deallocate(this->buffer_, this->buffer_size_); allocator.deallocate(this->buffer_, this->buffer_size_);
this->buffer_ = nullptr; this->buffer_ = nullptr;
this->data_start_ = nullptr; this->data_start_ = nullptr;

View File

@@ -480,7 +480,11 @@ void BedJetHub::set_clock(uint8_t hour, uint8_t minute) {
/* Internal */ /* 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::update() { this->dispatch_status_(); }
void BedJetHub::dump_config() { void BedJetHub::dump_config() {

View File

@@ -83,7 +83,11 @@ void BedJetClimate::reset_state_() {
this->publish_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) { void BedJetClimate::control(const ClimateCall &call) {
ESP_LOGD(TAG, "Received BedJetClimate::control"); ESP_LOGD(TAG, "Received BedJetClimate::control");

View File

@@ -7,11 +7,13 @@
extern "C" { extern "C" {
#include "rtos_pub.h" #include "rtos_pub.h"
#include "spi.h" // rtos_pub.h must be included before the rest of the includes
#include "arm_arch.h" #include "arm_arch.h"
#include "general_dma_pub.h" #include "general_dma_pub.h"
#include "gpio_pub.h" #include "gpio_pub.h"
#include "icu_pub.h" #include "icu_pub.h"
#include "spi.h"
#undef SPI_DAT #undef SPI_DAT
#undef SPI_BASE #undef SPI_BASE
}; };
@@ -124,7 +126,7 @@ void BekenSPILEDStripLightOutput::setup() {
size_t buffer_size = this->get_buffer_size_(); size_t buffer_size = this->get_buffer_size_();
size_t dma_buffer_size = (buffer_size * 8) + (2 * 64); 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); this->buf_ = allocator.allocate(buffer_size);
if (this->buf_ == nullptr) { if (this->buf_ == nullptr) {
ESP_LOGE(TAG, "Cannot allocate LED buffer!"); 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) // turn on (after one-shot sensor automatically powers down)
uint8_t turn_on = BH1750_COMMAND_POWER_ON; uint8_t turn_on = BH1750_COMMAND_POWER_ON;
if (this->write(&turn_on, 1) != i2c::ERROR_OK) { if (this->write(&turn_on, 1) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Turning on BH1750 failed"); ESP_LOGW(TAG, "Power on failed");
f(NAN); f(NAN);
return; 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_hi = BH1750_COMMAND_MT_REG_HI | ((mtreg >> 5) & 0b111);
uint8_t mtreg_lo = BH1750_COMMAND_MT_REG_LO | ((mtreg >> 0) & 0b11111); 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) { 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; active_mtreg_ = 0;
f(NAN); f(NAN);
return; return;
@@ -88,7 +88,7 @@ void BH1750Sensor::read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function<
return; return;
} }
if (this->write(&cmd, 1) != i2c::ERROR_OK) { if (this->write(&cmd, 1) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Starting measurement for BH1750 failed"); ESP_LOGW(TAG, "Start measurement failed");
f(NAN); f(NAN);
return; 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]() { this->set_timeout("read", meas_time, [this, mode, mtreg, f]() {
uint16_t raw_value; uint16_t raw_value;
if (this->read(reinterpret_cast<uint8_t *>(&raw_value), 2) != i2c::ERROR_OK) { 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); f(NAN);
return; return;
} }
@@ -156,7 +156,7 @@ void BH1750Sensor::update() {
this->publish_state(NAN); this->publish_state(NAN);
return; 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->status_clear_warning();
this->publish_state(val); this->publish_state(val);
}); });

View File

@@ -60,8 +60,8 @@ from esphome.const import (
DEVICE_CLASS_WINDOW, DEVICE_CLASS_WINDOW,
) )
from esphome.core import CORE, coroutine_with_priority 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_generator import MockObjClass
from esphome.cpp_helpers import setup_entity
from esphome.util import Registry from esphome.util import Registry
CODEOWNERS = ["@esphome/core"] CODEOWNERS = ["@esphome/core"]
@@ -148,6 +148,7 @@ BinarySensorCondition = binary_sensor_ns.class_("BinarySensorCondition", Conditi
# Filters # Filters
Filter = binary_sensor_ns.class_("Filter") Filter = binary_sensor_ns.class_("Filter")
TimeoutFilter = binary_sensor_ns.class_("TimeoutFilter", Filter, cg.Component)
DelayedOnOffFilter = binary_sensor_ns.class_("DelayedOnOffFilter", Filter, cg.Component) DelayedOnOffFilter = binary_sensor_ns.class_("DelayedOnOffFilter", Filter, cg.Component)
DelayedOnFilter = binary_sensor_ns.class_("DelayedOnFilter", Filter, cg.Component) DelayedOnFilter = binary_sensor_ns.class_("DelayedOnFilter", Filter, cg.Component)
DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", 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) 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( @register_filter(
"delayed_on_off", "delayed_on_off",
DelayedOnOffFilter, DelayedOnOffFilter,
@@ -491,6 +505,9 @@ _BINARY_SENSOR_SCHEMA = (
) )
_BINARY_SENSOR_SCHEMA.add_extra(entity_duplicate_validator("binary_sensor"))
def binary_sensor_schema( def binary_sensor_schema(
class_: MockObjClass = cv.UNDEFINED, 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): 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: if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
cg.add(var.set_device_class(device_class)) 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) { optional<bool> DelayedOnOffFilter::new_value(bool value) {
if (value) { if (value) {
this->set_timeout("ON_OFF", this->on_delay_.value(), [this]() { this->output(true); }); this->set_timeout("ON_OFF", this->on_delay_.value(), [this]() { this->output(true); });

View File

@@ -16,7 +16,7 @@ class Filter {
public: public:
virtual optional<bool> new_value(bool value) = 0; virtual optional<bool> new_value(bool value) = 0;
void input(bool value); virtual void input(bool value);
void output(bool value); void output(bool value);
@@ -28,6 +28,16 @@ class Filter {
Deduplicator<bool> dedup_; 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 { class DelayedOnOffFilter : public Filter, public Component {
public: public:
optional<bool> new_value(bool value) override; optional<bool> new_value(bool value) override;

View File

@@ -11,7 +11,11 @@ namespace ble_client {
static const char *const TAG = "ble_rssi_sensor"; 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() { void BLEClientRSSISensor::dump_config() {
LOG_SENSOR("", "BLE Client RSSI Sensor", this); LOG_SENSOR("", "BLE Client RSSI Sensor", this);

View File

@@ -11,7 +11,11 @@ namespace ble_client {
static const char *const TAG = "ble_sensor"; 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() { void BLESensor::dump_config() {
LOG_SENSOR("", "BLE Sensor", this); LOG_SENSOR("", "BLE Sensor", this);

View File

@@ -14,7 +14,11 @@ static const char *const TAG = "ble_text_sensor";
static const std::string EMPTY = ""; 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() { void BLETextSensor::dump_config() {
LOG_TEXT_SENSOR("", "BLE Text Sensor", this); LOG_TEXT_SENSOR("", "BLE Text Sensor", this);

View File

@@ -26,10 +26,17 @@ class BluetoothConnection : public esp32_ble_client::BLEClientBase {
protected: protected:
friend class BluetoothProxy; 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_; 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 } // namespace bluetooth_proxy

View File

@@ -58,7 +58,7 @@ static std::vector<api::BluetoothLERawAdvertisement> &get_batch_buffer() {
return 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_) if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || !this->raw_advertisements_)
return false; 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 // Add new advertisements to the batch buffer
for (size_t i = 0; i < count; i++) { 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; uint8_t length = result.adv_data_len + result.scan_rsp_len;
batch_buffer.emplace_back(); batch_buffer.emplace_back();

View File

@@ -52,7 +52,7 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
public: public:
BluetoothProxy(); BluetoothProxy();
bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; 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 dump_config() override;
void setup() override; void setup() override;
void loop() 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); BluetoothConnection *get_connection_(uint64_t address, bool reserve);
bool active_; // Memory optimized layout for 32-bit systems
// Group 1: Pointers (4 bytes each, naturally aligned)
std::vector<BluetoothConnection *> connections_{};
api::APIConnection *api_connection_{nullptr}; 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}; bool raw_advertisements_{false};
// 2 bytes used, 2 bytes padding
}; };
extern BluetoothProxy *global_bluetooth_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) 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 // 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. // 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) { if (this->is_failed()) {
this->component_state_ &= ~COMPONENT_STATE_MASK; this->reset_to_construction_state();
this->component_state_ |= COMPONENT_STATE_CONSTRUCTION;
} }
if (!this->read_byte(BME280_REGISTER_CHIPID, &chip_id)) { if (!this->read_byte(BME280_REGISTER_CHIPID, &chip_id)) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,8 +22,8 @@ from esphome.const import (
CONF_YEAR, CONF_YEAR,
) )
from esphome.core import CORE, coroutine_with_priority 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_generator import MockObjClass
from esphome.cpp_helpers import setup_entity
CODEOWNERS = ["@rfdarter", "@jesserockz"] CODEOWNERS = ["@rfdarter", "@jesserockz"]
@@ -84,6 +84,8 @@ _DATETIME_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
).add_extra(_validate_time_present) ).add_extra(_validate_time_present)
_DATETIME_SCHEMA.add_extra(entity_duplicate_validator("datetime"))
def date_schema(class_: MockObjClass) -> cv.Schema: def date_schema(class_: MockObjClass) -> cv.Schema:
schema = cv.Schema( schema = cv.Schema(
@@ -133,7 +135,7 @@ def datetime_schema(class_: MockObjClass) -> cv.Schema:
async def setup_datetime_core_(var, config): 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: if (mqtt_id := config.get(CONF_MQTT_ID)) is not None:
mqtt_ = cg.new_Pvariable(mqtt_id, var) 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() { void DateEntity::publish_state() {
if (this->year_ == 0 || this->month_ == 0 || this->day_ == 0) { if (this->year_ == 0 || this->month_ == 0 || this->day_ == 0) {
this->has_state_ = false; this->set_has_state(false);
return; return;
} }
if (this->year_ < 1970 || this->year_ > 3000) { 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"); ESP_LOGE(TAG, "Year must be between 1970 and 3000");
return; return;
} }
if (this->month_ < 1 || this->month_ > 12) { 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"); ESP_LOGE(TAG, "Month must be between 1 and 12");
return; return;
} }
if (this->day_ > days_in_month(this->month_, this->year_)) { 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_); ESP_LOGE(TAG, "Day must be between 1 and %d for month %d", days_in_month(this->month_, this->year_), this->month_);
return; 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_); ESP_LOGD(TAG, "'%s': Sending date %d-%d-%d", this->get_name().c_str(), this->year_, this->month_, this->day_);
this->state_callback_.call(); this->state_callback_.call();
} }

View File

@@ -13,9 +13,6 @@ namespace datetime {
class DateTimeBase : public EntityBase { class DateTimeBase : public EntityBase {
public: 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; virtual ESPTime state_as_esptime() const = 0;
void add_on_state_callback(std::function<void()> &&callback) { this->state_callback_.add(std::move(callback)); } 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 #ifdef USE_TIME
time::RealTimeClock *rtc_; time::RealTimeClock *rtc_;
#endif #endif
bool has_state_{false};
}; };
#ifdef USE_TIME #ifdef USE_TIME

View File

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

View File

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

View File

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

View File

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

View File

@@ -94,6 +94,13 @@ COMPILER_OPTIMIZATIONS = {
"SIZE": "CONFIG_COMPILER_OPTIMIZATION_SIZE", "SIZE": "CONFIG_COMPILER_OPTIMIZATION_SIZE",
} }
ARDUINO_ALLOWED_VARIANTS = [
VARIANT_ESP32,
VARIANT_ESP32C3,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
]
def get_cpu_frequencies(*frequencies): def get_cpu_frequencies(*frequencies):
return [str(x) + "MHZ" for x in frequencies] return [str(x) + "MHZ" for x in frequencies]
@@ -125,6 +132,8 @@ def set_core_data(config):
choices = CPU_FREQUENCIES[variant] choices = CPU_FREQUENCIES[variant]
if "160MHZ" in choices: if "160MHZ" in choices:
cpu_frequency = "160MHZ" cpu_frequency = "160MHZ"
elif "360MHZ" in choices:
cpu_frequency = "360MHZ"
else: else:
cpu_frequency = choices[-1] cpu_frequency = choices[-1]
config[CONF_CPU_FREQUENCY] = cpu_frequency config[CONF_CPU_FREQUENCY] = cpu_frequency
@@ -143,12 +152,17 @@ def set_core_data(config):
CORE.data[KEY_ESP32][KEY_COMPONENTS] = {} CORE.data[KEY_ESP32][KEY_COMPONENTS] = {}
elif conf[CONF_TYPE] == FRAMEWORK_ARDUINO: elif conf[CONF_TYPE] == FRAMEWORK_ARDUINO:
CORE.data[KEY_CORE][KEY_TARGET_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( CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse(
config[CONF_FRAMEWORK][CONF_VERSION] config[CONF_FRAMEWORK][CONF_VERSION]
) )
CORE.data[KEY_ESP32][KEY_BOARD] = config[CONF_BOARD] 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] = {} CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES] = {}
return config 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: def _format_framework_arduino_version(ver: cv.Version) -> str:
# format the given arduino (https://github.com/espressif/arduino-esp32/releases) version to # format the given arduino (https://github.com/espressif/arduino-esp32/releases) version to
# a PIO platformio/framework-arduinoespressif32 value # a PIO pioarduino/framework-arduinoespressif32 value
# List of package versions: https://api.registry.platformio.org/v3/packages/platformio/tool/framework-arduinoespressif32 return f"pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/{str(ver)}/esp32-{str(ver)}.zip"
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"
def _format_framework_espidf_version( def _format_framework_espidf_version(
@@ -305,12 +316,10 @@ def _format_framework_espidf_version(
# The default/recommended arduino framework version # The default/recommended arduino framework version
# - https://github.com/espressif/arduino-esp32/releases # - https://github.com/espressif/arduino-esp32/releases
# - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-arduinoespressif32 RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 1, 3)
RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(2, 0, 5) # The platform-espressif32 version to use for arduino frameworks
# The platformio/espressif32 version to use for arduino frameworks # - https://github.com/pioarduino/platform-espressif32/releases
# - https://github.com/platformio/platform-espressif32/releases ARDUINO_PLATFORM_VERSION = cv.Version(53, 3, 13)
# - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32
ARDUINO_PLATFORM_VERSION = cv.Version(5, 4, 0)
# The default/recommended esp-idf framework version # The default/recommended esp-idf framework version
# - https://github.com/espressif/esp-idf/releases # - https://github.com/espressif/esp-idf/releases
@@ -353,8 +362,8 @@ SUPPORTED_PIOARDUINO_ESP_IDF_5X = [
def _arduino_check_versions(value): def _arduino_check_versions(value):
value = value.copy() value = value.copy()
lookups = { lookups = {
"dev": (cv.Version(2, 1, 0), "https://github.com/espressif/arduino-esp32.git"), "dev": (cv.Version(3, 1, 3), "https://github.com/espressif/arduino-esp32.git"),
"latest": (cv.Version(2, 0, 9), None), "latest": (cv.Version(3, 1, 3), None),
"recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, 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)) 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: if version != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION:
_LOGGER.warning( _LOGGER.warning(
"The selected Arduino framework version is not the recommended one. " "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_ESP_IDF = "esp-idf"
FRAMEWORK_ARDUINO = "arduino" FRAMEWORK_ARDUINO = "arduino"
FRAMEWORK_SCHEMA = cv.typed_schema( FRAMEWORK_SCHEMA = cv.typed_schema(
@@ -627,7 +655,6 @@ FRAMEWORK_SCHEMA = cv.typed_schema(
}, },
lower=True, lower=True,
space="-", space="-",
default_type=FRAMEWORK_ARDUINO,
) )
@@ -654,10 +681,11 @@ CONFIG_SCHEMA = cv.All(
), ),
cv.Optional(CONF_PARTITIONS): cv.file_, cv.Optional(CONF_PARTITIONS): cv.file_,
cv.Optional(CONF_VARIANT): cv.one_of(*VARIANTS, upper=True), 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, _detect_variant,
_set_default_framework,
set_core_data, set_core_data,
) )
@@ -668,6 +696,7 @@ FINAL_VALIDATE_SCHEMA = cv.Schema(final_validate)
async def to_code(config): async def to_code(config):
cg.add_platformio_option("board", config[CONF_BOARD]) cg.add_platformio_option("board", config[CONF_BOARD])
cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE]) cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE])
cg.set_cpp_standard("gnu++17")
cg.add_build_flag("-DUSE_ESP32") cg.add_build_flag("-DUSE_ESP32")
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{config[CONF_VARIANT]}") 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_platformio_option("framework", "arduino")
cg.add_build_flag("-DUSE_ARDUINO") cg.add_build_flag("-DUSE_ARDUINO")
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO") cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO")
cg.add_platformio_option( cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]])
"platform_packages",
[f"platformio/framework-arduinoespressif32@{conf[CONF_SOURCE]}"],
)
if CONF_PARTITIONS in config: if CONF_PARTITIONS in config:
cg.add_platformio_option("board_build.partitions", config[CONF_PARTITIONS]) cg.add_platformio_option("board_build.partitions", config[CONF_PARTITIONS])

View File

@@ -1,8 +1,10 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
#include "ble.h" #include "ble.h"
#include "ble_event_pool.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <esp_bt.h> #include <esp_bt.h>
@@ -23,9 +25,6 @@ namespace esp32_ble {
static const char *const TAG = "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() { void ESP32BLE::setup() {
global_ble = this; global_ble = this;
ESP_LOGCONFIG(TAG, "Running setup"); ESP_LOGCONFIG(TAG, "Running setup");
@@ -304,82 +303,191 @@ void ESP32BLE::loop() {
BLEEvent *ble_event = this->ble_events_.pop(); BLEEvent *ble_event = this->ble_events_.pop();
while (ble_event != nullptr) { while (ble_event != nullptr) {
switch (ble_event->type_) { switch (ble_event->type_) {
case BLEEvent::GATTS: case BLEEvent::GATTS: {
this->real_gatts_event_handler_(ble_event->event_.gatts.gatts_event, ble_event->event_.gatts.gatts_if, esp_gatts_cb_event_t event = ble_event->event_.gatts.gatts_event;
&ble_event->event_.gatts.gatts_param); 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; break;
case BLEEvent::GATTC: }
this->real_gattc_event_handler_(ble_event->event_.gattc.gattc_event, ble_event->event_.gattc.gattc_if, case BLEEvent::GATTC: {
&ble_event->event_.gattc.gattc_param); 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; 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; break;
}
default: default:
break; break;
} }
ble_event->~BLEEvent(); // Return the event to the pool
EVENT_ALLOCATOR.deallocate(ble_event, 1); this->ble_event_pool_.release(ble_event);
ble_event = this->ble_events_.pop(); ble_event = this->ble_events_.pop();
} }
if (this->advertising_ != nullptr) { if (this->advertising_ != nullptr) {
this->advertising_->loop(); 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) { // Helper function to load new event data based on type
BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1); void load_ble_event(BLEEvent *event, esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
if (new_event == nullptr) { event->load_gap_event(e, p);
// Memory too fragmented to allocate new event. Can only drop it until memory comes back }
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; 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) { // Load new event data (replaces previous event)
ESP_LOGV(TAG, "(BLE) gap_event_handler - %d", event); load_ble_event(event, args...);
for (auto *gap_handler : this->gap_event_handlers_) {
gap_handler->gap_event_handler(event, param); // 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, void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
esp_ble_gatts_cb_param_t *param) { esp_ble_gatts_cb_param_t *param) {
BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1); enqueue_ble_event(event, gatts_if, param);
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);
}
} }
void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) { esp_ble_gattc_cb_param_t *param) {
BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1); enqueue_ble_event(event, gattc_if, param);
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);
}
} }
float ESP32BLE::get_setup_priority() const { return setup_priority::BLUETOOTH; } float ESP32BLE::get_setup_priority() const { return setup_priority::BLUETOOTH; }
@@ -409,13 +517,12 @@ void ESP32BLE::dump_config() {
break; break;
} }
ESP_LOGCONFIG(TAG, ESP_LOGCONFIG(TAG,
"ESP32 BLE:\n" "BLE:\n"
" MAC address: %02X:%02X:%02X:%02X:%02X:%02X\n" " MAC address: %s\n"
" IO Capability: %s", " IO Capability: %s",
mac_address[0], mac_address[1], mac_address[2], mac_address[3], mac_address[4], mac_address[5], format_mac_address_pretty(mac_address).c_str(), io_capability_s);
io_capability_s);
} else { } 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_advertising.h"
#include "ble_uuid.h" #include "ble_uuid.h"
#include "ble_scan_result.h"
#include <functional> #include <functional>
@@ -11,6 +12,7 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "ble_event.h" #include "ble_event.h"
#include "ble_event_pool.h"
#include "queue.h" #include "queue.h"
#ifdef USE_ESP32 #ifdef USE_ESP32
@@ -22,6 +24,16 @@
namespace esphome { namespace esphome {
namespace esp32_ble { 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); uint64_t ble_addr_to_uint64(const esp_bd_addr_t address);
// NOLINTNEXTLINE(modernize-use-using) // 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; 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 { class GATTcEventHandler {
public: public:
virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, 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 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_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_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_gatts_event_handler(GATTsEventHandler *handler) { this->gatts_event_handlers_.push_back(handler); }
void register_ble_status_event_handler(BLEStatusEventHandler *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 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); 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_setup_();
bool ble_dismantle_(); bool ble_dismantle_();
bool ble_pre_setup_(); bool ble_pre_setup_();
void advertising_init_(); void advertising_init_();
private:
template<typename... Args> friend void enqueue_ble_event(Args... args);
std::vector<GAPEventHandler *> gap_event_handlers_; std::vector<GAPEventHandler *> gap_event_handlers_;
std::vector<GAPScanEventHandler *> gap_scan_event_handlers_;
std::vector<GATTcEventHandler *> gattc_event_handlers_; std::vector<GATTcEventHandler *> gattc_event_handlers_;
std::vector<GATTsEventHandler *> gatts_event_handlers_; std::vector<GATTsEventHandler *> gatts_event_handlers_;
std::vector<BLEStatusEventHandler *> ble_status_event_handlers_; std::vector<BLEStatusEventHandler *> ble_status_event_handlers_;
BLEComponentState state_{BLE_COMPONENT_STATE_OFF}; 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_{}; BLEAdvertising *advertising_{};
esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE};
uint32_t advertising_cycle_time_{}; uint32_t advertising_cycle_time_{};

View File

@@ -2,92 +2,399 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
#include <cstddef> // for offsetof
#include <vector> #include <vector>
#include <esp_gap_ble_api.h> #include <esp_gap_ble_api.h>
#include <esp_gattc_api.h> #include <esp_gattc_api.h>
#include <esp_gatts_api.h> #include <esp_gatts_api.h>
#include "ble_scan_result.h"
namespace esphome { namespace esphome {
namespace esp32_ble { 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(). // 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 { class BLEEvent {
public: 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) // NOLINTNEXTLINE(readability-identifier-naming)
enum ble_event_t : uint8_t { enum ble_event_t : uint8_t {
GAP, GAP,
GATTC, GATTC,
GATTS, 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 esp32_ble
} // namespace esphome } // 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 #ifdef USE_ESP32
#include <mutex> #include <atomic>
#include <queue> #include <cstddef>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
/* /*
* BLE events come in from a separate Task (thread) in the ESP32 stack. Rather * 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 * than using mutex-based locking, this lock-free queue allows the BLE
* events will simply be placed on a semaphore guarded queue. The next time the * task to enqueue events without blocking. The main loop() then processes
* component runs loop(), these events are popped off the queue and handed at * these events at a safer time.
* this 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 esphome {
namespace esp32_ble { namespace esp32_ble {
template<class T> class Queue { template<class T, uint8_t SIZE> class LockFreeQueue {
public: public:
Queue() { m_ = xSemaphoreCreateMutex(); } LockFreeQueue() : head_(0), tail_(0), dropped_count_(0) {}
void push(T *element) { bool push(T *element) {
if (element == nullptr) if (element == nullptr)
return; return false;
// It is not called from main loop. Thus it won't block main thread.
xSemaphoreTake(m_, portMAX_DELAY); uint8_t current_tail = tail_.load(std::memory_order_relaxed);
q_.push(element); uint8_t next_tail = (current_tail + 1) % SIZE;
xSemaphoreGive(m_);
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 *pop() {
T *element = nullptr; uint8_t current_head = head_.load(std::memory_order_relaxed);
if (xSemaphoreTake(m_, 5L / portTICK_PERIOD_MS)) { if (current_head == tail_.load(std::memory_order_acquire)) {
if (!q_.empty()) { return nullptr; // Empty
element = q_.front();
q_.pop();
}
xSemaphoreGive(m_);
} }
T *element = buffer_[current_head];
head_.store((current_head + 1) % SIZE, std::memory_order_release);
return element; 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: protected:
std::queue<T *> q_; T *buffer_[SIZE];
SemaphoreHandle_t m_; // 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 } // namespace esp32_ble

View File

@@ -22,6 +22,16 @@ void BLEClientBase::setup() {
this->connection_index_ = connection_index++; 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() { void BLEClientBase::loop() {
if (!esp32_ble::global_ble->is_active()) { if (!esp32_ble::global_ble->is_active()) {
this->set_state(espbt::ClientState::INIT); this->set_state(espbt::ClientState::INIT);
@@ -37,9 +47,14 @@ void BLEClientBase::loop() {
} }
// READY_TO_CONNECT means we have discovered the device // READY_TO_CONNECT means we have discovered the device
// and the scanner has been stopped by the tracker. // 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(); 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; } 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; } 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: protected:
int gattc_if_; // Memory optimized layout for 32-bit systems
esp_bd_addr_t remote_bda_; // Group 1: 8-byte types
esp_ble_addr_type_t remote_addr_type_{BLE_ADDR_TYPE_PUBLIC};
uint16_t conn_id_{UNSET_CONN_ID};
uint64_t address_{0}; uint64_t address_{0};
bool auto_connect_{false};
// Group 2: Container types (grouped for memory optimization)
std::string address_str_{}; 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_; std::vector<BLEService *> services_;
// Group 3: 4-byte types
int gattc_if_;
esp_gatt_status_t status_{ESP_GATT_OK}; 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); 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]) parent = await cg.get_variable(config[esp32_ble.CONF_BLE_ID])
cg.add(parent.register_gap_event_handler(var)) 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_gattc_event_handler(var))
cg.add(parent.register_ble_status_event_handler(var)) cg.add(parent.register_ble_status_event_handler(var))
cg.add(var.set_parent(parent)) 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"); ESP_LOGE(TAG, "BLE Tracker was marked failed by ESP32BLE");
return; return;
} }
ExternalRAMAllocator<esp_ble_gap_cb_param_t::ble_scan_result_evt_param> allocator( RAMAllocator<BLEScanResult> allocator;
ExternalRAMAllocator<esp_ble_gap_cb_param_t::ble_scan_result_evt_param>::ALLOW_FAILURE); this->scan_ring_buffer_ = allocator.allocate(SCAN_RESULT_BUFFER_SIZE);
this->scan_result_buffer_ = allocator.allocate(ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE);
if (this->scan_result_buffer_ == nullptr) { if (this->scan_ring_buffer_ == nullptr) {
ESP_LOGE(TAG, "Could not allocate buffer for BLE Tracker!"); ESP_LOGE(TAG, "Could not allocate ring buffer for BLE Tracker!");
this->mark_failed(); this->mark_failed();
} }
global_esp32_ble_tracker = this; global_esp32_ble_tracker = this;
this->scan_result_lock_ = xSemaphoreCreateMutex();
#ifdef USE_OTA #ifdef USE_OTA
ota::get_global_ota_callback()->add_on_state_callback( ota::get_global_ota_callback()->add_on_state_callback(
@@ -120,27 +118,31 @@ void ESP32BLETracker::loop() {
} }
bool promote_to_connecting = discovered && !searching && !connecting; bool promote_to_connecting = discovered && !searching && !connecting;
if (this->scanner_state_ == ScannerState::RUNNING && // Process scan results from lock-free SPSC ring buffer
this->scan_result_index_ && // if it looks like we have a scan result we will take the lock // Consumer side: This runs in the main loop thread
xSemaphoreTake(this->scan_result_lock_, 0)) { if (this->scanner_state_ == ScannerState::RUNNING) {
uint32_t index = this->scan_result_index_; // Load our own index with relaxed ordering (we're the only writer)
if (index >= ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) { uint8_t read_idx = this->ring_read_index_.load(std::memory_order_relaxed);
ESP_LOGW(TAG, "Too many BLE events to process. Some devices may not show up.");
}
if (this->raw_advertisements_) { // Load producer's index with acquire to see their latest writes
for (auto *listener : this->listeners_) { uint8_t write_idx = this->ring_write_index_.load(std::memory_order_acquire);
listener->parse_devices(this->scan_result_buffer_, this->scan_result_index_);
}
for (auto *client : this->clients_) {
client->parse_devices(this->scan_result_buffer_, this->scan_result_index_);
}
}
if (this->parse_advertisements_) { while (read_idx != write_idx) {
for (size_t i = 0; i < index; i++) { // 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(&scan_result, 1);
}
for (auto *client : this->clients_) {
client->parse_devices(&scan_result, 1);
}
}
if (this->parse_advertisements_) {
ESPBTDevice device; ESPBTDevice device;
device.parse_scan_rst(this->scan_result_buffer_[i]); device.parse_scan_rst(scan_result);
bool found = false; bool found = false;
for (auto *listener : this->listeners_) { for (auto *listener : this->listeners_) {
@@ -161,9 +163,19 @@ void ESP32BLETracker::loop() {
this->print_bt_device_info(device); 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) { if (this->scanner_state_ == ScannerState::STOPPED) {
this->end_of_scan_(); // Change state to IDLE 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) { void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
switch (event) { 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: case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
this->gap_scan_set_param_complete_(param->scan_param_cmpl); this->gap_scan_set_param_complete_(param->scan_param_cmpl);
break; break;
@@ -385,11 +394,57 @@ void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_ga
default: default:
break; break;
} }
// Forward all events to clients (scan results are handled separately via gap_scan_event_handler)
for (auto *client : this->clients_) { for (auto *client : this->clients_) {
client->gap_event_handler(event, param); 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) { 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); ESP_LOGV(TAG, "gap_scan_set_param_complete - status %d", param.status);
if (param.status == ESP_BT_STATUS_DONE) { 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); 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, void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) { esp_ble_gattc_cb_param_t *param) {
for (auto *client : this->clients_) { for (auto *client : this->clients_) {
@@ -494,13 +521,16 @@ optional<ESPBLEiBeacon> ESPBLEiBeacon::from_manufacturer_data(const ServiceData
return ESPBLEiBeacon(data.data.data()); return ESPBLEiBeacon(data.data.data());
} }
void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param &param) { void ESPBTDevice::parse_scan_rst(const BLEScanResult &scan_result) {
this->scan_result_ = param; this->scan_result_ = &scan_result;
for (uint8_t i = 0; i < ESP_BD_ADDR_LEN; i++) for (uint8_t i = 0; i < ESP_BD_ADDR_LEN; i++)
this->address_[i] = param.bda[i]; this->address_[i] = scan_result.bda[i];
this->address_type_ = param.ble_addr_type; this->address_type_ = static_cast<esp_ble_addr_type_t>(scan_result.ble_addr_type);
this->rssi_ = param.rssi; this->rssi_ = scan_result.rssi;
this->parse_adv_(param);
// 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 #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
ESP_LOGVV(TAG, "Parse Result:"); 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, " 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 #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; 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) { while (offset + 2 < len) {
const uint8_t field_length = payload[offset++]; // First byte is length of adv record 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 "esphome/core/helpers.h"
#include <array> #include <array>
#include <atomic>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -62,7 +63,7 @@ class ESPBLEiBeacon {
class ESPBTDevice { class ESPBTDevice {
public: 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; std::string address_str() const;
@@ -84,7 +85,8 @@ class ESPBTDevice {
const std::vector<ServiceData> &get_service_datas() const { return service_datas_; } 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; bool resolve_irk(const uint8_t *irk) const;
@@ -98,7 +100,7 @@ class ESPBTDevice {
} }
protected: 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_{ esp_bd_addr_t address_{
0, 0,
@@ -112,7 +114,7 @@ class ESPBTDevice {
std::vector<ESPBTUUID> service_uuids_{}; std::vector<ESPBTUUID> service_uuids_{};
std::vector<ServiceData> manufacturer_datas_{}; std::vector<ServiceData> manufacturer_datas_{};
std::vector<ServiceData> service_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; class ESP32BLETracker;
@@ -121,9 +123,7 @@ class ESPBTDeviceListener {
public: public:
virtual void on_scan_end() {} virtual void on_scan_end() {}
virtual bool parse_device(const ESPBTDevice &device) = 0; 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) { virtual bool parse_devices(const BLEScanResult *scan_results, size_t count) { return false; };
return false;
};
virtual AdvertisementParserType get_advertisement_parser_type() { virtual AdvertisementParserType get_advertisement_parser_type() {
return AdvertisementParserType::PARSED_ADVERTISEMENTS; return AdvertisementParserType::PARSED_ADVERTISEMENTS;
}; };
@@ -133,7 +133,7 @@ class ESPBTDeviceListener {
ESP32BLETracker *parent_{nullptr}; ESP32BLETracker *parent_{nullptr};
}; };
enum class ClientState { enum class ClientState : uint8_t {
// Connection is allocated // Connection is allocated
INIT, INIT,
// Client is disconnecting // Client is disconnecting
@@ -169,7 +169,7 @@ enum class ScannerState {
STOPPED, STOPPED,
}; };
enum class ConnectionType { enum class ConnectionType : uint8_t {
// The default connection type, we hold all the services in ram // The default connection type, we hold all the services in ram
// for the duration of the connection. // for the duration of the connection.
V1, V1,
@@ -197,19 +197,24 @@ class ESPBTClient : public ESPBTDeviceListener {
} }
} }
ClientState state() const { return state_; } ClientState state() const { return state_; }
int app_id;
// Memory optimized layout
uint8_t app_id; // App IDs are small integers assigned sequentially
protected: protected:
// Group 1: 1-byte types
ClientState state_{ClientState::INIT}; ClientState state_{ClientState::INIT};
// want_disconnect_ is set to true when a disconnect is requested // want_disconnect_ is set to true when a disconnect is requested
// while the client is connecting. This is used to disconnect the // while the client is connecting. This is used to disconnect the
// client as soon as we get the connection id (conn_id_) from the // client as soon as we get the connection id (conn_id_) from the
// ESP_GATTC_OPEN_EVT event. // ESP_GATTC_OPEN_EVT event.
bool want_disconnect_{false}; bool want_disconnect_{false};
// 2 bytes used, 2 bytes padding
}; };
class ESP32BLETracker : public Component, class ESP32BLETracker : public Component,
public GAPEventHandler, public GAPEventHandler,
public GAPScanEventHandler,
public GATTcEventHandler, public GATTcEventHandler,
public BLEStatusEventHandler, public BLEStatusEventHandler,
public Parented<ESP32BLE> { 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, void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override; 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_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 ble_before_disabled_event_handler() override;
void add_scanner_state_callback(std::function<void(ScannerState)> &&callback) { 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. /// Called to set the scanner state. Will also call callbacks to let listeners know when state is changed.
void set_scanner_state_(ScannerState state); 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 /// Vector of addresses that have already been printed in print_bt_device_info
std::vector<uint64_t> already_discovered_; std::vector<uint64_t> already_discovered_;
@@ -285,14 +291,16 @@ class ESP32BLETracker : public Component,
bool ble_was_disabled_{true}; bool ble_was_disabled_{true};
bool raw_advertisements_{false}; bool raw_advertisements_{false};
bool parse_advertisements_{false}; bool parse_advertisements_{false};
SemaphoreHandle_t scan_result_lock_;
size_t scan_result_index_{0}; // Lock-free Single-Producer Single-Consumer (SPSC) ring buffer for scan results
#ifdef USE_PSRAM // Producer: ESP-IDF Bluetooth stack callback (gap_scan_event_handler)
const static u_int8_t SCAN_RESULT_BUFFER_SIZE = 32; // Consumer: ESPHome main loop (loop() method)
#else // This design ensures zero blocking in the BT callback and prevents scan result loss
const static u_int8_t SCAN_RESULT_BUFFER_SIZE = 20; BLEScanResult *scan_ring_buffer_;
#endif // USE_PSRAM std::atomic<uint8_t> ring_write_index_{0}; // Written only by BT callback (producer)
esp_ble_gap_cb_param_t::ble_scan_result_evt_param *scan_result_buffer_; 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_start_failed_{ESP_BT_STATUS_SUCCESS};
esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS}; esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS};
int connecting_{0}; int connecting_{0};

View File

@@ -1,5 +1,6 @@
from esphome import automation, pins from esphome import automation, pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import i2c
from esphome.components.esp32 import add_idf_component from esphome.components.esp32 import add_idf_component
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
@@ -7,6 +8,7 @@ from esphome.const import (
CONF_CONTRAST, CONF_CONTRAST,
CONF_DATA_PINS, CONF_DATA_PINS,
CONF_FREQUENCY, CONF_FREQUENCY,
CONF_I2C_ID,
CONF_ID, CONF_ID,
CONF_PIN, CONF_PIN,
CONF_RESET_PIN, CONF_RESET_PIN,
@@ -17,7 +19,7 @@ from esphome.const import (
CONF_VSYNC_PIN, CONF_VSYNC_PIN,
) )
from esphome.core import CORE from esphome.core import CORE
from esphome.cpp_helpers import setup_entity from esphome.core.entity_helpers import setup_entity
DEPENDENCIES = ["esp32"] DEPENDENCIES = ["esp32"]
@@ -149,93 +151,104 @@ CONF_ON_IMAGE = "on_image"
camera_range_param = cv.int_range(min=-2, max=2) camera_range_param = cv.int_range(min=-2, max=2)
CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( CONFIG_SCHEMA = cv.All(
{ cv.ENTITY_BASE_SCHEMA.extend(
cv.GenerateID(): cv.declare_id(ESP32Camera), {
# pin assignment cv.GenerateID(): cv.declare_id(ESP32Camera),
cv.Required(CONF_DATA_PINS): cv.All( # pin assignment
[pins.internal_gpio_input_pin_number], cv.Length(min=8, max=8) cv.Required(CONF_DATA_PINS): cv.All(
), [pins.internal_gpio_input_pin_number], cv.Length(min=8, max=8)
cv.Required(CONF_VSYNC_PIN): pins.internal_gpio_input_pin_number, ),
cv.Required(CONF_HREF_PIN): pins.internal_gpio_input_pin_number, cv.Required(CONF_VSYNC_PIN): pins.internal_gpio_input_pin_number,
cv.Required(CONF_PIXEL_CLOCK_PIN): pins.internal_gpio_input_pin_number, cv.Required(CONF_HREF_PIN): pins.internal_gpio_input_pin_number,
cv.Required(CONF_EXTERNAL_CLOCK): cv.Schema( cv.Required(CONF_PIXEL_CLOCK_PIN): pins.internal_gpio_input_pin_number,
{ cv.Required(CONF_EXTERNAL_CLOCK): cv.Schema(
cv.Required(CONF_PIN): pins.internal_gpio_input_pin_number, {
cv.Optional(CONF_FREQUENCY, default="20MHz"): cv.All( cv.Required(CONF_PIN): pins.internal_gpio_input_pin_number,
cv.frequency, cv.Range(min=8e6, max=20e6) cv.Optional(CONF_FREQUENCY, default="20MHz"): cv.All(
), cv.frequency, cv.Range(min=8e6, max=20e6)
} ),
), }
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.Required(CONF_SDA): pins.internal_gpio_output_pin_number,
} cv.Required(CONF_SCL): pins.internal_gpio_output_pin_number,
), }
cv.Optional(CONF_RESET_PIN): pins.internal_gpio_output_pin_number, ),
cv.Optional(CONF_POWER_DOWN_PIN): pins.internal_gpio_output_pin_number, cv.Optional(CONF_I2C_ID): cv.Any(
# image cv.use_id(i2c.InternalI2CBus),
cv.Optional(CONF_RESOLUTION, default="640X480"): cv.enum( msg="I2C bus must be an internal ESP32 I2C bus",
FRAME_SIZES, upper=True ),
), cv.Optional(CONF_RESET_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_JPEG_QUALITY, default=10): cv.int_range(min=6, max=63), cv.Optional(CONF_POWER_DOWN_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_CONTRAST, default=0): camera_range_param, # image
cv.Optional(CONF_BRIGHTNESS, default=0): camera_range_param, cv.Optional(CONF_RESOLUTION, default="640X480"): cv.enum(
cv.Optional(CONF_SATURATION, default=0): camera_range_param, FRAME_SIZES, upper=True
cv.Optional(CONF_VERTICAL_FLIP, default=True): cv.boolean, ),
cv.Optional(CONF_HORIZONTAL_MIRROR, default=True): cv.boolean, cv.Optional(CONF_JPEG_QUALITY, default=10): cv.int_range(min=6, max=63),
cv.Optional(CONF_SPECIAL_EFFECT, default="NONE"): cv.enum( cv.Optional(CONF_CONTRAST, default=0): camera_range_param,
ENUM_SPECIAL_EFFECT, upper=True cv.Optional(CONF_BRIGHTNESS, default=0): camera_range_param,
), cv.Optional(CONF_SATURATION, default=0): camera_range_param,
# exposure cv.Optional(CONF_VERTICAL_FLIP, default=True): cv.boolean,
cv.Optional(CONF_AGC_MODE, default="AUTO"): cv.enum( cv.Optional(CONF_HORIZONTAL_MIRROR, default=True): cv.boolean,
ENUM_GAIN_CONTROL_MODE, upper=True cv.Optional(CONF_SPECIAL_EFFECT, default="NONE"): cv.enum(
), ENUM_SPECIAL_EFFECT, upper=True
cv.Optional(CONF_AEC2, default=False): cv.boolean, ),
cv.Optional(CONF_AE_LEVEL, default=0): camera_range_param, # exposure
cv.Optional(CONF_AEC_VALUE, default=300): cv.int_range(min=0, max=1200), cv.Optional(CONF_AGC_MODE, default="AUTO"): cv.enum(
# gains ENUM_GAIN_CONTROL_MODE, upper=True
cv.Optional(CONF_AEC_MODE, default="AUTO"): cv.enum( ),
ENUM_GAIN_CONTROL_MODE, upper=True cv.Optional(CONF_AEC2, default=False): cv.boolean,
), cv.Optional(CONF_AE_LEVEL, default=0): camera_range_param,
cv.Optional(CONF_AGC_VALUE, default=0): cv.int_range(min=0, max=30), cv.Optional(CONF_AEC_VALUE, default=300): cv.int_range(min=0, max=1200),
cv.Optional(CONF_AGC_GAIN_CEILING, default="2X"): cv.enum( # gains
ENUM_GAIN_CEILING, upper=True cv.Optional(CONF_AEC_MODE, default="AUTO"): cv.enum(
), ENUM_GAIN_CONTROL_MODE, upper=True
# white balance ),
cv.Optional(CONF_WB_MODE, default="AUTO"): cv.enum(ENUM_WB_MODE, upper=True), cv.Optional(CONF_AGC_VALUE, default=0): cv.int_range(min=0, max=30),
# test pattern cv.Optional(CONF_AGC_GAIN_CEILING, default="2X"): cv.enum(
cv.Optional(CONF_TEST_PATTERN, default=False): cv.boolean, ENUM_GAIN_CEILING, upper=True
# framerates ),
cv.Optional(CONF_MAX_FRAMERATE, default="10 fps"): cv.All( # white balance
cv.framerate, cv.Range(min=0, min_included=False, max=60) cv.Optional(CONF_WB_MODE, default="AUTO"): cv.enum(
), ENUM_WB_MODE, upper=True
cv.Optional(CONF_IDLE_FRAMERATE, default="0.1 fps"): cv.All( ),
cv.framerate, cv.Range(min=0, max=1) # test pattern
), cv.Optional(CONF_TEST_PATTERN, default=False): cv.boolean,
cv.Optional(CONF_FRAME_BUFFER_COUNT, default=1): cv.int_range(min=1, max=2), # framerates
cv.Optional(CONF_ON_STREAM_START): automation.validate_automation( cv.Optional(CONF_MAX_FRAMERATE, default="10 fps"): cv.All(
{ cv.framerate, cv.Range(min=0, min_included=False, max=60)
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( ),
ESP32CameraStreamStartTrigger cv.Optional(CONF_IDLE_FRAMERATE, default="0.1 fps"): cv.All(
), cv.framerate, cv.Range(min=0, max=1)
} ),
), cv.Optional(CONF_FRAME_BUFFER_COUNT, default=1): cv.int_range(min=1, max=2),
cv.Optional(CONF_ON_STREAM_STOP): automation.validate_automation( cv.Optional(CONF_ON_STREAM_START): automation.validate_automation(
{ {
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
ESP32CameraStreamStopTrigger ESP32CameraStreamStartTrigger
), ),
} }
), ),
cv.Optional(CONF_ON_IMAGE): automation.validate_automation( cv.Optional(CONF_ON_STREAM_STOP): automation.validate_automation(
{ {
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESP32CameraImageTrigger), cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
} ESP32CameraStreamStopTrigger
), ),
} }
).extend(cv.COMPONENT_SCHEMA) ),
cv.Optional(CONF_ON_IMAGE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
ESP32CameraImageTrigger
),
}
),
}
).extend(cv.COMPONENT_SCHEMA),
cv.has_exactly_one_key(CONF_I2C_PINS, CONF_I2C_ID),
)
SETTERS = { SETTERS = {
# pin assignment # pin assignment
@@ -271,7 +284,7 @@ SETTERS = {
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await setup_entity(var, config) await setup_entity(var, config, "camera")
await cg.register_component(var, config) await cg.register_component(var, config)
for key, setter in SETTERS.items(): for key, setter in SETTERS.items():
@@ -280,8 +293,12 @@ async def to_code(config):
extclk = config[CONF_EXTERNAL_CLOCK] extclk = config[CONF_EXTERNAL_CLOCK]
cg.add(var.set_external_clock(extclk[CONF_PIN], extclk[CONF_FREQUENCY])) cg.add(var.set_external_clock(extclk[CONF_PIN], extclk[CONF_FREQUENCY]))
i2c_pins = config[CONF_I2C_PINS] if i2c_id := config.get(CONF_I2C_ID):
cg.add(var.set_i2c_pins(i2c_pins[CONF_SDA], i2c_pins[CONF_SCL])) 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])) cg.add(var.set_max_update_interval(1000 / config[CONF_MAX_FRAMERATE]))
if config[CONF_IDLE_FRAMERATE] == 0: if config[CONF_IDLE_FRAMERATE] == 0:
cg.add(var.set_idle_update_interval(0)) cg.add(var.set_idle_update_interval(0))

View File

@@ -1,9 +1,9 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
#include "esp32_camera.h" #include "esp32_camera.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include <freertos/task.h> #include <freertos/task.h>
@@ -16,6 +16,12 @@ static const char *const TAG = "esp32_camera";
void ESP32Camera::setup() { void ESP32Camera::setup() {
global_esp32_camera = this; 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 */ /* initialize time to now */
this->last_update_ = millis(); this->last_update_ = millis();
@@ -57,7 +63,7 @@ void ESP32Camera::dump_config() {
" External Clock: Pin:%d Frequency:%u\n" " External Clock: Pin:%d Frequency:%u\n"
" I2C Pins: SDA:%d SCL:%d\n" " I2C Pins: SDA:%d SCL:%d\n"
" Reset Pin: %d", " 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_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); conf.pin_xclk, conf.xclk_freq_hz, conf.pin_sccb_sda, conf.pin_sccb_scl, conf.pin_reset);
switch (this->config_.frame_size) { 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_sda = sda;
this->config_.pin_sccb_scl = scl; 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_reset_pin(uint8_t pin) { this->config_.pin_reset = pin; }
void ESP32Camera::set_power_down_pin(uint8_t pin) { this->config_.pin_pwdn = pin; } void ESP32Camera::set_power_down_pin(uint8_t pin) { this->config_.pin_pwdn = pin; }

View File

@@ -2,13 +2,17 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
#include <esp_camera.h>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/entity_base.h" #include "esphome/core/entity_base.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include <esp_camera.h>
#include <freertos/FreeRTOS.h> #ifdef USE_I2C
#include <freertos/queue.h> #include "esphome/components/i2c/i2c_bus.h"
#endif // USE_I2C
namespace esphome { namespace esphome {
namespace esp32_camera { namespace esp32_camera {
@@ -118,6 +122,9 @@ class ESP32Camera : public EntityBase, public Component {
void set_pixel_clock_pin(uint8_t pin); void set_pixel_clock_pin(uint8_t pin);
void set_external_clock(uint8_t pin, uint32_t frequency); void set_external_clock(uint8_t pin, uint32_t frequency);
void set_i2c_pins(uint8_t sda, uint8_t scl); 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_reset_pin(uint8_t pin);
void set_power_down_pin(uint8_t pin); void set_power_down_pin(uint8_t pin);
/* -- image */ /* -- image */
@@ -210,6 +217,9 @@ class ESP32Camera : public EntityBase, public Component {
uint32_t last_idle_request_{0}; uint32_t last_idle_request_{0};
uint32_t last_update_{0}; uint32_t last_update_{0};
#ifdef USE_I2C
i2c::InternalI2CBus *i2c_bus_{nullptr};
#endif // USE_I2C
}; };
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) // 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 from esphome.const import CONF_ID, CONF_MODE, CONF_PORT
CODEOWNERS = ["@ayufan"] CODEOWNERS = ["@ayufan"]
DEPENDENCIES = ["esp32_camera"] DEPENDENCIES = ["esp32_camera", "network"]
MULTI_CONF = True MULTI_CONF = True
esp32_camera_web_server_ns = cg.esphome_ns.namespace("esp32_camera_web_server") 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 import esphome.config_validation as cv
from esphome.const import ICON_MAGNET, STATE_CLASS_MEASUREMENT, UNIT_MICROTESLA
DEPENDENCIES = ["esp32"] 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."
esp32_hall_ns = cg.esphome_ns.namespace("esp32_hall")
ESP32HallSensor = esp32_hall_ns.class_(
"ESP32HallSensor", sensor.Sensor, cg.PollingComponent
) )
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: { case improv::STATE_PROVISIONED: {
this->incoming_data_.clear(); this->incoming_data_.clear();
this->set_status_indicator_state_(false); this->set_status_indicator_state_(false);
// Provisioning complete, no further loop execution needed
this->disable_loop();
break; break;
} }
} }
@@ -254,6 +256,7 @@ void ESP32ImprovComponent::start() {
ESP_LOGD(TAG, "Setting Improv to start"); ESP_LOGD(TAG, "Setting Improv to start");
this->should_start_ = true; this->should_start_ = true;
this->enable_loop();
} }
void ESP32ImprovComponent::stop() { void ESP32ImprovComponent::stop() {

View File

@@ -1,48 +1,8 @@
import esphome.codegen as cg
from esphome.components import esp32 from esphome.components import esp32
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import KEY_CORE, KEY_FRAMEWORK_VERSION
from esphome.core import CORE
CODEOWNERS = ["@jesserockz"] 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 validate_clock_resolution():
def _validator(value): def _validator(value):
@@ -60,21 +20,3 @@ def validate_clock_resolution():
return value return value
return _validator 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; 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); 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 // 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(); this->mark_failed();
return; 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, 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"); 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); 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) { if (error != ESP_OK) {
ESP_LOGE(TAG, "RMT TX timeout"); ESP_LOGE(TAG, "RMT TX timeout");
this->status_set_warning(); this->status_set_warning();
@@ -162,11 +127,7 @@ void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) {
size_t size = 0; size_t size = 0;
size_t len = 0; size_t len = 0;
uint8_t *psrc = this->buf_; uint8_t *psrc = this->buf_;
#if ESP_IDF_VERSION_MAJOR >= 5
rmt_symbol_word_t *pdest = this->rmt_buf_; rmt_symbol_word_t *pdest = this->rmt_buf_;
#else
rmt_item32_t *pdest = this->rmt_buf_;
#endif
while (size < buffer_size) { while (size < buffer_size) {
uint8_t b = *psrc; uint8_t b = *psrc;
for (int i = 0; i < 8; i++) { for (int i = 0; i < 8; i++) {
@@ -184,15 +145,11 @@ void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) {
len++; len++;
} }
#if ESP_IDF_VERSION_MAJOR >= 5
rmt_transmit_config_t config; rmt_transmit_config_t config;
memset(&config, 0, sizeof(config)); memset(&config, 0, sizeof(config));
config.loop_count = 0; config.loop_count = 0;
config.flags.eot_level = 0; config.flags.eot_level = 0;
error = rmt_transmit(this->channel_, this->encoder_, this->rmt_buf_, len * sizeof(rmt_symbol_word_t), &config); 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) { if (error != ESP_OK) {
ESP_LOGE(TAG, "RMT TX error"); ESP_LOGE(TAG, "RMT TX error");
this->status_set_warning(); this->status_set_warning();
@@ -251,11 +208,7 @@ void ESP32RMTLEDStripLightOutput::dump_config() {
"ESP32 RMT LED Strip:\n" "ESP32 RMT LED Strip:\n"
" Pin: %u", " Pin: %u",
this->pin_); this->pin_);
#if ESP_IDF_VERSION_MAJOR >= 5
ESP_LOGCONFIG(TAG, " RMT Symbols: %" PRIu32, this->rmt_symbols_); ESP_LOGCONFIG(TAG, " RMT Symbols: %" PRIu32, this->rmt_symbols_);
#else
ESP_LOGCONFIG(TAG, " Channel: %u", this->channel_);
#endif
const char *rgb_order; const char *rgb_order;
switch (this->rgb_order_) { switch (this->rgb_order_) {
case ORDER_RGB: case ORDER_RGB:

View File

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

View File

@@ -3,7 +3,7 @@ import logging
from esphome import pins from esphome import pins
import esphome.codegen as cg 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 import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_CHIPSET, CONF_CHIPSET,
@@ -13,11 +13,9 @@ from esphome.const import (
CONF_OUTPUT_ID, CONF_OUTPUT_ID,
CONF_PIN, CONF_PIN,
CONF_RGB_ORDER, CONF_RGB_ORDER,
CONF_RMT_CHANNEL,
CONF_RMT_SYMBOLS, CONF_RMT_SYMBOLS,
CONF_USE_DMA, CONF_USE_DMA,
) )
from esphome.core import CORE
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -69,53 +67,6 @@ CONF_RESET_HIGH = "reset_high"
CONF_RESET_LOW = "reset_low" 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( CONFIG_SCHEMA = cv.All(
light.ADDRESSABLE_LIGHT_SCHEMA.extend( 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_PIN): pins.internal_gpio_output_pin_number,
cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int, cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int,
cv.Required(CONF_RGB_ORDER): cv.enum(RGB_ORDERS, upper=True), cv.Required(CONF_RGB_ORDER): cv.enum(RGB_ORDERS, upper=True),
cv.Optional(CONF_RMT_CHANNEL): cv.All( cv.SplitDefault(
not_with_new_rmt_driver, esp32_rmt.validate_rmt_channel(tx=True)
),
OptionalForIDF5(
CONF_RMT_SYMBOLS, CONF_RMT_SYMBOLS,
esp32_idf=192, esp32=192,
esp32_s2_idf=192, esp32_s2=192,
esp32_s3_idf=192, esp32_s3=192,
esp32_p4_idf=192, esp32_p4=192,
esp32_c3_idf=96, esp32_c3=96,
esp32_c5_idf=96, esp32_c5=96,
esp32_c6_idf=96, esp32_c6=96,
esp32_h2_idf=96, esp32_h2=96,
): cv.All(only_with_new_rmt_driver, cv.int_range(min=2)), ): cv.int_range(min=2),
cv.Optional(CONF_MAX_REFRESH_RATE): cv.positive_time_period_microseconds, cv.Optional(CONF_MAX_REFRESH_RATE): cv.positive_time_period_microseconds,
cv.Optional(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True), cv.Optional(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True),
cv.Optional(CONF_IS_RGBW, default=False): cv.boolean, cv.Optional(CONF_IS_RGBW, default=False): cv.boolean,
@@ -145,7 +93,6 @@ CONFIG_SCHEMA = cv.All(
esp32.only_on_variant( esp32.only_on_variant(
supported=[esp32.const.VARIANT_ESP32S3, esp32.const.VARIANT_ESP32P4] supported=[esp32.const.VARIANT_ESP32S3, esp32.const.VARIANT_ESP32P4]
), ),
cv.only_with_esp_idf,
cv.boolean, cv.boolean,
), ),
cv.Optional(CONF_USE_PSRAM, default=True): 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_rgbw(config[CONF_IS_RGBW]))
cg.add(var.set_is_wrgb(config[CONF_IS_WRGB])) cg.add(var.set_is_wrgb(config[CONF_IS_WRGB]))
cg.add(var.set_use_psram(config[CONF_USE_PSRAM])) cg.add(var.set_use_psram(config[CONF_USE_PSRAM]))
cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS]))
if esp32_rmt.use_new_rmt_driver(): if CONF_USE_DMA in config:
cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS])) cg.add(var.set_use_dma(config[CONF_USE_DMA]))
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_platformio_option("board", config[CONF_BOARD])
cg.add_build_flag("-DUSE_ESP8266") cg.add_build_flag("-DUSE_ESP8266")
cg.set_cpp_standard("gnu++17")
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
cg.add_define("ESPHOME_VARIANT", "ESP8266") cg.add_define("ESPHOME_VARIANT", "ESP8266")

View File

@@ -26,19 +26,19 @@ void ESPHomeOTAComponent::setup() {
ota::register_ota_platform(this); ota::register_ota_platform(this);
#endif #endif
server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections this->server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections
if (server_ == nullptr) { if (this->server_ == nullptr) {
ESP_LOGW(TAG, "Could not create socket"); ESP_LOGW(TAG, "Could not create socket");
this->mark_failed(); this->mark_failed();
return; return;
} }
int enable = 1; 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) { if (err != 0) {
ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err); ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err);
// we can still continue // we can still continue
} }
err = server_->setblocking(false); err = this->server_->setblocking(false);
if (err != 0) { if (err != 0) {
ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err); ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err);
this->mark_failed(); this->mark_failed();
@@ -54,14 +54,14 @@ void ESPHomeOTAComponent::setup() {
return; return;
} }
err = server_->bind((struct sockaddr *) &server, sizeof(server)); err = this->server_->bind((struct sockaddr *) &server, sizeof(server));
if (err != 0) { if (err != 0) {
ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno); ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno);
this->mark_failed(); this->mark_failed();
return; return;
} }
err = server_->listen(4); err = this->server_->listen(4);
if (err != 0) { if (err != 0) {
ESP_LOGW(TAG, "Socket unable to listen: errno %d", errno); ESP_LOGW(TAG, "Socket unable to listen: errno %d", errno);
this->mark_failed(); this->mark_failed();
@@ -82,7 +82,14 @@ void ESPHomeOTAComponent::dump_config() {
#endif #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; static const uint8_t FEATURE_SUPPORTS_COMPRESSION = 0x01;
@@ -101,23 +108,21 @@ void ESPHomeOTAComponent::handle_() {
size_t size_acknowledged = 0; size_t size_acknowledged = 0;
#endif #endif
if (client_ == nullptr) { if (this->client_ == nullptr) {
// Check if the server socket is ready before accepting // We already checked server_->ready() in loop(), so we can accept directly
if (this->server_->ready()) { struct sockaddr_storage source_addr;
struct sockaddr_storage source_addr; socklen_t addr_len = sizeof(source_addr);
socklen_t addr_len = sizeof(source_addr); this->client_ = this->server_->accept((struct sockaddr *) &source_addr, &addr_len);
client_ = server_->accept((struct sockaddr *) &source_addr, &addr_len); if (this->client_ == nullptr)
} return;
} }
if (client_ == nullptr)
return;
int enable = 1; 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) { if (err != 0) {
ESP_LOGW(TAG, "Socket could not enable TCP nodelay, errno %d", errno); ESP_LOGW(TAG, "Socket could not enable TCP nodelay, errno %d", errno);
client_->close(); this->client_->close();
client_ = nullptr; this->client_ = nullptr;
return; return;
} }

View File

@@ -106,7 +106,7 @@ void EthernetComponent::setup() {
.post_cb = nullptr, .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); eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(host, &devcfg);
#else #else
spi_device_handle_t spi_handle = nullptr; spi_device_handle_t spi_handle = nullptr;
@@ -274,6 +274,9 @@ void EthernetComponent::loop() {
ESP_LOGW(TAG, "Connection lost; reconnecting"); ESP_LOGW(TAG, "Connection lost; reconnecting");
this->state_ = EthernetComponentState::CONNECTING; this->state_ = EthernetComponentState::CONNECTING;
this->start_connect_(); this->start_connect_();
} else {
// When connected and stable, disable the loop to save CPU cycles
this->disable_loop();
} }
break; break;
} }
@@ -397,11 +400,13 @@ void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base
case ETHERNET_EVENT_START: case ETHERNET_EVENT_START:
event_name = "ETH started"; event_name = "ETH started";
global_eth_component->started_ = true; global_eth_component->started_ = true;
global_eth_component->enable_loop_soon_any_context();
break; break;
case ETHERNET_EVENT_STOP: case ETHERNET_EVENT_STOP:
event_name = "ETH stopped"; event_name = "ETH stopped";
global_eth_component->started_ = false; global_eth_component->started_ = false;
global_eth_component->connected_ = false; global_eth_component->connected_ = false;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
break; break;
case ETHERNET_EVENT_CONNECTED: case ETHERNET_EVENT_CONNECTED:
event_name = "ETH 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: case ETHERNET_EVENT_DISCONNECTED:
event_name = "ETH disconnected"; event_name = "ETH disconnected";
global_eth_component->connected_ = false; global_eth_component->connected_ = false;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
break; break;
default: default:
return; 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; global_eth_component->got_ipv4_address_ = true;
#if USE_NETWORK_IPV6 && (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0) #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->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 #else
global_eth_component->connected_ = true; global_eth_component->connected_ = true;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
#endif /* USE_NETWORK_IPV6 */ #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) #if (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0)
global_eth_component->connected_ = global_eth_component->connected_ =
global_eth_component->got_ipv4_address_ && (global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT); 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 #else
global_eth_component->connected_ = global_eth_component->got_ipv4_address_; 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
} }
#endif /* USE_NETWORK_IPV6 */ #endif /* USE_NETWORK_IPV6 */
@@ -620,6 +630,7 @@ bool EthernetComponent::powerdown() {
} }
this->connected_ = false; this->connected_ = false;
this->started_ = 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) { if (this->phy_->pwrctl(this->phy_, false) != ESP_OK) {
ESP_LOGE(TAG, "Error powering down ethernet PHY"); ESP_LOGE(TAG, "Error powering down ethernet PHY");
return false; return false;

View File

@@ -18,8 +18,8 @@ from esphome.const import (
DEVICE_CLASS_MOTION, DEVICE_CLASS_MOTION,
) )
from esphome.core import CORE, coroutine_with_priority 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_generator import MockObjClass
from esphome.cpp_helpers import setup_entity
CODEOWNERS = ["@nohat"] CODEOWNERS = ["@nohat"]
IS_PLATFORM_COMPONENT = True IS_PLATFORM_COMPONENT = True
@@ -59,6 +59,9 @@ _EVENT_SCHEMA = (
) )
_EVENT_SCHEMA.add_extra(entity_duplicate_validator("event"))
def event_schema( def event_schema(
class_: MockObjClass = cv.UNDEFINED, 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]): 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, []): for conf in config.get(CONF_ON_EVENT, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)

View File

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

View File

@@ -41,39 +41,48 @@ void FanCall::perform() {
void FanCall::validate_() { void FanCall::validate_() {
auto traits = this->parent_.get_traits(); 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()); this->speed_ = clamp(*this->speed_, 1, traits.supported_speed_count());
if (this->binary_state_.has_value() && *this->binary_state_) { // https://developers.home-assistant.io/docs/core/entity/fan/#preset-modes
// when turning on, if neither current nor new speed available, set speed to 100% // "Manually setting a speed must disable any set preset mode"
if (traits.supports_speed() && !this->parent_.state && this->parent_.speed == 0 && !this->speed_.has_value()) { this->preset_mode_.clear();
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();
} }
if (!this->preset_mode_.empty()) { if (!this->preset_mode_.empty()) {
const auto &preset_modes = traits.supported_preset_modes(); const auto &preset_modes = traits.supported_preset_modes();
if (preset_modes.find(this->preset_mode_) == preset_modes.end()) { 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(), ESP_LOGW(TAG, "%s: Preset mode '%s' not supported", this->parent_.get_name().c_str(), this->preset_mode_.c_str());
this->preset_mode_.c_str());
this->preset_mode_.clear(); 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) { FanCall FanRestoreState::to_call(Fan &fan) {

View File

@@ -1,6 +1,7 @@
from collections.abc import MutableMapping from collections.abc import MutableMapping
import functools import functools
import hashlib import hashlib
from itertools import accumulate
import logging import logging
import os import os
from pathlib import Path from pathlib import Path
@@ -468,8 +469,9 @@ class EFont:
class GlyphInfo: class GlyphInfo:
def __init__(self, data_len, advance, offset_x, offset_y, width, height): def __init__(self, glyph, data, advance, offset_x, offset_y, width, height):
self.data_len = data_len self.glyph = glyph
self.bitmap_data = data
self.advance = advance self.advance = advance
self.offset_x = offset_x self.offset_x = offset_x
self.offset_y = offset_y self.offset_y = offset_y
@@ -477,6 +479,62 @@ class GlyphInfo:
self.height = height 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): async def to_code(config):
""" """
Collect all glyph codepoints, construct a map from a codepoint to a font file. 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 = list(point_set)
codepoints.sort(key=functools.cmp_to_key(glyph_comparator)) codepoints.sort(key=functools.cmp_to_key(glyph_comparator))
glyph_args = {}
data = []
bpp = config[CONF_BPP] bpp = config[CONF_BPP]
scale = 256 // (1 << bpp)
size = config[CONF_SIZE] size = config[CONF_SIZE]
# create the data array for all glyphs # create the data array for all glyphs
for codepoint in codepoints: glyph_args = [
font = point_font_map[codepoint] glyph_to_glyphinfo(x, point_font_map[x], size, bpp) for x in codepoints
if not font.is_scalable: ]
sizes = [pt_to_px(x.size) for x in font.available_sizes] rhs = [HexInt(x) for x in flatten([x.bitmap_data for x in glyph_args])]
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]
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
# Create the glyph table that points to data in the above array. # Create the glyph table that points to data in the above array.
glyph_initializer = [] glyph_initializer = [
for codepoint in codepoints: cg.StructInitializer(
glyph_initializer.append( GlyphData,
cg.StructInitializer( (
GlyphData, "a_char",
( cg.RawExpression(f"(const uint8_t *){cpp_string_escape(x.glyph)}"),
"a_char", ),
cg.RawExpression( (
f"(const uint8_t *){cpp_string_escape(codepoint)}" "data",
), cg.RawExpression(f"{str(prog_arr)} + {str(y - len(x.bitmap_data))}"),
), ),
( ("advance", x.advance),
"data", ("offset_x", x.offset_x),
cg.RawExpression( ("offset_y", x.offset_y),
f"{str(prog_arr)} + {str(glyph_args[codepoint].data_len)}" ("width", x.width),
), ("height", x.height),
),
("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),
)
) )
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) glyphs = cg.static_const_array(config[CONF_RAW_GLYPH_ID], glyph_initializer)
font_height = pt_to_px(base_font.size.height) font_height = pt_to_px(base_font.size.height)
ascender = pt_to_px(base_font.size.ascender) 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 font_height == 0:
if not base_font.is_scalable: if not base_font.is_scalable:
font_height = size font_height = size
@@ -610,5 +617,8 @@ async def to_code(config):
len(glyph_initializer), len(glyph_initializer),
ascender, ascender,
font_height, font_height,
descender,
xheight,
capheight,
bpp, 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; *height = this->glyph_data_->height;
} }
Font::Font(const GlyphData *data, int data_nr, int baseline, int height, uint8_t bpp) Font::Font(const GlyphData *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight,
: baseline_(baseline), height_(height), bpp_(bpp) { uint8_t bpp)
: baseline_(baseline),
height_(height),
descender_(descender),
linegap_(height - baseline - descender),
xheight_(xheight),
capheight_(capheight),
bpp_(bpp) {
glyphs_.reserve(data_nr); glyphs_.reserve(data_nr);
for (int i = 0; i < data_nr; ++i) for (int i = 0; i < data_nr; ++i)
glyphs_.emplace_back(&data[i]); glyphs_.emplace_back(&data[i]);

View File

@@ -50,11 +50,17 @@ class Font
public: public:
/** Construct the font with the given glyphs. /** 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 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); int match_next_glyph(const uint8_t *str, int *match_length);
@@ -65,14 +71,23 @@ class Font
#endif #endif
inline int get_baseline() { return this->baseline_; } inline int get_baseline() { return this->baseline_; }
inline int get_height() { return this->height_; } 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_; } 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: protected:
std::vector<Glyph, ExternalRAMAllocator<Glyph>> glyphs_; std::vector<Glyph, RAMAllocator<Glyph>> glyphs_;
int baseline_; int baseline_;
int height_; int height_;
int descender_;
int linegap_;
int xheight_;
int capheight_;
uint8_t bpp_; // bits per pixel 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_max_temperature(config[CONF_MAX_TEMPERATURE]))
cg.add(var.set_min_temperature(config[CONF_MIN_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: if CORE.is_libretiny:
CORE.add_platformio_option("lib_ignore", "IRremoteESP8266") CORE.add_platformio_option("lib_ignore", "IRremoteESP8266")

View File

@@ -41,6 +41,6 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config): async def to_code(config):
cg.add_build_flag("-DUSE_HOST") cg.add_build_flag("-DUSE_HOST")
cg.add_define("USE_ESPHOME_HOST_MAC_ADDRESS", config[CONF_MAC_ADDRESS].parts) 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_define("ESPHOME_BOARD", "host")
cg.add_platformio_option("platform", "platformio/native") cg.add_platformio_option("platform", "platformio/native")

View File

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

View File

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

View File

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

View File

@@ -54,7 +54,7 @@ void HttpRequestUpdate::update_task(void *params) {
UPDATE_RETURN; UPDATE_RETURN;
} }
ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE); RAMAllocator<uint8_t> allocator;
uint8_t *data = allocator.allocate(container->content_length); uint8_t *data = allocator.allocate(container->content_length);
if (data == nullptr) { if (data == nullptr) {
std::string msg = str_sprintf("Failed to allocate %d bytes for manifest", container->content_length); 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"] CODEOWNERS = ["@esphome/core"]
i2c_ns = cg.esphome_ns.namespace("i2c") i2c_ns = cg.esphome_ns.namespace("i2c")
I2CBus = i2c_ns.class_("I2CBus") I2CBus = i2c_ns.class_("I2CBus")
ArduinoI2CBus = i2c_ns.class_("ArduinoI2CBus", I2CBus, cg.Component) InternalI2CBus = i2c_ns.class_("InternalI2CBus", I2CBus)
IDFI2CBus = i2c_ns.class_("IDFI2CBus", I2CBus, cg.Component) ArduinoI2CBus = i2c_ns.class_("ArduinoI2CBus", InternalI2CBus, cg.Component)
IDFI2CBus = i2c_ns.class_("IDFI2CBus", InternalI2CBus, cg.Component)
I2CDevice = i2c_ns.class_("I2CDevice") I2CDevice = i2c_ns.class_("I2CDevice")
@@ -71,6 +72,7 @@ CONFIG_SCHEMA = cv.All(
@coroutine_with_priority(1.0) @coroutine_with_priority(1.0)
async def to_code(config): async def to_code(config):
cg.add_global(i2c_ns.using) cg.add_global(i2c_ns.using)
cg.add_define("USE_I2C")
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)

View File

@@ -1,6 +1,6 @@
#pragma once #pragma once
#include <cstdint>
#include <cstddef> #include <cstddef>
#include <cstdint>
#include <utility> #include <utility>
#include <vector> #include <vector>
@@ -108,5 +108,12 @@ class I2CBus {
bool scan_{false}; ///< Should we scan ? Can be set in the yaml 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 i2c
} // namespace esphome } // namespace esphome

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,7 @@ namespace i2s_audio {
static const char *const TAG = "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 :( static const uint8_t I2S_NUM_MAX = SOC_I2S_NUM; // because IDF 5+ took this away :(
#endif #endif
@@ -18,7 +18,7 @@ void I2SAudioComponent::setup() {
static i2s_port_t next_port_num = I2S_NUM_0; static i2s_port_t next_port_num = I2S_NUM_0;
if (next_port_num >= I2S_NUM_MAX) { if (next_port_num >= I2S_NUM_MAX) {
ESP_LOGE(TAG, "Too many I2S Audio components"); ESP_LOGE(TAG, "Too many components");
this->mark_failed(); this->mark_failed();
return; 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_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(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("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") cg.add_build_flag("-DAUDIO_NO_SD_FS")

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