1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-03 16:41:50 +00:00

Compare commits

...

196 Commits

Author SHA1 Message Date
Jesse Hills
0a3ee7d84e Merge pull request #10228 from esphome/bump-2025.8.0b2
2025.8.0b2
2025-08-15 08:46:15 +12:00
Jesse Hills
8d61b1e8df Bump version to 2025.8.0b2 2025-08-14 14:00:27 +12:00
dependabot[bot]
9c897993bb Bump esphome-dashboard from 20250514.0 to 20250814.0 (#10227)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-14 14:00:26 +12:00
dependabot[bot]
93f9475105 Bump aioesphomeapi from 38.2.1 to 39.0.0 (#10222)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-14 14:00:26 +12:00
Samuel Sieb
95cd224e3e [psram] allow disabling (#10224)
Co-authored-by: Samuel Sieb <samuel@sieb.net>
2025-08-14 14:00:26 +12:00
Jesse Hills
b7afeafda9 [espnow] Set state to enabled before adding initial peers (#10225) 2025-08-14 14:00:26 +12:00
Jesse Hills
7922462bcf [entity] Allow `device_id` to be blank on entities (#10217) 2025-08-14 14:00:26 +12:00
Jesse Hills
1c2e1ab3e5 Merge pull request #10214 from esphome/bump-2025.8.0b1
2025.8.0b1
2025-08-13 23:56:34 +12:00
J. Nick Koston
68ddd98f5f [CI] Fix CI job failures for PRs with >300 changed files (#10215) 2025-08-13 15:49:38 +12:00
Jesse Hills
6b7ced1970 Bump version to 2025.8.0b1 2025-08-13 14:46:50 +12:00
J. Nick Koston
ed2b76050b [bluetooth_proxy] Remove ESPBTUUID dependency to save 296 bytes of flash (#10213) 2025-08-13 14:18:53 +12:00
Samuel Sieb
113813617d [bme280_base, bmp280_base] add reasons to the fails, clean up logging (#10209)
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
2025-08-13 02:05:22 +00:00
Keith Burzinski
c3a209d3f4 [ld2450] Replace `throttle` with native filters (#10196)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-08-12 19:35:19 -05:00
John
7ffdaa1f06 [atm90e32] energy meter calibration log output enhancements & software SPI fix (#10143)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-08-12 20:26:53 +12:00
dependabot[bot]
3a857950bf Bump actions/checkout from 4 to 5 (#10198)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-08-12 20:23:41 +12:00
Rihan9
0256e0005e [ld2412] New component (#9075)
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-08-12 00:34:37 -05:00
Jesse Hills
c65af68e63 [core] Reset pin registry after target platform validations (#10199) 2025-08-12 16:33:07 +12:00
dependabot[bot]
ef2121a215 Bump aioesphomeapi from 38.1.0 to 38.2.1 (#10197)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-11 20:47:53 -05:00
Joshua Sing
bb40b7702d [const] Add CONF_POWER_MODE (#10173) 2025-08-12 11:13:24 +12:00
Kevin Ahrendt
6c48f3d719 [wifi] Remove restriction from using NONE power saving mode with BLE (#10181) 2025-08-12 11:09:58 +12:00
J. Nick Koston
ff52869b4c [api] Add constexpr optimizations to protobuf encoding (#10192) 2025-08-12 10:10:38 +12:00
J. Nick Koston
82b7c1224c [core] Improve entity duplicate validation error messages (#10184) 2025-08-12 09:58:51 +12:00
Jesse Hills
c14c4fb658 [substitutions] Add some safe built-in functions to jinja parsing (#10178)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-11 16:12:54 -05:00
J. Nick Koston
42aee53dde [bluetooth_proxy] Replace dynamic vector with fixed array for BLE advertisements (#10174) 2025-08-11 15:47:46 -05:00
J. Nick Koston
9aa21956c8 [api] Optimize single vector writes to use write() instead of writev() (#10193) 2025-08-11 15:41:08 -05:00
J. Nick Koston
4c2874a32b [esphome] Fix OTA watchdog resets during port scanning and network delays (#10152) 2025-08-11 15:37:01 -05:00
Keith Burzinski
45b88f2da9 [sensor] Extend timeout filter with option to return last value received (#10115) 2025-08-11 10:36:44 -05:00
dependabot[bot]
8f53961496 Bump pylint from 3.3.7 to 3.3.8 (#10177)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-11 01:05:14 -05:00
dependabot[bot]
5cf0e4d9dd Bump aioesphomeapi from 38.0.0 to 38.1.0 (#10176)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-11 05:11:22 +00:00
Chad Matsalla
b70983ed09 [display] Disallow `show_test_card: true and update_interval: never` (#9927)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-08-11 11:41:37 +12:00
tomaszduda23
ffa89eb2d3 [nrf52, zephyr_debug] add zephyr debug component (#8319) 2025-08-11 11:20:45 +12:00
Clyde Stubbs
8b67d6dfec [lvgl] fix allocation of reduced size buffer with rotation (#10147) 2025-08-11 10:32:01 +12:00
Clyde Stubbs
581b4ef5a1 [lvgl] Various validation fixes (#10141) 2025-08-11 10:27:54 +12:00
Jonathan Swoboda
da02f970d4 [neopixelbus] Fix neopixelbus on esp32 (#10123) 2025-08-11 10:24:12 +12:00
Jesse Hills
2fc0a11596 [CI] Print more info for when consts are duplicated (#10166) 2025-08-11 09:53:40 +12:00
J. Nick Koston
5a8f722316 Optimize subprocess performance with close_fds=False (#10145) 2025-08-11 09:14:13 +12:00
J. Nick Koston
279f56141e [ade7880] Fix duplicate sensor name validation error (#10155) 2025-08-11 09:12:36 +12:00
J. Nick Koston
6bfe281d18 [web_server] Reduce flash usage by consolidating parameter parsing (#10154) 2025-08-11 09:09:31 +12:00
J. Nick Koston
a1371aea37 [dashboard] Fix port fallback regression when device is offline (#10135) 2025-08-11 09:04:40 +12:00
Jonathan Swoboda
d5c9c10b3b [esp32] Add IDF log_level option (#10134) 2025-08-10 17:27:08 +00:00
J. Nick Koston
cef39e7c59 [esp32_ble_tracker] Fix false reboots when event loop is blocked (#10144) 2025-08-10 04:44:23 -05:00
Edward Firmo
2b9e1ce315 [switch] Add trigger `on_state` (#10108) 2025-08-09 21:09:40 +10:00
dependabot[bot]
ff9ddb9d68 Bump tornado from 6.5.1 to 6.5.2 (#10142)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-08 16:03:13 -05:00
Edward Firmo
676c51ffa0 [switch] Add control() method to API (#10118) 2025-08-08 05:51:19 +00:00
J. Nick Koston
7e4d09dbd8 [bluetooth_proxy] Optimize connection loop to reduce CPU usage (#10133) 2025-08-07 16:24:26 -10:00
J. Nick Koston
58504662d8 [cover] Reduce flash usage by optimizing validation messages (#10130) 2025-08-08 10:44:47 +10:00
J. Nick Koston
83b69519dd [wifi] Reduce flash usage by optimizing logging (#10127) 2025-08-08 10:43:13 +10:00
J. Nick Koston
d4d1a96f9b [esp32_ble_client] Reduce flash usage by optimizing logging strings (#10119) 2025-08-08 10:42:03 +10:00
J. Nick Koston
76fd104fb6 [mdns] Conditionally compile extra services to reduce flash usage (#10129) 2025-08-08 10:32:35 +10:00
Edward Firmo
c4d1b1317a [switch] Add switch.control automation action (#10105)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-08-08 08:55:54 +10:00
dependabot[bot]
14bc83342f Bump ruff from 0.12.7 to 0.12.8 (#10126)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-08-07 20:15:14 +00:00
dependabot[bot]
a1461c5293 Bump actions/cache from 4.2.3 to 4.2.4 (#10128)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-07 10:09:53 -10:00
dependabot[bot]
73b2db8af5 Bump actions/cache from 4.2.3 to 4.2.4 in /.github/actions/restore-python (#10125)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-07 09:16:58 -10:00
J. Nick Koston
a7a119f576 [bluetooth_proxy] Remove V1 connection support (#10107) 2025-08-07 03:52:46 -05:00
J. Nick Koston
1ba76f5f2e [esp32_ble_client] Conditionally compile BLE service classes to reduce flash usage (#10114) 2025-08-07 03:46:34 -05:00
J. Nick Koston
37a9ad6a0d [esp32_ble_tracker] Optimize member variable ordering to reduce memory padding (#10113) 2025-08-07 03:34:46 -05:00
J. Nick Koston
c0a62c0be1 [esp32_ble_client] Avoid iterating empty services vector for bluetooth_proxy connections (#10110) 2025-08-07 03:40:12 +00:00
J. Nick Koston
bfb14e1cf9 [esp32_touch] Restore get_value() for ESP32-S2/S3 variants (#10112) 2025-08-06 21:21:32 -05:00
mbo18
1415e02e40 Add device class absolute_humidity to the absolute humidity component (#10100)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-08-07 13:48:26 +12:00
dependabot[bot]
81f907e994 Bump actions/download-artifact from 4.3.0 to 5.0.0 (#10106)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-07 13:47:03 +12:00
J. Nick Koston
61008bc8a9 [bluetooth_proxy] Remove unnecessary heap allocation for response object (#10104) 2025-08-07 13:42:04 +12:00
J. Nick Koston
6d66ddd68d [bluetooth_proxy][esp32_ble_tracker][esp32_ble_client] Consolidate duplicate logging code to reduce flash usage (#10097) 2025-08-07 13:41:03 +12:00
J. Nick Koston
fc180251be [bluetooth_proxy] Consolidate dump_config() log calls (#10103) 2025-08-07 12:43:59 +12:00
J. Nick Koston
ee1d4f27ef [esp32_ble] Conditionally compile BLE advertising to reduce flash usage (#10099) 2025-08-07 12:29:24 +12:00
J. Nick Koston
325ec0a0ae [esp32_ble_client] Convert to C++17 nested namespace syntax (#10111) 2025-08-07 12:18:03 +12:00
Keith Burzinski
6071f4b02c [ld2410] Replace `throttle` with native filters (#10019) 2025-08-07 10:26:11 +12:00
dependabot[bot]
083ac8ce8e Bump aioesphomeapi from 37.2.5 to 38.0.0 (#10109)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-06 10:21:29 -10:00
J. Nick Koston
4ceda31f32 [bluetooth_proxy] Replace std::find with simple loop for small fixed array (#10102) 2025-08-07 07:53:42 +12:00
J. Nick Koston
5021cc6d5f [esp32_ble] Make BLE notification limit configurable to fix ESP_GATT_NO_RESOURCES errors (#10098) 2025-08-06 17:24:02 +00:00
Craig Andrews
2b3e546203 [deep_sleep] enable sleep pull up/down for wakeup pin (#9395) 2025-08-05 23:47:45 -07:00
J. Nick Koston
1642d34d29 [esp32_ble_tracker] Simplify state machine guards with helper functions (#10092) 2025-08-06 01:03:19 -05:00
J. Nick Koston
8ceb1b9d60 [bluetooth_proxy] Reduce flash usage by consolidating duplicate logging (#10094) 2025-08-06 00:49:20 -05:00
Jesse Hills
d872c8a999 [light] Allow light effect schema to be a schema object already (#10091) 2025-08-06 00:05:48 -05:00
Pawelo
99125c045f [bme680] Eliminate warnings due to unused functions (#9735)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-08-06 00:02:54 -05:00
Jonathan Swoboda
860a5ef5c0 [esp32_rmt_led_strip] Work around IDFGH-16195 (#10093) 2025-08-05 23:28:09 -05:00
J. Nick Koston
b01f03cc24 [esp32_ble_tracker] Refactor loop() method for improved readability and performance (#10074) 2025-08-06 14:26:11 +12:00
J. Nick Koston
cfb22e33c9 [esp32_ble_tracker] Add missing USE_ESP32_BLE_DEVICE guard for already_discovered_ member (#10085) 2025-08-06 14:22:32 +12:00
@RubenKelevra
96bbb58f34 update espressif's esp32-camera library to 2.1.1 (#10090) 2025-08-05 14:33:15 -10:00
Jesse Hills
3edd746c6c [mcp23xxx] Use CachedGpioExpander (#10078) 2025-08-06 11:01:57 +12:00
Copilot
c308e03e92 [select] Fix new_select() not forwarding constructor args while preserving keyword-only options parameter (#10036)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
Co-authored-by: jesserockz <3060199+jesserockz@users.noreply.github.com>
2025-08-06 08:09:36 +12:00
NP v/d Spek
bd2b3b9da5 [espnow] Small changes and fixes (#10014)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-08-06 07:46:40 +12:00
Kevin Ahrendt
d443a97dd8 [speaker] Media player fixes for IDF5.4 (#10088) 2025-08-05 14:55:40 -04:00
J. Nick Koston
58a088e06b Add myself to multiple bluetooth codeowners (#10083) 2025-08-05 09:00:04 +00:00
Jesse Hills
49a46883ed [core] Update core component codeowners to `@esphome/core` (#10082) 2025-08-05 06:24:24 +00:00
J. Nick Koston
bc03538e25 Support multiple --device arguments for address fallback (#10003)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-08-05 16:40:46 +12:00
dependabot[bot]
969034b61a Bump docker/login-action from 3.4.0 to 3.5.0 in the docker-actions group (#10081)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-05 16:18:42 +12:00
Jonathan Swoboda
06eb1b6014 [remote_transmitter] Add digital_write automation (#10069)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-08-05 16:09:37 +12:00
Jesse Hills
589d00f17f Merge branch 'release' into dev 2025-08-05 15:38:25 +12:00
Jesse Hills
68c0aa4d6d Merge pull request #10079 from esphome/bump-2025.7.5
2025.7.5
2025-08-05 15:37:42 +12:00
dependabot[bot]
2fddb061e1 Bump aioesphomeapi from 37.2.4 to 37.2.5 (#10080)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-04 16:51:42 -10:00
Jesse Hills
c85eb448e4 [gpio_expander] Fix bank caching (#10077)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-08-05 13:45:52 +12:00
Jesse Hills
396c02c6de [core] Allow extra args on cli and just ignore them (#9814)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-05 13:33:12 +12:00
Jesse Hills
52c4509208 [esp32_dac] Always use esp-idf APIs (#9833) 2025-08-05 13:31:56 +12:00
Jesse Hills
d29cae9c3b Bump version to 2025.7.5 2025-08-05 13:21:00 +12:00
Chris Beswick
532e3e370f [i2s_audio] Use high-pass filter for dc offset correction (#10005) 2025-08-05 13:21:00 +12:00
Clyde Stubbs
da573a217d [font] Catch file load exception (#10058)
Co-authored-by: clydeps <U5yx99dok9>
2025-08-05 13:21:00 +12:00
J. Nick Koston
a9b27d1966 [api] Fix OTA progress updates not being sent when main loop is blocked (#10049) 2025-08-05 13:21:00 +12:00
Clyde Stubbs
0aa3c9685e [lvgl] Bugfix for tileview (#9938) 2025-08-05 13:21:00 +12:00
J. Nick Koston
93b28447ee [bluetooth_proxy] Optimize memory usage with fixed-size array and const string references (#10015) 2025-08-05 13:13:55 +12:00
J. Nick Koston
52634dac2a [tests] Add datetime entities to host_mode_many_entities integration test (#10032) 2025-08-05 13:12:05 +12:00
J. Nick Koston
64c94c1440 [esp32_ble_client] Fix connection parameter timing by setting preferences before connection (#10059) 2025-08-05 13:11:32 +12:00
J. Nick Koston
f7bf1ef52c [esp32_ble_tracker] Eliminate redundant ring buffer for lower latency (#10057)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-08-05 13:10:32 +12:00
J. Nick Koston
fa8c5e880c [esp32_ble_tracker] Optimize connection by promoting client immediately after scan stop trigger (#10061)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-05 13:10:02 +12:00
J. Nick Koston
27ba90ea95 [esp32_ble_client] Start MTU negotiation earlier following ESP-IDF examples (#10062) 2025-08-05 12:59:23 +12:00
J. Nick Koston
469246b8d8 [bluetooth_proxy] Warn about BLE connection timeout mismatch on Arduino framework (#10063)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-05 12:58:41 +12:00
J. Nick Koston
50f15735dc [api] Add helpful compile-time errors for Custom API Device methods (#10076) 2025-08-05 12:57:31 +12:00
mschnaubelt
83d9c02a1b Add CO5300 display support (#9739) 2025-08-05 09:41:55 +10:00
Jonathan Swoboda
701e6099aa [espnow, web_server_idf] Fix IDF 5.5 compile issues (#10068) 2025-08-04 08:56:34 -10:00
Chris Beswick
d59476d0e1 [i2s_audio] Use high-pass filter for dc offset correction (#10005) 2025-08-04 10:43:44 -04:00
Djordje Mandic
fbbb791b0d [gt911] Use timeout instead of delay, shortened log msg (#10024)
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
2025-08-04 03:37:43 -05:00
J. Nick Koston
36c4430317 [esp32_ble] Fix BLE connection slot waste by aligning ESP-IDF timeout with client timeout (#10013) 2025-08-04 17:41:41 +12:00
J. Nick Koston
6be22a5ea9 [esp32_ble_client] Connect immediately on READY_TO_CONNECT to reduce latency (#10051) 2025-08-04 17:15:28 +12:00
J. Nick Koston
989058e6a9 [esp32_ble_client] Use FAST connection parameters for all v3 connections (#10052) 2025-08-04 17:12:06 +12:00
J. Nick Koston
7c297366c7 [esp32_ble_tracker] Remove unnecessary STOPPED scanner state to reduce latency (#10055) 2025-08-04 16:57:59 +12:00
Clyde Stubbs
bb3ebaf955 [font] Catch file load exception (#10058)
Co-authored-by: clydeps <U5yx99dok9>
2025-08-04 16:55:54 +12:00
Jesse Hills
3007ca4d57 [core] Move docs url generator to helpers.py (#10056)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-03 23:55:46 -05:00
Jesse Hills
a5f1661643 [nfc] Rename `binary_sensor` source files (#10053) 2025-08-03 23:43:04 -05:00
Jesse Hills
4d683d5a69 [AI] Add note about the defines.h file needing to include all new defines added (#10054) 2025-08-03 16:45:35 -10:00
J. Nick Koston
c0c0a42362 [api] Use static allocation for areas and devices in DeviceInfoResponse (#10038)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-08-04 02:37:47 +00:00
J. Nick Koston
6a5eb460ef [esp32] Add framework migration warning for upcoming ESP-IDF default change (#10030)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-04 14:27:05 +12:00
J. Nick Koston
ef372eeeb7 [wifi] Replace std::stable_sort with insertion sort to save 2.4KB flash (#10037) 2025-08-04 14:19:24 +12:00
J. Nick Koston
9aad0733ef [core] Update to esptool 5.0+ command syntax (#10011) 2025-08-04 14:14:17 +12:00
J. Nick Koston
494a1a216c [web_server] Conditionally compile authentication code to save flash memory (#10022) 2025-08-04 14:09:12 +12:00
J. Nick Koston
a75f73dbf0 [web_server] Reduce binary size by using EntityBase and minimizing template instantiations (#10033) 2025-08-04 14:03:37 +12:00
J. Nick Koston
c9d865a061 [core] Optimize Application::pre_setup() to reduce duplicate MAC address operations (#10039) 2025-08-04 14:02:10 +12:00
J. Nick Koston
3fbbdb4589 [web_server_idf] Replace std::find_if with simple loop to reduce binary size (#10042)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-04 14:00:56 +12:00
J. Nick Koston
cd6cf074d9 [core] Replace std::stable_sort with insertion sort to save 3.5KB flash (#10035) 2025-08-04 13:56:06 +12:00
J. Nick Koston
d86e1e29a9 [core] Convert components, devices, and areas vectors to static allocation (#10020) 2025-08-04 13:51:50 +12:00
J. Nick Koston
dbaf2cdd50 [core] Replace std::find and std::max_element with simple loops to reduce binary size (#10044) 2025-08-04 13:46:06 +12:00
dependabot[bot]
b44d2183aa Bump aioesphomeapi from 37.2.3 to 37.2.4 (#10050)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-04 01:19:12 +00:00
@RubenKelevra
0f13af0076 Update esp32-camera library version to 2.1.0 (#9527) 2025-08-03 15:08:11 -10:00
Clyde Stubbs
339c26c815 [color][lvgl] Allow Color to be used for lv_color_t (#10016) 2025-08-04 12:51:34 +12:00
J. Nick Koston
d69e98e15d [api] Fix OTA progress updates not being sent when main loop is blocked (#10049) 2025-08-04 00:23:45 +00:00
Clyde Stubbs
b1b0638fab [config] Fix reversion of excessive yaml output after error (#10043)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-08-02 22:35:52 -10:00
J. Nick Koston
296442d8f1 [core] Fix compilation errors when platform sections have no entities (#10023) 2025-08-02 13:59:20 -10:00
Copilot
fd442cc485 [syslog] Fix RFC3164 timestamp compliance for single-digit days (#10034)
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2025-08-03 08:21:54 +10:00
J. Nick Koston
4f58e1c8b9 [core] Convert entity vectors to static allocation for reduced memory usage (#10018) 2025-08-01 20:26:22 -10:00
J. Nick Koston
00d9baed11 [bluetooth_proxy] Eliminate heap allocations in connection state reporting (#10010) 2025-08-01 20:26:00 -10:00
dependabot[bot]
f1877ca084 Bump aioesphomeapi from 37.2.2 to 37.2.3 (#10012)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-01 20:16:28 +00:00
dependabot[bot]
1f7c59f88d Bump esptool from 4.9.0 to 5.0.2 (#9983)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-01 10:02:29 -10:00
dependabot[bot]
b5f42bc493 Bump aioesphomeapi from 37.2.1 to 37.2.2 (#10009)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-01 09:53:56 -10:00
Jesse Hills
d8a46c7482 [CI] Allow multiple grep options for clang-tidy (#10004) 2025-08-01 21:40:53 +12:00
J. Nick Koston
20ad1ab4eb [wifi] Fix crash during WiFi reconnection on ESP32 with poor signal quality (#9989) 2025-08-01 02:46:52 -05:00
Clyde Stubbs
940a8b43fa [esp32] Add config option to execute from PSRAM (#9907)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-08-01 16:07:11 +10:00
tomaszduda23
f761404bf6 [nrf52, gpio] check different port notation (#9737) 2025-08-01 16:54:20 +12:00
tomaszduda23
e4dc62ea74 [nrf52, debug] debug component for nrf52 (#8315) 2025-08-01 16:53:40 +12:00
NP v/d Spek
c42c5dd946 [espnow] Basic communication between ESP32 devices (#9582)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-08-01 16:51:01 +12:00
Keith Burzinski
291215909a [sensor] A little bit of filter clean-up (#9986) 2025-08-01 02:55:59 +00:00
Jonathan Swoboda
0954a6185c [sensor] Fix bug in percentage based delta filter (#8157)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-08-01 02:15:56 +00:00
J. Nick Koston
f13e742bd5 [ruff] Enable RET and fix all violations (#9929) 2025-08-01 02:10:56 +00:00
tomaszduda23
7a4738ec4e [nrf52] add adc (#9321)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-08-01 13:49:39 +12:00
Clyde Stubbs
549b0d12b6 [image] Improve schemas (#9791) 2025-08-01 13:19:32 +12:00
Djordje Mandic
412f4ac341 [midea] Use c++17 constexpr and inline static in IrFollowMeData (#10002) 2025-07-31 14:28:22 -10:00
J. Nick Koston
d4ff1bcf5c [bluetooth_proxy] Implement dynamic service batching based on MTU constraints (#10001)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-31 14:15:12 -10:00
Clyde Stubbs
161f51e1f4 [esp32] Fix strapping pin validation for P4 and H2 (#9980) 2025-08-01 11:48:25 +12:00
Jonathan Swoboda
da0c47629a [esp32] Bump ESP32 platform to 54.03.21-2 (#10000) 2025-07-31 21:58:57 +00:00
J. Nick Koston
28b277c1c4 [bluetooth_proxy] Optimize UUID transmission with efficient short_uuid field (#9995) 2025-07-31 16:20:53 -05:00
dependabot[bot]
936a090aaa Bump aioesphomeapi from 37.2.0 to 37.2.1 (#9998)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-31 04:41:19 -10:00
dependabot[bot]
1be6d27012 Bump aioesphomeapi from 37.1.6 to 37.2.0 (#9996)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-30 23:20:43 -10:00
J. Nick Koston
71557c9f58 [bluetooth_proxy] Batch BLE service discovery messages for 67% reduction in API traffic (#9992) 2025-07-30 23:11:11 -05:00
J. Nick Koston
88cfcc1967 [esp32_ble_client] Fix BLE connection stability for WiFi-based proxies (#9993)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-07-30 22:36:33 -05:00
GilDev
fb379bbb88 [wifi] Allow fast_connect with multiple networks (#9947) 2025-07-31 15:34:49 +12:00
mrtoy-me
88d8cfe6a2 [tm1651] Remove dependency on Arduino Library (#9645)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
2025-07-30 20:20:55 -05:00
J. Nick Koston
f25abc3248 [esp32_ble] Fix spurious BLE 5.0 event warnings on ESP32-S3 (#9969) 2025-07-30 20:18:50 -05:00
J. Nick Koston
5b6e152d6c [esp32_touch] Work around ESP-IDF v5.4 regression in touch_pad_read_filtered (#9957)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-30 20:17:35 -05:00
J. Nick Koston
1d0a38446f [api] Reduce flash usage through targeted optimizations (#9979) 2025-07-30 20:10:23 -05:00
rwrozelle
853dca6c5c [api] Bump APIVersion to 1.11 (#9990) 2025-07-30 15:02:09 -10:00
Jesse Hills
97560fd9ef [CI] Add labels for checkboxes (#9991) 2025-07-31 12:17:20 +12:00
Clyde Stubbs
4b7f3355ea [core] Fix regex for lambda id() replacement (#9975) 2025-07-30 12:56:43 -10:00
dependabot[bot]
110eac4f09 Bump aioesphomeapi from 37.1.5 to 37.1.6 (#9988)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-30 12:46:01 -10:00
rwrozelle
79533cb0d7 media_player add off on capability (#9294)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2025-07-30 12:02:53 -10:00
dependabot[bot]
f4f69e827b Bump ruff from 0.12.5 to 0.12.7 (#9976)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-07-30 09:17:47 +00:00
dependabot[bot]
48a4dde824 Bump aioesphomeapi from 37.1.4 to 37.1.5 (#9977)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-30 09:16:00 +00:00
J. Nick Koston
9b4fe54f45 [esp32_ble_client] Fix connection failures with short discovery timeout devices and speed up BLE connections (#9971) 2025-07-29 19:19:12 -10:00
Keith Burzinski
913c58cd2c [template] Add tests for more sensor filters (#9973) 2025-07-30 14:20:25 +12:00
Keith Burzinski
374858efeb [sensor] Add new filter: `throttle_with_priority` (#9937) 2025-07-30 12:53:14 +12:00
Samuel Sieb
14dd48f9c3 [wifi] add more disconnect reason descriptions (#9955)
Co-authored-by: Samuel Sieb <samuel@sieb.net>
2025-07-30 12:41:31 +12:00
J. Nick Koston
76d33308d9 [api] Eliminate heap allocations when populating repeated fields from containers (#9948) 2025-07-30 10:41:37 +12:00
Dayowe
daccaf36a7 Fix WiFi to prefer strongest AP when multiple APs have same SSID (#9963)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-07-30 09:10:53 +12:00
Clyde Stubbs
56c88807ee [mipi_dsi] Add dependencies (#9952) 2025-07-30 08:16:32 +12:00
dependabot[bot]
9c6dbbd8ea Bump aioesphomeapi from 37.1.3 to 37.1.4 (#9964) 2025-07-29 17:43:35 +00:00
rwrozelle
a7dd849a8e Media player API enumeration alignment and feature flags (#9949)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-07-29 07:00:47 -10:00
Clyde Stubbs
1f0c606be4 [component] Revert setup messages to LOG_CONFIG level (#9956) 2025-07-29 07:32:45 +00:00
Jesse Hills
ace375944c [esp32] Fix post build (#9951)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-29 06:44:45 +00:00
Clyde Stubbs
5f7c2f771f [adc] Enable ADC on ESP32-P4 (#9954) 2025-07-29 18:20:37 +12:00
Jonathan Swoboda
3d5b602288 [esp32] Bump platform to 54.03.21-1 and add support for tagged releases (#9926)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-07-29 05:52:34 +00:00
Djordje Mandic
6d30269565 [output] Add set_min_power & set_max_power actions for FloatOutput (#8934)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-29 17:22:44 +12:00
Keith Burzinski
4ff3137c0d [gps] Fix slow parsing (#9953) 2025-07-29 17:21:52 +12:00
rwrozelle
9d43ddd6f1 Openthread add Teardown (#9275)
Co-authored-by: mc <mc@debian>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-29 16:25:17 +12:00
Jonathan Swoboda
f733c43dec [heatpumpir] Fix issue with IRremoteESP8266 being included on ESP32 (#9950) 2025-07-29 15:59:58 +12:00
Keith Burzinski
f5f0a01a85 [text_sensor] Add support for default filters (#9936) 2025-07-29 11:35:40 +12:00
Keith Burzinski
908891a096 [binary_sensor] Add support for default filters (#9935) 2025-07-29 11:35:11 +12:00
Keith Burzinski
7657316a92 [sensor] Add support for default filters (#9934) 2025-07-29 11:34:52 +12:00
J. Nick Koston
4f425c700a [esp32] Enable LWIP core locking on ESP-IDF to reduce socket operation overhead (#9857) 2025-07-29 11:33:54 +12:00
J. Nick Koston
2c9987869e [api] Align ProtoSize API design with ProtoWriteBuffer pattern (#9920) 2025-07-29 10:28:32 +12:00
J. Nick Koston
68f388f78e [api] Optimize protobuf empty message handling to reduce flash and runtime overhead (#9908) 2025-07-29 10:25:07 +12:00
377 changed files with 11708 additions and 3797 deletions

View File

@@ -168,6 +168,8 @@ This document provides essential context for AI models interacting with this pro
* `platformio.ini`: Configures the PlatformIO build environments for different microcontrollers. * `platformio.ini`: Configures the PlatformIO build environments for different microcontrollers.
* `.pre-commit-config.yaml`: Configures the pre-commit hooks for linting and formatting. * `.pre-commit-config.yaml`: Configures the pre-commit hooks for linting and formatting.
* **CI/CD Pipeline:** Defined in `.github/workflows`. * **CI/CD Pipeline:** Defined in `.github/workflows`.
* **Static Analysis & Development:**
* `esphome/core/defines.h`: A comprehensive header file containing all `#define` directives that can be added by components using `cg.add_define()` in Python. This file is used exclusively for development, static analysis tools, and CI testing - it is not used during runtime compilation. When developing components that add new defines, they must be added to this file to ensure proper IDE support and static analysis coverage. The file includes feature flags, build configurations, and platform-specific defines that help static analyzers understand the complete codebase without needing to compile for specific platforms.
## 6. Development & Testing Workflow ## 6. Development & Testing Workflow

View File

@@ -1 +1 @@
b7056e39f1484500ca2d237068670b789fe9241786b48da0681d646b25af05d5 6af8b429b94191fe8e239fcb3b73f7982d0266cb5b05ffbc81edaeac1bc8c273

View File

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

View File

@@ -22,7 +22,7 @@ jobs:
if: github.event.action != 'labeled' || github.event.sender.type != 'Bot' if: github.event.action != 'labeled' || github.event.sender.type != 'Bot'
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4.2.2 uses: actions/checkout@v5.0.0
- name: Generate a token - name: Generate a token
id: generate-token id: generate-token
@@ -63,7 +63,11 @@ jobs:
'needs-docs', 'needs-docs',
'needs-codeowners', 'needs-codeowners',
'too-big', 'too-big',
'labeller-recheck' 'labeller-recheck',
'bugfix',
'new-feature',
'breaking-change',
'code-quality'
]; ];
const DOCS_PR_PATTERNS = [ const DOCS_PR_PATTERNS = [
@@ -341,6 +345,31 @@ jobs:
return labels; return labels;
} }
// Strategy: PR Template Checkbox detection
async function detectPRTemplateCheckboxes() {
const labels = new Set();
const prBody = context.payload.pull_request.body || '';
console.log('Checking PR template checkboxes...');
// Check for checked checkboxes in the "Types of changes" section
const checkboxPatterns = [
{ pattern: /- \[x\] Bugfix \(non-breaking change which fixes an issue\)/i, label: 'bugfix' },
{ pattern: /- \[x\] New feature \(non-breaking change which adds functionality\)/i, label: 'new-feature' },
{ pattern: /- \[x\] Breaking change \(fix or feature that would cause existing functionality to not work as expected\)/i, label: 'breaking-change' },
{ pattern: /- \[x\] Code quality improvements to existing code or addition of tests/i, label: 'code-quality' }
];
for (const { pattern, label } of checkboxPatterns) {
if (pattern.test(prBody)) {
console.log(`Found checked checkbox for: ${label}`);
labels.add(label);
}
}
return labels;
}
// Strategy: Requirements detection // Strategy: Requirements detection
async function detectRequirements(allLabels) { async function detectRequirements(allLabels) {
const labels = new Set(); const labels = new Set();
@@ -351,7 +380,7 @@ jobs:
} }
// Check for missing docs // Check for missing docs
if (allLabels.has('new-component') || allLabels.has('new-platform')) { if (allLabels.has('new-component') || allLabels.has('new-platform') || allLabels.has('new-feature')) {
const prBody = context.payload.pull_request.body || ''; const prBody = context.payload.pull_request.body || '';
const hasDocsLink = DOCS_PR_PATTERNS.some(pattern => pattern.test(prBody)); const hasDocsLink = DOCS_PR_PATTERNS.some(pattern => pattern.test(prBody));
@@ -535,7 +564,8 @@ jobs:
dashboardLabels, dashboardLabels,
actionsLabels, actionsLabels,
codeOwnerLabels, codeOwnerLabels,
testLabels testLabels,
checkboxLabels
] = await Promise.all([ ] = await Promise.all([
detectMergeBranch(), detectMergeBranch(),
detectComponentPlatforms(apiData), detectComponentPlatforms(apiData),
@@ -546,7 +576,8 @@ jobs:
detectDashboardChanges(), detectDashboardChanges(),
detectGitHubActionsChanges(), detectGitHubActionsChanges(),
detectCodeOwner(), detectCodeOwner(),
detectTests() detectTests(),
detectPRTemplateCheckboxes()
]); ]);
// Combine all labels // Combine all labels
@@ -560,7 +591,8 @@ jobs:
...dashboardLabels, ...dashboardLabels,
...actionsLabels, ...actionsLabels,
...codeOwnerLabels, ...codeOwnerLabels,
...testLabels ...testLabels,
...checkboxLabels
]); ]);
// Detect requirements based on all other labels // Detect requirements based on all other labels

View File

@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4.2.2 uses: actions/checkout@v5.0.0
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5.6.0 uses: actions/setup-python@v5.6.0
with: with:

View File

@@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4.2.2 uses: actions/checkout@v5.0.0
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5.6.0 uses: actions/setup-python@v5.6.0

View File

@@ -43,7 +43,7 @@ jobs:
- "docker" - "docker"
# - "lint" # - "lint"
steps: steps:
- uses: actions/checkout@v4.2.2 - uses: actions/checkout@v5.0.0
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5.6.0 uses: actions/setup-python@v5.6.0
with: with:

View File

@@ -36,7 +36,7 @@ jobs:
cache-key: ${{ steps.cache-key.outputs.key }} cache-key: ${{ steps.cache-key.outputs.key }}
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v5.0.0
- name: Generate cache-key - name: Generate cache-key
id: cache-key id: cache-key
run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
@@ -47,7 +47,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v4.2.3 uses: actions/cache@v4.2.4
with: with:
path: venv path: venv
# yamllint disable-line rule:line-length # yamllint disable-line rule:line-length
@@ -70,7 +70,7 @@ jobs:
if: needs.determine-jobs.outputs.python-linters == 'true' if: needs.determine-jobs.outputs.python-linters == 'true'
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v5.0.0
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
@@ -91,7 +91,7 @@ jobs:
- common - common
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v5.0.0
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
@@ -136,7 +136,7 @@ jobs:
- common - common
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v5.0.0
- name: Restore Python - name: Restore Python
id: restore-python id: restore-python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
@@ -161,7 +161,7 @@ jobs:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
- name: Save Python virtual environment cache - name: Save Python virtual environment cache
if: github.ref == 'refs/heads/dev' if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@v4.2.3 uses: actions/cache/save@v4.2.4
with: with:
path: venv path: venv
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }} key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
@@ -179,7 +179,7 @@ jobs:
component-test-count: ${{ steps.determine.outputs.component-test-count }} component-test-count: ${{ steps.determine.outputs.component-test-count }}
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v5.0.0
with: with:
# Fetch enough history to find the merge base # Fetch enough history to find the merge base
fetch-depth: 2 fetch-depth: 2
@@ -214,7 +214,7 @@ jobs:
if: needs.determine-jobs.outputs.integration-tests == 'true' if: needs.determine-jobs.outputs.integration-tests == 'true'
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v5.0.0
- name: Set up Python 3.13 - name: Set up Python 3.13
id: python id: python
uses: actions/setup-python@v5.6.0 uses: actions/setup-python@v5.6.0
@@ -222,7 +222,7 @@ jobs:
python-version: "3.13" python-version: "3.13"
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v4.2.3 uses: actions/cache@v4.2.4
with: with:
path: venv path: venv
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }} key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
@@ -281,13 +281,13 @@ jobs:
pio_cache_key: tidyesp32-idf pio_cache_key: tidyesp32-idf
- id: clang-tidy - id: clang-tidy
name: Run script/clang-tidy for ZEPHYR name: Run script/clang-tidy for ZEPHYR
options: --environment nrf52-tidy --grep USE_ZEPHYR options: --environment nrf52-tidy --grep USE_ZEPHYR --grep USE_NRF52
pio_cache_key: tidy-zephyr pio_cache_key: tidy-zephyr
ignore_errors: false ignore_errors: false
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v5.0.0
with: with:
# Need history for HEAD~1 to work for checking changed files # Need history for HEAD~1 to work for checking changed files
fetch-depth: 2 fetch-depth: 2
@@ -300,14 +300,14 @@ jobs:
- name: Cache platformio - name: Cache platformio
if: github.ref == 'refs/heads/dev' if: github.ref == 'refs/heads/dev'
uses: actions/cache@v4.2.3 uses: actions/cache@v4.2.4
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }} key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
- name: Cache platformio - name: Cache platformio
if: github.ref != 'refs/heads/dev' if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@v4.2.3 uses: actions/cache/restore@v4.2.4
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }} key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
@@ -374,7 +374,7 @@ jobs:
sudo apt-get install libsdl2-dev sudo apt-get install libsdl2-dev
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v5.0.0
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
@@ -400,7 +400,7 @@ jobs:
matrix: ${{ steps.split.outputs.components }} matrix: ${{ steps.split.outputs.components }}
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v5.0.0
- name: Split components into 20 groups - name: Split components into 20 groups
id: split id: split
run: | run: |
@@ -430,7 +430,7 @@ jobs:
sudo apt-get install libsdl2-dev sudo apt-get install libsdl2-dev
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v5.0.0
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
@@ -459,7 +459,7 @@ jobs:
if: github.event_name == 'pull_request' && github.base_ref != 'beta' && github.base_ref != 'release' if: github.event_name == 'pull_request' && github.base_ref != 'beta' && github.base_ref != 'release'
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v5.0.0
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:

View File

@@ -54,7 +54,7 @@ jobs:
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v5.0.0
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL

View File

@@ -20,7 +20,7 @@ jobs:
branch_build: ${{ steps.tag.outputs.branch_build }} branch_build: ${{ steps.tag.outputs.branch_build }}
deploy_env: ${{ steps.tag.outputs.deploy_env }} deploy_env: ${{ steps.tag.outputs.deploy_env }}
steps: steps:
- uses: actions/checkout@v4.2.2 - uses: actions/checkout@v5.0.0
- name: Get tag - name: Get tag
id: tag id: tag
# yamllint disable rule:line-length # yamllint disable rule:line-length
@@ -60,7 +60,7 @@ jobs:
contents: read contents: read
id-token: write id-token: write
steps: steps:
- uses: actions/checkout@v4.2.2 - uses: actions/checkout@v5.0.0
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5.6.0 uses: actions/setup-python@v5.6.0
with: with:
@@ -92,7 +92,7 @@ jobs:
os: "ubuntu-24.04-arm" os: "ubuntu-24.04-arm"
steps: steps:
- uses: actions/checkout@v4.2.2 - uses: actions/checkout@v5.0.0
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5.6.0 uses: actions/setup-python@v5.6.0
with: with:
@@ -102,12 +102,12 @@ jobs:
uses: docker/setup-buildx-action@v3.11.1 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.5.0
with: with:
username: ${{ secrets.DOCKER_USER }} username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry - name: Log in to the GitHub container registry
uses: docker/login-action@v3.4.0 uses: docker/login-action@v3.5.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
@@ -168,10 +168,10 @@ jobs:
- ghcr - ghcr
- dockerhub - dockerhub
steps: steps:
- uses: actions/checkout@v4.2.2 - uses: actions/checkout@v5.0.0
- name: Download digests - name: Download digests
uses: actions/download-artifact@v4.3.0 uses: actions/download-artifact@v5.0.0
with: with:
pattern: digests-* pattern: digests-*
path: /tmp/digests path: /tmp/digests
@@ -182,13 +182,13 @@ jobs:
- name: Log in to docker hub - name: Log in to docker hub
if: matrix.registry == 'dockerhub' if: matrix.registry == 'dockerhub'
uses: docker/login-action@v3.4.0 uses: docker/login-action@v3.5.0
with: with:
username: ${{ secrets.DOCKER_USER }} username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry - name: Log in to the GitHub container registry
if: matrix.registry == 'ghcr' if: matrix.registry == 'ghcr'
uses: docker/login-action@v3.4.0 uses: docker/login-action@v3.5.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}

View File

@@ -13,10 +13,10 @@ jobs:
if: github.repository == 'esphome/esphome' if: github.repository == 'esphome/esphome'
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4.2.2 uses: actions/checkout@v5.0.0
- name: Checkout Home Assistant - name: Checkout Home Assistant
uses: actions/checkout@v4.2.2 uses: actions/checkout@v5.0.0
with: with:
repository: home-assistant/core repository: home-assistant/core
path: lib/home-assistant path: lib/home-assistant

View File

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

View File

@@ -40,11 +40,11 @@ esphome/components/analog_threshold/* @ianchi
esphome/components/animation/* @syndlex esphome/components/animation/* @syndlex
esphome/components/anova/* @buxtronix esphome/components/anova/* @buxtronix
esphome/components/apds9306/* @aodrenah esphome/components/apds9306/* @aodrenah
esphome/components/api/* @OttoWinter esphome/components/api/* @esphome/core
esphome/components/as5600/* @ammmze esphome/components/as5600/* @ammmze
esphome/components/as5600/sensor/* @ammmze esphome/components/as5600/sensor/* @ammmze
esphome/components/as7341/* @mrgnr esphome/components/as7341/* @mrgnr
esphome/components/async_tcp/* @OttoWinter esphome/components/async_tcp/* @esphome/core
esphome/components/at581x/* @X-Ryl669 esphome/components/at581x/* @X-Ryl669
esphome/components/atc_mithermometer/* @ahpohl esphome/components/atc_mithermometer/* @ahpohl
esphome/components/atm90e26/* @danieltwagner esphome/components/atm90e26/* @danieltwagner
@@ -69,7 +69,7 @@ esphome/components/bl0939/* @ziceva
esphome/components/bl0940/* @tobias- esphome/components/bl0940/* @tobias-
esphome/components/bl0942/* @dbuezas @dwmw2 esphome/components/bl0942/* @dbuezas @dwmw2
esphome/components/ble_client/* @buxtronix @clydebarrow esphome/components/ble_client/* @buxtronix @clydebarrow
esphome/components/bluetooth_proxy/* @jesserockz esphome/components/bluetooth_proxy/* @bdraco @jesserockz
esphome/components/bme280_base/* @esphome/core esphome/components/bme280_base/* @esphome/core
esphome/components/bme280_spi/* @apbodrov esphome/components/bme280_spi/* @apbodrov
esphome/components/bme680_bsec/* @trvrnrth esphome/components/bme680_bsec/* @trvrnrth
@@ -91,7 +91,7 @@ esphome/components/bytebuffer/* @clydebarrow
esphome/components/camera/* @DT-art1 @bdraco esphome/components/camera/* @DT-art1 @bdraco
esphome/components/canbus/* @danielschramm @mvturnho esphome/components/canbus/* @danielschramm @mvturnho
esphome/components/cap1188/* @mreditor97 esphome/components/cap1188/* @mreditor97
esphome/components/captive_portal/* @OttoWinter esphome/components/captive_portal/* @esphome/core
esphome/components/ccs811/* @habbie esphome/components/ccs811/* @habbie
esphome/components/cd74hc4067/* @asoehlke esphome/components/cd74hc4067/* @asoehlke
esphome/components/ch422g/* @clydebarrow @jesterret esphome/components/ch422g/* @clydebarrow @jesterret
@@ -118,7 +118,7 @@ esphome/components/dallas_temp/* @ssieb
esphome/components/daly_bms/* @s1lvi0 esphome/components/daly_bms/* @s1lvi0
esphome/components/dashboard_import/* @esphome/core esphome/components/dashboard_import/* @esphome/core
esphome/components/datetime/* @jesserockz @rfdarter esphome/components/datetime/* @jesserockz @rfdarter
esphome/components/debug/* @OttoWinter esphome/components/debug/* @esphome/core
esphome/components/delonghi/* @grob6000 esphome/components/delonghi/* @grob6000
esphome/components/dfplayer/* @glmnet esphome/components/dfplayer/* @glmnet
esphome/components/dfrobot_sen0395/* @niklasweber esphome/components/dfrobot_sen0395/* @niklasweber
@@ -144,9 +144,10 @@ esphome/components/es8156/* @kbx81
esphome/components/es8311/* @kahrendt @kroimon esphome/components/es8311/* @kahrendt @kroimon
esphome/components/es8388/* @P4uLT esphome/components/es8388/* @P4uLT
esphome/components/esp32/* @esphome/core esphome/components/esp32/* @esphome/core
esphome/components/esp32_ble/* @Rapsssito @jesserockz esphome/components/esp32_ble/* @Rapsssito @bdraco @jesserockz
esphome/components/esp32_ble_client/* @jesserockz esphome/components/esp32_ble_client/* @bdraco @jesserockz
esphome/components/esp32_ble_server/* @Rapsssito @clydebarrow @jesserockz esphome/components/esp32_ble_server/* @Rapsssito @clydebarrow @jesserockz
esphome/components/esp32_ble_tracker/* @bdraco
esphome/components/esp32_camera_web_server/* @ayufan esphome/components/esp32_camera_web_server/* @ayufan
esphome/components/esp32_can/* @Sympatron esphome/components/esp32_can/* @Sympatron
esphome/components/esp32_hosted/* @swoboda1337 esphome/components/esp32_hosted/* @swoboda1337
@@ -155,6 +156,7 @@ esphome/components/esp32_rmt/* @jesserockz
esphome/components/esp32_rmt_led_strip/* @jesserockz esphome/components/esp32_rmt_led_strip/* @jesserockz
esphome/components/esp8266/* @esphome/core esphome/components/esp8266/* @esphome/core
esphome/components/esp_ldo/* @clydebarrow esphome/components/esp_ldo/* @clydebarrow
esphome/components/espnow/* @jesserockz
esphome/components/ethernet_info/* @gtjadsonsantos esphome/components/ethernet_info/* @gtjadsonsantos
esphome/components/event/* @nohat esphome/components/event/* @nohat
esphome/components/event_emitter/* @Rapsssito esphome/components/event_emitter/* @Rapsssito
@@ -236,7 +238,7 @@ esphome/components/integration/* @OttoWinter
esphome/components/internal_temperature/* @Mat931 esphome/components/internal_temperature/* @Mat931
esphome/components/interval/* @esphome/core esphome/components/interval/* @esphome/core
esphome/components/jsn_sr04t/* @Mafus1 esphome/components/jsn_sr04t/* @Mafus1
esphome/components/json/* @OttoWinter esphome/components/json/* @esphome/core
esphome/components/kamstrup_kmp/* @cfeenstra1024 esphome/components/kamstrup_kmp/* @cfeenstra1024
esphome/components/key_collector/* @ssieb esphome/components/key_collector/* @ssieb
esphome/components/key_provider/* @ssieb esphome/components/key_provider/* @ssieb
@@ -244,6 +246,7 @@ esphome/components/kuntze/* @ssieb
esphome/components/lc709203f/* @ilikecake esphome/components/lc709203f/* @ilikecake
esphome/components/lcd_menu/* @numo68 esphome/components/lcd_menu/* @numo68
esphome/components/ld2410/* @regevbr @sebcaps esphome/components/ld2410/* @regevbr @sebcaps
esphome/components/ld2412/* @Rihan9
esphome/components/ld2420/* @descipher esphome/components/ld2420/* @descipher
esphome/components/ld2450/* @hareeshmu esphome/components/ld2450/* @hareeshmu
esphome/components/ld24xx/* @kbx81 esphome/components/ld24xx/* @kbx81
@@ -466,13 +469,13 @@ esphome/components/template/event/* @nohat
esphome/components/template/fan/* @ssieb esphome/components/template/fan/* @ssieb
esphome/components/text/* @mauritskorse esphome/components/text/* @mauritskorse
esphome/components/thermostat/* @kbx81 esphome/components/thermostat/* @kbx81
esphome/components/time/* @OttoWinter esphome/components/time/* @esphome/core
esphome/components/tlc5947/* @rnauber esphome/components/tlc5947/* @rnauber
esphome/components/tlc5971/* @IJIJI esphome/components/tlc5971/* @IJIJI
esphome/components/tm1621/* @Philippe12 esphome/components/tm1621/* @Philippe12
esphome/components/tm1637/* @glmnet esphome/components/tm1637/* @glmnet
esphome/components/tm1638/* @skykingjwc esphome/components/tm1638/* @skykingjwc
esphome/components/tm1651/* @freekode esphome/components/tm1651/* @mrtoy-me
esphome/components/tmp102/* @timsavage esphome/components/tmp102/* @timsavage
esphome/components/tmp1075/* @sybrenstuvel esphome/components/tmp1075/* @sybrenstuvel
esphome/components/tmp117/* @Azimath esphome/components/tmp117/* @Azimath
@@ -510,7 +513,7 @@ esphome/components/wake_on_lan/* @clydebarrow @willwill2will54
esphome/components/watchdog/* @oarcher esphome/components/watchdog/* @oarcher
esphome/components/waveshare_epaper/* @clydebarrow esphome/components/waveshare_epaper/* @clydebarrow
esphome/components/web_server/ota/* @esphome/core esphome/components/web_server/ota/* @esphome/core
esphome/components/web_server_base/* @OttoWinter esphome/components/web_server_base/* @esphome/core
esphome/components/web_server_idf/* @dentra esphome/components/web_server_idf/* @dentra
esphome/components/weikai/* @DrCoolZic esphome/components/weikai/* @DrCoolZic
esphome/components/weikai_i2c/* @DrCoolZic esphome/components/weikai_i2c/* @DrCoolZic

View File

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

View File

@@ -90,7 +90,7 @@ def main():
def run_command(*cmd, ignore_error: bool = False): def run_command(*cmd, ignore_error: bool = False):
print(f"$ {shlex.join(list(cmd))}") print(f"$ {shlex.join(list(cmd))}")
if not args.dry_run: if not args.dry_run:
rc = subprocess.call(list(cmd)) rc = subprocess.call(list(cmd), close_fds=False)
if rc != 0 and not ignore_error: if rc != 0 and not ignore_error:
print("Command failed") print("Command failed")
sys.exit(1) sys.exit(1)

View File

@@ -9,6 +9,7 @@ import os
import re import re
import sys import sys
import time import time
from typing import Protocol
import argcomplete import argcomplete
@@ -44,6 +45,7 @@ from esphome.const import (
from esphome.core import CORE, EsphomeError, coroutine from esphome.core import CORE, EsphomeError, coroutine
from esphome.helpers import get_bool_env, indent, is_ip_address from esphome.helpers import get_bool_env, indent, is_ip_address
from esphome.log import AnsiFore, color, setup_log from esphome.log import AnsiFore, color, setup_log
from esphome.types import ConfigType
from esphome.util import ( from esphome.util import (
get_serial_ports, get_serial_ports,
list_yaml_files, list_yaml_files,
@@ -55,6 +57,23 @@ from esphome.util import (
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class ArgsProtocol(Protocol):
device: list[str] | None
reset: bool
username: str | None
password: str | None
client_id: str | None
topic: str | None
file: str | None
no_logs: bool
only_generate: bool
show_secrets: bool
dashboard: bool
configuration: str
name: str
upload_speed: str | None
def choose_prompt(options, purpose: str = None): def choose_prompt(options, purpose: str = None):
if not options: if not options:
raise EsphomeError( raise EsphomeError(
@@ -88,30 +107,54 @@ def choose_prompt(options, purpose: str = None):
def choose_upload_log_host( def choose_upload_log_host(
default, check_default, show_ota, show_mqtt, show_api, purpose: str = None default: list[str] | str | None,
): check_default: str | None,
show_ota: bool,
show_mqtt: bool,
show_api: bool,
purpose: str | None = None,
) -> list[str]:
# Convert to list for uniform handling
defaults = [default] if isinstance(default, str) else default or []
# If devices specified, resolve them
if defaults:
resolved: list[str] = []
for device in defaults:
if device == "SERIAL":
serial_ports = get_serial_ports()
if not serial_ports:
_LOGGER.warning("No serial ports found, skipping SERIAL device")
continue
options = [
(f"{port.path} ({port.description})", port.path)
for port in serial_ports
]
resolved.append(choose_prompt(options, purpose=purpose))
elif device == "OTA":
if (show_ota and "ota" in CORE.config) or (
show_api and "api" in CORE.config
):
resolved.append(CORE.address)
elif show_mqtt and has_mqtt_logging():
resolved.append("MQTT")
else:
resolved.append(device)
return resolved
# No devices specified, show interactive chooser
options = [ options = [
(f"{port.path} ({port.description})", port.path) for port in get_serial_ports() (f"{port.path} ({port.description})", port.path) for port in get_serial_ports()
] ]
if default == "SERIAL":
return choose_prompt(options, purpose=purpose)
if (show_ota and "ota" in CORE.config) or (show_api and "api" in CORE.config): if (show_ota and "ota" in CORE.config) or (show_api and "api" in CORE.config):
options.append((f"Over The Air ({CORE.address})", CORE.address)) options.append((f"Over The Air ({CORE.address})", CORE.address))
if default == "OTA": if show_mqtt and has_mqtt_logging():
return CORE.address mqtt_config = CORE.config[CONF_MQTT]
if (
show_mqtt
and (mqtt_config := CORE.config.get(CONF_MQTT))
and mqtt_logging_enabled(mqtt_config)
):
options.append((f"MQTT ({mqtt_config[CONF_BROKER]})", "MQTT")) options.append((f"MQTT ({mqtt_config[CONF_BROKER]})", "MQTT"))
if default == "OTA":
return "MQTT"
if default is not None:
return default
if check_default is not None and check_default in [opt[1] for opt in options]: if check_default is not None and check_default in [opt[1] for opt in options]:
return check_default return [check_default]
return choose_prompt(options, purpose=purpose) return [choose_prompt(options, purpose=purpose)]
def mqtt_logging_enabled(mqtt_config): def mqtt_logging_enabled(mqtt_config):
@@ -123,7 +166,14 @@ def mqtt_logging_enabled(mqtt_config):
return log_topic.get(CONF_LEVEL, None) != "NONE" return log_topic.get(CONF_LEVEL, None) != "NONE"
def get_port_type(port): def has_mqtt_logging() -> bool:
"""Check if MQTT logging is available."""
return (mqtt_config := CORE.config.get(CONF_MQTT)) and mqtt_logging_enabled(
mqtt_config
)
def get_port_type(port: str) -> str:
if port.startswith("/") or port.startswith("COM"): if port.startswith("/") or port.startswith("COM"):
return "SERIAL" return "SERIAL"
if port == "MQTT": if port == "MQTT":
@@ -131,7 +181,7 @@ def get_port_type(port):
return "NETWORK" return "NETWORK"
def run_miniterm(config, port, args): def run_miniterm(config: ConfigType, port: str, args) -> int:
from aioesphomeapi import LogParser from aioesphomeapi import LogParser
import serial import serial
@@ -208,7 +258,7 @@ def wrap_to_code(name, comp):
return wrapped return wrapped
def write_cpp(config): def write_cpp(config: ConfigType) -> int:
if not get_bool_env(ENV_NOGITIGNORE): if not get_bool_env(ENV_NOGITIGNORE):
writer.write_gitignore() writer.write_gitignore()
@@ -216,7 +266,7 @@ def write_cpp(config):
return write_cpp_file() return write_cpp_file()
def generate_cpp_contents(config): def generate_cpp_contents(config: ConfigType) -> None:
_LOGGER.info("Generating C++ source...") _LOGGER.info("Generating C++ source...")
for name, component, conf in iter_component_configs(CORE.config): for name, component, conf in iter_component_configs(CORE.config):
@@ -227,7 +277,7 @@ def generate_cpp_contents(config):
CORE.flush_tasks() CORE.flush_tasks()
def write_cpp_file(): def write_cpp_file() -> int:
code_s = indent(CORE.cpp_main_section) code_s = indent(CORE.cpp_main_section)
writer.write_cpp(code_s) writer.write_cpp(code_s)
@@ -238,7 +288,7 @@ def write_cpp_file():
return 0 return 0
def compile_program(args, config): def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
from esphome import platformio_api from esphome import platformio_api
_LOGGER.info("Compiling app...") _LOGGER.info("Compiling app...")
@@ -249,7 +299,9 @@ def compile_program(args, config):
return 0 if idedata is not None else 1 return 0 if idedata is not None else 1
def upload_using_esptool(config, port, file, speed): def upload_using_esptool(
config: ConfigType, port: str, file: str, speed: int
) -> str | int:
from esphome import platformio_api from esphome import platformio_api
first_baudrate = speed or config[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS].get( first_baudrate = speed or config[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS].get(
@@ -277,20 +329,20 @@ def upload_using_esptool(config, port, file, speed):
def run_esptool(baud_rate): def run_esptool(baud_rate):
cmd = [ cmd = [
"esptool.py", "esptool",
"--before", "--before",
"default_reset", "default-reset",
"--after", "--after",
"hard_reset", "hard-reset",
"--baud", "--baud",
str(baud_rate), str(baud_rate),
"--port", "--port",
port, port,
"--chip", "--chip",
mcu, mcu,
"write_flash", "write-flash",
"-z", "-z",
"--flash_size", "--flash-size",
"detect", "detect",
] ]
for img in flash_images: for img in flash_images:
@@ -314,7 +366,7 @@ def upload_using_esptool(config, port, file, speed):
return run_esptool(115200) return run_esptool(115200)
def upload_using_platformio(config, port): def upload_using_platformio(config: ConfigType, port: str):
from esphome import platformio_api from esphome import platformio_api
upload_args = ["-t", "upload", "-t", "nobuild"] upload_args = ["-t", "upload", "-t", "nobuild"]
@@ -323,7 +375,7 @@ def upload_using_platformio(config, port):
return platformio_api.run_platformio_cli_run(config, CORE.verbose, *upload_args) return platformio_api.run_platformio_cli_run(config, CORE.verbose, *upload_args)
def check_permissions(port): def check_permissions(port: str):
if os.name == "posix" and get_port_type(port) == "SERIAL": if os.name == "posix" and get_port_type(port) == "SERIAL":
# Check if we can open selected serial port # Check if we can open selected serial port
if not os.access(port, os.F_OK): if not os.access(port, os.F_OK):
@@ -341,7 +393,7 @@ def check_permissions(port):
) )
def upload_program(config, args, host): def upload_program(config: ConfigType, args: ArgsProtocol, host: str) -> int | str:
try: try:
module = importlib.import_module("esphome.components." + CORE.target_platform) module = importlib.import_module("esphome.components." + CORE.target_platform)
if getattr(module, "upload_program")(config, args, host): if getattr(module, "upload_program")(config, args, host):
@@ -356,7 +408,7 @@ def upload_program(config, args, host):
return upload_using_esptool(config, host, file, args.upload_speed) return upload_using_esptool(config, host, file, args.upload_speed)
if CORE.target_platform in (PLATFORM_RP2040): if CORE.target_platform in (PLATFORM_RP2040):
return upload_using_platformio(config, args.device) return upload_using_platformio(config, host)
if CORE.is_libretiny: if CORE.is_libretiny:
return upload_using_platformio(config, host) return upload_using_platformio(config, host)
@@ -379,9 +431,12 @@ def upload_program(config, args, host):
remote_port = int(ota_conf[CONF_PORT]) remote_port = int(ota_conf[CONF_PORT])
password = ota_conf.get(CONF_PASSWORD, "") password = ota_conf.get(CONF_PASSWORD, "")
# Check if we should use MQTT for address resolution
# This happens when no device was specified, or the current host is "MQTT"/"OTA"
devices: list[str] = args.device or []
if ( if (
CONF_MQTT in config # pylint: disable=too-many-boolean-expressions CONF_MQTT in config # pylint: disable=too-many-boolean-expressions
and (not args.device or args.device in ("MQTT", "OTA")) and (not devices or host in ("MQTT", "OTA"))
and ( and (
((config[CONF_MDNS][CONF_DISABLED]) and not is_ip_address(CORE.address)) ((config[CONF_MDNS][CONF_DISABLED]) and not is_ip_address(CORE.address))
or get_port_type(host) == "MQTT" or get_port_type(host) == "MQTT"
@@ -399,23 +454,28 @@ def upload_program(config, args, host):
return espota2.run_ota(host, remote_port, password, CORE.firmware_bin) return espota2.run_ota(host, remote_port, password, CORE.firmware_bin)
def show_logs(config, args, port): def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int | None:
if "logger" not in config: if "logger" not in config:
raise EsphomeError("Logger is not configured!") raise EsphomeError("Logger is not configured!")
port = devices[0]
if get_port_type(port) == "SERIAL": if get_port_type(port) == "SERIAL":
check_permissions(port) check_permissions(port)
return run_miniterm(config, port, args) return run_miniterm(config, port, args)
if get_port_type(port) == "NETWORK" and "api" in config: if get_port_type(port) == "NETWORK" and "api" in config:
addresses_to_use = devices
if config[CONF_MDNS][CONF_DISABLED] and CONF_MQTT in config: if config[CONF_MDNS][CONF_DISABLED] and CONF_MQTT in config:
from esphome import mqtt from esphome import mqtt
port = mqtt.get_esphome_device_ip( mqtt_address = mqtt.get_esphome_device_ip(
config, args.username, args.password, args.client_id config, args.username, args.password, args.client_id
)[0] )[0]
addresses_to_use = [mqtt_address]
from esphome.components.api.client import run_logs from esphome.components.api.client import run_logs
return run_logs(config, port) return run_logs(config, addresses_to_use)
if get_port_type(port) == "MQTT" and "mqtt" in config: if get_port_type(port) == "MQTT" and "mqtt" in config:
from esphome import mqtt from esphome import mqtt
@@ -426,7 +486,7 @@ def show_logs(config, args, port):
raise EsphomeError("No remote or local logging method configured (api/mqtt/logger)") raise EsphomeError("No remote or local logging method configured (api/mqtt/logger)")
def clean_mqtt(config, args): def clean_mqtt(config: ConfigType, args: ArgsProtocol) -> int | None:
from esphome import mqtt from esphome import mqtt
return mqtt.clear_topic( return mqtt.clear_topic(
@@ -434,13 +494,13 @@ def clean_mqtt(config, args):
) )
def command_wizard(args): def command_wizard(args: ArgsProtocol) -> int | None:
from esphome import wizard from esphome import wizard
return wizard.wizard(args.configuration) return wizard.wizard(args.configuration)
def command_config(args, config): def command_config(args: ArgsProtocol, config: ConfigType) -> int | None:
if not CORE.verbose: if not CORE.verbose:
config = strip_default_ids(config) config = strip_default_ids(config)
output = yaml_util.dump(config, args.show_secrets) output = yaml_util.dump(config, args.show_secrets)
@@ -455,7 +515,7 @@ def command_config(args, config):
return 0 return 0
def command_vscode(args): def command_vscode(args: ArgsProtocol) -> int | None:
from esphome import vscode from esphome import vscode
logging.disable(logging.INFO) logging.disable(logging.INFO)
@@ -463,7 +523,7 @@ def command_vscode(args):
vscode.read_config(args) vscode.read_config(args)
def command_compile(args, config): def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
exit_code = write_cpp(config) exit_code = write_cpp(config)
if exit_code != 0: if exit_code != 0:
return exit_code return exit_code
@@ -477,8 +537,9 @@ def command_compile(args, config):
return 0 return 0
def command_upload(args, config): def command_upload(args: ArgsProtocol, config: ConfigType) -> int | None:
port = choose_upload_log_host( # Get devices, resolving special identifiers like OTA
devices = choose_upload_log_host(
default=args.device, default=args.device,
check_default=None, check_default=None,
show_ota=True, show_ota=True,
@@ -486,14 +547,22 @@ def command_upload(args, config):
show_api=False, show_api=False,
purpose="uploading", purpose="uploading",
) )
exit_code = upload_program(config, args, port)
if exit_code != 0: # Try each device until one succeeds
return exit_code exit_code = 1
_LOGGER.info("Successfully uploaded program.") for device in devices:
return 0 _LOGGER.info("Uploading to %s", device)
exit_code = upload_program(config, args, device)
if exit_code == 0:
_LOGGER.info("Successfully uploaded program.")
return 0
if len(devices) > 1:
_LOGGER.warning("Failed to upload to %s", device)
return exit_code
def command_discover(args, config): def command_discover(args: ArgsProtocol, config: ConfigType) -> int | None:
if "mqtt" in config: if "mqtt" in config:
from esphome import mqtt from esphome import mqtt
@@ -502,8 +571,9 @@ def command_discover(args, config):
raise EsphomeError("No discover method configured (mqtt)") raise EsphomeError("No discover method configured (mqtt)")
def command_logs(args, config): def command_logs(args: ArgsProtocol, config: ConfigType) -> int | None:
port = choose_upload_log_host( # Get devices, resolving special identifiers like OTA
devices = choose_upload_log_host(
default=args.device, default=args.device,
check_default=None, check_default=None,
show_ota=False, show_ota=False,
@@ -511,10 +581,10 @@ def command_logs(args, config):
show_api=True, show_api=True,
purpose="logging", purpose="logging",
) )
return show_logs(config, args, port) return show_logs(config, args, devices)
def command_run(args, config): def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
exit_code = write_cpp(config) exit_code = write_cpp(config)
if exit_code != 0: if exit_code != 0:
return exit_code return exit_code
@@ -531,7 +601,8 @@ def command_run(args, config):
program_path = idedata.raw["prog_path"] program_path = idedata.raw["prog_path"]
return run_external_process(program_path) return run_external_process(program_path)
port = choose_upload_log_host( # Get devices, resolving special identifiers like OTA
devices = choose_upload_log_host(
default=args.device, default=args.device,
check_default=None, check_default=None,
show_ota=True, show_ota=True,
@@ -539,39 +610,53 @@ def command_run(args, config):
show_api=True, show_api=True,
purpose="uploading", purpose="uploading",
) )
exit_code = upload_program(config, args, port)
if exit_code != 0: # Try each device for upload until one succeeds
successful_device: str | None = None
for device in devices:
_LOGGER.info("Uploading to %s", device)
exit_code = upload_program(config, args, device)
if exit_code == 0:
_LOGGER.info("Successfully uploaded program.")
successful_device = device
break
if len(devices) > 1:
_LOGGER.warning("Failed to upload to %s", device)
if successful_device is None:
return exit_code return exit_code
_LOGGER.info("Successfully uploaded program.")
if args.no_logs: if args.no_logs:
return 0 return 0
port = choose_upload_log_host(
default=args.device, # For logs, prefer the device we successfully uploaded to
check_default=port, devices = choose_upload_log_host(
default=successful_device,
check_default=successful_device,
show_ota=False, show_ota=False,
show_mqtt=True, show_mqtt=True,
show_api=True, show_api=True,
purpose="logging", purpose="logging",
) )
return show_logs(config, args, port) return show_logs(config, args, devices)
def command_clean_mqtt(args, config): def command_clean_mqtt(args: ArgsProtocol, config: ConfigType) -> int | None:
return clean_mqtt(config, args) return clean_mqtt(config, args)
def command_mqtt_fingerprint(args, config): def command_mqtt_fingerprint(args: ArgsProtocol, config: ConfigType) -> int | None:
from esphome import mqtt from esphome import mqtt
return mqtt.get_fingerprint(config) return mqtt.get_fingerprint(config)
def command_version(args): def command_version(args: ArgsProtocol) -> int | None:
safe_print(f"Version: {const.__version__}") safe_print(f"Version: {const.__version__}")
return 0 return 0
def command_clean(args, config): def command_clean(args: ArgsProtocol, config: ConfigType) -> int | None:
try: try:
writer.clean_build() writer.clean_build()
except OSError as err: except OSError as err:
@@ -581,13 +666,13 @@ def command_clean(args, config):
return 0 return 0
def command_dashboard(args): def command_dashboard(args: ArgsProtocol) -> int | None:
from esphome.dashboard import dashboard from esphome.dashboard import dashboard
return dashboard.start_dashboard(args) return dashboard.start_dashboard(args)
def command_update_all(args): def command_update_all(args: ArgsProtocol) -> int | None:
import click import click
success = {} success = {}
@@ -634,7 +719,7 @@ def command_update_all(args):
return failed return failed
def command_idedata(args, config): def command_idedata(args: ArgsProtocol, config: ConfigType) -> int:
import json import json
from esphome import platformio_api from esphome import platformio_api
@@ -650,7 +735,7 @@ def command_idedata(args, config):
return 0 return 0
def command_rename(args, config): def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None:
for c in args.name: for c in args.name:
if c not in ALLOWED_NAME_CHARS: if c not in ALLOWED_NAME_CHARS:
print( print(
@@ -767,6 +852,12 @@ POST_CONFIG_ACTIONS = {
"discover": command_discover, "discover": command_discover,
} }
SIMPLE_CONFIG_ACTIONS = [
"clean",
"clean-mqtt",
"config",
]
def parse_args(argv): def parse_args(argv):
options_parser = argparse.ArgumentParser(add_help=False) options_parser = argparse.ArgumentParser(add_help=False)
@@ -854,7 +945,8 @@ def parse_args(argv):
) )
parser_upload.add_argument( parser_upload.add_argument(
"--device", "--device",
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.", action="append",
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.",
) )
parser_upload.add_argument( parser_upload.add_argument(
"--upload_speed", "--upload_speed",
@@ -876,7 +968,8 @@ def parse_args(argv):
) )
parser_logs.add_argument( parser_logs.add_argument(
"--device", "--device",
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.", action="append",
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.",
) )
parser_logs.add_argument( parser_logs.add_argument(
"--reset", "--reset",
@@ -905,7 +998,8 @@ def parse_args(argv):
) )
parser_run.add_argument( parser_run.add_argument(
"--device", "--device",
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.", action="append",
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.",
) )
parser_run.add_argument( parser_run.add_argument(
"--upload_speed", "--upload_speed",
@@ -1032,6 +1126,13 @@ def parse_args(argv):
arguments = argv[1:] arguments = argv[1:]
argcomplete.autocomplete(parser) argcomplete.autocomplete(parser)
if len(arguments) > 0 and arguments[0] in SIMPLE_CONFIG_ACTIONS:
args, unknown_args = parser.parse_known_args(arguments)
if unknown_args:
_LOGGER.warning("Ignored unrecognized arguments: %s", unknown_args)
return args
return parser.parse_args(arguments) return parser.parse_args(arguments)

View File

@@ -391,8 +391,7 @@ async def build_action(full_config, template_arg, args):
) )
action_id = full_config[CONF_TYPE_ID] action_id = full_config[CONF_TYPE_ID]
builder = registry_entry.coroutine_fun builder = registry_entry.coroutine_fun
ret = await builder(config, action_id, template_arg, args) return await builder(config, action_id, template_arg, args)
return ret
async def build_action_list(config, templ, arg_type): async def build_action_list(config, templ, arg_type):
@@ -409,8 +408,7 @@ async def build_condition(full_config, template_arg, args):
) )
action_id = full_config[CONF_TYPE_ID] action_id = full_config[CONF_TYPE_ID]
builder = registry_entry.coroutine_fun builder = registry_entry.coroutine_fun
ret = await builder(config, action_id, template_arg, args) return await builder(config, action_id, template_arg, args)
return ret
async def build_condition_list(config, templ, args): async def build_condition_list(config, templ, args):

View File

@@ -5,7 +5,7 @@ from esphome.const import (
CONF_EQUATION, CONF_EQUATION,
CONF_HUMIDITY, CONF_HUMIDITY,
CONF_TEMPERATURE, CONF_TEMPERATURE,
ICON_WATER, DEVICE_CLASS_ABSOLUTE_HUMIDITY,
STATE_CLASS_MEASUREMENT, STATE_CLASS_MEASUREMENT,
UNIT_GRAMS_PER_CUBIC_METER, UNIT_GRAMS_PER_CUBIC_METER,
) )
@@ -27,8 +27,8 @@ EQUATION = {
CONFIG_SCHEMA = ( CONFIG_SCHEMA = (
sensor.sensor_schema( sensor.sensor_schema(
unit_of_measurement=UNIT_GRAMS_PER_CUBIC_METER, unit_of_measurement=UNIT_GRAMS_PER_CUBIC_METER,
icon=ICON_WATER,
accuracy_decimals=2, accuracy_decimals=2,
device_class=DEVICE_CLASS_ABSOLUTE_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
) )
.extend( .extend(

View File

@@ -1,6 +1,6 @@
from esphome import pins from esphome import pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components.esp32 import get_esp32_variant from esphome.components.esp32 import VARIANT_ESP32P4, get_esp32_variant
from esphome.components.esp32.const import ( from esphome.components.esp32.const import (
VARIANT_ESP32, VARIANT_ESP32,
VARIANT_ESP32C2, VARIANT_ESP32C2,
@@ -140,6 +140,16 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
9: adc_channel_t.ADC_CHANNEL_8, 9: adc_channel_t.ADC_CHANNEL_8,
10: adc_channel_t.ADC_CHANNEL_9, 10: adc_channel_t.ADC_CHANNEL_9,
}, },
VARIANT_ESP32P4: {
16: adc_channel_t.ADC_CHANNEL_0,
17: adc_channel_t.ADC_CHANNEL_1,
18: adc_channel_t.ADC_CHANNEL_2,
19: adc_channel_t.ADC_CHANNEL_3,
20: adc_channel_t.ADC_CHANNEL_4,
21: adc_channel_t.ADC_CHANNEL_5,
22: adc_channel_t.ADC_CHANNEL_6,
23: adc_channel_t.ADC_CHANNEL_7,
},
} }
# pin to adc2 channel mapping # pin to adc2 channel mapping
@@ -198,6 +208,14 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
19: adc_channel_t.ADC_CHANNEL_8, 19: adc_channel_t.ADC_CHANNEL_8,
20: adc_channel_t.ADC_CHANNEL_9, 20: adc_channel_t.ADC_CHANNEL_9,
}, },
VARIANT_ESP32P4: {
49: adc_channel_t.ADC_CHANNEL_0,
50: adc_channel_t.ADC_CHANNEL_1,
51: adc_channel_t.ADC_CHANNEL_2,
52: adc_channel_t.ADC_CHANNEL_3,
53: adc_channel_t.ADC_CHANNEL_4,
54: adc_channel_t.ADC_CHANNEL_5,
},
} }
@@ -249,6 +267,11 @@ def validate_adc_pin(value):
{CONF_ANALOG: True, CONF_INPUT: True}, internal=True {CONF_ANALOG: True, CONF_INPUT: True}, internal=True
)(value) )(value)
if CORE.is_nrf52:
return pins.gpio_pin_schema(
{CONF_ANALOG: True, CONF_INPUT: True}, internal=True
)(value)
raise NotImplementedError raise NotImplementedError
@@ -265,5 +288,6 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
PlatformFramework.RTL87XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO,
PlatformFramework.LN882X_ARDUINO, PlatformFramework.LN882X_ARDUINO,
}, },
"adc_sensor_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR},
} }
) )

View File

@@ -13,6 +13,10 @@
#include "hal/adc_types.h" // This defines ADC_CHANNEL_MAX #include "hal/adc_types.h" // This defines ADC_CHANNEL_MAX
#endif // USE_ESP32 #endif // USE_ESP32
#ifdef USE_ZEPHYR
#include <zephyr/drivers/adc.h>
#endif
namespace esphome { namespace esphome {
namespace adc { namespace adc {
@@ -38,15 +42,15 @@ enum class SamplingMode : uint8_t {
const LogString *sampling_mode_to_str(SamplingMode mode); const LogString *sampling_mode_to_str(SamplingMode mode);
class Aggregator { template<typename T> class Aggregator {
public: public:
Aggregator(SamplingMode mode); Aggregator(SamplingMode mode);
void add_sample(uint32_t value); void add_sample(T value);
uint32_t aggregate(); T aggregate();
protected: protected:
uint32_t aggr_{0}; T aggr_{0};
uint32_t samples_{0}; uint8_t samples_{0};
SamplingMode mode_{SamplingMode::AVG}; SamplingMode mode_{SamplingMode::AVG};
}; };
@@ -69,6 +73,11 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
/// @return A float representing the setup priority. /// @return A float representing the setup priority.
float get_setup_priority() const override; float get_setup_priority() const override;
#ifdef USE_ZEPHYR
/// Set the ADC channel to be used by the ADC sensor.
/// @param channel Pointer to an adc_dt_spec structure representing the ADC channel.
void set_adc_channel(const adc_dt_spec *channel) { this->channel_ = channel; }
#endif
/// Set the GPIO pin to be used by the ADC sensor. /// Set the GPIO pin to be used by the ADC sensor.
/// @param pin Pointer to an InternalGPIOPin representing the ADC input pin. /// @param pin Pointer to an InternalGPIOPin representing the ADC input pin.
void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; } void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; }
@@ -136,8 +145,8 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
adc_oneshot_unit_handle_t adc_handle_{nullptr}; adc_oneshot_unit_handle_t adc_handle_{nullptr};
adc_cali_handle_t calibration_handle_{nullptr}; adc_cali_handle_t calibration_handle_{nullptr};
adc_atten_t attenuation_{ADC_ATTEN_DB_0}; adc_atten_t attenuation_{ADC_ATTEN_DB_0};
adc_channel_t channel_; adc_channel_t channel_{};
adc_unit_t adc_unit_; adc_unit_t adc_unit_{};
struct SetupFlags { struct SetupFlags {
uint8_t init_complete : 1; uint8_t init_complete : 1;
uint8_t config_complete : 1; uint8_t config_complete : 1;
@@ -151,6 +160,10 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
#ifdef USE_RP2040 #ifdef USE_RP2040
bool is_temperature_{false}; bool is_temperature_{false};
#endif // USE_RP2040 #endif // USE_RP2040
#ifdef USE_ZEPHYR
const struct adc_dt_spec *channel_ = nullptr;
#endif
}; };
} // namespace adc } // namespace adc

View File

@@ -18,15 +18,15 @@ const LogString *sampling_mode_to_str(SamplingMode mode) {
return LOG_STR("unknown"); return LOG_STR("unknown");
} }
Aggregator::Aggregator(SamplingMode mode) { template<typename T> Aggregator<T>::Aggregator(SamplingMode mode) {
this->mode_ = mode; this->mode_ = mode;
// set to max uint if mode is "min" // set to max uint if mode is "min"
if (mode == SamplingMode::MIN) { if (mode == SamplingMode::MIN) {
this->aggr_ = UINT32_MAX; this->aggr_ = std::numeric_limits<T>::max();
} }
} }
void Aggregator::add_sample(uint32_t value) { template<typename T> void Aggregator<T>::add_sample(T value) {
this->samples_ += 1; this->samples_ += 1;
switch (this->mode_) { switch (this->mode_) {
@@ -47,7 +47,7 @@ void Aggregator::add_sample(uint32_t value) {
} }
} }
uint32_t Aggregator::aggregate() { template<typename T> T Aggregator<T>::aggregate() {
if (this->mode_ == SamplingMode::AVG) { if (this->mode_ == SamplingMode::AVG) {
if (this->samples_ == 0) { if (this->samples_ == 0) {
return this->aggr_; return this->aggr_;
@@ -59,6 +59,12 @@ uint32_t Aggregator::aggregate() {
return this->aggr_; return this->aggr_;
} }
#ifdef USE_ZEPHYR
template class Aggregator<int32_t>;
#else
template class Aggregator<uint32_t>;
#endif
void ADCSensor::update() { void ADCSensor::update() {
float value_v = this->sample(); float value_v = this->sample();
ESP_LOGV(TAG, "'%s': Voltage=%.4fV", this->get_name().c_str(), value_v); ESP_LOGV(TAG, "'%s': Voltage=%.4fV", this->get_name().c_str(), value_v);

View File

@@ -72,10 +72,9 @@ void ADCSensor::setup() {
// Initialize ADC calibration // Initialize ADC calibration
if (this->calibration_handle_ == nullptr) { if (this->calibration_handle_ == nullptr) {
adc_cali_handle_t handle = nullptr; adc_cali_handle_t handle = nullptr;
esp_err_t err;
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
// RISC-V variants and S3 use curve fitting calibration // RISC-V variants and S3 use curve fitting calibration
adc_cali_curve_fitting_config_t cali_config = {}; // Zero initialize first adc_cali_curve_fitting_config_t cali_config = {}; // Zero initialize first
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
@@ -153,7 +152,7 @@ float ADCSensor::sample() {
} }
float ADCSensor::sample_fixed_attenuation_() { float ADCSensor::sample_fixed_attenuation_() {
auto aggr = Aggregator(this->sampling_mode_); auto aggr = Aggregator<uint32_t>(this->sampling_mode_);
for (uint8_t sample = 0; sample < this->sample_count_; sample++) { for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
int raw; int raw;
@@ -187,7 +186,7 @@ float ADCSensor::sample_fixed_attenuation_() {
ESP_LOGW(TAG, "ADC calibration conversion failed with error %d, disabling calibration", err); ESP_LOGW(TAG, "ADC calibration conversion failed with error %d, disabling calibration", err);
if (this->calibration_handle_ != nullptr) { if (this->calibration_handle_ != nullptr) {
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
adc_cali_delete_scheme_curve_fitting(this->calibration_handle_); adc_cali_delete_scheme_curve_fitting(this->calibration_handle_);
#else // Other ESP32 variants use line fitting calibration #else // Other ESP32 variants use line fitting calibration
adc_cali_delete_scheme_line_fitting(this->calibration_handle_); adc_cali_delete_scheme_line_fitting(this->calibration_handle_);
@@ -220,7 +219,7 @@ float ADCSensor::sample_autorange_() {
if (this->calibration_handle_ != nullptr) { if (this->calibration_handle_ != nullptr) {
// Delete old calibration handle // Delete old calibration handle
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
adc_cali_delete_scheme_curve_fitting(this->calibration_handle_); adc_cali_delete_scheme_curve_fitting(this->calibration_handle_);
#else #else
adc_cali_delete_scheme_line_fitting(this->calibration_handle_); adc_cali_delete_scheme_line_fitting(this->calibration_handle_);
@@ -232,7 +231,7 @@ float ADCSensor::sample_autorange_() {
adc_cali_handle_t handle = nullptr; adc_cali_handle_t handle = nullptr;
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
adc_cali_curve_fitting_config_t cali_config = {}; adc_cali_curve_fitting_config_t cali_config = {};
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
cali_config.chan = this->channel_; cali_config.chan = this->channel_;
@@ -261,7 +260,7 @@ float ADCSensor::sample_autorange_() {
ESP_LOGW(TAG, "ADC read failed in autorange with error %d", err); ESP_LOGW(TAG, "ADC read failed in autorange with error %d", err);
if (handle != nullptr) { if (handle != nullptr) {
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
adc_cali_delete_scheme_curve_fitting(handle); adc_cali_delete_scheme_curve_fitting(handle);
#else #else
adc_cali_delete_scheme_line_fitting(handle); adc_cali_delete_scheme_line_fitting(handle);
@@ -281,7 +280,7 @@ float ADCSensor::sample_autorange_() {
} }
// Clean up calibration handle // Clean up calibration handle
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
adc_cali_delete_scheme_curve_fitting(handle); adc_cali_delete_scheme_curve_fitting(handle);
#else #else
adc_cali_delete_scheme_line_fitting(handle); adc_cali_delete_scheme_line_fitting(handle);

View File

@@ -37,7 +37,7 @@ void ADCSensor::dump_config() {
} }
float ADCSensor::sample() { float ADCSensor::sample() {
auto aggr = Aggregator(this->sampling_mode_); auto aggr = Aggregator<uint32_t>(this->sampling_mode_);
for (uint8_t sample = 0; sample < this->sample_count_; sample++) { for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
uint32_t raw = 0; uint32_t raw = 0;

View File

@@ -30,7 +30,7 @@ void ADCSensor::dump_config() {
float ADCSensor::sample() { float ADCSensor::sample() {
uint32_t raw = 0; uint32_t raw = 0;
auto aggr = Aggregator(this->sampling_mode_); auto aggr = Aggregator<uint32_t>(this->sampling_mode_);
if (this->output_raw_) { if (this->output_raw_) {
for (uint8_t sample = 0; sample < this->sample_count_; sample++) { for (uint8_t sample = 0; sample < this->sample_count_; sample++) {

View File

@@ -41,7 +41,7 @@ void ADCSensor::dump_config() {
float ADCSensor::sample() { float ADCSensor::sample() {
uint32_t raw = 0; uint32_t raw = 0;
auto aggr = Aggregator(this->sampling_mode_); auto aggr = Aggregator<uint32_t>(this->sampling_mode_);
if (this->is_temperature_) { if (this->is_temperature_) {
adc_set_temp_sensor_enabled(true); adc_set_temp_sensor_enabled(true);

View File

@@ -0,0 +1,207 @@
#include "adc_sensor.h"
#ifdef USE_ZEPHYR
#include "esphome/core/log.h"
#include "hal/nrf_saadc.h"
namespace esphome {
namespace adc {
static const char *const TAG = "adc.zephyr";
void ADCSensor::setup() {
if (!adc_is_ready_dt(this->channel_)) {
ESP_LOGE(TAG, "ADC controller device %s not ready", this->channel_->dev->name);
return;
}
auto err = adc_channel_setup_dt(this->channel_);
if (err < 0) {
ESP_LOGE(TAG, "Could not setup channel %s (%d)", this->channel_->dev->name, err);
return;
}
}
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
static const LogString *gain_to_str(enum adc_gain gain) {
switch (gain) {
case ADC_GAIN_1_6:
return LOG_STR("1/6");
case ADC_GAIN_1_5:
return LOG_STR("1/5");
case ADC_GAIN_1_4:
return LOG_STR("1/4");
case ADC_GAIN_1_3:
return LOG_STR("1/3");
case ADC_GAIN_2_5:
return LOG_STR("2/5");
case ADC_GAIN_1_2:
return LOG_STR("1/2");
case ADC_GAIN_2_3:
return LOG_STR("2/3");
case ADC_GAIN_4_5:
return LOG_STR("4/5");
case ADC_GAIN_1:
return LOG_STR("1");
case ADC_GAIN_2:
return LOG_STR("2");
case ADC_GAIN_3:
return LOG_STR("3");
case ADC_GAIN_4:
return LOG_STR("4");
case ADC_GAIN_6:
return LOG_STR("6");
case ADC_GAIN_8:
return LOG_STR("8");
case ADC_GAIN_12:
return LOG_STR("12");
case ADC_GAIN_16:
return LOG_STR("16");
case ADC_GAIN_24:
return LOG_STR("24");
case ADC_GAIN_32:
return LOG_STR("32");
case ADC_GAIN_64:
return LOG_STR("64");
case ADC_GAIN_128:
return LOG_STR("128");
}
return LOG_STR("undefined gain");
}
static const LogString *reference_to_str(enum adc_reference reference) {
switch (reference) {
case ADC_REF_VDD_1:
return LOG_STR("VDD");
case ADC_REF_VDD_1_2:
return LOG_STR("VDD/2");
case ADC_REF_VDD_1_3:
return LOG_STR("VDD/3");
case ADC_REF_VDD_1_4:
return LOG_STR("VDD/4");
case ADC_REF_INTERNAL:
return LOG_STR("INTERNAL");
case ADC_REF_EXTERNAL0:
return LOG_STR("External, input 0");
case ADC_REF_EXTERNAL1:
return LOG_STR("External, input 1");
}
return LOG_STR("undefined reference");
}
static const LogString *input_to_str(uint8_t input) {
switch (input) {
case NRF_SAADC_INPUT_AIN0:
return LOG_STR("AIN0");
case NRF_SAADC_INPUT_AIN1:
return LOG_STR("AIN1");
case NRF_SAADC_INPUT_AIN2:
return LOG_STR("AIN2");
case NRF_SAADC_INPUT_AIN3:
return LOG_STR("AIN3");
case NRF_SAADC_INPUT_AIN4:
return LOG_STR("AIN4");
case NRF_SAADC_INPUT_AIN5:
return LOG_STR("AIN5");
case NRF_SAADC_INPUT_AIN6:
return LOG_STR("AIN6");
case NRF_SAADC_INPUT_AIN7:
return LOG_STR("AIN7");
case NRF_SAADC_INPUT_VDD:
return LOG_STR("VDD");
case NRF_SAADC_INPUT_VDDHDIV5:
return LOG_STR("VDDHDIV5");
}
return LOG_STR("undefined input");
}
#endif // ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
void ADCSensor::dump_config() {
LOG_SENSOR("", "ADC Sensor", this);
LOG_PIN(" Pin: ", this->pin_);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
ESP_LOGV(TAG,
" Name: %s\n"
" Channel: %d\n"
" vref_mv: %d\n"
" Resolution %d\n"
" Oversampling %d",
this->channel_->dev->name, this->channel_->channel_id, this->channel_->vref_mv, this->channel_->resolution,
this->channel_->oversampling);
ESP_LOGV(TAG,
" Gain: %s\n"
" reference: %s\n"
" acquisition_time: %d\n"
" differential %s",
LOG_STR_ARG(gain_to_str(this->channel_->channel_cfg.gain)),
LOG_STR_ARG(reference_to_str(this->channel_->channel_cfg.reference)),
this->channel_->channel_cfg.acquisition_time, YESNO(this->channel_->channel_cfg.differential));
if (this->channel_->channel_cfg.differential) {
ESP_LOGV(TAG,
" Positive: %s\n"
" Negative: %s",
LOG_STR_ARG(input_to_str(this->channel_->channel_cfg.input_positive)),
LOG_STR_ARG(input_to_str(this->channel_->channel_cfg.input_negative)));
} else {
ESP_LOGV(TAG, " Positive: %s", LOG_STR_ARG(input_to_str(this->channel_->channel_cfg.input_positive)));
}
#endif
LOG_UPDATE_INTERVAL(this);
}
float ADCSensor::sample() {
auto aggr = Aggregator<int32_t>(this->sampling_mode_);
int err;
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
int16_t buf = 0;
struct adc_sequence sequence = {
.buffer = &buf,
/* buffer size in bytes, not number of samples */
.buffer_size = sizeof(buf),
};
int32_t val_raw;
err = adc_sequence_init_dt(this->channel_, &sequence);
if (err < 0) {
ESP_LOGE(TAG, "Could sequence init %s (%d)", this->channel_->dev->name, err);
return 0.0;
}
err = adc_read(this->channel_->dev, &sequence);
if (err < 0) {
ESP_LOGE(TAG, "Could not read %s (%d)", this->channel_->dev->name, err);
return 0.0;
}
val_raw = (int32_t) buf;
if (!this->channel_->channel_cfg.differential) {
// https://github.com/adafruit/Adafruit_nRF52_Arduino/blob/0ed4d9ffc674ae407be7cacf5696a02f5e789861/cores/nRF5/wiring_analog_nRF52.c#L222
if (val_raw < 0) {
val_raw = 0;
}
}
aggr.add_sample(val_raw);
}
int32_t val_mv = aggr.aggregate();
if (this->output_raw_) {
return val_mv;
}
err = adc_raw_to_millivolts_dt(this->channel_, &val_mv);
/* conversion to mV may not be supported, skip if not */
if (err < 0) {
ESP_LOGE(TAG, "Value in mV not available %s (%d)", this->channel_->dev->name, err);
return 0.0;
}
return val_mv / 1000.0f;
}
} // namespace adc
} // namespace esphome
#endif

View File

@@ -3,6 +3,12 @@ import logging
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import sensor, voltage_sampler from esphome.components import sensor, voltage_sampler
from esphome.components.esp32 import get_esp32_variant from esphome.components.esp32 import get_esp32_variant
from esphome.components.nrf52.const import AIN_TO_GPIO, EXTRA_ADC
from esphome.components.zephyr import (
zephyr_add_overlay,
zephyr_add_prj_conf,
zephyr_add_user,
)
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_ATTENUATION, CONF_ATTENUATION,
@@ -11,6 +17,7 @@ from esphome.const import (
CONF_PIN, CONF_PIN,
CONF_RAW, CONF_RAW,
DEVICE_CLASS_VOLTAGE, DEVICE_CLASS_VOLTAGE,
PLATFORM_NRF52,
STATE_CLASS_MEASUREMENT, STATE_CLASS_MEASUREMENT,
UNIT_VOLT, UNIT_VOLT,
) )
@@ -60,6 +67,10 @@ ADCSensor = adc_ns.class_(
"ADCSensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler "ADCSensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler
) )
CONF_NRF_SAADC = "nrf_saadc"
adc_dt_spec = cg.global_ns.class_("adc_dt_spec")
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
sensor.sensor_schema( sensor.sensor_schema(
ADCSensor, ADCSensor,
@@ -75,6 +86,7 @@ CONFIG_SCHEMA = cv.All(
cv.SplitDefault(CONF_ATTENUATION, esp32="0db"): cv.All( cv.SplitDefault(CONF_ATTENUATION, esp32="0db"): cv.All(
cv.only_on_esp32, _attenuation cv.only_on_esp32, _attenuation
), ),
cv.OnlyWith(CONF_NRF_SAADC, PLATFORM_NRF52): cv.declare_id(adc_dt_spec),
cv.Optional(CONF_SAMPLES, default=1): cv.int_range(min=1, max=255), cv.Optional(CONF_SAMPLES, default=1): cv.int_range(min=1, max=255),
cv.Optional(CONF_SAMPLING_MODE, default="avg"): _sampling_mode, cv.Optional(CONF_SAMPLING_MODE, default="avg"): _sampling_mode,
} }
@@ -83,6 +95,8 @@ CONFIG_SCHEMA = cv.All(
validate_config, validate_config,
) )
CONF_ADC_CHANNEL_ID = "adc_channel_id"
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
@@ -93,7 +107,7 @@ async def to_code(config):
cg.add_define("USE_ADC_SENSOR_VCC") cg.add_define("USE_ADC_SENSOR_VCC")
elif config[CONF_PIN] == "TEMPERATURE": elif config[CONF_PIN] == "TEMPERATURE":
cg.add(var.set_is_temperature()) cg.add(var.set_is_temperature())
else: elif not CORE.is_nrf52 or config[CONF_PIN][CONF_NUMBER] not in EXTRA_ADC:
pin = await cg.gpio_pin_expression(config[CONF_PIN]) pin = await cg.gpio_pin_expression(config[CONF_PIN])
cg.add(var.set_pin(pin)) cg.add(var.set_pin(pin))
@@ -122,3 +136,41 @@ async def to_code(config):
): ):
chan = ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant][pin_num] chan = ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant][pin_num]
cg.add(var.set_channel(adc_unit_t.ADC_UNIT_2, chan)) cg.add(var.set_channel(adc_unit_t.ADC_UNIT_2, chan))
elif CORE.is_nrf52:
CORE.data.setdefault(CONF_ADC_CHANNEL_ID, 0)
channel_id = CORE.data[CONF_ADC_CHANNEL_ID]
CORE.data[CONF_ADC_CHANNEL_ID] = channel_id + 1
zephyr_add_prj_conf("ADC", True)
nrf_saadc = config[CONF_NRF_SAADC]
rhs = cg.RawExpression(
f"ADC_DT_SPEC_GET_BY_IDX(DT_PATH(zephyr_user), {channel_id})"
)
adc = cg.new_Pvariable(nrf_saadc, rhs)
cg.add(var.set_adc_channel(adc))
gain = "ADC_GAIN_1_6"
pin_number = config[CONF_PIN][CONF_NUMBER]
if pin_number == "VDDHDIV5":
gain = "ADC_GAIN_1_2"
if isinstance(pin_number, int):
GPIO_TO_AIN = {v: k for k, v in AIN_TO_GPIO.items()}
pin_number = GPIO_TO_AIN[pin_number]
zephyr_add_user("io-channels", f"<&adc {channel_id}>")
zephyr_add_overlay(
f"""
&adc {{
#address-cells = <1>;
#size-cells = <0>;
channel@{channel_id} {{
reg = <{channel_id}>;
zephyr,gain = "{gain}";
zephyr,reference = "ADC_REF_INTERNAL";
zephyr,acquisition-time = <ADC_ACQ_TIME_DEFAULT>;
zephyr,input-positive = <NRF_SAADC_{pin_number}>;
zephyr,resolution = <14>;
zephyr,oversampling = <8>;
}};
}};
"""
)

View File

@@ -36,6 +36,7 @@ from esphome.const import (
UNIT_WATT, UNIT_WATT,
UNIT_WATT_HOURS, UNIT_WATT_HOURS,
) )
from esphome.types import ConfigType
DEPENDENCIES = ["i2c"] DEPENDENCIES = ["i2c"]
@@ -51,6 +52,20 @@ CONF_POWER_GAIN = "power_gain"
CONF_NEUTRAL = "neutral" CONF_NEUTRAL = "neutral"
# Tuple of power channel phases
POWER_PHASES = (CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C)
# Tuple of sensor types that can be configured for power channels
POWER_SENSOR_TYPES = (
CONF_CURRENT,
CONF_VOLTAGE,
CONF_ACTIVE_POWER,
CONF_APPARENT_POWER,
CONF_POWER_FACTOR,
CONF_FORWARD_ACTIVE_ENERGY,
CONF_REVERSE_ACTIVE_ENERGY,
)
NEUTRAL_CHANNEL_SCHEMA = cv.Schema( NEUTRAL_CHANNEL_SCHEMA = cv.Schema(
{ {
cv.GenerateID(): cv.declare_id(NeutralChannel), cv.GenerateID(): cv.declare_id(NeutralChannel),
@@ -150,7 +165,64 @@ POWER_CHANNEL_SCHEMA = cv.Schema(
} }
) )
CONFIG_SCHEMA = (
def prefix_sensor_name(
sensor_conf: ConfigType,
channel_name: str,
channel_config: ConfigType,
sensor_type: str,
) -> None:
"""Helper to prefix sensor name with channel name.
Args:
sensor_conf: The sensor configuration (dict or string)
channel_name: The channel name to prefix with
channel_config: The channel configuration to update
sensor_type: The sensor type key in the channel config
"""
if isinstance(sensor_conf, dict) and CONF_NAME in sensor_conf:
sensor_name = sensor_conf[CONF_NAME]
if sensor_name and not sensor_name.startswith(channel_name):
sensor_conf[CONF_NAME] = f"{channel_name} {sensor_name}"
elif isinstance(sensor_conf, str):
# Simple value case - convert to dict with prefixed name
channel_config[sensor_type] = {CONF_NAME: f"{channel_name} {sensor_conf}"}
def process_channel_sensors(
config: ConfigType, channel_key: str, sensor_types: tuple
) -> None:
"""Process sensors for a channel and prefix their names.
Args:
config: The main configuration
channel_key: The channel key (e.g., CONF_PHASE_A, CONF_NEUTRAL)
sensor_types: Tuple of sensor types to process for this channel
"""
if not (channel_config := config.get(channel_key)) or not (
channel_name := channel_config.get(CONF_NAME)
):
return
for sensor_type in sensor_types:
if sensor_conf := channel_config.get(sensor_type):
prefix_sensor_name(sensor_conf, channel_name, channel_config, sensor_type)
def preprocess_channels(config: ConfigType) -> ConfigType:
"""Preprocess channel configurations to add channel name prefix to sensor names."""
# Process power channels
for channel in POWER_PHASES:
process_channel_sensors(config, channel, POWER_SENSOR_TYPES)
# Process neutral channel
process_channel_sensors(config, CONF_NEUTRAL, (CONF_CURRENT,))
return config
CONFIG_SCHEMA = cv.All(
preprocess_channels,
cv.Schema( cv.Schema(
{ {
cv.GenerateID(): cv.declare_id(ADE7880), cv.GenerateID(): cv.declare_id(ADE7880),
@@ -167,7 +239,7 @@ CONFIG_SCHEMA = (
} }
) )
.extend(cv.polling_component_schema("60s")) .extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x38)) .extend(i2c.i2c_device_schema(0x38)),
) )
@@ -188,15 +260,7 @@ async def neutral_channel(config):
async def power_channel(config): async def power_channel(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
for sensor_type in [ for sensor_type in POWER_SENSOR_TYPES:
CONF_CURRENT,
CONF_VOLTAGE,
CONF_ACTIVE_POWER,
CONF_APPARENT_POWER,
CONF_POWER_FACTOR,
CONF_FORWARD_ACTIVE_ENERGY,
CONF_REVERSE_ACTIVE_ENERGY,
]:
if conf := config.get(sensor_type): if conf := config.get(sensor_type):
sens = await sensor.new_sensor(conf) sens = await sensor.new_sensor(conf)
cg.add(getattr(var, f"set_{sensor_type}")(sens)) cg.add(getattr(var, f"set_{sensor_type}")(sens))
@@ -216,44 +280,6 @@ async def power_channel(config):
return var return var
def final_validate(config):
for channel in [CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C]:
if channel := config.get(channel):
channel_name = channel.get(CONF_NAME)
for sensor_type in [
CONF_CURRENT,
CONF_VOLTAGE,
CONF_ACTIVE_POWER,
CONF_APPARENT_POWER,
CONF_POWER_FACTOR,
CONF_FORWARD_ACTIVE_ENERGY,
CONF_REVERSE_ACTIVE_ENERGY,
]:
if conf := channel.get(sensor_type):
sensor_name = conf.get(CONF_NAME)
if (
sensor_name
and channel_name
and not sensor_name.startswith(channel_name)
):
conf[CONF_NAME] = f"{channel_name} {sensor_name}"
if channel := config.get(CONF_NEUTRAL):
channel_name = channel.get(CONF_NAME)
if conf := channel.get(CONF_CURRENT):
sensor_name = conf.get(CONF_NAME)
if (
sensor_name
and channel_name
and not sensor_name.startswith(channel_name)
):
conf[CONF_NAME] = f"{channel_name} {sensor_name}"
FINAL_VALIDATE_SCHEMA = final_validate
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 cg.register_component(var, config) await cg.register_component(var, config)

View File

@@ -301,8 +301,7 @@ async def alarm_action_disarm_to_code(config, action_id, template_arg, args):
) )
async def alarm_action_pending_to_code(config, action_id, template_arg, args): async def alarm_action_pending_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID]) paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren) return cg.new_Pvariable(action_id, template_arg, paren)
return var
@automation.register_action( @automation.register_action(
@@ -310,8 +309,7 @@ async def alarm_action_pending_to_code(config, action_id, template_arg, args):
) )
async def alarm_action_trigger_to_code(config, action_id, template_arg, args): async def alarm_action_trigger_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID]) paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren) return cg.new_Pvariable(action_id, template_arg, paren)
return var
@automation.register_action( @automation.register_action(
@@ -319,8 +317,7 @@ async def alarm_action_trigger_to_code(config, action_id, template_arg, args):
) )
async def alarm_action_chime_to_code(config, action_id, template_arg, args): async def alarm_action_chime_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID]) paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren) return cg.new_Pvariable(action_id, template_arg, paren)
return var
@automation.register_action( @automation.register_action(
@@ -333,8 +330,7 @@ async def alarm_action_chime_to_code(config, action_id, template_arg, args):
) )
async def alarm_action_ready_to_code(config, action_id, template_arg, args): async def alarm_action_ready_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID]) paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren) return cg.new_Pvariable(action_id, template_arg, paren)
return var
@automation.register_condition( @automation.register_condition(
@@ -352,4 +348,3 @@ async def alarm_control_panel_is_armed_to_code(
@coroutine_with_priority(100.0) @coroutine_with_priority(100.0)
async def to_code(config): async def to_code(config):
cg.add_global(alarm_control_panel_ns.using) cg.add_global(alarm_control_panel_ns.using)
cg.add_define("USE_ALARM_CONTROL_PANEL")

View File

@@ -34,17 +34,20 @@ SetFrameAction = animation_ns.class_(
"AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_) "AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_)
) )
CONFIG_SCHEMA = espImage.IMAGE_SCHEMA.extend( CONFIG_SCHEMA = cv.All(
{ espImage.IMAGE_SCHEMA.extend(
cv.Required(CONF_ID): cv.declare_id(Animation_), {
cv.Optional(CONF_LOOP): cv.All( cv.Required(CONF_ID): cv.declare_id(Animation_),
{ cv.Optional(CONF_LOOP): cv.All(
cv.Optional(CONF_START_FRAME, default=0): cv.positive_int, {
cv.Optional(CONF_END_FRAME): cv.positive_int, cv.Optional(CONF_START_FRAME, default=0): cv.positive_int,
cv.Optional(CONF_REPEAT): cv.positive_int, cv.Optional(CONF_END_FRAME): cv.positive_int,
} cv.Optional(CONF_REPEAT): cv.positive_int,
), }
}, ),
},
),
espImage.validate_settings,
) )

View File

@@ -29,7 +29,7 @@ from esphome.core import CORE, coroutine_with_priority
DOMAIN = "api" DOMAIN = "api"
DEPENDENCIES = ["network"] DEPENDENCIES = ["network"]
AUTO_LOAD = ["socket"] AUTO_LOAD = ["socket"]
CODEOWNERS = ["@OttoWinter"] CODEOWNERS = ["@esphome/core"]
api_ns = cg.esphome_ns.namespace("api") api_ns = cg.esphome_ns.namespace("api")
APIServer = api_ns.class_("APIServer", cg.Component, cg.Controller) APIServer = api_ns.class_("APIServer", cg.Component, cg.Controller)

View File

@@ -250,8 +250,8 @@ message DeviceInfoResponse {
// Supports receiving and saving api encryption key // Supports receiving and saving api encryption key
bool api_encryption_supported = 19 [(field_ifdef) = "USE_API_NOISE"]; bool api_encryption_supported = 19 [(field_ifdef) = "USE_API_NOISE"];
repeated DeviceInfo devices = 20 [(field_ifdef) = "USE_DEVICES"]; repeated DeviceInfo devices = 20 [(field_ifdef) = "USE_DEVICES", (fixed_array_size_define) = "ESPHOME_DEVICE_COUNT"];
repeated AreaInfo areas = 21 [(field_ifdef) = "USE_AREAS"]; repeated AreaInfo areas = 21 [(field_ifdef) = "USE_AREAS", (fixed_array_size_define) = "ESPHOME_AREA_COUNT"];
// Top-level area info to phase out suggested_area // Top-level area info to phase out suggested_area
AreaInfo area = 22 [(field_ifdef) = "USE_AREAS"]; AreaInfo area = 22 [(field_ifdef) = "USE_AREAS"];
@@ -419,7 +419,7 @@ message ListEntitiesFanResponse {
bool disabled_by_default = 9; bool disabled_by_default = 9;
string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"]; string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"];
EntityCategory entity_category = 11; EntityCategory entity_category = 11;
repeated string supported_preset_modes = 12; repeated string supported_preset_modes = 12 [(container_pointer) = "std::set"];
uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"]; uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"];
} }
// Deprecated in API version 1.6 - only used in deprecated fields // Deprecated in API version 1.6 - only used in deprecated fields
@@ -500,7 +500,7 @@ message ListEntitiesLightResponse {
string name = 3; string name = 3;
reserved 4; // Deprecated: was string unique_id reserved 4; // Deprecated: was string unique_id
repeated ColorMode supported_color_modes = 12; repeated ColorMode supported_color_modes = 12 [(container_pointer) = "std::set<light::ColorMode>"];
// next four supports_* are for legacy clients, newer clients should use color modes // next four supports_* are for legacy clients, newer clients should use color modes
// Deprecated in API version 1.6 // Deprecated in API version 1.6
bool legacy_supports_brightness = 5 [deprecated=true]; bool legacy_supports_brightness = 5 [deprecated=true];
@@ -966,7 +966,7 @@ message ListEntitiesClimateResponse {
bool supports_current_temperature = 5; bool supports_current_temperature = 5;
bool supports_two_point_target_temperature = 6; bool supports_two_point_target_temperature = 6;
repeated ClimateMode supported_modes = 7; repeated ClimateMode supported_modes = 7 [(container_pointer) = "std::set<climate::ClimateMode>"];
float visual_min_temperature = 8; float visual_min_temperature = 8;
float visual_max_temperature = 9; float visual_max_temperature = 9;
float visual_target_temperature_step = 10; float visual_target_temperature_step = 10;
@@ -975,11 +975,11 @@ message ListEntitiesClimateResponse {
// Deprecated in API version 1.5 // Deprecated in API version 1.5
bool legacy_supports_away = 11 [deprecated=true]; bool legacy_supports_away = 11 [deprecated=true];
bool supports_action = 12; bool supports_action = 12;
repeated ClimateFanMode supported_fan_modes = 13; repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer) = "std::set<climate::ClimateFanMode>"];
repeated ClimateSwingMode supported_swing_modes = 14; repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer) = "std::set<climate::ClimateSwingMode>"];
repeated string supported_custom_fan_modes = 15; repeated string supported_custom_fan_modes = 15 [(container_pointer) = "std::set"];
repeated ClimatePreset supported_presets = 16; repeated ClimatePreset supported_presets = 16 [(container_pointer) = "std::set<climate::ClimatePreset>"];
repeated string supported_custom_presets = 17; repeated string supported_custom_presets = 17 [(container_pointer) = "std::set"];
bool disabled_by_default = 18; bool disabled_by_default = 18;
string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"]; string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"];
EntityCategory entity_category = 20; EntityCategory entity_category = 20;
@@ -1119,7 +1119,7 @@ message ListEntitiesSelectResponse {
reserved 4; // Deprecated: was string unique_id reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
repeated string options = 6; repeated string options = 6 [(container_pointer) = "std::vector"];
bool disabled_by_default = 7; bool disabled_by_default = 7;
EntityCategory entity_category = 8; EntityCategory entity_category = 8;
uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"]; uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"];
@@ -1297,6 +1297,9 @@ enum MediaPlayerState {
MEDIA_PLAYER_STATE_IDLE = 1; MEDIA_PLAYER_STATE_IDLE = 1;
MEDIA_PLAYER_STATE_PLAYING = 2; MEDIA_PLAYER_STATE_PLAYING = 2;
MEDIA_PLAYER_STATE_PAUSED = 3; MEDIA_PLAYER_STATE_PAUSED = 3;
MEDIA_PLAYER_STATE_ANNOUNCING = 4;
MEDIA_PLAYER_STATE_OFF = 5;
MEDIA_PLAYER_STATE_ON = 6;
} }
enum MediaPlayerCommand { enum MediaPlayerCommand {
MEDIA_PLAYER_COMMAND_PLAY = 0; MEDIA_PLAYER_COMMAND_PLAY = 0;
@@ -1304,6 +1307,15 @@ enum MediaPlayerCommand {
MEDIA_PLAYER_COMMAND_STOP = 2; MEDIA_PLAYER_COMMAND_STOP = 2;
MEDIA_PLAYER_COMMAND_MUTE = 3; MEDIA_PLAYER_COMMAND_MUTE = 3;
MEDIA_PLAYER_COMMAND_UNMUTE = 4; MEDIA_PLAYER_COMMAND_UNMUTE = 4;
MEDIA_PLAYER_COMMAND_TOGGLE = 5;
MEDIA_PLAYER_COMMAND_VOLUME_UP = 6;
MEDIA_PLAYER_COMMAND_VOLUME_DOWN = 7;
MEDIA_PLAYER_COMMAND_ENQUEUE = 8;
MEDIA_PLAYER_COMMAND_REPEAT_ONE = 9;
MEDIA_PLAYER_COMMAND_REPEAT_OFF = 10;
MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST = 11;
MEDIA_PLAYER_COMMAND_TURN_ON = 12;
MEDIA_PLAYER_COMMAND_TURN_OFF = 13;
} }
enum MediaPlayerFormatPurpose { enum MediaPlayerFormatPurpose {
MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT = 0; MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT = 0;
@@ -1338,6 +1350,8 @@ message ListEntitiesMediaPlayerResponse {
repeated MediaPlayerSupportedFormat supported_formats = 9; repeated MediaPlayerSupportedFormat supported_formats = 9;
uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"]; uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"];
uint32 feature_flags = 11;
} }
message MediaPlayerStateResponse { message MediaPlayerStateResponse {
option (id) = 64; option (id) = 64;
@@ -1424,11 +1438,11 @@ message BluetoothLERawAdvertisementsResponse {
option (ifdef) = "USE_BLUETOOTH_PROXY"; option (ifdef) = "USE_BLUETOOTH_PROXY";
option (no_delay) = true; option (no_delay) = true;
repeated BluetoothLERawAdvertisement advertisements = 1; repeated BluetoothLERawAdvertisement advertisements = 1 [(fixed_array_with_length_define) = "BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE"];
} }
enum BluetoothDeviceRequestType { enum BluetoothDeviceRequestType {
BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT = 0; BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT = 0 [deprecated = true]; // V1 removed, use V3 variants
BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT = 1; BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT = 1;
BLUETOOTH_DEVICE_REQUEST_TYPE_PAIR = 2; BLUETOOTH_DEVICE_REQUEST_TYPE_PAIR = 2;
BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR = 3; BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR = 3;
@@ -1468,21 +1482,39 @@ message BluetoothGATTGetServicesRequest {
} }
message BluetoothGATTDescriptor { message BluetoothGATTDescriptor {
repeated uint64 uuid = 1 [(fixed_array_size) = 2]; repeated uint64 uuid = 1 [(fixed_array_size) = 2, (fixed_array_skip_zero) = true];
uint32 handle = 2; uint32 handle = 2;
// New field for efficient UUID (v1.12+)
// Only one of uuid or short_uuid will be set.
// short_uuid is used for both 16-bit and 32-bit UUIDs with v1.12+ clients.
// 128-bit UUIDs always use the uuid field for backwards compatibility.
uint32 short_uuid = 3; // 16-bit or 32-bit UUID
} }
message BluetoothGATTCharacteristic { message BluetoothGATTCharacteristic {
repeated uint64 uuid = 1 [(fixed_array_size) = 2]; repeated uint64 uuid = 1 [(fixed_array_size) = 2, (fixed_array_skip_zero) = true];
uint32 handle = 2; uint32 handle = 2;
uint32 properties = 3; uint32 properties = 3;
repeated BluetoothGATTDescriptor descriptors = 4; repeated BluetoothGATTDescriptor descriptors = 4;
// New field for efficient UUID (v1.12+)
// Only one of uuid or short_uuid will be set.
// short_uuid is used for both 16-bit and 32-bit UUIDs with v1.12+ clients.
// 128-bit UUIDs always use the uuid field for backwards compatibility.
uint32 short_uuid = 5; // 16-bit or 32-bit UUID
} }
message BluetoothGATTService { message BluetoothGATTService {
repeated uint64 uuid = 1 [(fixed_array_size) = 2]; repeated uint64 uuid = 1 [(fixed_array_size) = 2, (fixed_array_skip_zero) = true];
uint32 handle = 2; uint32 handle = 2;
repeated BluetoothGATTCharacteristic characteristics = 3; repeated BluetoothGATTCharacteristic characteristics = 3;
// New field for efficient UUID (v1.12+)
// Only one of uuid or short_uuid will be set.
// short_uuid is used for both 16-bit and 32-bit UUIDs with v1.12+ clients.
// 128-bit UUIDs always use the uuid field for backwards compatibility.
uint32 short_uuid = 4; // 16-bit or 32-bit UUID
} }
message BluetoothGATTGetServicesResponse { message BluetoothGATTGetServicesResponse {
@@ -1491,7 +1523,7 @@ message BluetoothGATTGetServicesResponse {
option (ifdef) = "USE_BLUETOOTH_PROXY"; option (ifdef) = "USE_BLUETOOTH_PROXY";
uint64 address = 1; uint64 address = 1;
repeated BluetoothGATTService services = 2 [(fixed_array_size) = 1]; repeated BluetoothGATTService services = 2;
} }
message BluetoothGATTGetServicesDoneResponse { message BluetoothGATTGetServicesDoneResponse {
@@ -1589,7 +1621,10 @@ message BluetoothConnectionsFreeResponse {
uint32 free = 1; uint32 free = 1;
uint32 limit = 2; uint32 limit = 2;
repeated uint64 allocated = 3; repeated uint64 allocated = 3 [
(fixed_array_size_define) = "BLUETOOTH_PROXY_MAX_CONNECTIONS",
(fixed_array_skip_zero) = true
];
} }
message BluetoothGATTErrorResponse { message BluetoothGATTErrorResponse {
@@ -1834,7 +1869,7 @@ message VoiceAssistantConfigurationResponse {
option (ifdef) = "USE_VOICE_ASSISTANT"; option (ifdef) = "USE_VOICE_ASSISTANT";
repeated VoiceAssistantWakeWord available_wake_words = 1; repeated VoiceAssistantWakeWord available_wake_words = 1;
repeated string active_wake_words = 2; repeated string active_wake_words = 2 [(container_pointer) = "std::vector"];
uint32 max_active_wake_words = 3; uint32 max_active_wake_words = 3;
} }

View File

@@ -112,8 +112,7 @@ 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->get_client_combined_info().c_str(), api_error_to_str(err), this->log_warning_("Helper init failed", err);
errno);
return; return;
} }
this->client_info_.peername = helper_->getpeername(); this->client_info_.peername = helper_->getpeername();
@@ -144,8 +143,7 @@ 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->get_client_combined_info().c_str(), this->log_socket_operation_failed_(err);
api_error_to_str(err), errno);
return; return;
} }
@@ -161,8 +159,7 @@ void APIConnection::loop() {
break; break;
} else if (err != APIError::OK) { } else if (err != APIError::OK) {
on_fatal_error(); on_fatal_error();
ESP_LOGW(TAG, "%s: Reading failed %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err), this->log_warning_("Reading failed", err);
errno);
return; return;
} else { } else {
this->last_traffic_ = now; this->last_traffic_ = now;
@@ -276,8 +273,9 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint8_t mess
#endif #endif
// Calculate size // Calculate size
uint32_t calculated_size = 0; ProtoSize size_calc;
msg.calculate_size(calculated_size); msg.calculate_size(size_calc);
uint32_t calculated_size = size_calc.get_size();
// Cache frame sizes to avoid repeated virtual calls // Cache frame sizes to avoid repeated virtual calls
const uint8_t header_padding = conn->helper_->frame_header_padding(); const uint8_t header_padding = conn->helper_->frame_header_padding();
@@ -412,8 +410,7 @@ uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *con
msg.supports_speed = traits.supports_speed(); msg.supports_speed = traits.supports_speed();
msg.supports_direction = traits.supports_direction(); msg.supports_direction = traits.supports_direction();
msg.supported_speed_count = traits.supported_speed_count(); msg.supported_speed_count = traits.supported_speed_count();
for (auto const &preset : traits.supported_preset_modes()) msg.supported_preset_modes = &traits.supported_preset_modes_for_api_();
msg.supported_preset_modes.push_back(preset);
return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::fan_command(const FanCommandRequest &msg) { void APIConnection::fan_command(const FanCommandRequest &msg) {
@@ -469,8 +466,7 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c
auto *light = static_cast<light::LightState *>(entity); auto *light = static_cast<light::LightState *>(entity);
ListEntitiesLightResponse msg; ListEntitiesLightResponse msg;
auto traits = light->get_traits(); auto traits = light->get_traits();
for (auto mode : traits.get_supported_color_modes()) msg.supported_color_modes = &traits.get_supported_color_modes_for_api_();
msg.supported_color_modes.push_back(static_cast<enums::ColorMode>(mode));
if (traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) || if (traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) ||
traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)) { traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)) {
msg.min_mireds = traits.get_min_mireds(); msg.min_mireds = traits.get_min_mireds();
@@ -656,8 +652,7 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection
msg.supports_current_humidity = traits.get_supports_current_humidity(); msg.supports_current_humidity = traits.get_supports_current_humidity();
msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature(); msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature();
msg.supports_target_humidity = traits.get_supports_target_humidity(); msg.supports_target_humidity = traits.get_supports_target_humidity();
for (auto mode : traits.get_supported_modes()) msg.supported_modes = &traits.get_supported_modes_for_api_();
msg.supported_modes.push_back(static_cast<enums::ClimateMode>(mode));
msg.visual_min_temperature = traits.get_visual_min_temperature(); msg.visual_min_temperature = traits.get_visual_min_temperature();
msg.visual_max_temperature = traits.get_visual_max_temperature(); msg.visual_max_temperature = traits.get_visual_max_temperature();
msg.visual_target_temperature_step = traits.get_visual_target_temperature_step(); msg.visual_target_temperature_step = traits.get_visual_target_temperature_step();
@@ -665,16 +660,11 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection
msg.visual_min_humidity = traits.get_visual_min_humidity(); msg.visual_min_humidity = traits.get_visual_min_humidity();
msg.visual_max_humidity = traits.get_visual_max_humidity(); msg.visual_max_humidity = traits.get_visual_max_humidity();
msg.supports_action = traits.get_supports_action(); msg.supports_action = traits.get_supports_action();
for (auto fan_mode : traits.get_supported_fan_modes()) msg.supported_fan_modes = &traits.get_supported_fan_modes_for_api_();
msg.supported_fan_modes.push_back(static_cast<enums::ClimateFanMode>(fan_mode)); msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes_for_api_();
for (auto const &custom_fan_mode : traits.get_supported_custom_fan_modes()) msg.supported_presets = &traits.get_supported_presets_for_api_();
msg.supported_custom_fan_modes.push_back(custom_fan_mode); msg.supported_custom_presets = &traits.get_supported_custom_presets_for_api_();
for (auto preset : traits.get_supported_presets()) msg.supported_swing_modes = &traits.get_supported_swing_modes_for_api_();
msg.supported_presets.push_back(static_cast<enums::ClimatePreset>(preset));
for (auto const &custom_preset : traits.get_supported_custom_presets())
msg.supported_custom_presets.push_back(custom_preset);
for (auto swing_mode : traits.get_supported_swing_modes())
msg.supported_swing_modes.push_back(static_cast<enums::ClimateSwingMode>(swing_mode));
return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size, return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size,
is_single); is_single);
} }
@@ -880,8 +870,7 @@ uint16_t APIConnection::try_send_select_info(EntityBase *entity, APIConnection *
bool is_single) { bool is_single) {
auto *select = static_cast<select::Select *>(entity); auto *select = static_cast<select::Select *>(entity);
ListEntitiesSelectResponse msg; ListEntitiesSelectResponse msg;
for (const auto &option : select->traits.get_options()) msg.options = &select->traits.get_options();
msg.options.push_back(option);
return fill_and_encode_entity_info(select, msg, ListEntitiesSelectResponse::MESSAGE_TYPE, conn, remaining_size, return fill_and_encode_entity_info(select, msg, ListEntitiesSelectResponse::MESSAGE_TYPE, conn, remaining_size,
is_single); is_single);
} }
@@ -1007,6 +996,7 @@ uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnec
ListEntitiesMediaPlayerResponse msg; ListEntitiesMediaPlayerResponse msg;
auto traits = media_player->get_traits(); auto traits = media_player->get_traits();
msg.supports_pause = traits.get_supports_pause(); msg.supports_pause = traits.get_supports_pause();
msg.feature_flags = traits.get_feature_flags();
for (auto &supported_format : traits.get_supported_formats()) { for (auto &supported_format : traits.get_supported_formats()) {
msg.supported_formats.emplace_back(); msg.supported_formats.emplace_back();
auto &media_format = msg.supported_formats.back(); auto &media_format = msg.supported_formats.back();
@@ -1115,10 +1105,8 @@ void APIConnection::bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg)
bool APIConnection::send_subscribe_bluetooth_connections_free_response( bool APIConnection::send_subscribe_bluetooth_connections_free_response(
const SubscribeBluetoothConnectionsFreeRequest &msg) { const SubscribeBluetoothConnectionsFreeRequest &msg) {
BluetoothConnectionsFreeResponse resp; bluetooth_proxy::global_bluetooth_proxy->send_connections_free(this);
resp.free = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_connections_free(); return true;
resp.limit = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_connections_limit();
return this->send_message(resp, BluetoothConnectionsFreeResponse::MESSAGE_TYPE);
} }
void APIConnection::bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) { void APIConnection::bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) {
@@ -1195,9 +1183,7 @@ bool APIConnection::send_voice_assistant_get_configuration_response(const VoiceA
resp_wake_word.trained_languages.push_back(lang); resp_wake_word.trained_languages.push_back(lang);
} }
} }
for (auto &wake_word_id : config.active_wake_words) { resp.active_wake_words = &config.active_wake_words;
resp.active_wake_words.push_back(wake_word_id);
}
resp.max_active_wake_words = config.max_active_wake_words; resp.max_active_wake_words = config.max_active_wake_words;
return this->send_message(resp, VoiceAssistantConfigurationResponse::MESSAGE_TYPE); return this->send_message(resp, VoiceAssistantConfigurationResponse::MESSAGE_TYPE);
} }
@@ -1375,7 +1361,7 @@ bool APIConnection::send_hello_response(const HelloRequest &msg) {
HelloResponse resp; HelloResponse resp;
resp.api_version_major = 1; resp.api_version_major = 1;
resp.api_version_minor = 10; resp.api_version_minor = 12;
// Temporary string for concatenation - will be valid during send_message call // Temporary string for concatenation - will be valid during send_message call
std::string server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")"; std::string server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")";
resp.set_server_info(StringRef(server_info)); resp.set_server_info(StringRef(server_info));
@@ -1476,18 +1462,22 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
resp.api_encryption_supported = true; resp.api_encryption_supported = true;
#endif #endif
#ifdef USE_DEVICES #ifdef USE_DEVICES
size_t device_index = 0;
for (auto const &device : App.get_devices()) { for (auto const &device : App.get_devices()) {
resp.devices.emplace_back(); if (device_index >= ESPHOME_DEVICE_COUNT)
auto &device_info = resp.devices.back(); break;
auto &device_info = resp.devices[device_index++];
device_info.device_id = device->get_device_id(); device_info.device_id = device->get_device_id();
device_info.set_name(StringRef(device->get_name())); device_info.set_name(StringRef(device->get_name()));
device_info.area_id = device->get_area_id(); device_info.area_id = device->get_area_id();
} }
#endif #endif
#ifdef USE_AREAS #ifdef USE_AREAS
size_t area_index = 0;
for (auto const &area : App.get_areas()) { for (auto const &area : App.get_areas()) {
resp.areas.emplace_back(); if (area_index >= ESPHOME_AREA_COUNT)
auto &area_info = resp.areas.back(); break;
auto &area_info = resp.areas[area_index++];
area_info.area_id = area->get_area_id(); area_info.area_id = area->get_area_id();
area_info.set_name(StringRef(area->get_name())); area_info.set_name(StringRef(area->get_name()));
} }
@@ -1549,8 +1539,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->get_client_combined_info().c_str(), this->log_socket_operation_failed_(err);
api_error_to_str(err), errno);
return false; return false;
} }
if (this->helper_->can_write_without_blocking()) if (this->helper_->can_write_without_blocking())
@@ -1570,8 +1559,7 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
return false; return false;
if (err != APIError::OK) { if (err != APIError::OK) {
on_fatal_error(); on_fatal_error();
ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", this->get_client_combined_info().c_str(), this->log_warning_("Packet write failed", err);
api_error_to_str(err), errno);
return false; return false;
} }
// Do not set last_traffic_ on send // Do not set last_traffic_ on send
@@ -1656,6 +1644,8 @@ void APIConnection::process_batch_() {
return; return;
} }
// Get shared buffer reference once to avoid multiple calls
auto &shared_buf = this->parent_->get_shared_buffer_ref();
size_t num_items = this->deferred_batch_.size(); size_t num_items = this->deferred_batch_.size();
// Fast path for single message - allocate exact size needed // Fast path for single message - allocate exact size needed
@@ -1666,8 +1656,7 @@ void APIConnection::process_batch_() {
uint16_t payload_size = uint16_t payload_size =
item.creator(item.entity, this, std::numeric_limits<uint16_t>::max(), true, item.message_type); 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{&shared_buf}, item.message_type)) {
this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, item.message_type)) {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
// Log messages after send attempt for VV debugging // Log messages after send attempt for VV debugging
// It's safe to use the buffer for logging at this point regardless of send result // It's safe to use the buffer for logging at this point regardless of send result
@@ -1694,20 +1683,18 @@ void APIConnection::process_batch_() {
const uint8_t footer_size = this->helper_->frame_footer_size(); const uint8_t footer_size = this->helper_->frame_footer_size();
// Initialize buffer and tracking variables // Initialize buffer and tracking variables
this->parent_->get_shared_buffer_ref().clear(); shared_buf.clear();
// Pre-calculate exact buffer size needed based on message types // Pre-calculate exact buffer size needed based on message types
uint32_t total_estimated_size = 0; uint32_t total_estimated_size = num_items * (header_padding + footer_size);
for (size_t i = 0; i < this->deferred_batch_.size(); i++) { for (size_t i = 0; i < this->deferred_batch_.size(); i++) {
const auto &item = this->deferred_batch_[i]; const auto &item = this->deferred_batch_[i];
total_estimated_size += item.estimated_size; total_estimated_size += item.estimated_size;
} }
// Calculate total overhead for all messages // Calculate total overhead for all messages
uint32_t total_overhead = (header_padding + footer_size) * num_items;
// Reserve based on estimated size (much more accurate than 24-byte worst-case) // Reserve based on estimated size (much more accurate than 24-byte worst-case)
this->parent_->get_shared_buffer_ref().reserve(total_estimated_size + total_overhead); shared_buf.reserve(total_estimated_size);
this->flags_.batch_first_message = true; this->flags_.batch_first_message = true;
size_t items_processed = 0; size_t items_processed = 0;
@@ -1749,7 +1736,7 @@ void APIConnection::process_batch_() {
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 = shared_buf.size() + footer_size;
} }
if (items_processed == 0) { if (items_processed == 0) {
@@ -1759,17 +1746,15 @@ void APIConnection::process_batch_() {
// Add footer space for the last message (for Noise protocol MAC) // Add footer space for the last message (for Noise protocol MAC)
if (footer_size > 0) { if (footer_size > 0) {
auto &shared_buf = this->parent_->get_shared_buffer_ref();
shared_buf.resize(shared_buf.size() + footer_size); shared_buf.resize(shared_buf.size() + footer_size);
} }
// Send all collected packets // Send all collected packets
APIError err = this->helper_->write_protobuf_packets(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, APIError err = this->helper_->write_protobuf_packets(ProtoWriteBuffer{&shared_buf},
std::span<const PacketInfo>(packet_info, packet_count)); std::span<const PacketInfo>(packet_info, packet_count));
if (err != APIError::OK && err != APIError::WOULD_BLOCK) { if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
on_fatal_error(); on_fatal_error();
ESP_LOGW(TAG, "%s: Batch write failed %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err), this->log_warning_("Batch write failed", err);
errno);
} }
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@@ -1847,5 +1832,11 @@ void APIConnection::process_state_subscriptions_() {
} }
#endif // USE_API_HOMEASSISTANT_STATES #endif // USE_API_HOMEASSISTANT_STATES
void APIConnection::log_warning_(const char *message, APIError err) {
ESP_LOGW(TAG, "%s: %s %s errno=%d", this->get_client_combined_info().c_str(), message, api_error_to_str(err), errno);
}
void APIConnection::log_socket_operation_failed_(APIError err) { this->log_warning_("Socket operation failed", err); }
} // namespace esphome::api } // namespace esphome::api
#endif #endif

View File

@@ -235,6 +235,13 @@ class APIConnection : public APIServerConnection {
this->is_authenticated(); this->is_authenticated();
} }
uint8_t get_log_subscription_level() const { return this->flags_.log_subscription; } uint8_t get_log_subscription_level() const { return this->flags_.log_subscription; }
// Get client API version for feature detection
bool client_supports_api_version(uint16_t major, uint16_t minor) const {
return this->client_api_version_major_ > major ||
(this->client_api_version_major_ == major && this->client_api_version_minor_ >= minor);
}
void on_fatal_error() override; void on_fatal_error() override;
#ifdef USE_API_PASSWORD #ifdef USE_API_PASSWORD
void on_unauthenticated_access() override; void on_unauthenticated_access() override;
@@ -696,10 +703,16 @@ class APIConnection : public APIServerConnection {
bool send_message_smart_(EntityBase *entity, MessageCreatorPtr creator, uint8_t message_type, bool send_message_smart_(EntityBase *entity, MessageCreatorPtr creator, uint8_t message_type,
uint8_t estimated_size) { uint8_t estimated_size) {
// Try to send immediately if: // Try to send immediately if:
// 1. We should try to send immediately (should_try_send_immediately = true) // 1. It's an UpdateStateResponse (always send immediately to handle cases where
// 2. Batch delay is 0 (user has opted in to immediate sending) // the main loop is blocked, e.g., during OTA updates)
// 3. Buffer has space available // 2. OR: We should try to send immediately (should_try_send_immediately = true)
if (this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0 && // AND Batch delay is 0 (user has opted in to immediate sending)
// 3. AND: Buffer has space available
if ((
#ifdef USE_UPDATE
message_type == UpdateStateResponse::MESSAGE_TYPE ||
#endif
(this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0)) &&
this->helper_->can_write_without_blocking()) { this->helper_->can_write_without_blocking()) {
// Now actually encode and send // Now actually encode and send
if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true) && if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true) &&
@@ -736,6 +749,11 @@ class APIConnection : public APIServerConnection {
this->deferred_batch_.add_item_front(entity, MessageCreator(function_ptr), message_type, estimated_size); this->deferred_batch_.add_item_front(entity, MessageCreator(function_ptr), message_type, estimated_size);
return this->schedule_batch_(); return this->schedule_batch_();
} }
// Helper function to log API errors with errno
void log_warning_(const char *message, APIError err);
// Specific helper for duplicated error message
void log_socket_operation_failed_(APIError err);
}; };
} // namespace esphome::api } // namespace esphome::api

View File

@@ -156,7 +156,9 @@ APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_
} }
// Try to send directly if no buffered data // Try to send directly if no buffered data
ssize_t sent = this->socket_->writev(iov, iovcnt); // Optimize for single iovec case (common for plaintext API)
ssize_t sent =
(iovcnt == 1) ? this->socket_->write(iov[0].iov_base, iov[0].iov_len) : this->socket_->writev(iov, iovcnt);
if (sent == -1) { if (sent == -1) {
APIError err = this->handle_socket_write_error_(); APIError err = this->handle_socket_write_error_();

View File

@@ -28,4 +28,33 @@ extend google.protobuf.FieldOptions {
optional string field_ifdef = 1042; optional string field_ifdef = 1042;
optional uint32 fixed_array_size = 50007; optional uint32 fixed_array_size = 50007;
optional bool no_zero_copy = 50008 [default=false]; optional bool no_zero_copy = 50008 [default=false];
optional bool fixed_array_skip_zero = 50009 [default=false];
optional string fixed_array_size_define = 50010;
optional string fixed_array_with_length_define = 50011;
// container_pointer: Zero-copy optimization for repeated fields.
//
// When container_pointer is set on a repeated field, the generated message will
// store a pointer to an existing container instead of copying the data into the
// message's own repeated field. This eliminates heap allocations and improves performance.
//
// Requirements for safe usage:
// 1. The source container must remain valid until the message is encoded
// 2. Messages must be encoded immediately (which ESPHome does by default)
// 3. The container type must match the field type exactly
//
// Supported container types:
// - "std::vector<T>" for most repeated fields
// - "std::set<T>" for unique/sorted data
// - Full type specification required for enums (e.g., "std::set<climate::ClimateMode>")
//
// Example usage in .proto file:
// repeated string supported_modes = 12 [(container_pointer) = "std::set"];
// repeated ColorMode color_modes = 13 [(container_pointer) = "std::set<light::ColorMode>"];
//
// The corresponding C++ code must provide const reference access to a container
// that matches the specified type and remains valid during message encoding.
// This is typically done through methods returning const T& or special accessor
// methods like get_options() or supported_modes_for_api_().
optional string container_pointer = 50001;
} }

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@
#include "esphome/core/string_ref.h" #include "esphome/core/string_ref.h"
#include "proto.h" #include "proto.h"
#include "api_pb2_includes.h"
namespace esphome::api { namespace esphome::api {
@@ -149,6 +150,9 @@ enum MediaPlayerState : uint32_t {
MEDIA_PLAYER_STATE_IDLE = 1, MEDIA_PLAYER_STATE_IDLE = 1,
MEDIA_PLAYER_STATE_PLAYING = 2, MEDIA_PLAYER_STATE_PLAYING = 2,
MEDIA_PLAYER_STATE_PAUSED = 3, MEDIA_PLAYER_STATE_PAUSED = 3,
MEDIA_PLAYER_STATE_ANNOUNCING = 4,
MEDIA_PLAYER_STATE_OFF = 5,
MEDIA_PLAYER_STATE_ON = 6,
}; };
enum MediaPlayerCommand : uint32_t { enum MediaPlayerCommand : uint32_t {
MEDIA_PLAYER_COMMAND_PLAY = 0, MEDIA_PLAYER_COMMAND_PLAY = 0,
@@ -156,6 +160,15 @@ enum MediaPlayerCommand : uint32_t {
MEDIA_PLAYER_COMMAND_STOP = 2, MEDIA_PLAYER_COMMAND_STOP = 2,
MEDIA_PLAYER_COMMAND_MUTE = 3, MEDIA_PLAYER_COMMAND_MUTE = 3,
MEDIA_PLAYER_COMMAND_UNMUTE = 4, MEDIA_PLAYER_COMMAND_UNMUTE = 4,
MEDIA_PLAYER_COMMAND_TOGGLE = 5,
MEDIA_PLAYER_COMMAND_VOLUME_UP = 6,
MEDIA_PLAYER_COMMAND_VOLUME_DOWN = 7,
MEDIA_PLAYER_COMMAND_ENQUEUE = 8,
MEDIA_PLAYER_COMMAND_REPEAT_ONE = 9,
MEDIA_PLAYER_COMMAND_REPEAT_OFF = 10,
MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST = 11,
MEDIA_PLAYER_COMMAND_TURN_ON = 12,
MEDIA_PLAYER_COMMAND_TURN_OFF = 13,
}; };
enum MediaPlayerFormatPurpose : uint32_t { enum MediaPlayerFormatPurpose : uint32_t {
MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT = 0, MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT = 0,
@@ -340,7 +353,7 @@ class HelloResponse : public ProtoMessage {
StringRef name_ref_{}; StringRef name_ref_{};
void set_name(const StringRef &ref) { this->name_ref_ = ref; } void set_name(const StringRef &ref) { this->name_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -371,14 +384,14 @@ class ConnectResponse : public ProtoMessage {
#endif #endif
bool invalid_password{false}; bool invalid_password{false};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
protected: protected:
}; };
class DisconnectRequest : public ProtoDecodableMessage { class DisconnectRequest : public ProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 5; static constexpr uint8_t MESSAGE_TYPE = 5;
static constexpr uint8_t ESTIMATED_SIZE = 0; static constexpr uint8_t ESTIMATED_SIZE = 0;
@@ -391,7 +404,7 @@ class DisconnectRequest : public ProtoDecodableMessage {
protected: protected:
}; };
class DisconnectResponse : public ProtoDecodableMessage { class DisconnectResponse : public ProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 6; static constexpr uint8_t MESSAGE_TYPE = 6;
static constexpr uint8_t ESTIMATED_SIZE = 0; static constexpr uint8_t ESTIMATED_SIZE = 0;
@@ -404,7 +417,7 @@ class DisconnectResponse : public ProtoDecodableMessage {
protected: protected:
}; };
class PingRequest : public ProtoDecodableMessage { class PingRequest : public ProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 7; static constexpr uint8_t MESSAGE_TYPE = 7;
static constexpr uint8_t ESTIMATED_SIZE = 0; static constexpr uint8_t ESTIMATED_SIZE = 0;
@@ -417,7 +430,7 @@ class PingRequest : public ProtoDecodableMessage {
protected: protected:
}; };
class PingResponse : public ProtoDecodableMessage { class PingResponse : public ProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 8; static constexpr uint8_t MESSAGE_TYPE = 8;
static constexpr uint8_t ESTIMATED_SIZE = 0; static constexpr uint8_t ESTIMATED_SIZE = 0;
@@ -430,7 +443,7 @@ class PingResponse : public ProtoDecodableMessage {
protected: protected:
}; };
class DeviceInfoRequest : public ProtoDecodableMessage { class DeviceInfoRequest : public ProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 9; static constexpr uint8_t MESSAGE_TYPE = 9;
static constexpr uint8_t ESTIMATED_SIZE = 0; static constexpr uint8_t ESTIMATED_SIZE = 0;
@@ -450,7 +463,7 @@ class AreaInfo : public ProtoMessage {
StringRef name_ref_{}; StringRef name_ref_{};
void set_name(const StringRef &ref) { this->name_ref_ = ref; } void set_name(const StringRef &ref) { this->name_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -466,7 +479,7 @@ class DeviceInfo : public ProtoMessage {
void set_name(const StringRef &ref) { this->name_ref_ = ref; } void set_name(const StringRef &ref) { this->name_ref_ = ref; }
uint32_t area_id{0}; uint32_t area_id{0};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -477,7 +490,7 @@ class DeviceInfo : public ProtoMessage {
class DeviceInfoResponse : public ProtoMessage { class DeviceInfoResponse : public ProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 10; static constexpr uint8_t MESSAGE_TYPE = 10;
static constexpr uint8_t ESTIMATED_SIZE = 211; static constexpr uint8_t ESTIMATED_SIZE = 247;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "device_info_response"; } const char *message_name() const override { return "device_info_response"; }
#endif #endif
@@ -530,23 +543,23 @@ class DeviceInfoResponse : public ProtoMessage {
bool api_encryption_supported{false}; bool api_encryption_supported{false};
#endif #endif
#ifdef USE_DEVICES #ifdef USE_DEVICES
std::vector<DeviceInfo> devices{}; std::array<DeviceInfo, ESPHOME_DEVICE_COUNT> devices{};
#endif #endif
#ifdef USE_AREAS #ifdef USE_AREAS
std::vector<AreaInfo> areas{}; std::array<AreaInfo, ESPHOME_AREA_COUNT> areas{};
#endif #endif
#ifdef USE_AREAS #ifdef USE_AREAS
AreaInfo area{}; AreaInfo area{};
#endif #endif
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
protected: protected:
}; };
class ListEntitiesRequest : public ProtoDecodableMessage { class ListEntitiesRequest : public ProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 11; static constexpr uint8_t MESSAGE_TYPE = 11;
static constexpr uint8_t ESTIMATED_SIZE = 0; static constexpr uint8_t ESTIMATED_SIZE = 0;
@@ -572,7 +585,7 @@ class ListEntitiesDoneResponse : public ProtoMessage {
protected: protected:
}; };
class SubscribeStatesRequest : public ProtoDecodableMessage { class SubscribeStatesRequest : public ProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 20; static constexpr uint8_t MESSAGE_TYPE = 20;
static constexpr uint8_t ESTIMATED_SIZE = 0; static constexpr uint8_t ESTIMATED_SIZE = 0;
@@ -597,7 +610,7 @@ class ListEntitiesBinarySensorResponse : public InfoResponseProtoMessage {
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
bool is_status_binary_sensor{false}; bool is_status_binary_sensor{false};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -614,7 +627,7 @@ class BinarySensorStateResponse : public StateResponseProtoMessage {
bool state{false}; bool state{false};
bool missing_state{false}; bool missing_state{false};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -637,7 +650,7 @@ class ListEntitiesCoverResponse : public InfoResponseProtoMessage {
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
bool supports_stop{false}; bool supports_stop{false};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -655,7 +668,7 @@ class CoverStateResponse : public StateResponseProtoMessage {
float tilt{0.0f}; float tilt{0.0f};
enums::CoverOperation current_operation{}; enums::CoverOperation current_operation{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -695,9 +708,9 @@ class ListEntitiesFanResponse : public InfoResponseProtoMessage {
bool supports_speed{false}; bool supports_speed{false};
bool supports_direction{false}; bool supports_direction{false};
int32_t supported_speed_count{0}; int32_t supported_speed_count{0};
std::vector<std::string> supported_preset_modes{}; const std::set<std::string> *supported_preset_modes{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -718,7 +731,7 @@ class FanStateResponse : public StateResponseProtoMessage {
StringRef preset_mode_ref_{}; StringRef preset_mode_ref_{};
void set_preset_mode(const StringRef &ref) { this->preset_mode_ref_ = ref; } void set_preset_mode(const StringRef &ref) { this->preset_mode_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -760,12 +773,12 @@ class ListEntitiesLightResponse : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_light_response"; } const char *message_name() const override { return "list_entities_light_response"; }
#endif #endif
std::vector<enums::ColorMode> supported_color_modes{}; const std::set<light::ColorMode> *supported_color_modes{};
float min_mireds{0.0f}; float min_mireds{0.0f};
float max_mireds{0.0f}; float max_mireds{0.0f};
std::vector<std::string> effects{}; std::vector<std::string> effects{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -793,7 +806,7 @@ class LightStateResponse : public StateResponseProtoMessage {
StringRef effect_ref_{}; StringRef effect_ref_{};
void set_effect(const StringRef &ref) { this->effect_ref_ = ref; } void set_effect(const StringRef &ref) { this->effect_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -859,7 +872,7 @@ class ListEntitiesSensorResponse : public InfoResponseProtoMessage {
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
enums::SensorStateClass state_class{}; enums::SensorStateClass state_class{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -876,7 +889,7 @@ class SensorStateResponse : public StateResponseProtoMessage {
float state{0.0f}; float state{0.0f};
bool missing_state{false}; bool missing_state{false};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -896,7 +909,7 @@ class ListEntitiesSwitchResponse : public InfoResponseProtoMessage {
StringRef device_class_ref_{}; StringRef device_class_ref_{};
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -912,7 +925,7 @@ class SwitchStateResponse : public StateResponseProtoMessage {
#endif #endif
bool state{false}; bool state{false};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -947,7 +960,7 @@ class ListEntitiesTextSensorResponse : public InfoResponseProtoMessage {
StringRef device_class_ref_{}; StringRef device_class_ref_{};
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -965,7 +978,7 @@ class TextSensorStateResponse : public StateResponseProtoMessage {
void set_state(const StringRef &ref) { this->state_ref_ = ref; } void set_state(const StringRef &ref) { this->state_ref_ = ref; }
bool missing_state{false}; bool missing_state{false};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -1004,7 +1017,7 @@ class SubscribeLogsResponse : public ProtoMessage {
this->message_len_ = len; this->message_len_ = len;
} }
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -1036,7 +1049,7 @@ class NoiseEncryptionSetKeyResponse : public ProtoMessage {
#endif #endif
bool success{false}; bool success{false};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -1045,7 +1058,7 @@ class NoiseEncryptionSetKeyResponse : public ProtoMessage {
}; };
#endif #endif
#ifdef USE_API_HOMEASSISTANT_SERVICES #ifdef USE_API_HOMEASSISTANT_SERVICES
class SubscribeHomeassistantServicesRequest : public ProtoDecodableMessage { class SubscribeHomeassistantServicesRequest : public ProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 34; static constexpr uint8_t MESSAGE_TYPE = 34;
static constexpr uint8_t ESTIMATED_SIZE = 0; static constexpr uint8_t ESTIMATED_SIZE = 0;
@@ -1064,7 +1077,7 @@ class HomeassistantServiceMap : public ProtoMessage {
void set_key(const StringRef &ref) { this->key_ref_ = ref; } void set_key(const StringRef &ref) { this->key_ref_ = ref; }
std::string value{}; std::string value{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -1085,7 +1098,7 @@ class HomeassistantServiceResponse : public ProtoMessage {
std::vector<HomeassistantServiceMap> variables{}; std::vector<HomeassistantServiceMap> variables{};
bool is_event{false}; bool is_event{false};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -1094,7 +1107,7 @@ class HomeassistantServiceResponse : public ProtoMessage {
}; };
#endif #endif
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
class SubscribeHomeAssistantStatesRequest : public ProtoDecodableMessage { class SubscribeHomeAssistantStatesRequest : public ProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 38; static constexpr uint8_t MESSAGE_TYPE = 38;
static constexpr uint8_t ESTIMATED_SIZE = 0; static constexpr uint8_t ESTIMATED_SIZE = 0;
@@ -1120,7 +1133,7 @@ class SubscribeHomeAssistantStateResponse : public ProtoMessage {
void set_attribute(const StringRef &ref) { this->attribute_ref_ = ref; } void set_attribute(const StringRef &ref) { this->attribute_ref_ = ref; }
bool once{false}; bool once{false};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -1145,7 +1158,7 @@ class HomeAssistantStateResponse : public ProtoDecodableMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
}; };
#endif #endif
class GetTimeRequest : public ProtoDecodableMessage { class GetTimeRequest : public ProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 36; static constexpr uint8_t MESSAGE_TYPE = 36;
static constexpr uint8_t ESTIMATED_SIZE = 0; static constexpr uint8_t ESTIMATED_SIZE = 0;
@@ -1167,7 +1180,7 @@ class GetTimeResponse : public ProtoDecodableMessage {
#endif #endif
uint32_t epoch_seconds{0}; uint32_t epoch_seconds{0};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -1182,7 +1195,7 @@ class ListEntitiesServicesArgument : public ProtoMessage {
void set_name(const StringRef &ref) { this->name_ref_ = ref; } void set_name(const StringRef &ref) { this->name_ref_ = ref; }
enums::ServiceArgType type{}; enums::ServiceArgType type{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -1201,7 +1214,7 @@ class ListEntitiesServicesResponse : public ProtoMessage {
uint32_t key{0}; uint32_t key{0};
std::vector<ListEntitiesServicesArgument> args{}; std::vector<ListEntitiesServicesArgument> args{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -1255,7 +1268,7 @@ class ListEntitiesCameraResponse : public InfoResponseProtoMessage {
const char *message_name() const override { return "list_entities_camera_response"; } const char *message_name() const override { return "list_entities_camera_response"; }
#endif #endif
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -1277,7 +1290,7 @@ class CameraImageResponse : public StateResponseProtoMessage {
} }
bool done{false}; bool done{false};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -1311,23 +1324,23 @@ class ListEntitiesClimateResponse : public InfoResponseProtoMessage {
#endif #endif
bool supports_current_temperature{false}; bool supports_current_temperature{false};
bool supports_two_point_target_temperature{false}; bool supports_two_point_target_temperature{false};
std::vector<enums::ClimateMode> supported_modes{}; const std::set<climate::ClimateMode> *supported_modes{};
float visual_min_temperature{0.0f}; float visual_min_temperature{0.0f};
float visual_max_temperature{0.0f}; float visual_max_temperature{0.0f};
float visual_target_temperature_step{0.0f}; float visual_target_temperature_step{0.0f};
bool supports_action{false}; bool supports_action{false};
std::vector<enums::ClimateFanMode> supported_fan_modes{}; const std::set<climate::ClimateFanMode> *supported_fan_modes{};
std::vector<enums::ClimateSwingMode> supported_swing_modes{}; const std::set<climate::ClimateSwingMode> *supported_swing_modes{};
std::vector<std::string> supported_custom_fan_modes{}; const std::set<std::string> *supported_custom_fan_modes{};
std::vector<enums::ClimatePreset> supported_presets{}; const std::set<climate::ClimatePreset> *supported_presets{};
std::vector<std::string> supported_custom_presets{}; const std::set<std::string> *supported_custom_presets{};
float visual_current_temperature_step{0.0f}; float visual_current_temperature_step{0.0f};
bool supports_current_humidity{false}; bool supports_current_humidity{false};
bool supports_target_humidity{false}; bool supports_target_humidity{false};
float visual_min_humidity{0.0f}; float visual_min_humidity{0.0f};
float visual_max_humidity{0.0f}; float visual_max_humidity{0.0f};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -1357,7 +1370,7 @@ class ClimateStateResponse : public StateResponseProtoMessage {
float current_humidity{0.0f}; float current_humidity{0.0f};
float target_humidity{0.0f}; float target_humidity{0.0f};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -1418,7 +1431,7 @@ class ListEntitiesNumberResponse : public InfoResponseProtoMessage {
StringRef device_class_ref_{}; StringRef device_class_ref_{};
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -1435,7 +1448,7 @@ class NumberStateResponse : public StateResponseProtoMessage {
float state{0.0f}; float state{0.0f};
bool missing_state{false}; bool missing_state{false};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -1467,9 +1480,9 @@ class ListEntitiesSelectResponse : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_select_response"; } const char *message_name() const override { return "list_entities_select_response"; }
#endif #endif
std::vector<std::string> options{}; const std::vector<std::string> *options{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -1487,7 +1500,7 @@ class SelectStateResponse : public StateResponseProtoMessage {
void set_state(const StringRef &ref) { this->state_ref_ = ref; } void set_state(const StringRef &ref) { this->state_ref_ = ref; }
bool missing_state{false}; bool missing_state{false};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -1524,7 +1537,7 @@ class ListEntitiesSirenResponse : public InfoResponseProtoMessage {
bool supports_duration{false}; bool supports_duration{false};
bool supports_volume{false}; bool supports_volume{false};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -1540,7 +1553,7 @@ class SirenStateResponse : public StateResponseProtoMessage {
#endif #endif
bool state{false}; bool state{false};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -1586,7 +1599,7 @@ class ListEntitiesLockResponse : public InfoResponseProtoMessage {
StringRef code_format_ref_{}; StringRef code_format_ref_{};
void set_code_format(const StringRef &ref) { this->code_format_ref_ = ref; } void set_code_format(const StringRef &ref) { this->code_format_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -1602,7 +1615,7 @@ class LockStateResponse : public StateResponseProtoMessage {
#endif #endif
enums::LockState state{}; enums::LockState state{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -1640,7 +1653,7 @@ class ListEntitiesButtonResponse : public InfoResponseProtoMessage {
StringRef device_class_ref_{}; StringRef device_class_ref_{};
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -1673,7 +1686,7 @@ class MediaPlayerSupportedFormat : public ProtoMessage {
enums::MediaPlayerFormatPurpose purpose{}; enums::MediaPlayerFormatPurpose purpose{};
uint32_t sample_bytes{0}; uint32_t sample_bytes{0};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -1683,14 +1696,15 @@ class MediaPlayerSupportedFormat : public ProtoMessage {
class ListEntitiesMediaPlayerResponse : public InfoResponseProtoMessage { class ListEntitiesMediaPlayerResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 63; static constexpr uint8_t MESSAGE_TYPE = 63;
static constexpr uint8_t ESTIMATED_SIZE = 76; static constexpr uint8_t ESTIMATED_SIZE = 80;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_media_player_response"; } const char *message_name() const override { return "list_entities_media_player_response"; }
#endif #endif
bool supports_pause{false}; bool supports_pause{false};
std::vector<MediaPlayerSupportedFormat> supported_formats{}; std::vector<MediaPlayerSupportedFormat> supported_formats{};
uint32_t feature_flags{0};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -1708,7 +1722,7 @@ class MediaPlayerStateResponse : public StateResponseProtoMessage {
float volume{0.0f}; float volume{0.0f};
bool muted{false}; bool muted{false};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -1764,7 +1778,7 @@ class BluetoothLERawAdvertisement : public ProtoMessage {
uint8_t data[62]{}; uint8_t data[62]{};
uint8_t data_len{0}; uint8_t data_len{0};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -1774,13 +1788,14 @@ class BluetoothLERawAdvertisement : public ProtoMessage {
class BluetoothLERawAdvertisementsResponse : public ProtoMessage { class BluetoothLERawAdvertisementsResponse : public ProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 93; static constexpr uint8_t MESSAGE_TYPE = 93;
static constexpr uint8_t ESTIMATED_SIZE = 34; static constexpr uint8_t ESTIMATED_SIZE = 136;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "bluetooth_le_raw_advertisements_response"; } const char *message_name() const override { return "bluetooth_le_raw_advertisements_response"; }
#endif #endif
std::vector<BluetoothLERawAdvertisement> advertisements{}; std::array<BluetoothLERawAdvertisement, BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE> advertisements{};
uint16_t advertisements_len{0};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -1817,7 +1832,7 @@ class BluetoothDeviceConnectionResponse : public ProtoMessage {
uint32_t mtu{0}; uint32_t mtu{0};
int32_t error{0}; int32_t error{0};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -1843,8 +1858,9 @@ class BluetoothGATTDescriptor : public ProtoMessage {
public: public:
std::array<uint64_t, 2> uuid{}; std::array<uint64_t, 2> uuid{};
uint32_t handle{0}; uint32_t handle{0};
uint32_t short_uuid{0};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -1857,8 +1873,9 @@ class BluetoothGATTCharacteristic : public ProtoMessage {
uint32_t handle{0}; uint32_t handle{0};
uint32_t properties{0}; uint32_t properties{0};
std::vector<BluetoothGATTDescriptor> descriptors{}; std::vector<BluetoothGATTDescriptor> descriptors{};
uint32_t short_uuid{0};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -1870,8 +1887,9 @@ class BluetoothGATTService : public ProtoMessage {
std::array<uint64_t, 2> uuid{}; std::array<uint64_t, 2> uuid{};
uint32_t handle{0}; uint32_t handle{0};
std::vector<BluetoothGATTCharacteristic> characteristics{}; std::vector<BluetoothGATTCharacteristic> characteristics{};
uint32_t short_uuid{0};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -1881,14 +1899,14 @@ class BluetoothGATTService : public ProtoMessage {
class BluetoothGATTGetServicesResponse : public ProtoMessage { class BluetoothGATTGetServicesResponse : public ProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 71; static constexpr uint8_t MESSAGE_TYPE = 71;
static constexpr uint8_t ESTIMATED_SIZE = 21; static constexpr uint8_t ESTIMATED_SIZE = 38;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "bluetooth_gatt_get_services_response"; } const char *message_name() const override { return "bluetooth_gatt_get_services_response"; }
#endif #endif
uint64_t address{0}; uint64_t address{0};
std::array<BluetoothGATTService, 1> services{}; std::vector<BluetoothGATTService> services{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -1904,7 +1922,7 @@ class BluetoothGATTGetServicesDoneResponse : public ProtoMessage {
#endif #endif
uint64_t address{0}; uint64_t address{0};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -1943,7 +1961,7 @@ class BluetoothGATTReadResponse : public ProtoMessage {
this->data_len_ = len; this->data_len_ = len;
} }
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -2036,14 +2054,14 @@ class BluetoothGATTNotifyDataResponse : public ProtoMessage {
this->data_len_ = len; this->data_len_ = len;
} }
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
protected: protected:
}; };
class SubscribeBluetoothConnectionsFreeRequest : public ProtoDecodableMessage { class SubscribeBluetoothConnectionsFreeRequest : public ProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 80; static constexpr uint8_t MESSAGE_TYPE = 80;
static constexpr uint8_t ESTIMATED_SIZE = 0; static constexpr uint8_t ESTIMATED_SIZE = 0;
@@ -2059,15 +2077,15 @@ class SubscribeBluetoothConnectionsFreeRequest : public ProtoDecodableMessage {
class BluetoothConnectionsFreeResponse : public ProtoMessage { class BluetoothConnectionsFreeResponse : public ProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 81; static constexpr uint8_t MESSAGE_TYPE = 81;
static constexpr uint8_t ESTIMATED_SIZE = 16; static constexpr uint8_t ESTIMATED_SIZE = 20;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "bluetooth_connections_free_response"; } const char *message_name() const override { return "bluetooth_connections_free_response"; }
#endif #endif
uint32_t free{0}; uint32_t free{0};
uint32_t limit{0}; uint32_t limit{0};
std::vector<uint64_t> allocated{}; std::array<uint64_t, BLUETOOTH_PROXY_MAX_CONNECTIONS> allocated{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -2085,7 +2103,7 @@ class BluetoothGATTErrorResponse : public ProtoMessage {
uint32_t handle{0}; uint32_t handle{0};
int32_t error{0}; int32_t error{0};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -2102,7 +2120,7 @@ class BluetoothGATTWriteResponse : public ProtoMessage {
uint64_t address{0}; uint64_t address{0};
uint32_t handle{0}; uint32_t handle{0};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -2119,7 +2137,7 @@ class BluetoothGATTNotifyResponse : public ProtoMessage {
uint64_t address{0}; uint64_t address{0};
uint32_t handle{0}; uint32_t handle{0};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -2137,7 +2155,7 @@ class BluetoothDevicePairingResponse : public ProtoMessage {
bool paired{false}; bool paired{false};
int32_t error{0}; int32_t error{0};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -2155,14 +2173,14 @@ class BluetoothDeviceUnpairingResponse : public ProtoMessage {
bool success{false}; bool success{false};
int32_t error{0}; int32_t error{0};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
protected: protected:
}; };
class UnsubscribeBluetoothLEAdvertisementsRequest : public ProtoDecodableMessage { class UnsubscribeBluetoothLEAdvertisementsRequest : public ProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 87; static constexpr uint8_t MESSAGE_TYPE = 87;
static constexpr uint8_t ESTIMATED_SIZE = 0; static constexpr uint8_t ESTIMATED_SIZE = 0;
@@ -2186,7 +2204,7 @@ class BluetoothDeviceClearCacheResponse : public ProtoMessage {
bool success{false}; bool success{false};
int32_t error{0}; int32_t error{0};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -2203,7 +2221,7 @@ class BluetoothScannerStateResponse : public ProtoMessage {
enums::BluetoothScannerState state{}; enums::BluetoothScannerState state{};
enums::BluetoothScannerMode mode{}; enums::BluetoothScannerMode mode{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -2249,7 +2267,7 @@ class VoiceAssistantAudioSettings : public ProtoMessage {
uint32_t auto_gain{0}; uint32_t auto_gain{0};
float volume_multiplier{0.0f}; float volume_multiplier{0.0f};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -2271,7 +2289,7 @@ class VoiceAssistantRequest : public ProtoMessage {
StringRef wake_word_phrase_ref_{}; StringRef wake_word_phrase_ref_{};
void set_wake_word_phrase(const StringRef &ref) { this->wake_word_phrase_ref_ = ref; } void set_wake_word_phrase(const StringRef &ref) { this->wake_word_phrase_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -2338,7 +2356,7 @@ class VoiceAssistantAudio : public ProtoDecodableMessage {
} }
bool end{false}; bool end{false};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -2396,7 +2414,7 @@ class VoiceAssistantAnnounceFinished : public ProtoMessage {
#endif #endif
bool success{false}; bool success{false};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -2411,14 +2429,14 @@ class VoiceAssistantWakeWord : public ProtoMessage {
void set_wake_word(const StringRef &ref) { this->wake_word_ref_ = ref; } void set_wake_word(const StringRef &ref) { this->wake_word_ref_ = ref; }
std::vector<std::string> trained_languages{}; std::vector<std::string> trained_languages{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
protected: protected:
}; };
class VoiceAssistantConfigurationRequest : public ProtoDecodableMessage { class VoiceAssistantConfigurationRequest : public ProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 121; static constexpr uint8_t MESSAGE_TYPE = 121;
static constexpr uint8_t ESTIMATED_SIZE = 0; static constexpr uint8_t ESTIMATED_SIZE = 0;
@@ -2439,10 +2457,10 @@ class VoiceAssistantConfigurationResponse : public ProtoMessage {
const char *message_name() const override { return "voice_assistant_configuration_response"; } const char *message_name() const override { return "voice_assistant_configuration_response"; }
#endif #endif
std::vector<VoiceAssistantWakeWord> available_wake_words{}; std::vector<VoiceAssistantWakeWord> available_wake_words{};
std::vector<std::string> active_wake_words{}; const std::vector<std::string> *active_wake_words{};
uint32_t max_active_wake_words{0}; uint32_t max_active_wake_words{0};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -2477,7 +2495,7 @@ class ListEntitiesAlarmControlPanelResponse : public InfoResponseProtoMessage {
bool requires_code{false}; bool requires_code{false};
bool requires_code_to_arm{false}; bool requires_code_to_arm{false};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -2493,7 +2511,7 @@ class AlarmControlPanelStateResponse : public StateResponseProtoMessage {
#endif #endif
enums::AlarmControlPanelState state{}; enums::AlarmControlPanelState state{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -2533,7 +2551,7 @@ class ListEntitiesTextResponse : public InfoResponseProtoMessage {
void set_pattern(const StringRef &ref) { this->pattern_ref_ = ref; } void set_pattern(const StringRef &ref) { this->pattern_ref_ = ref; }
enums::TextMode mode{}; enums::TextMode mode{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -2551,7 +2569,7 @@ class TextStateResponse : public StateResponseProtoMessage {
void set_state(const StringRef &ref) { this->state_ref_ = ref; } void set_state(const StringRef &ref) { this->state_ref_ = ref; }
bool missing_state{false}; bool missing_state{false};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -2585,7 +2603,7 @@ class ListEntitiesDateResponse : public InfoResponseProtoMessage {
const char *message_name() const override { return "list_entities_date_response"; } const char *message_name() const override { return "list_entities_date_response"; }
#endif #endif
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -2604,7 +2622,7 @@ class DateStateResponse : public StateResponseProtoMessage {
uint32_t month{0}; uint32_t month{0};
uint32_t day{0}; uint32_t day{0};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -2639,7 +2657,7 @@ class ListEntitiesTimeResponse : public InfoResponseProtoMessage {
const char *message_name() const override { return "list_entities_time_response"; } const char *message_name() const override { return "list_entities_time_response"; }
#endif #endif
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -2658,7 +2676,7 @@ class TimeStateResponse : public StateResponseProtoMessage {
uint32_t minute{0}; uint32_t minute{0};
uint32_t second{0}; uint32_t second{0};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -2696,7 +2714,7 @@ class ListEntitiesEventResponse : public InfoResponseProtoMessage {
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
std::vector<std::string> event_types{}; std::vector<std::string> event_types{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -2713,7 +2731,7 @@ class EventResponse : public StateResponseProtoMessage {
StringRef event_type_ref_{}; StringRef event_type_ref_{};
void set_event_type(const StringRef &ref) { this->event_type_ref_ = ref; } void set_event_type(const StringRef &ref) { this->event_type_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -2735,7 +2753,7 @@ class ListEntitiesValveResponse : public InfoResponseProtoMessage {
bool supports_position{false}; bool supports_position{false};
bool supports_stop{false}; bool supports_stop{false};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -2752,7 +2770,7 @@ class ValveStateResponse : public StateResponseProtoMessage {
float position{0.0f}; float position{0.0f};
enums::ValveOperation current_operation{}; enums::ValveOperation current_operation{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -2787,7 +2805,7 @@ class ListEntitiesDateTimeResponse : public InfoResponseProtoMessage {
const char *message_name() const override { return "list_entities_date_time_response"; } const char *message_name() const override { return "list_entities_date_time_response"; }
#endif #endif
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -2804,7 +2822,7 @@ class DateTimeStateResponse : public StateResponseProtoMessage {
bool missing_state{false}; bool missing_state{false};
uint32_t epoch_seconds{0}; uint32_t epoch_seconds{0};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -2839,7 +2857,7 @@ class ListEntitiesUpdateResponse : public InfoResponseProtoMessage {
StringRef device_class_ref_{}; StringRef device_class_ref_{};
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -2868,7 +2886,7 @@ class UpdateStateResponse : public StateResponseProtoMessage {
StringRef release_url_ref_{}; StringRef release_url_ref_{};
void set_release_url(const StringRef &ref) { this->release_url_ref_ = ref; } void set_release_url(const StringRef &ref) { this->release_url_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif

View File

@@ -383,6 +383,12 @@ template<> const char *proto_enum_to_string<enums::MediaPlayerState>(enums::Medi
return "MEDIA_PLAYER_STATE_PLAYING"; return "MEDIA_PLAYER_STATE_PLAYING";
case enums::MEDIA_PLAYER_STATE_PAUSED: case enums::MEDIA_PLAYER_STATE_PAUSED:
return "MEDIA_PLAYER_STATE_PAUSED"; return "MEDIA_PLAYER_STATE_PAUSED";
case enums::MEDIA_PLAYER_STATE_ANNOUNCING:
return "MEDIA_PLAYER_STATE_ANNOUNCING";
case enums::MEDIA_PLAYER_STATE_OFF:
return "MEDIA_PLAYER_STATE_OFF";
case enums::MEDIA_PLAYER_STATE_ON:
return "MEDIA_PLAYER_STATE_ON";
default: default:
return "UNKNOWN"; return "UNKNOWN";
} }
@@ -399,6 +405,24 @@ template<> const char *proto_enum_to_string<enums::MediaPlayerCommand>(enums::Me
return "MEDIA_PLAYER_COMMAND_MUTE"; return "MEDIA_PLAYER_COMMAND_MUTE";
case enums::MEDIA_PLAYER_COMMAND_UNMUTE: case enums::MEDIA_PLAYER_COMMAND_UNMUTE:
return "MEDIA_PLAYER_COMMAND_UNMUTE"; return "MEDIA_PLAYER_COMMAND_UNMUTE";
case enums::MEDIA_PLAYER_COMMAND_TOGGLE:
return "MEDIA_PLAYER_COMMAND_TOGGLE";
case enums::MEDIA_PLAYER_COMMAND_VOLUME_UP:
return "MEDIA_PLAYER_COMMAND_VOLUME_UP";
case enums::MEDIA_PLAYER_COMMAND_VOLUME_DOWN:
return "MEDIA_PLAYER_COMMAND_VOLUME_DOWN";
case enums::MEDIA_PLAYER_COMMAND_ENQUEUE:
return "MEDIA_PLAYER_COMMAND_ENQUEUE";
case enums::MEDIA_PLAYER_COMMAND_REPEAT_ONE:
return "MEDIA_PLAYER_COMMAND_REPEAT_ONE";
case enums::MEDIA_PLAYER_COMMAND_REPEAT_OFF:
return "MEDIA_PLAYER_COMMAND_REPEAT_OFF";
case enums::MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST:
return "MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST";
case enums::MEDIA_PLAYER_COMMAND_TURN_ON:
return "MEDIA_PLAYER_COMMAND_TURN_ON";
case enums::MEDIA_PLAYER_COMMAND_TURN_OFF:
return "MEDIA_PLAYER_COMMAND_TURN_OFF";
default: default:
return "UNKNOWN"; return "UNKNOWN";
} }
@@ -814,7 +838,7 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const {
dump_field(out, "icon", this->icon_ref_); dump_field(out, "icon", this->icon_ref_);
#endif #endif
dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category)); dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category));
for (const auto &it : this->supported_preset_modes) { for (const auto &it : *this->supported_preset_modes) {
dump_field(out, "supported_preset_modes", it, 4); dump_field(out, "supported_preset_modes", it, 4);
} }
#ifdef USE_DEVICES #ifdef USE_DEVICES
@@ -857,7 +881,7 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const {
dump_field(out, "object_id", this->object_id_ref_); dump_field(out, "object_id", this->object_id_ref_);
dump_field(out, "key", this->key); dump_field(out, "key", this->key);
dump_field(out, "name", this->name_ref_); dump_field(out, "name", this->name_ref_);
for (const auto &it : this->supported_color_modes) { for (const auto &it : *this->supported_color_modes) {
dump_field(out, "supported_color_modes", static_cast<enums::ColorMode>(it), 4); dump_field(out, "supported_color_modes", static_cast<enums::ColorMode>(it), 4);
} }
dump_field(out, "min_mireds", this->min_mireds); dump_field(out, "min_mireds", this->min_mireds);
@@ -1173,26 +1197,26 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const {
dump_field(out, "name", this->name_ref_); dump_field(out, "name", this->name_ref_);
dump_field(out, "supports_current_temperature", this->supports_current_temperature); dump_field(out, "supports_current_temperature", this->supports_current_temperature);
dump_field(out, "supports_two_point_target_temperature", this->supports_two_point_target_temperature); dump_field(out, "supports_two_point_target_temperature", this->supports_two_point_target_temperature);
for (const auto &it : this->supported_modes) { for (const auto &it : *this->supported_modes) {
dump_field(out, "supported_modes", static_cast<enums::ClimateMode>(it), 4); dump_field(out, "supported_modes", static_cast<enums::ClimateMode>(it), 4);
} }
dump_field(out, "visual_min_temperature", this->visual_min_temperature); dump_field(out, "visual_min_temperature", this->visual_min_temperature);
dump_field(out, "visual_max_temperature", this->visual_max_temperature); dump_field(out, "visual_max_temperature", this->visual_max_temperature);
dump_field(out, "visual_target_temperature_step", this->visual_target_temperature_step); dump_field(out, "visual_target_temperature_step", this->visual_target_temperature_step);
dump_field(out, "supports_action", this->supports_action); dump_field(out, "supports_action", this->supports_action);
for (const auto &it : this->supported_fan_modes) { for (const auto &it : *this->supported_fan_modes) {
dump_field(out, "supported_fan_modes", static_cast<enums::ClimateFanMode>(it), 4); dump_field(out, "supported_fan_modes", static_cast<enums::ClimateFanMode>(it), 4);
} }
for (const auto &it : this->supported_swing_modes) { for (const auto &it : *this->supported_swing_modes) {
dump_field(out, "supported_swing_modes", static_cast<enums::ClimateSwingMode>(it), 4); dump_field(out, "supported_swing_modes", static_cast<enums::ClimateSwingMode>(it), 4);
} }
for (const auto &it : this->supported_custom_fan_modes) { for (const auto &it : *this->supported_custom_fan_modes) {
dump_field(out, "supported_custom_fan_modes", it, 4); dump_field(out, "supported_custom_fan_modes", it, 4);
} }
for (const auto &it : this->supported_presets) { for (const auto &it : *this->supported_presets) {
dump_field(out, "supported_presets", static_cast<enums::ClimatePreset>(it), 4); dump_field(out, "supported_presets", static_cast<enums::ClimatePreset>(it), 4);
} }
for (const auto &it : this->supported_custom_presets) { for (const auto &it : *this->supported_custom_presets) {
dump_field(out, "supported_custom_presets", it, 4); dump_field(out, "supported_custom_presets", it, 4);
} }
dump_field(out, "disabled_by_default", this->disabled_by_default); dump_field(out, "disabled_by_default", this->disabled_by_default);
@@ -1305,7 +1329,7 @@ void ListEntitiesSelectResponse::dump_to(std::string &out) const {
#ifdef USE_ENTITY_ICON #ifdef USE_ENTITY_ICON
dump_field(out, "icon", this->icon_ref_); dump_field(out, "icon", this->icon_ref_);
#endif #endif
for (const auto &it : this->options) { for (const auto &it : *this->options) {
dump_field(out, "options", it, 4); dump_field(out, "options", it, 4);
} }
dump_field(out, "disabled_by_default", this->disabled_by_default); dump_field(out, "disabled_by_default", this->disabled_by_default);
@@ -1466,6 +1490,7 @@ void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const {
#ifdef USE_DEVICES #ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id); dump_field(out, "device_id", this->device_id);
#endif #endif
dump_field(out, "feature_flags", this->feature_flags);
} }
void MediaPlayerStateResponse::dump_to(std::string &out) const { void MediaPlayerStateResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "MediaPlayerStateResponse"); MessageDumpHelper helper(out, "MediaPlayerStateResponse");
@@ -1509,9 +1534,9 @@ void BluetoothLERawAdvertisement::dump_to(std::string &out) const {
} }
void BluetoothLERawAdvertisementsResponse::dump_to(std::string &out) const { void BluetoothLERawAdvertisementsResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "BluetoothLERawAdvertisementsResponse"); MessageDumpHelper helper(out, "BluetoothLERawAdvertisementsResponse");
for (const auto &it : this->advertisements) { for (uint16_t i = 0; i < this->advertisements_len; i++) {
out.append(" advertisements: "); out.append(" advertisements: ");
it.dump_to(out); this->advertisements[i].dump_to(out);
out.append("\n"); out.append("\n");
} }
} }
@@ -1536,6 +1561,7 @@ void BluetoothGATTDescriptor::dump_to(std::string &out) const {
dump_field(out, "uuid", it, 4); dump_field(out, "uuid", it, 4);
} }
dump_field(out, "handle", this->handle); dump_field(out, "handle", this->handle);
dump_field(out, "short_uuid", this->short_uuid);
} }
void BluetoothGATTCharacteristic::dump_to(std::string &out) const { void BluetoothGATTCharacteristic::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "BluetoothGATTCharacteristic"); MessageDumpHelper helper(out, "BluetoothGATTCharacteristic");
@@ -1549,6 +1575,7 @@ void BluetoothGATTCharacteristic::dump_to(std::string &out) const {
it.dump_to(out); it.dump_to(out);
out.append("\n"); out.append("\n");
} }
dump_field(out, "short_uuid", this->short_uuid);
} }
void BluetoothGATTService::dump_to(std::string &out) const { void BluetoothGATTService::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "BluetoothGATTService"); MessageDumpHelper helper(out, "BluetoothGATTService");
@@ -1561,6 +1588,7 @@ void BluetoothGATTService::dump_to(std::string &out) const {
it.dump_to(out); it.dump_to(out);
out.append("\n"); out.append("\n");
} }
dump_field(out, "short_uuid", this->short_uuid);
} }
void BluetoothGATTGetServicesResponse::dump_to(std::string &out) const { void BluetoothGATTGetServicesResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "BluetoothGATTGetServicesResponse"); MessageDumpHelper helper(out, "BluetoothGATTGetServicesResponse");
@@ -1769,7 +1797,7 @@ void VoiceAssistantConfigurationResponse::dump_to(std::string &out) const {
it.dump_to(out); it.dump_to(out);
out.append("\n"); out.append("\n");
} }
for (const auto &it : this->active_wake_words) { for (const auto &it : *this->active_wake_words) {
dump_field(out, "active_wake_words", it, 4); dump_field(out, "active_wake_words", it, 4);
} }
dump_field(out, "max_active_wake_words", this->max_active_wake_words); dump_field(out, "max_active_wake_words", this->max_active_wake_words);

View File

@@ -0,0 +1,34 @@
#pragma once
#include "esphome/core/defines.h"
// This file provides includes needed by the generated protobuf code
// when using pointer optimizations for component-specific types
#ifdef USE_CLIMATE
#include "esphome/components/climate/climate_mode.h"
#include "esphome/components/climate/climate_traits.h"
#endif
#ifdef USE_LIGHT
#include "esphome/components/light/light_traits.h"
#endif
#ifdef USE_FAN
#include "esphome/components/fan/fan_traits.h"
#endif
#ifdef USE_SELECT
#include "esphome/components/select/select_traits.h"
#endif
// Standard library includes that might be needed
#include <set>
#include <vector>
#include <string>
namespace esphome::api {
// This file only provides includes, no actual code
} // namespace esphome::api

View File

@@ -35,7 +35,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
case DisconnectRequest::MESSAGE_TYPE: { case DisconnectRequest::MESSAGE_TYPE: {
DisconnectRequest msg; DisconnectRequest msg;
msg.decode(msg_data, msg_size); // Empty message: no decode needed
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_disconnect_request: %s", msg.dump().c_str()); ESP_LOGVV(TAG, "on_disconnect_request: %s", msg.dump().c_str());
#endif #endif
@@ -44,7 +44,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
case DisconnectResponse::MESSAGE_TYPE: { case DisconnectResponse::MESSAGE_TYPE: {
DisconnectResponse msg; DisconnectResponse msg;
msg.decode(msg_data, msg_size); // Empty message: no decode needed
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_disconnect_response: %s", msg.dump().c_str()); ESP_LOGVV(TAG, "on_disconnect_response: %s", msg.dump().c_str());
#endif #endif
@@ -53,7 +53,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
case PingRequest::MESSAGE_TYPE: { case PingRequest::MESSAGE_TYPE: {
PingRequest msg; PingRequest msg;
msg.decode(msg_data, msg_size); // Empty message: no decode needed
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_ping_request: %s", msg.dump().c_str()); ESP_LOGVV(TAG, "on_ping_request: %s", msg.dump().c_str());
#endif #endif
@@ -62,7 +62,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
case PingResponse::MESSAGE_TYPE: { case PingResponse::MESSAGE_TYPE: {
PingResponse msg; PingResponse msg;
msg.decode(msg_data, msg_size); // Empty message: no decode needed
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_ping_response: %s", msg.dump().c_str()); ESP_LOGVV(TAG, "on_ping_response: %s", msg.dump().c_str());
#endif #endif
@@ -71,7 +71,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
case DeviceInfoRequest::MESSAGE_TYPE: { case DeviceInfoRequest::MESSAGE_TYPE: {
DeviceInfoRequest msg; DeviceInfoRequest msg;
msg.decode(msg_data, msg_size); // Empty message: no decode needed
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_device_info_request: %s", msg.dump().c_str()); ESP_LOGVV(TAG, "on_device_info_request: %s", msg.dump().c_str());
#endif #endif
@@ -80,7 +80,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
case ListEntitiesRequest::MESSAGE_TYPE: { case ListEntitiesRequest::MESSAGE_TYPE: {
ListEntitiesRequest msg; ListEntitiesRequest msg;
msg.decode(msg_data, msg_size); // Empty message: no decode needed
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_list_entities_request: %s", msg.dump().c_str()); ESP_LOGVV(TAG, "on_list_entities_request: %s", msg.dump().c_str());
#endif #endif
@@ -89,7 +89,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
case SubscribeStatesRequest::MESSAGE_TYPE: { case SubscribeStatesRequest::MESSAGE_TYPE: {
SubscribeStatesRequest msg; SubscribeStatesRequest msg;
msg.decode(msg_data, msg_size); // Empty message: no decode needed
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_subscribe_states_request: %s", msg.dump().c_str()); ESP_LOGVV(TAG, "on_subscribe_states_request: %s", msg.dump().c_str());
#endif #endif
@@ -152,7 +152,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
#ifdef USE_API_HOMEASSISTANT_SERVICES #ifdef USE_API_HOMEASSISTANT_SERVICES
case SubscribeHomeassistantServicesRequest::MESSAGE_TYPE: { case SubscribeHomeassistantServicesRequest::MESSAGE_TYPE: {
SubscribeHomeassistantServicesRequest msg; SubscribeHomeassistantServicesRequest msg;
msg.decode(msg_data, msg_size); // Empty message: no decode needed
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_subscribe_homeassistant_services_request: %s", msg.dump().c_str()); ESP_LOGVV(TAG, "on_subscribe_homeassistant_services_request: %s", msg.dump().c_str());
#endif #endif
@@ -162,7 +162,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
#endif #endif
case GetTimeRequest::MESSAGE_TYPE: { case GetTimeRequest::MESSAGE_TYPE: {
GetTimeRequest msg; GetTimeRequest msg;
msg.decode(msg_data, msg_size); // Empty message: no decode needed
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_get_time_request: %s", msg.dump().c_str()); ESP_LOGVV(TAG, "on_get_time_request: %s", msg.dump().c_str());
#endif #endif
@@ -181,7 +181,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
case SubscribeHomeAssistantStatesRequest::MESSAGE_TYPE: { case SubscribeHomeAssistantStatesRequest::MESSAGE_TYPE: {
SubscribeHomeAssistantStatesRequest msg; SubscribeHomeAssistantStatesRequest msg;
msg.decode(msg_data, msg_size); // Empty message: no decode needed
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_subscribe_home_assistant_states_request: %s", msg.dump().c_str()); ESP_LOGVV(TAG, "on_subscribe_home_assistant_states_request: %s", msg.dump().c_str());
#endif #endif
@@ -390,7 +390,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
case SubscribeBluetoothConnectionsFreeRequest::MESSAGE_TYPE: { case SubscribeBluetoothConnectionsFreeRequest::MESSAGE_TYPE: {
SubscribeBluetoothConnectionsFreeRequest msg; SubscribeBluetoothConnectionsFreeRequest msg;
msg.decode(msg_data, msg_size); // Empty message: no decode needed
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_subscribe_bluetooth_connections_free_request: %s", msg.dump().c_str()); ESP_LOGVV(TAG, "on_subscribe_bluetooth_connections_free_request: %s", msg.dump().c_str());
#endif #endif
@@ -401,7 +401,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
case UnsubscribeBluetoothLEAdvertisementsRequest::MESSAGE_TYPE: { case UnsubscribeBluetoothLEAdvertisementsRequest::MESSAGE_TYPE: {
UnsubscribeBluetoothLEAdvertisementsRequest msg; UnsubscribeBluetoothLEAdvertisementsRequest msg;
msg.decode(msg_data, msg_size); // Empty message: no decode needed
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_unsubscribe_bluetooth_le_advertisements_request: %s", msg.dump().c_str()); ESP_LOGVV(TAG, "on_unsubscribe_bluetooth_le_advertisements_request: %s", msg.dump().c_str());
#endif #endif
@@ -555,7 +555,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
case VoiceAssistantConfigurationRequest::MESSAGE_TYPE: { case VoiceAssistantConfigurationRequest::MESSAGE_TYPE: {
VoiceAssistantConfigurationRequest msg; VoiceAssistantConfigurationRequest msg;
msg.decode(msg_data, msg_size); // Empty message: no decode needed
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_voice_assistant_configuration_request: %s", msg.dump().c_str()); ESP_LOGVV(TAG, "on_voice_assistant_configuration_request: %s", msg.dump().c_str());
#endif #endif

View File

@@ -30,7 +30,7 @@ if TYPE_CHECKING:
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
async def async_run_logs(config: dict[str, Any], address: str) -> None: async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None:
"""Run the logs command in the event loop.""" """Run the logs command in the event loop."""
conf = config["api"] conf = config["api"]
name = config["esphome"]["name"] name = config["esphome"]["name"]
@@ -39,13 +39,21 @@ async def async_run_logs(config: dict[str, Any], address: str) -> None:
noise_psk: str | None = None noise_psk: str | None = None
if (encryption := conf.get(CONF_ENCRYPTION)) and (key := encryption.get(CONF_KEY)): if (encryption := conf.get(CONF_ENCRYPTION)) and (key := encryption.get(CONF_KEY)):
noise_psk = key noise_psk = key
_LOGGER.info("Starting log output from %s using esphome API", address)
if len(addresses) == 1:
_LOGGER.info("Starting log output from %s using esphome API", addresses[0])
else:
_LOGGER.info(
"Starting log output from %s using esphome API", " or ".join(addresses)
)
cli = APIClient( cli = APIClient(
address, addresses[0], # Primary address for compatibility
port, port,
password, password,
client_info=f"ESPHome Logs {__version__}", client_info=f"ESPHome Logs {__version__}",
noise_psk=noise_psk, noise_psk=noise_psk,
addresses=addresses, # Pass all addresses for automatic retry
) )
dashboard = CORE.dashboard dashboard = CORE.dashboard
@@ -66,7 +74,7 @@ async def async_run_logs(config: dict[str, Any], address: str) -> None:
await stop() await stop()
def run_logs(config: dict[str, Any], address: str) -> None: def run_logs(config: dict[str, Any], addresses: list[str]) -> None:
"""Run the logs command.""" """Run the logs command."""
with contextlib.suppress(KeyboardInterrupt): with contextlib.suppress(KeyboardInterrupt):
asyncio.run(async_run_logs(config, address)) asyncio.run(async_run_logs(config, addresses))

View File

@@ -56,6 +56,14 @@ class CustomAPIDevice {
auto *service = new CustomAPIDeviceService<T, Ts...>(name, arg_names, (T *) this, callback); // NOLINT auto *service = new CustomAPIDeviceService<T, Ts...>(name, arg_names, (T *) this, callback); // NOLINT
global_api_server->register_user_service(service); global_api_server->register_user_service(service);
} }
#else
template<typename T, typename... Ts>
void register_service(void (T::*callback)(Ts...), const std::string &name,
const std::array<std::string, sizeof...(Ts)> &arg_names) {
static_assert(
sizeof(T) == 0,
"register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration");
}
#endif #endif
/** Register a custom native API service that will show up in Home Assistant. /** Register a custom native API service that will show up in Home Assistant.
@@ -81,6 +89,12 @@ class CustomAPIDevice {
auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback); // NOLINT auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback); // NOLINT
global_api_server->register_user_service(service); global_api_server->register_user_service(service);
} }
#else
template<typename T> void register_service(void (T::*callback)(), const std::string &name) {
static_assert(
sizeof(T) == 0,
"register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration");
}
#endif #endif
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
@@ -135,6 +149,22 @@ class CustomAPIDevice {
auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1); auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1);
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), f); global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), f);
} }
#else
template<typename T>
void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id,
const std::string &attribute = "") {
static_assert(sizeof(T) == 0,
"subscribe_homeassistant_state() requires 'homeassistant_states: true' in the 'api:' section "
"of your YAML configuration");
}
template<typename T>
void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id,
const std::string &attribute = "") {
static_assert(sizeof(T) == 0,
"subscribe_homeassistant_state() requires 'homeassistant_states: true' in the 'api:' section "
"of your YAML configuration");
}
#endif #endif
#ifdef USE_API_HOMEASSISTANT_SERVICES #ifdef USE_API_HOMEASSISTANT_SERVICES
@@ -222,6 +252,28 @@ class CustomAPIDevice {
} }
global_api_server->send_homeassistant_service_call(resp); global_api_server->send_homeassistant_service_call(resp);
} }
#else
template<typename T = void> void call_homeassistant_service(const std::string &service_name) {
static_assert(sizeof(T) == 0, "call_homeassistant_service() requires 'homeassistant_services: true' in the 'api:' "
"section of your YAML configuration");
}
template<typename T = void>
void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) {
static_assert(sizeof(T) == 0, "call_homeassistant_service() requires 'homeassistant_services: true' in the 'api:' "
"section of your YAML configuration");
}
template<typename T = void> void fire_homeassistant_event(const std::string &event_name) {
static_assert(sizeof(T) == 0, "fire_homeassistant_event() requires 'homeassistant_services: true' in the 'api:' "
"section of your YAML configuration");
}
template<typename T = void>
void fire_homeassistant_event(const std::string &service_name, const std::map<std::string, std::string> &data) {
static_assert(sizeof(T) == 0, "fire_homeassistant_event() requires 'homeassistant_services: true' in the 'api:' "
"section of your YAML configuration");
}
#endif #endif
}; };

View File

@@ -15,6 +15,23 @@
namespace esphome::api { namespace esphome::api {
// Helper functions for ZigZag encoding/decoding
inline constexpr uint32_t encode_zigzag32(int32_t value) {
return (static_cast<uint32_t>(value) << 1) ^ (static_cast<uint32_t>(value >> 31));
}
inline constexpr uint64_t encode_zigzag64(int64_t value) {
return (static_cast<uint64_t>(value) << 1) ^ (static_cast<uint64_t>(value >> 63));
}
inline constexpr int32_t decode_zigzag32(uint32_t value) {
return (value & 1) ? static_cast<int32_t>(~(value >> 1)) : static_cast<int32_t>(value >> 1);
}
inline constexpr int64_t decode_zigzag64(uint64_t value) {
return (value & 1) ? static_cast<int64_t>(~(value >> 1)) : static_cast<int64_t>(value >> 1);
}
/* /*
* StringRef Ownership Model for API Protocol Messages * StringRef Ownership Model for API Protocol Messages
* =================================================== * ===================================================
@@ -87,33 +104,25 @@ class ProtoVarInt {
return {}; // Incomplete or invalid varint return {}; // Incomplete or invalid varint
} }
uint16_t as_uint16() const { return this->value_; } constexpr uint16_t as_uint16() const { return this->value_; }
uint32_t as_uint32() const { return this->value_; } constexpr uint32_t as_uint32() const { return this->value_; }
uint64_t as_uint64() const { return this->value_; } constexpr uint64_t as_uint64() const { return this->value_; }
bool as_bool() const { return this->value_; } constexpr bool as_bool() const { return this->value_; }
int32_t as_int32() const { constexpr int32_t as_int32() const {
// Not ZigZag encoded // Not ZigZag encoded
return static_cast<int32_t>(this->as_int64()); return static_cast<int32_t>(this->as_int64());
} }
int64_t as_int64() const { constexpr int64_t as_int64() const {
// Not ZigZag encoded // Not ZigZag encoded
return static_cast<int64_t>(this->value_); return static_cast<int64_t>(this->value_);
} }
int32_t as_sint32() const { constexpr int32_t as_sint32() const {
// with ZigZag encoding // with ZigZag encoding
if (this->value_ & 1) { return decode_zigzag32(static_cast<uint32_t>(this->value_));
return static_cast<int32_t>(~(this->value_ >> 1));
} else {
return static_cast<int32_t>(this->value_ >> 1);
}
} }
int64_t as_sint64() const { constexpr int64_t as_sint64() const {
// with ZigZag encoding // with ZigZag encoding
if (this->value_ & 1) { return decode_zigzag64(this->value_);
return static_cast<int64_t>(~(this->value_ >> 1));
} else {
return static_cast<int64_t>(this->value_ >> 1);
}
} }
/** /**
* Encode the varint value to a pre-allocated buffer without bounds checking. * Encode the varint value to a pre-allocated buffer without bounds checking.
@@ -309,22 +318,10 @@ class ProtoWriteBuffer {
this->encode_uint64(field_id, static_cast<uint64_t>(value), force); this->encode_uint64(field_id, static_cast<uint64_t>(value), force);
} }
void encode_sint32(uint32_t field_id, int32_t value, bool force = false) { void encode_sint32(uint32_t field_id, int32_t value, bool force = false) {
uint32_t uvalue; this->encode_uint32(field_id, encode_zigzag32(value), force);
if (value < 0) {
uvalue = ~(value << 1);
} else {
uvalue = value << 1;
}
this->encode_uint32(field_id, uvalue, force);
} }
void encode_sint64(uint32_t field_id, int64_t value, bool force = false) { void encode_sint64(uint32_t field_id, int64_t value, bool force = false) {
uint64_t uvalue; this->encode_uint64(field_id, encode_zigzag64(value), force);
if (value < 0) {
uvalue = ~(value << 1);
} else {
uvalue = value << 1;
}
this->encode_uint64(field_id, uvalue, force);
} }
void encode_message(uint32_t field_id, const ProtoMessage &value, bool force = false); void encode_message(uint32_t field_id, const ProtoMessage &value, bool force = false);
std::vector<uint8_t> *get_buffer() const { return buffer_; } std::vector<uint8_t> *get_buffer() const { return buffer_; }
@@ -333,13 +330,16 @@ class ProtoWriteBuffer {
std::vector<uint8_t> *buffer_; std::vector<uint8_t> *buffer_;
}; };
// Forward declaration
class ProtoSize;
class ProtoMessage { class ProtoMessage {
public: public:
virtual ~ProtoMessage() = default; virtual ~ProtoMessage() = default;
// Default implementation for messages with no fields // Default implementation for messages with no fields
virtual void encode(ProtoWriteBuffer buffer) const {} virtual void encode(ProtoWriteBuffer buffer) const {}
// Default implementation for messages with no fields // Default implementation for messages with no fields
virtual void calculate_size(uint32_t &total_size) const {} virtual void calculate_size(ProtoSize &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;
@@ -360,31 +360,39 @@ class ProtoDecodableMessage : public ProtoMessage {
}; };
class ProtoSize { class ProtoSize {
private:
uint32_t total_size_ = 0;
public: public:
/** /**
* @brief ProtoSize class for Protocol Buffer serialization size calculation * @brief ProtoSize class for Protocol Buffer serialization size calculation
* *
* This class provides static methods to calculate the exact byte counts needed * This class provides methods to calculate the exact byte counts needed
* for encoding various Protocol Buffer field types. All methods are designed to be * for encoding various Protocol Buffer field types. The class now uses an
* efficient for the common case where many fields have default values. * object-based approach to reduce parameter passing overhead while keeping
* varint calculation methods static for external use.
* *
* Implements Protocol Buffer encoding size calculation according to: * Implements Protocol Buffer encoding size calculation according to:
* https://protobuf.dev/programming-guides/encoding/ * https://protobuf.dev/programming-guides/encoding/
* *
* Key features: * Key features:
* - Object-based approach reduces flash usage by eliminating parameter passing
* - Early-return optimization for zero/default values * - Early-return optimization for zero/default values
* - Direct total_size updates to avoid unnecessary additions * - Static varint methods for external callers
* - Specialized handling for different field types according to protobuf spec * - Specialized handling for different field types according to protobuf spec
* - Templated helpers for repeated fields and messages
*/ */
ProtoSize() = default;
uint32_t get_size() const { return total_size_; }
/** /**
* @brief Calculates the size in bytes needed to encode a uint32_t value as a varint * @brief Calculates the size in bytes needed to encode a uint32_t value as a varint
* *
* @param value The uint32_t value to calculate size for * @param value The uint32_t value to calculate size for
* @return The number of bytes needed to encode the value * @return The number of bytes needed to encode the value
*/ */
static inline uint32_t varint(uint32_t value) { static constexpr uint32_t varint(uint32_t value) {
// Optimized varint size calculation using leading zeros // Optimized varint size calculation using leading zeros
// Each 7 bits requires one byte in the varint encoding // Each 7 bits requires one byte in the varint encoding
if (value < 128) if (value < 128)
@@ -408,7 +416,7 @@ class ProtoSize {
* @param value The uint64_t value to calculate size for * @param value The uint64_t value to calculate size for
* @return The number of bytes needed to encode the value * @return The number of bytes needed to encode the value
*/ */
static inline uint32_t varint(uint64_t value) { static constexpr uint32_t varint(uint64_t value) {
// Handle common case of values fitting in uint32_t (vast majority of use cases) // Handle common case of values fitting in uint32_t (vast majority of use cases)
if (value <= UINT32_MAX) { if (value <= UINT32_MAX) {
return varint(static_cast<uint32_t>(value)); return varint(static_cast<uint32_t>(value));
@@ -439,7 +447,7 @@ class ProtoSize {
* @param value The int32_t value to calculate size for * @param value The int32_t value to calculate size for
* @return The number of bytes needed to encode the value * @return The number of bytes needed to encode the value
*/ */
static inline uint32_t varint(int32_t value) { static constexpr uint32_t varint(int32_t value) {
// Negative values are sign-extended to 64 bits in protocol buffers, // Negative values are sign-extended to 64 bits in protocol buffers,
// which always results in a 10-byte varint for negative int32 // which always results in a 10-byte varint for negative int32
if (value < 0) { if (value < 0) {
@@ -455,7 +463,7 @@ class ProtoSize {
* @param value The int64_t value to calculate size for * @param value The int64_t value to calculate size for
* @return The number of bytes needed to encode the value * @return The number of bytes needed to encode the value
*/ */
static inline uint32_t varint(int64_t value) { static constexpr uint32_t varint(int64_t value) {
// For int64_t, we convert to uint64_t and calculate the size // For int64_t, we convert to uint64_t and calculate the size
// This works because the bit pattern determines the encoding size, // This works because the bit pattern determines the encoding size,
// and we've handled negative int32 values as a special case above // and we've handled negative int32 values as a special case above
@@ -469,7 +477,7 @@ class ProtoSize {
* @param type The wire type value (from the WireType enum in the protobuf spec) * @param type The wire type value (from the WireType enum in the protobuf spec)
* @return The number of bytes needed to encode the field ID and wire type * @return The number of bytes needed to encode the field ID and wire type
*/ */
static inline uint32_t field(uint32_t field_id, uint32_t type) { static constexpr uint32_t field(uint32_t field_id, uint32_t type) {
uint32_t tag = (field_id << 3) | (type & 0b111); uint32_t tag = (field_id << 3) | (type & 0b111);
return varint(tag); return varint(tag);
} }
@@ -478,9 +486,7 @@ class ProtoSize {
* @brief Common parameters for all add_*_field methods * @brief Common parameters for all add_*_field methods
* *
* All add_*_field methods follow these common patterns: * All add_*_field methods follow these common patterns:
* * * @param field_id_size Pre-calculated size of the field ID in bytes
* @param total_size Reference to the total message size to update
* @param field_id_size Pre-calculated size of the field ID in bytes
* @param value The value to calculate size for (type varies) * @param value The value to calculate size for (type varies)
* @param force Whether to calculate size even if the value is default/zero/empty * @param force Whether to calculate size even if the value is default/zero/empty
* *
@@ -493,85 +499,63 @@ class ProtoSize {
/** /**
* @brief Calculates and adds the size of an int32 field to the total message size * @brief Calculates and adds the size of an int32 field to the total message size
*/ */
static inline void add_int32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value) { inline void add_int32(uint32_t field_id_size, int32_t value) {
// Skip calculation if value is zero if (value != 0) {
if (value == 0) { add_int32_force(field_id_size, value);
return; // No need to update total_size
}
// Calculate and directly add to total_size
if (value < 0) {
// Negative values are encoded as 10-byte varints in protobuf
total_size += field_id_size + 10;
} else {
// For non-negative values, use the standard varint size
total_size += field_id_size + varint(static_cast<uint32_t>(value));
} }
} }
/** /**
* @brief Calculates and adds the size of an int32 field to the total message size (repeated field version) * @brief Calculates and adds the size of an int32 field to the total message size (force version)
*/ */
static inline void add_int32_field_repeated(uint32_t &total_size, uint32_t field_id_size, int32_t value) { inline void add_int32_force(uint32_t field_id_size, int32_t value) {
// Always calculate size for repeated fields // Always calculate size when forced
if (value < 0) { // Negative values are encoded as 10-byte varints in protobuf
// Negative values are encoded as 10-byte varints in protobuf total_size_ += field_id_size + (value < 0 ? 10 : varint(static_cast<uint32_t>(value)));
total_size += field_id_size + 10;
} else {
// For non-negative values, use the standard varint size
total_size += field_id_size + varint(static_cast<uint32_t>(value));
}
} }
/** /**
* @brief Calculates and adds the size of a uint32 field to the total message size * @brief Calculates and adds the size of a uint32 field to the total message size
*/ */
static inline void add_uint32_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value) { inline void add_uint32(uint32_t field_id_size, uint32_t value) {
// Skip calculation if value is zero if (value != 0) {
if (value == 0) { add_uint32_force(field_id_size, value);
return; // No need to update total_size
} }
// Calculate and directly add to total_size
total_size += field_id_size + varint(value);
} }
/** /**
* @brief Calculates and adds the size of a uint32 field to the total message size (repeated field version) * @brief Calculates and adds the size of a uint32 field to the total message size (force version)
*/ */
static inline void add_uint32_field_repeated(uint32_t &total_size, uint32_t field_id_size, uint32_t value) { inline void add_uint32_force(uint32_t field_id_size, uint32_t value) {
// Always calculate size for repeated fields // Always calculate size when force is true
total_size += field_id_size + varint(value); total_size_ += field_id_size + varint(value);
} }
/** /**
* @brief Calculates and adds the size of a boolean field to the total message size * @brief Calculates and adds the size of a boolean field to the total message size
*/ */
static inline void add_bool_field(uint32_t &total_size, uint32_t field_id_size, bool value) { inline void add_bool(uint32_t field_id_size, bool value) {
// Skip calculation if value is false if (value) {
if (!value) { // Boolean fields always use 1 byte when true
return; // No need to update total_size total_size_ += field_id_size + 1;
} }
// Boolean fields always use 1 byte when true
total_size += field_id_size + 1;
} }
/** /**
* @brief Calculates and adds the size of a boolean field to the total message size (repeated field version) * @brief Calculates and adds the size of a boolean field to the total message size (force version)
*/ */
static inline void add_bool_field_repeated(uint32_t &total_size, uint32_t field_id_size, bool value) { inline void add_bool_force(uint32_t field_id_size, bool value) {
// Always calculate size for repeated fields // Always calculate size when force is true
// Boolean fields always use 1 byte // Boolean fields always use 1 byte
total_size += field_id_size + 1; total_size_ += field_id_size + 1;
} }
/** /**
* @brief Calculates and adds the size of a float field to the total message size * @brief Calculates and adds the size of a float field to the total message size
*/ */
static inline void add_float_field(uint32_t &total_size, uint32_t field_id_size, float value) { inline void add_float(uint32_t field_id_size, float value) {
if (value != 0.0f) { if (value != 0.0f) {
total_size += field_id_size + 4; total_size_ += field_id_size + 4;
} }
} }
@@ -581,9 +565,9 @@ class ProtoSize {
/** /**
* @brief Calculates and adds the size of a fixed32 field to the total message size * @brief Calculates and adds the size of a fixed32 field to the total message size
*/ */
static inline void add_fixed32_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value) { inline void add_fixed32(uint32_t field_id_size, uint32_t value) {
if (value != 0) { if (value != 0) {
total_size += field_id_size + 4; total_size_ += field_id_size + 4;
} }
} }
@@ -593,149 +577,103 @@ class ProtoSize {
/** /**
* @brief Calculates and adds the size of a sfixed32 field to the total message size * @brief Calculates and adds the size of a sfixed32 field to the total message size
*/ */
static inline void add_sfixed32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value) { inline void add_sfixed32(uint32_t field_id_size, int32_t value) {
if (value != 0) { if (value != 0) {
total_size += field_id_size + 4; total_size_ += field_id_size + 4;
} }
} }
// NOTE: add_sfixed64_field removed - wire type 1 (64-bit: sfixed64) not supported // NOTE: add_sfixed64_field removed - wire type 1 (64-bit: sfixed64) not supported
// to reduce overhead on embedded systems // to reduce overhead on embedded systems
/**
* @brief Calculates and adds the size of an enum field to the total message size
*
* Enum fields are encoded as uint32 varints.
*/
static inline void add_enum_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value) {
// Skip calculation if value is zero
if (value == 0) {
return; // No need to update total_size
}
// Enums are encoded as uint32
total_size += field_id_size + varint(value);
}
/**
* @brief Calculates and adds the size of an enum field to the total message size (repeated field version)
*
* Enum fields are encoded as uint32 varints.
*/
static inline void add_enum_field_repeated(uint32_t &total_size, uint32_t field_id_size, uint32_t value) {
// Always calculate size for repeated fields
// Enums are encoded as uint32
total_size += field_id_size + varint(value);
}
/** /**
* @brief Calculates and adds the size of a sint32 field to the total message size * @brief Calculates and adds the size of a sint32 field to the total message size
* *
* Sint32 fields use ZigZag encoding, which is more efficient for negative values. * Sint32 fields use ZigZag encoding, which is more efficient for negative values.
*/ */
static inline void add_sint32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value) { inline void add_sint32(uint32_t field_id_size, int32_t value) {
// Skip calculation if value is zero if (value != 0) {
if (value == 0) { add_sint32_force(field_id_size, value);
return; // No need to update total_size
} }
// ZigZag encoding for sint32: (n << 1) ^ (n >> 31)
uint32_t zigzag = (static_cast<uint32_t>(value) << 1) ^ (static_cast<uint32_t>(value >> 31));
total_size += field_id_size + varint(zigzag);
} }
/** /**
* @brief Calculates and adds the size of a sint32 field to the total message size (repeated field version) * @brief Calculates and adds the size of a sint32 field to the total message size (force version)
* *
* Sint32 fields use ZigZag encoding, which is more efficient for negative values. * Sint32 fields use ZigZag encoding, which is more efficient for negative values.
*/ */
static inline void add_sint32_field_repeated(uint32_t &total_size, uint32_t field_id_size, int32_t value) { inline void add_sint32_force(uint32_t field_id_size, int32_t value) {
// Always calculate size for repeated fields // Always calculate size when force is true
// ZigZag encoding for sint32: (n << 1) ^ (n >> 31) // ZigZag encoding for sint32
uint32_t zigzag = (static_cast<uint32_t>(value) << 1) ^ (static_cast<uint32_t>(value >> 31)); total_size_ += field_id_size + varint(encode_zigzag32(value));
total_size += field_id_size + varint(zigzag);
} }
/** /**
* @brief Calculates and adds the size of an int64 field to the total message size * @brief Calculates and adds the size of an int64 field to the total message size
*/ */
static inline void add_int64_field(uint32_t &total_size, uint32_t field_id_size, int64_t value) { inline void add_int64(uint32_t field_id_size, int64_t value) {
// Skip calculation if value is zero if (value != 0) {
if (value == 0) { add_int64_force(field_id_size, value);
return; // No need to update total_size
} }
// Calculate and directly add to total_size
total_size += field_id_size + varint(value);
} }
/** /**
* @brief Calculates and adds the size of an int64 field to the total message size (repeated field version) * @brief Calculates and adds the size of an int64 field to the total message size (force version)
*/ */
static inline void add_int64_field_repeated(uint32_t &total_size, uint32_t field_id_size, int64_t value) { inline void add_int64_force(uint32_t field_id_size, int64_t value) {
// Always calculate size for repeated fields // Always calculate size when force is true
total_size += field_id_size + varint(value); total_size_ += field_id_size + varint(value);
} }
/** /**
* @brief Calculates and adds the size of a uint64 field to the total message size * @brief Calculates and adds the size of a uint64 field to the total message size
*/ */
static inline void add_uint64_field(uint32_t &total_size, uint32_t field_id_size, uint64_t value) { inline void add_uint64(uint32_t field_id_size, uint64_t value) {
// Skip calculation if value is zero if (value != 0) {
if (value == 0) { add_uint64_force(field_id_size, value);
return; // No need to update total_size
} }
// Calculate and directly add to total_size
total_size += field_id_size + varint(value);
} }
/** /**
* @brief Calculates and adds the size of a uint64 field to the total message size (repeated field version) * @brief Calculates and adds the size of a uint64 field to the total message size (force version)
*/ */
static inline void add_uint64_field_repeated(uint32_t &total_size, uint32_t field_id_size, uint64_t value) { inline void add_uint64_force(uint32_t field_id_size, uint64_t value) {
// Always calculate size for repeated fields // Always calculate size when force is true
total_size += field_id_size + varint(value); total_size_ += field_id_size + varint(value);
} }
// NOTE: sint64 support functions (add_sint64_field, add_sint64_field_repeated) removed // NOTE: sint64 support functions (add_sint64_field, add_sint64_field_force) removed
// sint64 type is not supported by ESPHome API to reduce overhead on embedded systems // sint64 type is not supported by ESPHome API to reduce overhead on embedded systems
/** /**
* @brief Calculates and adds the size of a string field using length * @brief Calculates and adds the size of a length-delimited field (string/bytes) to the total message size
*/ */
static inline void add_string_field(uint32_t &total_size, uint32_t field_id_size, size_t len) { inline void add_length(uint32_t field_id_size, size_t len) {
// Skip calculation if string is empty if (len != 0) {
if (len == 0) { add_length_force(field_id_size, len);
return; // No need to update total_size
} }
// Field ID + length varint + string bytes
total_size += field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len);
} }
/** /**
* @brief Calculates and adds the size of a string/bytes field to the total message size (repeated field version) * @brief Calculates and adds the size of a length-delimited field (string/bytes) to the total message size (repeated
* field version)
*/ */
static inline void add_string_field_repeated(uint32_t &total_size, uint32_t field_id_size, const std::string &str) { inline void add_length_force(uint32_t field_id_size, size_t len) {
// Always calculate size for repeated fields // Always calculate size when force is true
const uint32_t str_size = static_cast<uint32_t>(str.size());
total_size += field_id_size + varint(str_size) + str_size;
}
/**
* @brief Calculates and adds the size of a bytes field to the total message size
*/
static inline void add_bytes_field(uint32_t &total_size, uint32_t field_id_size, size_t len) {
// Skip calculation if bytes is empty
if (len == 0) {
return; // No need to update total_size
}
// Field ID + length varint + data bytes // Field ID + length varint + data bytes
total_size += field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len); total_size_ += field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len);
} }
/**
* @brief Adds a pre-calculated size directly to the total
*
* This is used when we can calculate the total size by multiplying the number
* of elements by the bytes per element (for repeated fixed-size types like float, fixed32, etc.)
*
* @param size The pre-calculated total size to add
*/
inline void add_precalculated_size(uint32_t size) { total_size_ += size; }
/** /**
* @brief Calculates and adds the size of a nested message field to the total message size * @brief Calculates and adds the size of a nested message field to the total message size
* *
@@ -744,26 +682,21 @@ class ProtoSize {
* *
* @param nested_size The pre-calculated size of the nested message * @param nested_size The pre-calculated size of the nested message
*/ */
static inline void add_message_field(uint32_t &total_size, uint32_t field_id_size, uint32_t nested_size) { inline void add_message_field(uint32_t field_id_size, uint32_t nested_size) {
// Skip calculation if nested message is empty if (nested_size != 0) {
if (nested_size == 0) { add_message_field_force(field_id_size, nested_size);
return; // No need to update total_size
} }
// Calculate and directly add to total_size
// Field ID + length varint + nested message content
total_size += field_id_size + varint(nested_size) + nested_size;
} }
/** /**
* @brief Calculates and adds the size of a nested message field to the total message size (repeated field version) * @brief Calculates and adds the size of a nested message field to the total message size (force version)
* *
* @param nested_size The pre-calculated size of the nested message * @param nested_size The pre-calculated size of the nested message
*/ */
static inline void add_message_field_repeated(uint32_t &total_size, uint32_t field_id_size, uint32_t nested_size) { inline void add_message_field_force(uint32_t field_id_size, uint32_t nested_size) {
// Always calculate size for repeated fields // Always calculate size when force is true
// Field ID + length varint + nested message content // Field ID + length varint + nested message content
total_size += field_id_size + varint(nested_size) + nested_size; total_size_ += field_id_size + varint(nested_size) + nested_size;
} }
/** /**
@@ -775,26 +708,29 @@ class ProtoSize {
* *
* @param message The nested message object * @param message The nested message object
*/ */
static inline void add_message_object(uint32_t &total_size, uint32_t field_id_size, const ProtoMessage &message) { inline void add_message_object(uint32_t field_id_size, const ProtoMessage &message) {
uint32_t nested_size = 0; // Calculate nested message size by creating a temporary ProtoSize
message.calculate_size(nested_size); ProtoSize nested_calc;
message.calculate_size(nested_calc);
uint32_t nested_size = nested_calc.get_size();
// Use the base implementation with the calculated nested_size // Use the base implementation with the calculated nested_size
add_message_field(total_size, field_id_size, nested_size); add_message_field(field_id_size, nested_size);
} }
/** /**
* @brief Calculates and adds the size of a nested message field to the total message size (repeated field version) * @brief Calculates and adds the size of a nested message field to the total message size (force version)
* *
* @param message The nested message object * @param message The nested message object
*/ */
static inline void add_message_object_repeated(uint32_t &total_size, uint32_t field_id_size, inline void add_message_object_force(uint32_t field_id_size, const ProtoMessage &message) {
const ProtoMessage &message) { // Calculate nested message size by creating a temporary ProtoSize
uint32_t nested_size = 0; ProtoSize nested_calc;
message.calculate_size(nested_size); message.calculate_size(nested_calc);
uint32_t nested_size = nested_calc.get_size();
// Use the base implementation with the calculated nested_size // Use the base implementation with the calculated nested_size
add_message_field_repeated(total_size, field_id_size, nested_size); add_message_field_force(field_id_size, nested_size);
} }
/** /**
@@ -807,16 +743,15 @@ class ProtoSize {
* @param messages Vector of message objects * @param messages Vector of message objects
*/ */
template<typename MessageType> template<typename MessageType>
static inline void add_repeated_message(uint32_t &total_size, uint32_t field_id_size, inline void add_repeated_message(uint32_t field_id_size, const std::vector<MessageType> &messages) {
const std::vector<MessageType> &messages) {
// Skip if the vector is empty // Skip if the vector is empty
if (messages.empty()) { if (messages.empty()) {
return; return;
} }
// Use the repeated field version for all messages // Use the force version for all messages in the repeated field
for (const auto &message : messages) { for (const auto &message : messages) {
add_message_object_repeated(total_size, field_id_size, message); add_message_object_force(field_id_size, message);
} }
} }
}; };
@@ -826,8 +761,9 @@ inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessa
this->encode_field_raw(field_id, 2); // type 2: Length-delimited message this->encode_field_raw(field_id, 2); // type 2: Length-delimited message
// Calculate the message size first // Calculate the message size first
uint32_t msg_length_bytes = 0; ProtoSize msg_size;
value.calculate_size(msg_length_bytes); value.calculate_size(msg_size);
uint32_t msg_length_bytes = msg_size.get_size();
// Calculate how many bytes the length varint needs // Calculate how many bytes the length varint needs
uint32_t varint_length_bytes = ProtoSize::varint(msg_length_bytes); uint32_t varint_length_bytes = ProtoSize::varint(msg_length_bytes);
@@ -876,8 +812,9 @@ class ProtoService {
// Optimized method that pre-allocates buffer based on message size // Optimized method that pre-allocates buffer based on message size
bool send_message_(const ProtoMessage &msg, uint8_t message_type) { bool send_message_(const ProtoMessage &msg, uint8_t message_type) {
uint32_t msg_size = 0; ProtoSize size;
msg.calculate_size(msg_size); msg.calculate_size(size);
uint32_t msg_size = size.get_size();
// Create a pre-sized buffer // Create a pre-sized buffer
auto buffer = this->create_buffer(msg_size); auto buffer = this->create_buffer(msg_size);

View File

@@ -7,6 +7,7 @@ from esphome.const import (
CONF_DIRECTION, CONF_DIRECTION,
CONF_HYSTERESIS, CONF_HYSTERESIS,
CONF_ID, CONF_ID,
CONF_POWER_MODE,
CONF_RANGE, CONF_RANGE,
) )
@@ -57,7 +58,6 @@ FAST_FILTER = {
CONF_RAW_ANGLE = "raw_angle" CONF_RAW_ANGLE = "raw_angle"
CONF_RAW_POSITION = "raw_position" CONF_RAW_POSITION = "raw_position"
CONF_WATCHDOG = "watchdog" CONF_WATCHDOG = "watchdog"
CONF_POWER_MODE = "power_mode"
CONF_SLOW_FILTER = "slow_filter" CONF_SLOW_FILTER = "slow_filter"
CONF_FAST_FILTER = "fast_filter" CONF_FAST_FILTER = "fast_filter"
CONF_START_POSITION = "start_position" CONF_START_POSITION = "start_position"

View File

@@ -24,7 +24,6 @@ AS5600Sensor = as5600_ns.class_("AS5600Sensor", sensor.Sensor, cg.PollingCompone
CONF_RAW_ANGLE = "raw_angle" CONF_RAW_ANGLE = "raw_angle"
CONF_RAW_POSITION = "raw_position" CONF_RAW_POSITION = "raw_position"
CONF_WATCHDOG = "watchdog" CONF_WATCHDOG = "watchdog"
CONF_POWER_MODE = "power_mode"
CONF_SLOW_FILTER = "slow_filter" CONF_SLOW_FILTER = "slow_filter"
CONF_FAST_FILTER = "fast_filter" CONF_FAST_FILTER = "fast_filter"
CONF_PWM_FREQUENCY = "pwm_frequency" CONF_PWM_FREQUENCY = "pwm_frequency"

View File

@@ -10,7 +10,7 @@ from esphome.const import (
) )
from esphome.core import CORE, coroutine_with_priority from esphome.core import CORE, coroutine_with_priority
CODEOWNERS = ["@OttoWinter"] CODEOWNERS = ["@esphome/core"]
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
cv.Schema({}), cv.Schema({}),

View File

@@ -110,6 +110,8 @@ void ATM90E32Component::update() {
void ATM90E32Component::setup() { void ATM90E32Component::setup() {
this->spi_setup(); this->spi_setup();
this->cs_summary_ = this->cs_->dump_summary();
const char *cs = this->cs_summary_.c_str();
uint16_t mmode0 = 0x87; // 3P4W 50Hz uint16_t mmode0 = 0x87; // 3P4W 50Hz
uint16_t high_thresh = 0; uint16_t high_thresh = 0;
@@ -130,9 +132,9 @@ void ATM90E32Component::setup() {
mmode0 |= 0 << 1; // sets 1st bit to 0, phase b is not counted into the all-phase sum energy/power (P/Q/S) mmode0 |= 0 << 1; // sets 1st bit to 0, phase b is not counted into the all-phase sum energy/power (P/Q/S)
} }
this->write16_(ATM90E32_REGISTER_SOFTRESET, 0x789A); // Perform soft reset this->write16_(ATM90E32_REGISTER_SOFTRESET, 0x789A, false); // Perform soft reset
delay(6); // Wait for the minimum 5ms + 1ms delay(6); // Wait for the minimum 5ms + 1ms
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA); // enable register config access this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA); // enable register config access
if (!this->validate_spi_read_(0x55AA, "setup()")) { if (!this->validate_spi_read_(0x55AA, "setup()")) {
ESP_LOGW(TAG, "Could not initialize ATM90E32 IC, check SPI settings"); ESP_LOGW(TAG, "Could not initialize ATM90E32 IC, check SPI settings");
this->mark_failed(); this->mark_failed();
@@ -156,16 +158,17 @@ void ATM90E32Component::setup() {
if (this->enable_offset_calibration_) { if (this->enable_offset_calibration_) {
// Initialize flash storage for offset calibrations // Initialize flash storage for offset calibrations
uint32_t o_hash = fnv1_hash(std::string("_offset_calibration_") + this->cs_->dump_summary()); uint32_t o_hash = fnv1_hash(std::string("_offset_calibration_") + this->cs_summary_);
this->offset_pref_ = global_preferences->make_preference<OffsetCalibration[3]>(o_hash, true); this->offset_pref_ = global_preferences->make_preference<OffsetCalibration[3]>(o_hash, true);
this->restore_offset_calibrations_(); this->restore_offset_calibrations_();
// Initialize flash storage for power offset calibrations // Initialize flash storage for power offset calibrations
uint32_t po_hash = fnv1_hash(std::string("_power_offset_calibration_") + this->cs_->dump_summary()); uint32_t po_hash = fnv1_hash(std::string("_power_offset_calibration_") + this->cs_summary_);
this->power_offset_pref_ = global_preferences->make_preference<PowerOffsetCalibration[3]>(po_hash, true); this->power_offset_pref_ = global_preferences->make_preference<PowerOffsetCalibration[3]>(po_hash, true);
this->restore_power_offset_calibrations_(); this->restore_power_offset_calibrations_();
} else { } else {
ESP_LOGI(TAG, "[CALIBRATION] Power & Voltage/Current offset calibration is disabled. Using config file values."); ESP_LOGI(TAG, "[CALIBRATION][%s] Power & Voltage/Current offset calibration is disabled. Using config file values.",
cs);
for (uint8_t phase = 0; phase < 3; ++phase) { for (uint8_t phase = 0; phase < 3; ++phase) {
this->write16_(this->voltage_offset_registers[phase], this->write16_(this->voltage_offset_registers[phase],
static_cast<uint16_t>(this->offset_phase_[phase].voltage_offset_)); static_cast<uint16_t>(this->offset_phase_[phase].voltage_offset_));
@@ -180,21 +183,18 @@ void ATM90E32Component::setup() {
if (this->enable_gain_calibration_) { if (this->enable_gain_calibration_) {
// Initialize flash storage for gain calibration // Initialize flash storage for gain calibration
uint32_t g_hash = fnv1_hash(std::string("_gain_calibration_") + this->cs_->dump_summary()); uint32_t g_hash = fnv1_hash(std::string("_gain_calibration_") + this->cs_summary_);
this->gain_calibration_pref_ = global_preferences->make_preference<GainCalibration[3]>(g_hash, true); this->gain_calibration_pref_ = global_preferences->make_preference<GainCalibration[3]>(g_hash, true);
this->restore_gain_calibrations_(); this->restore_gain_calibrations_();
if (this->using_saved_calibrations_) { if (!this->using_saved_calibrations_) {
ESP_LOGI(TAG, "[CALIBRATION] Successfully restored gain calibration from memory.");
} else {
for (uint8_t phase = 0; phase < 3; ++phase) { for (uint8_t phase = 0; phase < 3; ++phase) {
this->write16_(voltage_gain_registers[phase], this->phase_[phase].voltage_gain_); this->write16_(voltage_gain_registers[phase], this->phase_[phase].voltage_gain_);
this->write16_(current_gain_registers[phase], this->phase_[phase].ct_gain_); this->write16_(current_gain_registers[phase], this->phase_[phase].ct_gain_);
} }
} }
} else { } else {
ESP_LOGI(TAG, "[CALIBRATION] Gain calibration is disabled. Using config file values."); ESP_LOGI(TAG, "[CALIBRATION][%s] Gain calibration is disabled. Using config file values.", cs);
for (uint8_t phase = 0; phase < 3; ++phase) { for (uint8_t phase = 0; phase < 3; ++phase) {
this->write16_(voltage_gain_registers[phase], this->phase_[phase].voltage_gain_); this->write16_(voltage_gain_registers[phase], this->phase_[phase].voltage_gain_);
this->write16_(current_gain_registers[phase], this->phase_[phase].ct_gain_); this->write16_(current_gain_registers[phase], this->phase_[phase].ct_gain_);
@@ -213,6 +213,122 @@ void ATM90E32Component::setup() {
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000); // end configuration this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000); // end configuration
} }
void ATM90E32Component::log_calibration_status_() {
const char *cs = this->cs_summary_.c_str();
bool offset_mismatch = false;
bool power_mismatch = false;
bool gain_mismatch = false;
for (uint8_t phase = 0; phase < 3; ++phase) {
offset_mismatch |= this->offset_calibration_mismatch_[phase];
power_mismatch |= this->power_offset_calibration_mismatch_[phase];
gain_mismatch |= this->gain_calibration_mismatch_[phase];
}
if (offset_mismatch) {
ESP_LOGW(TAG, "[CALIBRATION][%s] ", cs);
ESP_LOGW(TAG,
"[CALIBRATION][%s] ===================== Offset mismatch: using flash values =====================", cs);
ESP_LOGW(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------------------",
cs);
ESP_LOGW(TAG, "[CALIBRATION][%s] | Phase | offset_voltage | offset_current |", cs);
ESP_LOGW(TAG, "[CALIBRATION][%s] | | config | flash | config | flash |", cs);
ESP_LOGW(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------------------",
cs);
for (uint8_t phase = 0; phase < 3; ++phase) {
ESP_LOGW(TAG, "[CALIBRATION][%s] | %c | %6d | %6d | %6d | %6d |", cs, 'A' + phase,
this->config_offset_phase_[phase].voltage_offset_, this->offset_phase_[phase].voltage_offset_,
this->config_offset_phase_[phase].current_offset_, this->offset_phase_[phase].current_offset_);
}
ESP_LOGW(TAG,
"[CALIBRATION][%s] ===============================================================================", cs);
}
if (power_mismatch) {
ESP_LOGW(TAG, "[CALIBRATION][%s] ", cs);
ESP_LOGW(TAG,
"[CALIBRATION][%s] ================= Power offset mismatch: using flash values =================", cs);
ESP_LOGW(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------------------",
cs);
ESP_LOGW(TAG, "[CALIBRATION][%s] | Phase | offset_active_power|offset_reactive_power|", cs);
ESP_LOGW(TAG, "[CALIBRATION][%s] | | config | flash | config | flash |", cs);
ESP_LOGW(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------------------",
cs);
for (uint8_t phase = 0; phase < 3; ++phase) {
ESP_LOGW(TAG, "[CALIBRATION][%s] | %c | %6d | %6d | %6d | %6d |", cs, 'A' + phase,
this->config_power_offset_phase_[phase].active_power_offset,
this->power_offset_phase_[phase].active_power_offset,
this->config_power_offset_phase_[phase].reactive_power_offset,
this->power_offset_phase_[phase].reactive_power_offset);
}
ESP_LOGW(TAG,
"[CALIBRATION][%s] ===============================================================================", cs);
}
if (gain_mismatch) {
ESP_LOGW(TAG, "[CALIBRATION][%s] ", cs);
ESP_LOGW(TAG,
"[CALIBRATION][%s] ====================== Gain mismatch: using flash values =====================", cs);
ESP_LOGW(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------------------",
cs);
ESP_LOGW(TAG, "[CALIBRATION][%s] | Phase | voltage_gain | current_gain |", cs);
ESP_LOGW(TAG, "[CALIBRATION][%s] | | config | flash | config | flash |", cs);
ESP_LOGW(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------------------",
cs);
for (uint8_t phase = 0; phase < 3; ++phase) {
ESP_LOGW(TAG, "[CALIBRATION][%s] | %c | %6u | %6u | %6u | %6u |", cs, 'A' + phase,
this->config_gain_phase_[phase].voltage_gain, this->gain_phase_[phase].voltage_gain,
this->config_gain_phase_[phase].current_gain, this->gain_phase_[phase].current_gain);
}
ESP_LOGW(TAG,
"[CALIBRATION][%s] ===============================================================================", cs);
}
if (!this->enable_offset_calibration_) {
ESP_LOGI(TAG, "[CALIBRATION][%s] Power & Voltage/Current offset calibration is disabled. Using config file values.",
cs);
} else if (this->restored_offset_calibration_ && !offset_mismatch) {
ESP_LOGI(TAG, "[CALIBRATION][%s] ", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] ============== Restored offset calibration from memory ==============", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_voltage | offset_current |", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs);
for (uint8_t phase = 0; phase < 3; phase++) {
ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase,
this->offset_phase_[phase].voltage_offset_, this->offset_phase_[phase].current_offset_);
}
ESP_LOGI(TAG, "[CALIBRATION][%s] ==============================================================\\n", cs);
}
if (this->restored_power_offset_calibration_ && !power_mismatch) {
ESP_LOGI(TAG, "[CALIBRATION][%s] ", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] ============ Restored power offset calibration from memory ============", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_active_power | offset_reactive_power |", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
for (uint8_t phase = 0; phase < 3; phase++) {
ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase,
this->power_offset_phase_[phase].active_power_offset,
this->power_offset_phase_[phase].reactive_power_offset);
}
ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs);
}
if (!this->enable_gain_calibration_) {
ESP_LOGI(TAG, "[CALIBRATION][%s] Gain calibration is disabled. Using config file values.", cs);
} else if (this->restored_gain_calibration_ && !gain_mismatch) {
ESP_LOGI(TAG, "[CALIBRATION][%s] ", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] ============ Restoring saved gain calibrations to registers ============", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | voltage_gain | current_gain |", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
for (uint8_t phase = 0; phase < 3; phase++) {
ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6u | %6u |", cs, 'A' + phase,
this->gain_phase_[phase].voltage_gain, this->gain_phase_[phase].current_gain);
}
ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\\n", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] Gain calibration loaded and verified successfully.\n", cs);
}
this->calibration_message_printed_ = true;
}
void ATM90E32Component::dump_config() { void ATM90E32Component::dump_config() {
ESP_LOGCONFIG("", "ATM90E32:"); ESP_LOGCONFIG("", "ATM90E32:");
LOG_PIN(" CS Pin: ", this->cs_); LOG_PIN(" CS Pin: ", this->cs_);
@@ -255,6 +371,10 @@ void ATM90E32Component::dump_config() {
LOG_SENSOR(" ", "Peak Current C", this->phase_[PHASEC].peak_current_sensor_); LOG_SENSOR(" ", "Peak Current C", this->phase_[PHASEC].peak_current_sensor_);
LOG_SENSOR(" ", "Frequency", this->freq_sensor_); LOG_SENSOR(" ", "Frequency", this->freq_sensor_);
LOG_SENSOR(" ", "Chip Temp", this->chip_temperature_sensor_); LOG_SENSOR(" ", "Chip Temp", this->chip_temperature_sensor_);
if (this->restored_offset_calibration_ || this->restored_power_offset_calibration_ ||
this->restored_gain_calibration_ || !this->enable_offset_calibration_ || !this->enable_gain_calibration_) {
this->log_calibration_status_();
}
} }
float ATM90E32Component::get_setup_priority() const { return setup_priority::IO; } float ATM90E32Component::get_setup_priority() const { return setup_priority::IO; }
@@ -262,26 +382,35 @@ float ATM90E32Component::get_setup_priority() const { return setup_priority::IO;
// R/C registers can conly be cleared after the LastSPIData register is updated (register 78H) // R/C registers can conly be cleared after the LastSPIData register is updated (register 78H)
// Peakdetect period: 05H. Bit 15:8 are PeakDet_period in ms. 7:0 are Sag_period // Peakdetect period: 05H. Bit 15:8 are PeakDet_period in ms. 7:0 are Sag_period
// Default is 143FH (20ms, 63ms) // Default is 143FH (20ms, 63ms)
uint16_t ATM90E32Component::read16_(uint16_t a_register) { uint16_t ATM90E32Component::read16_transaction_(uint16_t a_register) {
uint8_t addrh = (1 << 7) | ((a_register >> 8) & 0x03); uint8_t addrh = (1 << 7) | ((a_register >> 8) & 0x03);
uint8_t addrl = (a_register & 0xFF); uint8_t addrl = (a_register & 0xFF);
uint8_t data[2]; uint8_t data[4] = {addrh, addrl, 0x00, 0x00};
uint16_t output; this->transfer_array(data, 4);
this->enable(); uint16_t output = encode_uint16(data[2], data[3]);
delay_microseconds_safe(1); // min delay between CS low and first SCK is 200ns - 1ms is plenty
this->write_byte(addrh);
this->write_byte(addrl);
this->read_array(data, 2);
this->disable();
output = (uint16_t(data[0] & 0xFF) << 8) | (data[1] & 0xFF);
ESP_LOGVV(TAG, "read16_ 0x%04" PRIX16 " output 0x%04" PRIX16, a_register, output); ESP_LOGVV(TAG, "read16_ 0x%04" PRIX16 " output 0x%04" PRIX16, a_register, output);
return output; return output;
} }
uint16_t ATM90E32Component::read16_(uint16_t a_register) {
this->enable();
delay_microseconds_safe(1); // min delay between CS low and first SCK is 200ns - 1us is plenty
uint16_t output = this->read16_transaction_(a_register);
delay_microseconds_safe(1); // allow the last clock to propagate before releasing CS
this->disable();
delay_microseconds_safe(1); // meet minimum CS high time before next transaction
return output;
}
int ATM90E32Component::read32_(uint16_t addr_h, uint16_t addr_l) { int ATM90E32Component::read32_(uint16_t addr_h, uint16_t addr_l) {
const uint16_t val_h = this->read16_(addr_h); this->enable();
const uint16_t val_l = this->read16_(addr_l); delay_microseconds_safe(1);
const uint16_t val_h = this->read16_transaction_(addr_h);
delay_microseconds_safe(1);
const uint16_t val_l = this->read16_transaction_(addr_l);
delay_microseconds_safe(1);
this->disable();
delay_microseconds_safe(1);
const int32_t val = (val_h << 16) | val_l; const int32_t val = (val_h << 16) | val_l;
ESP_LOGVV(TAG, ESP_LOGVV(TAG,
@@ -292,13 +421,19 @@ int ATM90E32Component::read32_(uint16_t addr_h, uint16_t addr_l) {
return val; return val;
} }
void ATM90E32Component::write16_(uint16_t a_register, uint16_t val) { void ATM90E32Component::write16_(uint16_t a_register, uint16_t val, bool validate) {
ESP_LOGVV(TAG, "write16_ 0x%04" PRIX16 " val 0x%04" PRIX16, a_register, val); ESP_LOGVV(TAG, "write16_ 0x%04" PRIX16 " val 0x%04" PRIX16, a_register, val);
uint8_t addrh = ((a_register >> 8) & 0x03);
uint8_t addrl = (a_register & 0xFF);
uint8_t data[4] = {addrh, addrl, uint8_t((val >> 8) & 0xFF), uint8_t(val & 0xFF)};
this->enable(); this->enable();
this->write_byte16(a_register); delay_microseconds_safe(1); // ensure CS setup time
this->write_byte16(val); this->write_array(data, 4);
delay_microseconds_safe(1); // allow clock to settle before raising CS
this->disable(); this->disable();
this->validate_spi_read_(val, "write16()"); delay_microseconds_safe(1); // ensure minimum CS high time
if (validate)
this->validate_spi_read_(val, "write16()");
} }
float ATM90E32Component::get_local_phase_voltage_(uint8_t phase) { return this->phase_[phase].voltage_; } float ATM90E32Component::get_local_phase_voltage_(uint8_t phase) { return this->phase_[phase].voltage_; }
@@ -441,8 +576,10 @@ float ATM90E32Component::get_chip_temperature_() {
} }
void ATM90E32Component::run_gain_calibrations() { void ATM90E32Component::run_gain_calibrations() {
const char *cs = this->cs_summary_.c_str();
if (!this->enable_gain_calibration_) { if (!this->enable_gain_calibration_) {
ESP_LOGW(TAG, "[CALIBRATION] Gain calibration is disabled! Enable it first with enable_gain_calibration: true"); ESP_LOGW(TAG, "[CALIBRATION][%s] Gain calibration is disabled! Enable it first with enable_gain_calibration: true",
cs);
return; return;
} }
@@ -454,12 +591,14 @@ void ATM90E32Component::run_gain_calibrations() {
float ref_currents[3] = {this->get_reference_current(0), this->get_reference_current(1), float ref_currents[3] = {this->get_reference_current(0), this->get_reference_current(1),
this->get_reference_current(2)}; this->get_reference_current(2)};
ESP_LOGI(TAG, "[CALIBRATION] "); ESP_LOGI(TAG, "[CALIBRATION][%s] ", cs);
ESP_LOGI(TAG, "[CALIBRATION] ========================= Gain Calibration ========================="); ESP_LOGI(TAG, "[CALIBRATION][%s] ========================= Gain Calibration =========================", cs);
ESP_LOGI(TAG, "[CALIBRATION] ---------------------------------------------------------------------"); ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
ESP_LOGI(TAG, ESP_LOGI(
"[CALIBRATION] | Phase | V_meas (V) | I_meas (A) | V_ref | I_ref | V_gain (old→new) | I_gain (old→new) |"); TAG,
ESP_LOGI(TAG, "[CALIBRATION] ---------------------------------------------------------------------"); "[CALIBRATION][%s] | Phase | V_meas (V) | I_meas (A) | V_ref | I_ref | V_gain (old→new) | I_gain (old→new) |",
cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
for (uint8_t phase = 0; phase < 3; phase++) { for (uint8_t phase = 0; phase < 3; phase++) {
float measured_voltage = this->get_phase_voltage_avg_(phase); float measured_voltage = this->get_phase_voltage_avg_(phase);
@@ -476,22 +615,22 @@ void ATM90E32Component::run_gain_calibrations() {
// Voltage calibration // Voltage calibration
if (ref_voltage <= 0.0f) { if (ref_voltage <= 0.0f) {
ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping voltage calibration: reference voltage is 0.", ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping voltage calibration: reference voltage is 0.", cs,
phase_labels[phase]); phase_labels[phase]);
} else if (measured_voltage == 0.0f) { } else if (measured_voltage == 0.0f) {
ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping voltage calibration: measured voltage is 0.", ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping voltage calibration: measured voltage is 0.", cs,
phase_labels[phase]); phase_labels[phase]);
} else { } else {
uint32_t new_voltage_gain = static_cast<uint16_t>((ref_voltage / measured_voltage) * current_voltage_gain); uint32_t new_voltage_gain = static_cast<uint16_t>((ref_voltage / measured_voltage) * current_voltage_gain);
if (new_voltage_gain == 0) { if (new_voltage_gain == 0) {
ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Voltage gain would be 0. Check reference and measured voltage.", ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Voltage gain would be 0. Check reference and measured voltage.", cs,
phase_labels[phase]); phase_labels[phase]);
} else { } else {
if (new_voltage_gain >= 65535) { if (new_voltage_gain >= 65535) {
ESP_LOGW( ESP_LOGW(TAG,
TAG, "[CALIBRATION][%s] Phase %s - Voltage gain exceeds 65535. You may need a higher output voltage "
"[CALIBRATION] Phase %s - Voltage gain exceeds 65535. You may need a higher output voltage transformer.", "transformer.",
phase_labels[phase]); cs, phase_labels[phase]);
new_voltage_gain = 65535; new_voltage_gain = 65535;
} }
this->gain_phase_[phase].voltage_gain = static_cast<uint16_t>(new_voltage_gain); this->gain_phase_[phase].voltage_gain = static_cast<uint16_t>(new_voltage_gain);
@@ -501,20 +640,20 @@ void ATM90E32Component::run_gain_calibrations() {
// Current calibration // Current calibration
if (ref_current == 0.0f) { if (ref_current == 0.0f) {
ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping current calibration: reference current is 0.", ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping current calibration: reference current is 0.", cs,
phase_labels[phase]); phase_labels[phase]);
} else if (measured_current == 0.0f) { } else if (measured_current == 0.0f) {
ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping current calibration: measured current is 0.", ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping current calibration: measured current is 0.", cs,
phase_labels[phase]); phase_labels[phase]);
} else { } else {
uint32_t new_current_gain = static_cast<uint16_t>((ref_current / measured_current) * current_current_gain); uint32_t new_current_gain = static_cast<uint16_t>((ref_current / measured_current) * current_current_gain);
if (new_current_gain == 0) { if (new_current_gain == 0) {
ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Current gain would be 0. Check reference and measured current.", ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Current gain would be 0. Check reference and measured current.", cs,
phase_labels[phase]); phase_labels[phase]);
} else { } else {
if (new_current_gain >= 65535) { if (new_current_gain >= 65535) {
ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Current gain exceeds 65535. You may need to turn up pga gain.", ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Current gain exceeds 65535. You may need to turn up pga gain.",
phase_labels[phase]); cs, phase_labels[phase]);
new_current_gain = 65535; new_current_gain = 65535;
} }
this->gain_phase_[phase].current_gain = static_cast<uint16_t>(new_current_gain); this->gain_phase_[phase].current_gain = static_cast<uint16_t>(new_current_gain);
@@ -523,13 +662,13 @@ void ATM90E32Component::run_gain_calibrations() {
} }
// Final row output // Final row output
ESP_LOGI(TAG, "[CALIBRATION] | %c | %9.2f | %9.4f | %5.2f | %6.4f | %5u → %-5u | %5u → %-5u |", ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %9.2f | %9.4f | %5.2f | %6.4f | %5u → %-5u | %5u → %-5u |", cs,
'A' + phase, measured_voltage, measured_current, ref_voltage, ref_current, current_voltage_gain, 'A' + phase, measured_voltage, measured_current, ref_voltage, ref_current, current_voltage_gain,
did_voltage ? this->gain_phase_[phase].voltage_gain : current_voltage_gain, current_current_gain, did_voltage ? this->gain_phase_[phase].voltage_gain : current_voltage_gain, current_current_gain,
did_current ? this->gain_phase_[phase].current_gain : current_current_gain); did_current ? this->gain_phase_[phase].current_gain : current_current_gain);
} }
ESP_LOGI(TAG, "[CALIBRATION] =====================================================================\n"); ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs);
this->save_gain_calibration_to_memory_(); this->save_gain_calibration_to_memory_();
this->write_gains_to_registers_(); this->write_gains_to_registers_();
@@ -537,54 +676,108 @@ void ATM90E32Component::run_gain_calibrations() {
} }
void ATM90E32Component::save_gain_calibration_to_memory_() { void ATM90E32Component::save_gain_calibration_to_memory_() {
const char *cs = this->cs_summary_.c_str();
bool success = this->gain_calibration_pref_.save(&this->gain_phase_); bool success = this->gain_calibration_pref_.save(&this->gain_phase_);
global_preferences->sync();
if (success) { if (success) {
this->using_saved_calibrations_ = true; this->using_saved_calibrations_ = true;
ESP_LOGI(TAG, "[CALIBRATION] Gain calibration saved to memory."); ESP_LOGI(TAG, "[CALIBRATION][%s] Gain calibration saved to memory.", cs);
} else { } else {
this->using_saved_calibrations_ = false; this->using_saved_calibrations_ = false;
ESP_LOGE(TAG, "[CALIBRATION] Failed to save gain calibration to memory!"); ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to save gain calibration to memory!", cs);
}
}
void ATM90E32Component::save_offset_calibration_to_memory_() {
const char *cs = this->cs_summary_.c_str();
bool success = this->offset_pref_.save(&this->offset_phase_);
global_preferences->sync();
if (success) {
this->using_saved_calibrations_ = true;
this->restored_offset_calibration_ = true;
for (bool &phase : this->offset_calibration_mismatch_)
phase = false;
ESP_LOGI(TAG, "[CALIBRATION][%s] Offset calibration saved to memory.", cs);
} else {
this->using_saved_calibrations_ = false;
ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to save offset calibration to memory!", cs);
}
}
void ATM90E32Component::save_power_offset_calibration_to_memory_() {
const char *cs = this->cs_summary_.c_str();
bool success = this->power_offset_pref_.save(&this->power_offset_phase_);
global_preferences->sync();
if (success) {
this->using_saved_calibrations_ = true;
this->restored_power_offset_calibration_ = true;
for (bool &phase : this->power_offset_calibration_mismatch_)
phase = false;
ESP_LOGI(TAG, "[CALIBRATION][%s] Power offset calibration saved to memory.", cs);
} else {
this->using_saved_calibrations_ = false;
ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to save power offset calibration to memory!", cs);
} }
} }
void ATM90E32Component::run_offset_calibrations() { void ATM90E32Component::run_offset_calibrations() {
const char *cs = this->cs_summary_.c_str();
if (!this->enable_offset_calibration_) { if (!this->enable_offset_calibration_) {
ESP_LOGW(TAG, "[CALIBRATION] Offset calibration is disabled! Enable it first with enable_offset_calibration: true"); ESP_LOGW(TAG,
"[CALIBRATION][%s] Offset calibration is disabled! Enable it first with enable_offset_calibration: true",
cs);
return; return;
} }
ESP_LOGI(TAG, "[CALIBRATION][%s] ", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] ======================== Offset Calibration ========================", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_voltage | offset_current |", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------", cs);
for (uint8_t phase = 0; phase < 3; phase++) { for (uint8_t phase = 0; phase < 3; phase++) {
int16_t voltage_offset = calibrate_offset(phase, true); int16_t voltage_offset = calibrate_offset(phase, true);
int16_t current_offset = calibrate_offset(phase, false); int16_t current_offset = calibrate_offset(phase, false);
this->write_offsets_to_registers_(phase, voltage_offset, current_offset); this->write_offsets_to_registers_(phase, voltage_offset, current_offset);
ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_voltage: %d, offset_current: %d", 'A' + phase, voltage_offset, ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, voltage_offset,
current_offset); current_offset);
} }
this->offset_pref_.save(&this->offset_phase_); // Save to flash ESP_LOGI(TAG, "[CALIBRATION][%s] ==================================================================\n", cs);
this->save_offset_calibration_to_memory_();
} }
void ATM90E32Component::run_power_offset_calibrations() { void ATM90E32Component::run_power_offset_calibrations() {
const char *cs = this->cs_summary_.c_str();
if (!this->enable_offset_calibration_) { if (!this->enable_offset_calibration_) {
ESP_LOGW( ESP_LOGW(
TAG, TAG,
"[CALIBRATION] Offset power calibration is disabled! Enable it first with enable_offset_calibration: true"); "[CALIBRATION][%s] Offset power calibration is disabled! Enable it first with enable_offset_calibration: true",
cs);
return; return;
} }
ESP_LOGI(TAG, "[CALIBRATION][%s] ", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] ===================== Power Offset Calibration =====================", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_active_power | offset_reactive_power |", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
for (uint8_t phase = 0; phase < 3; ++phase) { for (uint8_t phase = 0; phase < 3; ++phase) {
int16_t active_offset = calibrate_power_offset(phase, false); int16_t active_offset = calibrate_power_offset(phase, false);
int16_t reactive_offset = calibrate_power_offset(phase, true); int16_t reactive_offset = calibrate_power_offset(phase, true);
this->write_power_offsets_to_registers_(phase, active_offset, reactive_offset); this->write_power_offsets_to_registers_(phase, active_offset, reactive_offset);
ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_active_power: %d, offset_reactive_power: %d", 'A' + phase, ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, active_offset,
active_offset, reactive_offset); reactive_offset);
} }
ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs);
this->power_offset_pref_.save(&this->power_offset_phase_); // Save to flash this->save_power_offset_calibration_to_memory_();
} }
void ATM90E32Component::write_gains_to_registers_() { void ATM90E32Component::write_gains_to_registers_() {
@@ -631,102 +824,276 @@ void ATM90E32Component::write_power_offsets_to_registers_(uint8_t phase, int16_t
} }
void ATM90E32Component::restore_gain_calibrations_() { void ATM90E32Component::restore_gain_calibrations_() {
if (this->gain_calibration_pref_.load(&this->gain_phase_)) { const char *cs = this->cs_summary_.c_str();
ESP_LOGI(TAG, "[CALIBRATION] Restoring saved gain calibrations to registers:"); for (uint8_t i = 0; i < 3; ++i) {
this->config_gain_phase_[i].voltage_gain = this->phase_[i].voltage_gain_;
for (uint8_t phase = 0; phase < 3; phase++) { this->config_gain_phase_[i].current_gain = this->phase_[i].ct_gain_;
uint16_t v_gain = this->gain_phase_[phase].voltage_gain; this->gain_phase_[i] = this->config_gain_phase_[i];
uint16_t i_gain = this->gain_phase_[phase].current_gain;
ESP_LOGI(TAG, "[CALIBRATION] Phase %c - Voltage Gain: %u, Current Gain: %u", 'A' + phase, v_gain, i_gain);
}
this->write_gains_to_registers_();
if (this->verify_gain_writes_()) {
this->using_saved_calibrations_ = true;
ESP_LOGI(TAG, "[CALIBRATION] Gain calibration loaded and verified successfully.");
} else {
this->using_saved_calibrations_ = false;
ESP_LOGE(TAG, "[CALIBRATION] Gain verification failed! Calibration may not be applied correctly.");
}
} else {
this->using_saved_calibrations_ = false;
ESP_LOGW(TAG, "[CALIBRATION] No stored gain calibrations found. Using config file values.");
} }
if (this->gain_calibration_pref_.load(&this->gain_phase_)) {
bool all_zero = true;
bool same_as_config = true;
for (uint8_t phase = 0; phase < 3; ++phase) {
const auto &cfg = this->config_gain_phase_[phase];
const auto &saved = this->gain_phase_[phase];
if (saved.voltage_gain != 0 || saved.current_gain != 0)
all_zero = false;
if (saved.voltage_gain != cfg.voltage_gain || saved.current_gain != cfg.current_gain)
same_as_config = false;
}
if (!all_zero && !same_as_config) {
for (uint8_t phase = 0; phase < 3; ++phase) {
bool mismatch = false;
if (this->has_config_voltage_gain_[phase] &&
this->gain_phase_[phase].voltage_gain != this->config_gain_phase_[phase].voltage_gain)
mismatch = true;
if (this->has_config_current_gain_[phase] &&
this->gain_phase_[phase].current_gain != this->config_gain_phase_[phase].current_gain)
mismatch = true;
if (mismatch)
this->gain_calibration_mismatch_[phase] = true;
}
this->write_gains_to_registers_();
if (this->verify_gain_writes_()) {
this->using_saved_calibrations_ = true;
this->restored_gain_calibration_ = true;
return;
}
this->using_saved_calibrations_ = false;
ESP_LOGE(TAG, "[CALIBRATION][%s] Gain verification failed! Calibration may not be applied correctly.", cs);
}
}
this->using_saved_calibrations_ = false;
for (uint8_t i = 0; i < 3; ++i)
this->gain_phase_[i] = this->config_gain_phase_[i];
this->write_gains_to_registers_();
ESP_LOGW(TAG, "[CALIBRATION][%s] No stored gain calibrations found. Using config file values.", cs);
} }
void ATM90E32Component::restore_offset_calibrations_() { void ATM90E32Component::restore_offset_calibrations_() {
if (this->offset_pref_.load(&this->offset_phase_)) { const char *cs = this->cs_summary_.c_str();
ESP_LOGI(TAG, "[CALIBRATION] Successfully restored offset calibration from memory."); for (uint8_t i = 0; i < 3; ++i)
this->config_offset_phase_[i] = this->offset_phase_[i];
bool have_data = this->offset_pref_.load(&this->offset_phase_);
bool all_zero = true;
if (have_data) {
for (auto &phase : this->offset_phase_) {
if (phase.voltage_offset_ != 0 || phase.current_offset_ != 0) {
all_zero = false;
break;
}
}
}
if (have_data && !all_zero) {
this->restored_offset_calibration_ = true;
for (uint8_t phase = 0; phase < 3; phase++) { for (uint8_t phase = 0; phase < 3; phase++) {
auto &offset = this->offset_phase_[phase]; auto &offset = this->offset_phase_[phase];
write_offsets_to_registers_(phase, offset.voltage_offset_, offset.current_offset_); bool mismatch = false;
ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_voltage:: %d, offset_current: %d", 'A' + phase, if (this->has_config_voltage_offset_[phase] &&
offset.voltage_offset_, offset.current_offset_); offset.voltage_offset_ != this->config_offset_phase_[phase].voltage_offset_)
mismatch = true;
if (this->has_config_current_offset_[phase] &&
offset.current_offset_ != this->config_offset_phase_[phase].current_offset_)
mismatch = true;
if (mismatch)
this->offset_calibration_mismatch_[phase] = true;
} }
} else { } else {
ESP_LOGW(TAG, "[CALIBRATION] No stored offset calibrations found. Using default values."); for (uint8_t phase = 0; phase < 3; phase++)
this->offset_phase_[phase] = this->config_offset_phase_[phase];
ESP_LOGW(TAG, "[CALIBRATION][%s] No stored offset calibrations found. Using default values.", cs);
}
for (uint8_t phase = 0; phase < 3; phase++) {
write_offsets_to_registers_(phase, this->offset_phase_[phase].voltage_offset_,
this->offset_phase_[phase].current_offset_);
} }
} }
void ATM90E32Component::restore_power_offset_calibrations_() { void ATM90E32Component::restore_power_offset_calibrations_() {
if (this->power_offset_pref_.load(&this->power_offset_phase_)) { const char *cs = this->cs_summary_.c_str();
ESP_LOGI(TAG, "[CALIBRATION] Successfully restored power offset calibration from memory."); for (uint8_t i = 0; i < 3; ++i)
this->config_power_offset_phase_[i] = this->power_offset_phase_[i];
bool have_data = this->power_offset_pref_.load(&this->power_offset_phase_);
bool all_zero = true;
if (have_data) {
for (auto &phase : this->power_offset_phase_) {
if (phase.active_power_offset != 0 || phase.reactive_power_offset != 0) {
all_zero = false;
break;
}
}
}
if (have_data && !all_zero) {
this->restored_power_offset_calibration_ = true;
for (uint8_t phase = 0; phase < 3; ++phase) { for (uint8_t phase = 0; phase < 3; ++phase) {
auto &offset = this->power_offset_phase_[phase]; auto &offset = this->power_offset_phase_[phase];
write_power_offsets_to_registers_(phase, offset.active_power_offset, offset.reactive_power_offset); bool mismatch = false;
ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_active_power: %d, offset_reactive_power: %d", 'A' + phase, if (this->has_config_active_power_offset_[phase] &&
offset.active_power_offset, offset.reactive_power_offset); offset.active_power_offset != this->config_power_offset_phase_[phase].active_power_offset)
mismatch = true;
if (this->has_config_reactive_power_offset_[phase] &&
offset.reactive_power_offset != this->config_power_offset_phase_[phase].reactive_power_offset)
mismatch = true;
if (mismatch)
this->power_offset_calibration_mismatch_[phase] = true;
} }
} else { } else {
ESP_LOGW(TAG, "[CALIBRATION] No stored power offsets found. Using default values."); for (uint8_t phase = 0; phase < 3; ++phase)
this->power_offset_phase_[phase] = this->config_power_offset_phase_[phase];
ESP_LOGW(TAG, "[CALIBRATION][%s] No stored power offsets found. Using default values.", cs);
}
for (uint8_t phase = 0; phase < 3; ++phase) {
write_power_offsets_to_registers_(phase, this->power_offset_phase_[phase].active_power_offset,
this->power_offset_phase_[phase].reactive_power_offset);
} }
} }
void ATM90E32Component::clear_gain_calibrations() { void ATM90E32Component::clear_gain_calibrations() {
ESP_LOGI(TAG, "[CALIBRATION] Clearing stored gain calibrations and restoring config-defined values"); const char *cs = this->cs_summary_.c_str();
if (!this->using_saved_calibrations_) {
for (int phase = 0; phase < 3; phase++) { ESP_LOGI(TAG, "[CALIBRATION][%s] No stored gain calibrations to clear. Current values:", cs);
gain_phase_[phase].voltage_gain = this->phase_[phase].voltage_gain_; ESP_LOGI(TAG, "[CALIBRATION][%s] ----------------------------------------------------------", cs);
gain_phase_[phase].current_gain = this->phase_[phase].ct_gain_; ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | voltage_gain | current_gain |", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] ----------------------------------------------------------", cs);
for (int phase = 0; phase < 3; phase++) {
ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6u | %6u |", cs, 'A' + phase,
this->gain_phase_[phase].voltage_gain, this->gain_phase_[phase].current_gain);
}
ESP_LOGI(TAG, "[CALIBRATION][%s] ==========================================================\n", cs);
return;
} }
bool success = this->gain_calibration_pref_.save(&this->gain_phase_); ESP_LOGI(TAG, "[CALIBRATION][%s] Clearing stored gain calibrations and restoring config-defined values", cs);
this->using_saved_calibrations_ = false; ESP_LOGI(TAG, "[CALIBRATION][%s] ----------------------------------------------------------", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | voltage_gain | current_gain |", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] ----------------------------------------------------------", cs);
if (success) { for (int phase = 0; phase < 3; phase++) {
ESP_LOGI(TAG, "[CALIBRATION] Gain calibrations cleared. Config values restored:"); uint16_t voltage_gain = this->phase_[phase].voltage_gain_;
for (int phase = 0; phase < 3; phase++) { uint16_t current_gain = this->phase_[phase].ct_gain_;
ESP_LOGI(TAG, "[CALIBRATION] Phase %c - Voltage Gain: %u, Current Gain: %u", 'A' + phase,
gain_phase_[phase].voltage_gain, gain_phase_[phase].current_gain); this->config_gain_phase_[phase].voltage_gain = voltage_gain;
} this->config_gain_phase_[phase].current_gain = current_gain;
} else { this->gain_phase_[phase].voltage_gain = voltage_gain;
ESP_LOGE(TAG, "[CALIBRATION] Failed to clear gain calibrations!"); this->gain_phase_[phase].current_gain = current_gain;
ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6u | %6u |", cs, 'A' + phase, voltage_gain, current_gain);
}
ESP_LOGI(TAG, "[CALIBRATION][%s] ==========================================================\n", cs);
GainCalibration zero_gains[3]{{0, 0}, {0, 0}, {0, 0}};
bool success = this->gain_calibration_pref_.save(&zero_gains);
global_preferences->sync();
this->using_saved_calibrations_ = false;
this->restored_gain_calibration_ = false;
for (bool &phase : this->gain_calibration_mismatch_)
phase = false;
if (!success) {
ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to clear gain calibrations!", cs);
} }
this->write_gains_to_registers_(); // Apply them to the chip immediately this->write_gains_to_registers_(); // Apply them to the chip immediately
} }
void ATM90E32Component::clear_offset_calibrations() { void ATM90E32Component::clear_offset_calibrations() {
for (uint8_t phase = 0; phase < 3; phase++) { const char *cs = this->cs_summary_.c_str();
this->write_offsets_to_registers_(phase, 0, 0); if (!this->restored_offset_calibration_) {
ESP_LOGI(TAG, "[CALIBRATION][%s] No stored offset calibrations to clear. Current values:", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_voltage | offset_current |", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs);
for (uint8_t phase = 0; phase < 3; phase++) {
ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase,
this->offset_phase_[phase].voltage_offset_, this->offset_phase_[phase].current_offset_);
}
ESP_LOGI(TAG, "[CALIBRATION][%s] ==============================================================\n", cs);
return;
} }
this->offset_pref_.save(&this->offset_phase_); // Save cleared values to flash memory ESP_LOGI(TAG, "[CALIBRATION][%s] Clearing stored offset calibrations and restoring config-defined values", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_voltage | offset_current |", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs);
ESP_LOGI(TAG, "[CALIBRATION] Offsets cleared."); for (uint8_t phase = 0; phase < 3; phase++) {
int16_t voltage_offset =
this->has_config_voltage_offset_[phase] ? this->config_offset_phase_[phase].voltage_offset_ : 0;
int16_t current_offset =
this->has_config_current_offset_[phase] ? this->config_offset_phase_[phase].current_offset_ : 0;
this->write_offsets_to_registers_(phase, voltage_offset, current_offset);
ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, voltage_offset,
current_offset);
}
ESP_LOGI(TAG, "[CALIBRATION][%s] ==============================================================\n", cs);
OffsetCalibration zero_offsets[3]{{0, 0}, {0, 0}, {0, 0}};
this->offset_pref_.save(&zero_offsets); // Clear stored values in flash
global_preferences->sync();
this->restored_offset_calibration_ = false;
for (bool &phase : this->offset_calibration_mismatch_)
phase = false;
ESP_LOGI(TAG, "[CALIBRATION][%s] Offsets cleared.", cs);
} }
void ATM90E32Component::clear_power_offset_calibrations() { void ATM90E32Component::clear_power_offset_calibrations() {
for (uint8_t phase = 0; phase < 3; phase++) { const char *cs = this->cs_summary_.c_str();
this->write_power_offsets_to_registers_(phase, 0, 0); if (!this->restored_power_offset_calibration_) {
ESP_LOGI(TAG, "[CALIBRATION][%s] No stored power offsets to clear. Current values:", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_active_power | offset_reactive_power |", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
for (uint8_t phase = 0; phase < 3; phase++) {
ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase,
this->power_offset_phase_[phase].active_power_offset,
this->power_offset_phase_[phase].reactive_power_offset);
}
ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs);
return;
} }
this->power_offset_pref_.save(&this->power_offset_phase_); ESP_LOGI(TAG, "[CALIBRATION][%s] Clearing stored power offsets and restoring config-defined values", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_active_power | offset_reactive_power |", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
ESP_LOGI(TAG, "[CALIBRATION] Power offsets cleared."); for (uint8_t phase = 0; phase < 3; phase++) {
int16_t active_offset =
this->has_config_active_power_offset_[phase] ? this->config_power_offset_phase_[phase].active_power_offset : 0;
int16_t reactive_offset = this->has_config_reactive_power_offset_[phase]
? this->config_power_offset_phase_[phase].reactive_power_offset
: 0;
this->write_power_offsets_to_registers_(phase, active_offset, reactive_offset);
ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, active_offset,
reactive_offset);
}
ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs);
PowerOffsetCalibration zero_power_offsets[3]{{0, 0}, {0, 0}, {0, 0}};
this->power_offset_pref_.save(&zero_power_offsets);
global_preferences->sync();
this->restored_power_offset_calibration_ = false;
for (bool &phase : this->power_offset_calibration_mismatch_)
phase = false;
ESP_LOGI(TAG, "[CALIBRATION][%s] Power offsets cleared.", cs);
} }
int16_t ATM90E32Component::calibrate_offset(uint8_t phase, bool voltage) { int16_t ATM90E32Component::calibrate_offset(uint8_t phase, bool voltage) {
@@ -747,20 +1114,21 @@ int16_t ATM90E32Component::calibrate_offset(uint8_t phase, bool voltage) {
int16_t ATM90E32Component::calibrate_power_offset(uint8_t phase, bool reactive) { int16_t ATM90E32Component::calibrate_power_offset(uint8_t phase, bool reactive) {
const uint8_t num_reads = 5; const uint8_t num_reads = 5;
uint64_t total_value = 0; int64_t total_value = 0;
for (uint8_t i = 0; i < num_reads; ++i) { for (uint8_t i = 0; i < num_reads; ++i) {
uint32_t reading = reactive ? this->read32_(ATM90E32_REGISTER_QMEAN + phase, ATM90E32_REGISTER_QMEANLSB + phase) int32_t reading = reactive ? this->read32_(ATM90E32_REGISTER_QMEAN + phase, ATM90E32_REGISTER_QMEANLSB + phase)
: this->read32_(ATM90E32_REGISTER_PMEAN + phase, ATM90E32_REGISTER_PMEANLSB + phase); : this->read32_(ATM90E32_REGISTER_PMEAN + phase, ATM90E32_REGISTER_PMEANLSB + phase);
total_value += reading; total_value += reading;
} }
const uint32_t average_value = total_value / num_reads; int32_t average_value = total_value / num_reads;
const uint32_t power_offset = ~average_value + 1; int32_t power_offset = -average_value;
return static_cast<int16_t>(power_offset); // Takes the lower 16 bits return static_cast<int16_t>(power_offset); // Takes the lower 16 bits
} }
bool ATM90E32Component::verify_gain_writes_() { bool ATM90E32Component::verify_gain_writes_() {
const char *cs = this->cs_summary_.c_str();
bool success = true; bool success = true;
for (uint8_t phase = 0; phase < 3; phase++) { for (uint8_t phase = 0; phase < 3; phase++) {
uint16_t read_voltage = this->read16_(voltage_gain_registers[phase]); uint16_t read_voltage = this->read16_(voltage_gain_registers[phase]);
@@ -768,7 +1136,7 @@ bool ATM90E32Component::verify_gain_writes_() {
if (read_voltage != this->gain_phase_[phase].voltage_gain || if (read_voltage != this->gain_phase_[phase].voltage_gain ||
read_current != this->gain_phase_[phase].current_gain) { read_current != this->gain_phase_[phase].current_gain) {
ESP_LOGE(TAG, "[CALIBRATION] Mismatch detected for Phase %s!", phase_labels[phase]); ESP_LOGE(TAG, "[CALIBRATION][%s] Mismatch detected for Phase %s!", cs, phase_labels[phase]);
success = false; success = false;
} }
} }
@@ -791,16 +1159,16 @@ void ATM90E32Component::check_phase_status() {
status += "Phase Loss; "; status += "Phase Loss; ";
auto *sensor = this->phase_status_text_sensor_[phase]; auto *sensor = this->phase_status_text_sensor_[phase];
const char *phase_name = sensor ? sensor->get_name().c_str() : "Unknown Phase"; if (sensor == nullptr)
continue;
if (!status.empty()) { if (!status.empty()) {
status.pop_back(); // remove space status.pop_back(); // remove space
status.pop_back(); // remove semicolon status.pop_back(); // remove semicolon
ESP_LOGW(TAG, "%s: %s", phase_name, status.c_str()); ESP_LOGW(TAG, "%s: %s", sensor->get_name().c_str(), status.c_str());
if (sensor != nullptr) sensor->publish_state(status);
sensor->publish_state(status);
} else { } else {
if (sensor != nullptr) sensor->publish_state("Okay");
sensor->publish_state("Okay");
} }
} }
} }
@@ -817,9 +1185,12 @@ void ATM90E32Component::check_freq_status() {
} else { } else {
freq_status = "Normal"; freq_status = "Normal";
} }
ESP_LOGW(TAG, "Frequency status: %s", freq_status.c_str());
if (this->freq_status_text_sensor_ != nullptr) { if (this->freq_status_text_sensor_ != nullptr) {
if (freq_status == "Normal") {
ESP_LOGD(TAG, "Frequency status: %s", freq_status.c_str());
} else {
ESP_LOGW(TAG, "Frequency status: %s", freq_status.c_str());
}
this->freq_status_text_sensor_->publish_state(freq_status); this->freq_status_text_sensor_->publish_state(freq_status);
} }
} }

View File

@@ -61,15 +61,29 @@ class ATM90E32Component : public PollingComponent,
this->phase_[phase].harmonic_active_power_sensor_ = obj; this->phase_[phase].harmonic_active_power_sensor_ = obj;
} }
void set_peak_current_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].peak_current_sensor_ = obj; } void set_peak_current_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].peak_current_sensor_ = obj; }
void set_volt_gain(int phase, uint16_t gain) { this->phase_[phase].voltage_gain_ = gain; } void set_volt_gain(int phase, uint16_t gain) {
void set_ct_gain(int phase, uint16_t gain) { this->phase_[phase].ct_gain_ = gain; } this->phase_[phase].voltage_gain_ = gain;
void set_voltage_offset(uint8_t phase, int16_t offset) { this->offset_phase_[phase].voltage_offset_ = offset; } this->has_config_voltage_gain_[phase] = true;
void set_current_offset(uint8_t phase, int16_t offset) { this->offset_phase_[phase].current_offset_ = offset; } }
void set_ct_gain(int phase, uint16_t gain) {
this->phase_[phase].ct_gain_ = gain;
this->has_config_current_gain_[phase] = true;
}
void set_voltage_offset(uint8_t phase, int16_t offset) {
this->offset_phase_[phase].voltage_offset_ = offset;
this->has_config_voltage_offset_[phase] = true;
}
void set_current_offset(uint8_t phase, int16_t offset) {
this->offset_phase_[phase].current_offset_ = offset;
this->has_config_current_offset_[phase] = true;
}
void set_active_power_offset(uint8_t phase, int16_t offset) { void set_active_power_offset(uint8_t phase, int16_t offset) {
this->power_offset_phase_[phase].active_power_offset = offset; this->power_offset_phase_[phase].active_power_offset = offset;
this->has_config_active_power_offset_[phase] = true;
} }
void set_reactive_power_offset(uint8_t phase, int16_t offset) { void set_reactive_power_offset(uint8_t phase, int16_t offset) {
this->power_offset_phase_[phase].reactive_power_offset = offset; this->power_offset_phase_[phase].reactive_power_offset = offset;
this->has_config_reactive_power_offset_[phase] = true;
} }
void set_freq_sensor(sensor::Sensor *freq_sensor) { freq_sensor_ = freq_sensor; } void set_freq_sensor(sensor::Sensor *freq_sensor) { freq_sensor_ = freq_sensor; }
void set_peak_current_signed(bool flag) { peak_current_signed_ = flag; } void set_peak_current_signed(bool flag) { peak_current_signed_ = flag; }
@@ -126,8 +140,9 @@ class ATM90E32Component : public PollingComponent,
number::Number *ref_currents_[3]{nullptr, nullptr, nullptr}; number::Number *ref_currents_[3]{nullptr, nullptr, nullptr};
#endif #endif
uint16_t read16_(uint16_t a_register); uint16_t read16_(uint16_t a_register);
uint16_t read16_transaction_(uint16_t a_register);
int read32_(uint16_t addr_h, uint16_t addr_l); int read32_(uint16_t addr_h, uint16_t addr_l);
void write16_(uint16_t a_register, uint16_t val); void write16_(uint16_t a_register, uint16_t val, bool validate = true);
float get_local_phase_voltage_(uint8_t phase); float get_local_phase_voltage_(uint8_t phase);
float get_local_phase_current_(uint8_t phase); float get_local_phase_current_(uint8_t phase);
float get_local_phase_active_power_(uint8_t phase); float get_local_phase_active_power_(uint8_t phase);
@@ -159,12 +174,15 @@ class ATM90E32Component : public PollingComponent,
void restore_offset_calibrations_(); void restore_offset_calibrations_();
void restore_power_offset_calibrations_(); void restore_power_offset_calibrations_();
void restore_gain_calibrations_(); void restore_gain_calibrations_();
void save_offset_calibration_to_memory_();
void save_gain_calibration_to_memory_(); void save_gain_calibration_to_memory_();
void save_power_offset_calibration_to_memory_();
void write_offsets_to_registers_(uint8_t phase, int16_t voltage_offset, int16_t current_offset); void write_offsets_to_registers_(uint8_t phase, int16_t voltage_offset, int16_t current_offset);
void write_power_offsets_to_registers_(uint8_t phase, int16_t p_offset, int16_t q_offset); void write_power_offsets_to_registers_(uint8_t phase, int16_t p_offset, int16_t q_offset);
void write_gains_to_registers_(); void write_gains_to_registers_();
bool verify_gain_writes_(); bool verify_gain_writes_();
bool validate_spi_read_(uint16_t expected, const char *context = nullptr); bool validate_spi_read_(uint16_t expected, const char *context = nullptr);
void log_calibration_status_();
struct ATM90E32Phase { struct ATM90E32Phase {
uint16_t voltage_gain_{0}; uint16_t voltage_gain_{0};
@@ -204,19 +222,33 @@ class ATM90E32Component : public PollingComponent,
int16_t current_offset_{0}; int16_t current_offset_{0};
} offset_phase_[3]; } offset_phase_[3];
OffsetCalibration config_offset_phase_[3];
struct PowerOffsetCalibration { struct PowerOffsetCalibration {
int16_t active_power_offset{0}; int16_t active_power_offset{0};
int16_t reactive_power_offset{0}; int16_t reactive_power_offset{0};
} power_offset_phase_[3]; } power_offset_phase_[3];
PowerOffsetCalibration config_power_offset_phase_[3];
struct GainCalibration { struct GainCalibration {
uint16_t voltage_gain{1}; uint16_t voltage_gain{1};
uint16_t current_gain{1}; uint16_t current_gain{1};
} gain_phase_[3]; } gain_phase_[3];
GainCalibration config_gain_phase_[3];
bool has_config_voltage_offset_[3]{false, false, false};
bool has_config_current_offset_[3]{false, false, false};
bool has_config_active_power_offset_[3]{false, false, false};
bool has_config_reactive_power_offset_[3]{false, false, false};
bool has_config_voltage_gain_[3]{false, false, false};
bool has_config_current_gain_[3]{false, false, false};
ESPPreferenceObject offset_pref_; ESPPreferenceObject offset_pref_;
ESPPreferenceObject power_offset_pref_; ESPPreferenceObject power_offset_pref_;
ESPPreferenceObject gain_calibration_pref_; ESPPreferenceObject gain_calibration_pref_;
std::string cs_summary_;
sensor::Sensor *freq_sensor_{nullptr}; sensor::Sensor *freq_sensor_{nullptr};
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
@@ -231,6 +263,13 @@ class ATM90E32Component : public PollingComponent,
bool peak_current_signed_{false}; bool peak_current_signed_{false};
bool enable_offset_calibration_{false}; bool enable_offset_calibration_{false};
bool enable_gain_calibration_{false}; bool enable_gain_calibration_{false};
bool restored_offset_calibration_{false};
bool restored_power_offset_calibration_{false};
bool restored_gain_calibration_{false};
bool calibration_message_printed_{false};
bool offset_calibration_mismatch_[3]{false, false, false};
bool power_offset_calibration_mismatch_[3]{false, false, false};
bool gain_calibration_mismatch_[3]{false, false, false};
}; };
} // namespace atm90e32 } // namespace atm90e32

View File

@@ -516,6 +516,7 @@ def binary_sensor_schema(
icon: str = cv.UNDEFINED, icon: str = cv.UNDEFINED,
entity_category: str = cv.UNDEFINED, entity_category: str = cv.UNDEFINED,
device_class: str = cv.UNDEFINED, device_class: str = cv.UNDEFINED,
filters: list = cv.UNDEFINED,
) -> cv.Schema: ) -> cv.Schema:
schema = {} schema = {}
@@ -527,6 +528,7 @@ def binary_sensor_schema(
(CONF_ICON, icon, cv.icon), (CONF_ICON, icon, cv.icon),
(CONF_ENTITY_CATEGORY, entity_category, cv.entity_category), (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category),
(CONF_DEVICE_CLASS, device_class, validate_device_class), (CONF_DEVICE_CLASS, device_class, validate_device_class),
(CONF_FILTERS, filters, validate_filters),
]: ]:
if default is not cv.UNDEFINED: if default is not cv.UNDEFINED:
schema[cv.Optional(key, default=default)] = validator schema[cv.Optional(key, default=default)] = validator
@@ -652,7 +654,6 @@ async def binary_sensor_is_off_to_code(config, condition_id, template_arg, args)
@coroutine_with_priority(100.0) @coroutine_with_priority(100.0)
async def to_code(config): async def to_code(config):
cg.add_define("USE_BINARY_SENSOR")
cg.add_global(binary_sensor_ns.using) cg.add_global(binary_sensor_ns.using)

View File

@@ -175,8 +175,7 @@ BLE_REMOVE_BOND_ACTION_SCHEMA = cv.Schema(
) )
async def ble_disconnect_to_code(config, action_id, template_arg, args): async def ble_disconnect_to_code(config, action_id, template_arg, args):
parent = await cg.get_variable(config[CONF_ID]) parent = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, parent) return cg.new_Pvariable(action_id, template_arg, parent)
return var
@automation.register_action( @automation.register_action(
@@ -184,8 +183,7 @@ async def ble_disconnect_to_code(config, action_id, template_arg, args):
) )
async def ble_connect_to_code(config, action_id, template_arg, args): async def ble_connect_to_code(config, action_id, template_arg, args):
parent = await cg.get_variable(config[CONF_ID]) parent = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, parent) return cg.new_Pvariable(action_id, template_arg, parent)
return var
@automation.register_action( @automation.register_action(
@@ -282,14 +280,13 @@ async def passkey_reply_to_code(config, action_id, template_arg, args):
) )
async def remove_bond_to_code(config, action_id, template_arg, args): async def remove_bond_to_code(config, action_id, template_arg, args):
parent = await cg.get_variable(config[CONF_ID]) parent = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, parent) return cg.new_Pvariable(action_id, template_arg, parent)
return var
async def to_code(config): async def to_code(config):
# Register the loggers this component needs # Register the loggers this component needs
esp32_ble.register_bt_logger(BTLoggers.GATT, BTLoggers.SMP) esp32_ble.register_bt_logger(BTLoggers.GATT, BTLoggers.SMP)
cg.add_define("USE_ESP32_BLE_UUID")
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,13 +1,19 @@
import logging
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import esp32_ble, esp32_ble_client, esp32_ble_tracker from esphome.components import esp32_ble, esp32_ble_client, esp32_ble_tracker
from esphome.components.esp32 import add_idf_sdkconfig_option from esphome.components.esp32 import add_idf_sdkconfig_option
from esphome.components.esp32_ble import BTLoggers from esphome.components.esp32_ble import BTLoggers
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ACTIVE, CONF_ID from esphome.const import CONF_ACTIVE, CONF_ID
from esphome.core import CORE
from esphome.log import AnsiFore, color
AUTO_LOAD = ["esp32_ble_client", "esp32_ble_tracker"] AUTO_LOAD = ["esp32_ble_client", "esp32_ble_tracker"]
DEPENDENCIES = ["api", "esp32"] DEPENDENCIES = ["api", "esp32"]
CODEOWNERS = ["@jesserockz"] CODEOWNERS = ["@jesserockz", "@bdraco"]
_LOGGER = logging.getLogger(__name__)
CONF_CONNECTION_SLOTS = "connection_slots" CONF_CONNECTION_SLOTS = "connection_slots"
CONF_CACHE_SERVICES = "cache_services" CONF_CACHE_SERVICES = "cache_services"
@@ -41,6 +47,27 @@ def validate_connections(config):
esp32_ble_tracker.consume_connection_slots(connection_slots, "bluetooth_proxy")( esp32_ble_tracker.consume_connection_slots(connection_slots, "bluetooth_proxy")(
config config
) )
# Warn about connection slot waste when using Arduino framework
if CORE.using_arduino and connection_slots:
_LOGGER.warning(
"Bluetooth Proxy with active connections on Arduino framework has suboptimal performance.\n"
"If BLE connections fail, they can waste connection slots for 10 seconds because\n"
"Arduino doesn't allow configuring the BLE connection timeout (fixed at 30s).\n"
"ESP-IDF framework allows setting it to 20s to match client timeouts.\n"
"\n"
"To switch to ESP-IDF, add this to your YAML:\n"
" esp32:\n"
" framework:\n"
" type: esp-idf\n"
"\n"
"For detailed migration instructions, see:\n"
"%s",
color(
AnsiFore.BLUE, "https://esphome.io/guides/esp32_arduino_to_idf.html"
),
)
return { return {
**config, **config,
CONF_CONNECTIONS: [CONNECTION_SCHEMA({}) for _ in range(connection_slots)], CONF_CONNECTIONS: [CONNECTION_SCHEMA({}) for _ in range(connection_slots)],
@@ -87,6 +114,16 @@ async def to_code(config):
cg.add(var.set_active(config[CONF_ACTIVE])) cg.add(var.set_active(config[CONF_ACTIVE]))
await esp32_ble_tracker.register_raw_ble_device(var, config) await esp32_ble_tracker.register_raw_ble_device(var, config)
# Define max connections for protobuf fixed array
connection_count = len(config.get(CONF_CONNECTIONS, []))
cg.add_define("BLUETOOTH_PROXY_MAX_CONNECTIONS", connection_count)
# Define batch size for BLE advertisements
# Each advertisement is up to 80 bytes when packaged (including protocol overhead)
# 16 advertisements × 80 bytes (worst case) = 1280 bytes out of ~1320 bytes usable payload
# This achieves ~97% WiFi MTU utilization while staying under the limit
cg.add_define("BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE", 16)
for connection_conf in config.get(CONF_CONNECTIONS, []): for connection_conf in config.get(CONF_CONNECTIONS, []):
connection_var = cg.new_Pvariable(connection_conf[CONF_ID]) connection_var = cg.new_Pvariable(connection_conf[CONF_ID])
await cg.register_component(connection_var, connection_conf) await cg.register_component(connection_var, connection_conf)

View File

@@ -12,16 +12,79 @@ namespace esphome::bluetooth_proxy {
static const char *const TAG = "bluetooth_proxy.connection"; static const char *const TAG = "bluetooth_proxy.connection";
// This function is allocation-free and directly packs UUIDs into the output array
// using precalculated constants for the Bluetooth base UUID
static void fill_128bit_uuid_array(std::array<uint64_t, 2> &out, esp_bt_uuid_t uuid_source) { static void fill_128bit_uuid_array(std::array<uint64_t, 2> &out, esp_bt_uuid_t uuid_source) {
esp_bt_uuid_t uuid = espbt::ESPBTUUID::from_uuid(uuid_source).as_128bit().get_uuid(); // Bluetooth base UUID: 00000000-0000-1000-8000-00805F9B34FB
out[0] = ((uint64_t) uuid.uuid.uuid128[15] << 56) | ((uint64_t) uuid.uuid.uuid128[14] << 48) | // out[0] = bytes 8-15 (big-endian)
((uint64_t) uuid.uuid.uuid128[13] << 40) | ((uint64_t) uuid.uuid.uuid128[12] << 32) | // - For 128-bit UUIDs: use bytes 8-15 as-is
((uint64_t) uuid.uuid.uuid128[11] << 24) | ((uint64_t) uuid.uuid.uuid128[10] << 16) | // - For 16/32-bit UUIDs: insert into bytes 12-15, use 0x00001000 for bytes 8-11
((uint64_t) uuid.uuid.uuid128[9] << 8) | ((uint64_t) uuid.uuid.uuid128[8]); out[0] = uuid_source.len == ESP_UUID_LEN_128
out[1] = ((uint64_t) uuid.uuid.uuid128[7] << 56) | ((uint64_t) uuid.uuid.uuid128[6] << 48) | ? (((uint64_t) uuid_source.uuid.uuid128[15] << 56) | ((uint64_t) uuid_source.uuid.uuid128[14] << 48) |
((uint64_t) uuid.uuid.uuid128[5] << 40) | ((uint64_t) uuid.uuid.uuid128[4] << 32) | ((uint64_t) uuid_source.uuid.uuid128[13] << 40) | ((uint64_t) uuid_source.uuid.uuid128[12] << 32) |
((uint64_t) uuid.uuid.uuid128[3] << 24) | ((uint64_t) uuid.uuid.uuid128[2] << 16) | ((uint64_t) uuid_source.uuid.uuid128[11] << 24) | ((uint64_t) uuid_source.uuid.uuid128[10] << 16) |
((uint64_t) uuid.uuid.uuid128[1] << 8) | ((uint64_t) uuid.uuid.uuid128[0]); ((uint64_t) uuid_source.uuid.uuid128[9] << 8) | ((uint64_t) uuid_source.uuid.uuid128[8]))
: (((uint64_t) (uuid_source.len == ESP_UUID_LEN_16 ? uuid_source.uuid.uuid16 : uuid_source.uuid.uuid32)
<< 32) |
0x00001000ULL); // Base UUID bytes 8-11
// out[1] = bytes 0-7 (big-endian)
// - For 128-bit UUIDs: use bytes 0-7 as-is
// - For 16/32-bit UUIDs: use precalculated base UUID constant
out[1] = uuid_source.len == ESP_UUID_LEN_128
? ((uint64_t) uuid_source.uuid.uuid128[7] << 56) | ((uint64_t) uuid_source.uuid.uuid128[6] << 48) |
((uint64_t) uuid_source.uuid.uuid128[5] << 40) | ((uint64_t) uuid_source.uuid.uuid128[4] << 32) |
((uint64_t) uuid_source.uuid.uuid128[3] << 24) | ((uint64_t) uuid_source.uuid.uuid128[2] << 16) |
((uint64_t) uuid_source.uuid.uuid128[1] << 8) | ((uint64_t) uuid_source.uuid.uuid128[0])
: 0x800000805F9B34FBULL; // Base UUID bytes 0-7: 80-00-00-80-5F-9B-34-FB
}
// Helper to fill UUID in the appropriate format based on client support and UUID type
static void fill_gatt_uuid(std::array<uint64_t, 2> &uuid_128, uint32_t &short_uuid, const esp_bt_uuid_t &uuid,
bool use_efficient_uuids) {
if (!use_efficient_uuids || uuid.len == ESP_UUID_LEN_128) {
// Use 128-bit format for old clients or when UUID is already 128-bit
fill_128bit_uuid_array(uuid_128, uuid);
} else if (uuid.len == ESP_UUID_LEN_16) {
short_uuid = uuid.uuid.uuid16;
} else if (uuid.len == ESP_UUID_LEN_32) {
short_uuid = uuid.uuid.uuid32;
}
}
// Constants for size estimation
static constexpr uint8_t SERVICE_OVERHEAD_LEGACY = 25; // UUID(20) + handle(4) + overhead(1)
static constexpr uint8_t SERVICE_OVERHEAD_EFFICIENT = 10; // UUID(6) + handle(4)
static constexpr uint8_t CHAR_SIZE_128BIT = 35; // UUID(20) + handle(4) + props(4) + overhead(7)
static constexpr uint8_t DESC_SIZE_128BIT = 25; // UUID(20) + handle(4) + overhead(1)
static constexpr uint8_t DESC_SIZE_16BIT = 10; // UUID(6) + handle(4)
static constexpr uint8_t DESC_PER_CHAR = 1; // Assume 1 descriptor per characteristic
// Helper to estimate service size before fetching all data
/**
* Estimate the size of a Bluetooth service based on the number of characteristics and UUID format.
*
* @param char_count The number of characteristics in the service.
* @param use_efficient_uuids Whether to use efficient UUIDs (16-bit or 32-bit) for newer APIVersions.
* @return The estimated size of the service in bytes.
*
* This function calculates the size of a Bluetooth service by considering:
* - A service overhead, which depends on whether efficient UUIDs are used.
* - The size of each characteristic, assuming 128-bit UUIDs for safety.
* - The size of descriptors, assuming one 128-bit descriptor per characteristic.
*/
static size_t estimate_service_size(uint16_t char_count, bool use_efficient_uuids) {
size_t service_overhead = use_efficient_uuids ? SERVICE_OVERHEAD_EFFICIENT : SERVICE_OVERHEAD_LEGACY;
// Always assume 128-bit UUIDs for characteristics to be safe
size_t char_size = CHAR_SIZE_128BIT;
// Assume one 128-bit descriptor per characteristic
size_t desc_size = DESC_SIZE_128BIT * DESC_PER_CHAR;
return service_overhead + (char_size + desc_size) * char_count;
}
bool BluetoothConnection::supports_efficient_uuids_() const {
auto *api_conn = this->proxy_->get_api_connection();
return api_conn && api_conn->client_supports_api_version(1, 12);
} }
void BluetoothConnection::dump_config() { void BluetoothConnection::dump_config() {
@@ -29,16 +92,53 @@ void BluetoothConnection::dump_config() {
BLEClientBase::dump_config(); BLEClientBase::dump_config();
} }
void BluetoothConnection::update_allocated_slot_(uint64_t find_value, uint64_t set_value) {
auto &allocated = this->proxy_->connections_free_response_.allocated;
for (auto &slot : allocated) {
if (slot == find_value) {
slot = set_value;
return;
}
}
}
void BluetoothConnection::set_address(uint64_t address) {
// If we're clearing an address (disconnecting), update the pre-allocated message
if (address == 0 && this->address_ != 0) {
this->proxy_->connections_free_response_.free++;
this->update_allocated_slot_(this->address_, 0);
}
// If we're setting a new address (connecting), update the pre-allocated message
else if (address != 0 && this->address_ == 0) {
this->proxy_->connections_free_response_.free--;
this->update_allocated_slot_(0, address);
}
// Call parent implementation to actually set the address
BLEClientBase::set_address(address);
}
void BluetoothConnection::loop() { void BluetoothConnection::loop() {
BLEClientBase::loop(); BLEClientBase::loop();
// Early return if no active connection or not in service discovery phase // Early return if no active connection
if (this->address_ == 0 || this->send_service_ < 0 || this->send_service_ > this->service_count_) { if (this->address_ == 0) {
return; return;
} }
// Handle service discovery // Handle service discovery if in valid range
this->send_service_for_discovery_(); if (this->send_service_ >= 0 && this->send_service_ <= this->service_count_) {
this->send_service_for_discovery_();
}
// Check if we should disable the loop
// - For V3_WITH_CACHE: Services are never sent, disable after INIT state
// - For other connections: Disable only after service discovery is complete
// (send_service_ == DONE_SENDING_SERVICES, which is only set after services are sent)
if (this->state_ != espbt::ClientState::INIT && (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
this->send_service_ == DONE_SENDING_SERVICES)) {
this->disable_loop();
}
} }
void BluetoothConnection::reset_connection_(esp_err_t reason) { void BluetoothConnection::reset_connection_(esp_err_t reason) {
@@ -52,12 +152,12 @@ void BluetoothConnection::reset_connection_(esp_err_t reason) {
// to detect incomplete service discovery rather than relying on us to // to detect incomplete service discovery rather than relying on us to
// tell them about a partial list. // tell them about a partial list.
this->set_address(0); this->set_address(0);
this->send_service_ = DONE_SENDING_SERVICES; this->send_service_ = INIT_SENDING_SERVICES;
this->proxy_->send_connections_free(); this->proxy_->send_connections_free();
} }
void BluetoothConnection::send_service_for_discovery_() { void BluetoothConnection::send_service_for_discovery_() {
if (this->send_service_ == this->service_count_) { if (this->send_service_ >= this->service_count_) {
this->send_service_ = DONE_SENDING_SERVICES; this->send_service_ = DONE_SENDING_SERVICES;
this->proxy_->send_gatt_services_done(this->address_); this->proxy_->send_gatt_services_done(this->address_);
if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE || if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
@@ -70,122 +170,207 @@ void BluetoothConnection::send_service_for_discovery_() {
// Early return if no API connection // Early return if no API connection
auto *api_conn = this->proxy_->get_api_connection(); auto *api_conn = this->proxy_->get_api_connection();
if (api_conn == nullptr) { if (api_conn == nullptr) {
this->send_service_ = DONE_SENDING_SERVICES;
return; return;
} }
// Send next service // Check if client supports efficient UUIDs
esp_gattc_service_elem_t service_result; bool use_efficient_uuids = this->supports_efficient_uuids_();
uint16_t service_count = 1;
esp_gatt_status_t service_status = esp_ble_gattc_get_service(this->gattc_if_, this->conn_id_, nullptr,
&service_result, &service_count, this->send_service_);
this->send_service_++;
if (service_status != ESP_GATT_OK || service_count == 0) {
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service %s, status=%d, service_count=%d, offset=%d",
this->connection_index_, this->address_str().c_str(), service_status != ESP_GATT_OK ? "error" : "missing",
service_status, service_count, this->send_service_ - 1);
return;
}
// Prepare response
api::BluetoothGATTGetServicesResponse resp; api::BluetoothGATTGetServicesResponse resp;
resp.address = this->address_; resp.address = this->address_;
auto &service_resp = resp.services[0];
fill_128bit_uuid_array(service_resp.uuid, service_result.uuid);
service_resp.handle = service_result.start_handle;
// Get the number of characteristics directly with one call // Dynamic batching based on actual size
uint16_t total_char_count = 0; // Conservative MTU limit for API messages (accounts for WPA3 overhead)
esp_gatt_status_t char_count_status = static constexpr size_t MAX_PACKET_SIZE = 1360;
esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_CHARACTERISTIC,
service_result.start_handle, service_result.end_handle, 0, &total_char_count);
if (char_count_status != ESP_GATT_OK) { // Keep running total of actual message size
ESP_LOGW(TAG, "[%d] [%s] Error getting characteristic count, status=%d", this->connection_index_, size_t current_size = 0;
this->address_str().c_str(), char_count_status); api::ProtoSize size;
return; resp.calculate_size(size);
} current_size = size.get_size();
if (total_char_count == 0) { while (this->send_service_ < this->service_count_) {
// No characteristics, just send the service response esp_gattc_service_elem_t service_result;
api_conn->send_message(resp, api::BluetoothGATTGetServicesResponse::MESSAGE_TYPE); uint16_t service_count = 1;
return; esp_gatt_status_t service_status = esp_ble_gattc_get_service(this->gattc_if_, this->conn_id_, nullptr,
} &service_result, &service_count, this->send_service_);
// Reserve space and process characteristics if (service_status != ESP_GATT_OK || service_count == 0) {
service_resp.characteristics.reserve(total_char_count); ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service %s, status=%d, service_count=%d, offset=%d",
uint16_t char_offset = 0; this->connection_index_, this->address_str().c_str(),
esp_gattc_char_elem_t char_result; service_status != ESP_GATT_OK ? "error" : "missing", service_status, service_count, this->send_service_);
while (true) { // characteristics this->send_service_ = DONE_SENDING_SERVICES;
uint16_t char_count = 1;
esp_gatt_status_t char_status =
esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, service_result.start_handle,
service_result.end_handle, &char_result, &char_count, char_offset);
if (char_status == ESP_GATT_INVALID_OFFSET || char_status == ESP_GATT_NOT_FOUND) {
break;
}
if (char_status != ESP_GATT_OK) {
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", this->connection_index_,
this->address_str().c_str(), char_status);
return; return;
} }
if (char_count == 0) {
// Get the number of characteristics BEFORE adding to response
uint16_t total_char_count = 0;
esp_gatt_status_t char_count_status =
esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_CHARACTERISTIC,
service_result.start_handle, service_result.end_handle, 0, &total_char_count);
if (char_count_status != ESP_GATT_OK) {
this->log_connection_error_("esp_ble_gattc_get_attr_count", char_count_status);
this->send_service_ = DONE_SENDING_SERVICES;
return;
}
// If this service likely won't fit, send current batch (unless it's the first)
size_t estimated_size = estimate_service_size(total_char_count, use_efficient_uuids);
if (!resp.services.empty() && (current_size + estimated_size > MAX_PACKET_SIZE)) {
// This service likely won't fit, send current batch
break; break;
} }
service_resp.characteristics.emplace_back(); // Now add the service since we know it will likely fit
auto &characteristic_resp = service_resp.characteristics.back(); resp.services.emplace_back();
fill_128bit_uuid_array(characteristic_resp.uuid, char_result.uuid); auto &service_resp = resp.services.back();
characteristic_resp.handle = char_result.char_handle;
characteristic_resp.properties = char_result.properties;
char_offset++;
// Get the number of descriptors directly with one call fill_gatt_uuid(service_resp.uuid, service_resp.short_uuid, service_result.uuid, use_efficient_uuids);
uint16_t total_desc_count = 0;
esp_gatt_status_t desc_count_status = esp_ble_gattc_get_attr_count(
this->gattc_if_, this->conn_id_, ESP_GATT_DB_DESCRIPTOR, 0, 0, char_result.char_handle, &total_desc_count);
if (desc_count_status != ESP_GATT_OK) { service_resp.handle = service_result.start_handle;
ESP_LOGW(TAG, "[%d] [%s] Error getting descriptor count for char handle %d, status=%d", this->connection_index_,
this->address_str().c_str(), char_result.char_handle, desc_count_status); if (total_char_count > 0) {
return; // Reserve space and process characteristics
} service_resp.characteristics.reserve(total_char_count);
if (total_desc_count == 0) { uint16_t char_offset = 0;
// No descriptors, continue to next characteristic esp_gattc_char_elem_t char_result;
continue; while (true) { // characteristics
uint16_t char_count = 1;
esp_gatt_status_t char_status =
esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, service_result.start_handle,
service_result.end_handle, &char_result, &char_count, char_offset);
if (char_status == ESP_GATT_INVALID_OFFSET || char_status == ESP_GATT_NOT_FOUND) {
break;
}
if (char_status != ESP_GATT_OK) {
this->log_connection_error_("esp_ble_gattc_get_all_char", char_status);
this->send_service_ = DONE_SENDING_SERVICES;
return;
}
if (char_count == 0) {
break;
}
service_resp.characteristics.emplace_back();
auto &characteristic_resp = service_resp.characteristics.back();
fill_gatt_uuid(characteristic_resp.uuid, characteristic_resp.short_uuid, char_result.uuid, use_efficient_uuids);
characteristic_resp.handle = char_result.char_handle;
characteristic_resp.properties = char_result.properties;
char_offset++;
// Get the number of descriptors directly with one call
uint16_t total_desc_count = 0;
esp_gatt_status_t desc_count_status = esp_ble_gattc_get_attr_count(
this->gattc_if_, this->conn_id_, ESP_GATT_DB_DESCRIPTOR, 0, 0, char_result.char_handle, &total_desc_count);
if (desc_count_status != ESP_GATT_OK) {
this->log_connection_error_("esp_ble_gattc_get_attr_count", desc_count_status);
this->send_service_ = DONE_SENDING_SERVICES;
return;
}
if (total_desc_count == 0) {
// No descriptors, continue to next characteristic
continue;
}
// Reserve space and process descriptors
characteristic_resp.descriptors.reserve(total_desc_count);
uint16_t desc_offset = 0;
esp_gattc_descr_elem_t desc_result;
while (true) { // descriptors
uint16_t desc_count = 1;
esp_gatt_status_t desc_status = esp_ble_gattc_get_all_descr(
this->gattc_if_, this->conn_id_, char_result.char_handle, &desc_result, &desc_count, desc_offset);
if (desc_status == ESP_GATT_INVALID_OFFSET || desc_status == ESP_GATT_NOT_FOUND) {
break;
}
if (desc_status != ESP_GATT_OK) {
this->log_connection_error_("esp_ble_gattc_get_all_descr", desc_status);
this->send_service_ = DONE_SENDING_SERVICES;
return;
}
if (desc_count == 0) {
break; // No more descriptors
}
characteristic_resp.descriptors.emplace_back();
auto &descriptor_resp = characteristic_resp.descriptors.back();
fill_gatt_uuid(descriptor_resp.uuid, descriptor_resp.short_uuid, desc_result.uuid, use_efficient_uuids);
descriptor_resp.handle = desc_result.handle;
desc_offset++;
}
}
} // end if (total_char_count > 0)
// Calculate the actual size of just this service
api::ProtoSize service_sizer;
service_resp.calculate_size(service_sizer);
size_t service_size = service_sizer.get_size() + 1; // +1 for field tag
// Check if adding this service would exceed the limit
if (current_size + service_size > MAX_PACKET_SIZE) {
// We would go over - pop the last service if we have more than one
if (resp.services.size() > 1) {
resp.services.pop_back();
ESP_LOGD(TAG, "[%d] [%s] Service %d would exceed limit (current: %d + service: %d > %d), sending current batch",
this->connection_index_, this->address_str().c_str(), this->send_service_, current_size, service_size,
MAX_PACKET_SIZE);
// Don't increment send_service_ - we'll retry this service in next batch
} else {
// This single service is too large, but we have to send it anyway
ESP_LOGV(TAG, "[%d] [%s] Service %d is too large (%d bytes) but sending anyway", this->connection_index_,
this->address_str().c_str(), this->send_service_, service_size);
// Increment so we don't get stuck
this->send_service_++;
}
// Send what we have
break;
} }
// Reserve space and process descriptors // Now we know we're keeping this service, add its size
characteristic_resp.descriptors.reserve(total_desc_count); current_size += service_size;
uint16_t desc_offset = 0; // Successfully added this service, increment counter
esp_gattc_descr_elem_t desc_result; this->send_service_++;
while (true) { // descriptors
uint16_t desc_count = 1;
esp_gatt_status_t desc_status = esp_ble_gattc_get_all_descr(
this->gattc_if_, this->conn_id_, char_result.char_handle, &desc_result, &desc_count, desc_offset);
if (desc_status == ESP_GATT_INVALID_OFFSET || desc_status == ESP_GATT_NOT_FOUND) {
break;
}
if (desc_status != ESP_GATT_OK) {
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_descr error, status=%d", this->connection_index_,
this->address_str().c_str(), desc_status);
return;
}
if (desc_count == 0) {
break; // No more descriptors
}
characteristic_resp.descriptors.emplace_back();
auto &descriptor_resp = characteristic_resp.descriptors.back();
fill_128bit_uuid_array(descriptor_resp.uuid, desc_result.uuid);
descriptor_resp.handle = desc_result.handle;
desc_offset++;
}
} }
// Send the message (we already checked api_conn is not null at the beginning) // Send the message with dynamically batched services
api_conn->send_message(resp, api::BluetoothGATTGetServicesResponse::MESSAGE_TYPE); api_conn->send_message(resp, api::BluetoothGATTGetServicesResponse::MESSAGE_TYPE);
} }
void BluetoothConnection::log_connection_error_(const char *operation, esp_gatt_status_t status) {
ESP_LOGE(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str().c_str(), operation,
status);
}
void BluetoothConnection::log_connection_warning_(const char *operation, esp_err_t err) {
ESP_LOGW(TAG, "[%d] [%s] %s failed, err=%d", this->connection_index_, this->address_str().c_str(), operation, err);
}
void BluetoothConnection::log_gatt_not_connected_(const char *action, const char *type) {
ESP_LOGW(TAG, "[%d] [%s] Cannot %s GATT %s, not connected.", this->connection_index_, this->address_str().c_str(),
action, type);
}
void BluetoothConnection::log_gatt_operation_error_(const char *operation, uint16_t handle, esp_gatt_status_t status) {
ESP_LOGW(TAG, "[%d] [%s] Error %s for handle 0x%2X, status=%d", this->connection_index_, this->address_str().c_str(),
operation, handle, status);
}
esp_err_t BluetoothConnection::check_and_log_error_(const char *operation, esp_err_t err) {
if (err != ESP_OK) {
this->log_connection_warning_(operation, err);
return err;
}
return ESP_OK;
}
bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, bool BluetoothConnection::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) {
if (!BLEClientBase::gattc_event_handler(event, gattc_if, param)) if (!BLEClientBase::gattc_event_handler(event, gattc_if, param))
@@ -226,8 +411,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
case ESP_GATTC_READ_DESCR_EVT: case ESP_GATTC_READ_DESCR_EVT:
case ESP_GATTC_READ_CHAR_EVT: { case ESP_GATTC_READ_CHAR_EVT: {
if (param->read.status != ESP_GATT_OK) { if (param->read.status != ESP_GATT_OK) {
ESP_LOGW(TAG, "[%d] [%s] Error reading char/descriptor at handle 0x%2X, status=%d", this->connection_index_, this->log_gatt_operation_error_("reading char/descriptor", param->read.handle, param->read.status);
this->address_str_.c_str(), param->read.handle, param->read.status);
this->proxy_->send_gatt_error(this->address_, param->read.handle, param->read.status); this->proxy_->send_gatt_error(this->address_, param->read.handle, param->read.status);
break; break;
} }
@@ -241,8 +425,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
case ESP_GATTC_WRITE_CHAR_EVT: case ESP_GATTC_WRITE_CHAR_EVT:
case ESP_GATTC_WRITE_DESCR_EVT: { case ESP_GATTC_WRITE_DESCR_EVT: {
if (param->write.status != ESP_GATT_OK) { if (param->write.status != ESP_GATT_OK) {
ESP_LOGW(TAG, "[%d] [%s] Error writing char/descriptor at handle 0x%2X, status=%d", this->connection_index_, this->log_gatt_operation_error_("writing char/descriptor", param->write.handle, param->write.status);
this->address_str_.c_str(), param->write.handle, param->write.status);
this->proxy_->send_gatt_error(this->address_, param->write.handle, param->write.status); this->proxy_->send_gatt_error(this->address_, param->write.handle, param->write.status);
break; break;
} }
@@ -254,9 +437,8 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
} }
case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: { case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: {
if (param->unreg_for_notify.status != ESP_GATT_OK) { if (param->unreg_for_notify.status != ESP_GATT_OK) {
ESP_LOGW(TAG, "[%d] [%s] Error unregistering notifications for handle 0x%2X, status=%d", this->log_gatt_operation_error_("unregistering notifications", param->unreg_for_notify.handle,
this->connection_index_, this->address_str_.c_str(), param->unreg_for_notify.handle, param->unreg_for_notify.status);
param->unreg_for_notify.status);
this->proxy_->send_gatt_error(this->address_, param->unreg_for_notify.handle, param->unreg_for_notify.status); this->proxy_->send_gatt_error(this->address_, param->unreg_for_notify.handle, param->unreg_for_notify.status);
break; break;
} }
@@ -268,8 +450,8 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
} }
case ESP_GATTC_REG_FOR_NOTIFY_EVT: { case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
if (param->reg_for_notify.status != ESP_GATT_OK) { if (param->reg_for_notify.status != ESP_GATT_OK) {
ESP_LOGW(TAG, "[%d] [%s] Error registering notifications for handle 0x%2X, status=%d", this->connection_index_, this->log_gatt_operation_error_("registering notifications", param->reg_for_notify.handle,
this->address_str_.c_str(), param->reg_for_notify.handle, param->reg_for_notify.status); param->reg_for_notify.status);
this->proxy_->send_gatt_error(this->address_, param->reg_for_notify.handle, param->reg_for_notify.status); this->proxy_->send_gatt_error(this->address_, param->reg_for_notify.handle, param->reg_for_notify.status);
break; break;
} }
@@ -315,8 +497,7 @@ void BluetoothConnection::gap_event_handler(esp_gap_ble_cb_event_t event, esp_bl
esp_err_t BluetoothConnection::read_characteristic(uint16_t handle) { esp_err_t BluetoothConnection::read_characteristic(uint16_t handle) {
if (!this->connected()) { if (!this->connected()) {
ESP_LOGW(TAG, "[%d] [%s] Cannot read GATT characteristic, not connected.", this->connection_index_, this->log_gatt_not_connected_("read", "characteristic");
this->address_str_.c_str());
return ESP_GATT_NOT_CONNECTED; return ESP_GATT_NOT_CONNECTED;
} }
@@ -324,18 +505,12 @@ esp_err_t BluetoothConnection::read_characteristic(uint16_t handle) {
handle); handle);
esp_err_t err = esp_ble_gattc_read_char(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE); esp_err_t err = esp_ble_gattc_read_char(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE);
if (err != ERR_OK) { return this->check_and_log_error_("esp_ble_gattc_read_char", err);
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_read_char error, err=%d", this->connection_index_,
this->address_str_.c_str(), err);
return err;
}
return ESP_OK;
} }
esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const std::string &data, bool response) { esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const std::string &data, bool response) {
if (!this->connected()) { if (!this->connected()) {
ESP_LOGW(TAG, "[%d] [%s] Cannot write GATT characteristic, not connected.", this->connection_index_, this->log_gatt_not_connected_("write", "characteristic");
this->address_str_.c_str());
return ESP_GATT_NOT_CONNECTED; return ESP_GATT_NOT_CONNECTED;
} }
ESP_LOGV(TAG, "[%d] [%s] Writing GATT characteristic handle %d", this->connection_index_, this->address_str_.c_str(), ESP_LOGV(TAG, "[%d] [%s] Writing GATT characteristic handle %d", this->connection_index_, this->address_str_.c_str(),
@@ -344,36 +519,24 @@ esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const std::
esp_err_t err = esp_err_t err =
esp_ble_gattc_write_char(this->gattc_if_, this->conn_id_, handle, data.size(), (uint8_t *) data.data(), esp_ble_gattc_write_char(this->gattc_if_, this->conn_id_, handle, data.size(), (uint8_t *) data.data(),
response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
if (err != ERR_OK) { return this->check_and_log_error_("esp_ble_gattc_write_char", err);
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_write_char error, err=%d", this->connection_index_,
this->address_str_.c_str(), err);
return err;
}
return ESP_OK;
} }
esp_err_t BluetoothConnection::read_descriptor(uint16_t handle) { esp_err_t BluetoothConnection::read_descriptor(uint16_t handle) {
if (!this->connected()) { if (!this->connected()) {
ESP_LOGW(TAG, "[%d] [%s] Cannot read GATT descriptor, not connected.", this->connection_index_, this->log_gatt_not_connected_("read", "descriptor");
this->address_str_.c_str());
return ESP_GATT_NOT_CONNECTED; return ESP_GATT_NOT_CONNECTED;
} }
ESP_LOGV(TAG, "[%d] [%s] Reading GATT descriptor handle %d", this->connection_index_, this->address_str_.c_str(), ESP_LOGV(TAG, "[%d] [%s] Reading GATT descriptor handle %d", this->connection_index_, this->address_str_.c_str(),
handle); handle);
esp_err_t err = esp_ble_gattc_read_char_descr(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE); esp_err_t err = esp_ble_gattc_read_char_descr(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE);
if (err != ERR_OK) { return this->check_and_log_error_("esp_ble_gattc_read_char_descr", err);
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_read_char_descr error, err=%d", this->connection_index_,
this->address_str_.c_str(), err);
return err;
}
return ESP_OK;
} }
esp_err_t BluetoothConnection::write_descriptor(uint16_t handle, const std::string &data, bool response) { esp_err_t BluetoothConnection::write_descriptor(uint16_t handle, const std::string &data, bool response) {
if (!this->connected()) { if (!this->connected()) {
ESP_LOGW(TAG, "[%d] [%s] Cannot write GATT descriptor, not connected.", this->connection_index_, this->log_gatt_not_connected_("write", "descriptor");
this->address_str_.c_str());
return ESP_GATT_NOT_CONNECTED; return ESP_GATT_NOT_CONNECTED;
} }
ESP_LOGV(TAG, "[%d] [%s] Writing GATT descriptor handle %d", this->connection_index_, this->address_str_.c_str(), ESP_LOGV(TAG, "[%d] [%s] Writing GATT descriptor handle %d", this->connection_index_, this->address_str_.c_str(),
@@ -382,18 +545,12 @@ esp_err_t BluetoothConnection::write_descriptor(uint16_t handle, const std::stri
esp_err_t err = esp_ble_gattc_write_char_descr( esp_err_t err = esp_ble_gattc_write_char_descr(
this->gattc_if_, this->conn_id_, handle, data.size(), (uint8_t *) data.data(), this->gattc_if_, this->conn_id_, handle, data.size(), (uint8_t *) data.data(),
response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
if (err != ERR_OK) { return this->check_and_log_error_("esp_ble_gattc_write_char_descr", err);
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_write_char_descr error, err=%d", this->connection_index_,
this->address_str_.c_str(), err);
return err;
}
return ESP_OK;
} }
esp_err_t BluetoothConnection::notify_characteristic(uint16_t handle, bool enable) { esp_err_t BluetoothConnection::notify_characteristic(uint16_t handle, bool enable) {
if (!this->connected()) { if (!this->connected()) {
ESP_LOGW(TAG, "[%d] [%s] Cannot notify GATT characteristic, not connected.", this->connection_index_, this->log_gatt_not_connected_("notify", "characteristic");
this->address_str_.c_str());
return ESP_GATT_NOT_CONNECTED; return ESP_GATT_NOT_CONNECTED;
} }
@@ -401,22 +558,13 @@ esp_err_t BluetoothConnection::notify_characteristic(uint16_t handle, bool enabl
ESP_LOGV(TAG, "[%d] [%s] Registering for GATT characteristic notifications handle %d", this->connection_index_, ESP_LOGV(TAG, "[%d] [%s] Registering for GATT characteristic notifications handle %d", this->connection_index_,
this->address_str_.c_str(), handle); this->address_str_.c_str(), handle);
esp_err_t err = esp_ble_gattc_register_for_notify(this->gattc_if_, this->remote_bda_, handle); esp_err_t err = esp_ble_gattc_register_for_notify(this->gattc_if_, this->remote_bda_, handle);
if (err != ESP_OK) { return this->check_and_log_error_("esp_ble_gattc_register_for_notify", err);
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_register_for_notify failed, err=%d", this->connection_index_,
this->address_str_.c_str(), err);
return err;
}
} else {
ESP_LOGV(TAG, "[%d] [%s] Unregistering for GATT characteristic notifications handle %d", this->connection_index_,
this->address_str_.c_str(), handle);
esp_err_t err = esp_ble_gattc_unregister_for_notify(this->gattc_if_, this->remote_bda_, handle);
if (err != ESP_OK) {
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_unregister_for_notify failed, err=%d", this->connection_index_,
this->address_str_.c_str(), err);
return err;
}
} }
return ESP_OK;
ESP_LOGV(TAG, "[%d] [%s] Unregistering for GATT characteristic notifications handle %d", this->connection_index_,
this->address_str_.c_str(), handle);
esp_err_t err = esp_ble_gattc_unregister_for_notify(this->gattc_if_, this->remote_bda_, handle);
return this->check_and_log_error_("esp_ble_gattc_unregister_for_notify", err);
} }
esp32_ble_tracker::AdvertisementParserType BluetoothConnection::get_advertisement_parser_type() { esp32_ble_tracker::AdvertisementParserType BluetoothConnection::get_advertisement_parser_type() {

View File

@@ -24,18 +24,27 @@ class BluetoothConnection : public esp32_ble_client::BLEClientBase {
esp_err_t notify_characteristic(uint16_t handle, bool enable); esp_err_t notify_characteristic(uint16_t handle, bool enable);
void set_address(uint64_t address) override;
protected: protected:
friend class BluetoothProxy; friend class BluetoothProxy;
bool supports_efficient_uuids_() const;
void send_service_for_discovery_(); void send_service_for_discovery_();
void reset_connection_(esp_err_t reason); void reset_connection_(esp_err_t reason);
void update_allocated_slot_(uint64_t find_value, uint64_t set_value);
void log_connection_error_(const char *operation, esp_gatt_status_t status);
void log_connection_warning_(const char *operation, esp_err_t err);
void log_gatt_not_connected_(const char *action, const char *type);
void log_gatt_operation_error_(const char *operation, uint16_t handle, esp_gatt_status_t status);
esp_err_t check_and_log_error_(const char *operation, esp_err_t err);
// Memory optimized layout for 32-bit systems // Memory optimized layout for 32-bit systems
// Group 1: Pointers (4 bytes each, naturally aligned) // Group 1: Pointers (4 bytes each, naturally aligned)
BluetoothProxy *proxy_; BluetoothProxy *proxy_;
// Group 2: 2-byte types // Group 2: 2-byte types
int16_t send_service_{-2}; // Needs to handle negative values and service count int16_t send_service_{-3}; // -3 = INIT_SENDING_SERVICES, -2 = DONE_SENDING_SERVICES, >=0 = service index
// Group 3: 1-byte types // Group 3: 1-byte types
bool seen_mtu_or_services_{false}; bool seen_mtu_or_services_{false};

View File

@@ -11,12 +11,8 @@ namespace esphome::bluetooth_proxy {
static const char *const TAG = "bluetooth_proxy"; static const char *const TAG = "bluetooth_proxy";
// Batch size for BLE advertisements to maximize WiFi efficiency // BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE is defined during code generation
// Each advertisement is up to 80 bytes when packaged (including protocol overhead) // It sets the batch size for BLE advertisements to maximize WiFi efficiency
// Most advertisements are 20-30 bytes, allowing even more to fit per packet
// 16 advertisements × 80 bytes (worst case) = 1280 bytes out of ~1320 bytes usable payload
// This achieves ~97% WiFi MTU utilization while staying under the limit
static constexpr size_t FLUSH_BATCH_SIZE = 16;
// Verify BLE advertisement data array size matches the BLE specification (31 bytes adv + 31 bytes scan response) // Verify BLE advertisement data array size matches the BLE specification (31 bytes adv + 31 bytes scan response)
static_assert(sizeof(((api::BluetoothLERawAdvertisement *) nullptr)->data) == 62, static_assert(sizeof(((api::BluetoothLERawAdvertisement *) nullptr)->data) == 62,
@@ -25,15 +21,8 @@ static_assert(sizeof(((api::BluetoothLERawAdvertisement *) nullptr)->data) == 62
BluetoothProxy::BluetoothProxy() { global_bluetooth_proxy = this; } BluetoothProxy::BluetoothProxy() { global_bluetooth_proxy = this; }
void BluetoothProxy::setup() { void BluetoothProxy::setup() {
// Pre-allocate response object this->connections_free_response_.limit = BLUETOOTH_PROXY_MAX_CONNECTIONS;
this->response_ = std::make_unique<api::BluetoothLERawAdvertisementsResponse>(); this->connections_free_response_.free = BLUETOOTH_PROXY_MAX_CONNECTIONS;
// Reserve capacity but start with size 0
// Reserve 50% since we'll grow naturally and flush at FLUSH_BATCH_SIZE
this->response_->advertisements.reserve(FLUSH_BATCH_SIZE / 2);
// Don't pre-allocate pool - let it grow only if needed in busy environments
// Many devices in quiet areas will never need the overflow pool
this->parent_->add_scanner_state_callback([this](esp32_ble_tracker::ScannerState state) { this->parent_->add_scanner_state_callback([this](esp32_ble_tracker::ScannerState state) {
if (this->api_connection_ != nullptr) { if (this->api_connection_ != nullptr) {
@@ -50,6 +39,26 @@ void BluetoothProxy::send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerSta
this->api_connection_->send_message(resp, api::BluetoothScannerStateResponse::MESSAGE_TYPE); this->api_connection_->send_message(resp, api::BluetoothScannerStateResponse::MESSAGE_TYPE);
} }
void BluetoothProxy::log_connection_request_ignored_(BluetoothConnection *connection, espbt::ClientState state) {
ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, state: %s", connection->get_connection_index(),
connection->address_str().c_str(), espbt::client_state_to_string(state));
}
void BluetoothProxy::log_connection_info_(BluetoothConnection *connection, const char *message) {
ESP_LOGI(TAG, "[%d] [%s] Connecting %s", connection->get_connection_index(), connection->address_str().c_str(),
message);
}
void BluetoothProxy::log_not_connected_gatt_(const char *action, const char *type) {
ESP_LOGW(TAG, "Cannot %s GATT %s, not connected", action, type);
}
void BluetoothProxy::handle_gatt_not_connected_(uint64_t address, uint16_t handle, const char *action,
const char *type) {
this->log_not_connected_gatt_(action, type);
this->send_gatt_error(address, handle, ESP_GATT_NOT_CONNECTED);
}
#ifdef USE_ESP32_BLE_DEVICE #ifdef USE_ESP32_BLE_DEVICE
bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
// This method should never be called since bluetooth_proxy always uses raw advertisements // This method should never be called since bluetooth_proxy always uses raw advertisements
@@ -62,39 +71,27 @@ bool BluetoothProxy::parse_devices(const esp32_ble::BLEScanResult *scan_results,
if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr) if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr)
return false; return false;
auto &advertisements = this->response_->advertisements; auto &advertisements = this->response_.advertisements;
for (size_t i = 0; i < count; i++) { for (size_t i = 0; i < count; i++) {
auto &result = scan_results[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;
// Check if we need to expand the vector
if (this->advertisement_count_ >= advertisements.size()) {
if (this->advertisement_pool_.empty()) {
// No room in pool, need to allocate
advertisements.emplace_back();
} else {
// Pull from pool
advertisements.push_back(std::move(this->advertisement_pool_.back()));
this->advertisement_pool_.pop_back();
}
}
// Fill in the data directly at current position // Fill in the data directly at current position
auto &adv = advertisements[this->advertisement_count_]; auto &adv = advertisements[this->response_.advertisements_len];
adv.address = esp32_ble::ble_addr_to_uint64(result.bda); adv.address = esp32_ble::ble_addr_to_uint64(result.bda);
adv.rssi = result.rssi; adv.rssi = result.rssi;
adv.address_type = result.ble_addr_type; adv.address_type = result.ble_addr_type;
adv.data_len = length; adv.data_len = length;
std::memcpy(adv.data, result.ble_adv, length); std::memcpy(adv.data, result.ble_adv, length);
this->advertisement_count_++; this->response_.advertisements_len++;
ESP_LOGV(TAG, "Queuing raw packet from %02X:%02X:%02X:%02X:%02X:%02X, length %d. RSSI: %d dB", result.bda[0], ESP_LOGV(TAG, "Queuing raw packet from %02X:%02X:%02X:%02X:%02X:%02X, length %d. RSSI: %d dB", result.bda[0],
result.bda[1], result.bda[2], result.bda[3], result.bda[4], result.bda[5], length, result.rssi); result.bda[1], result.bda[2], result.bda[3], result.bda[4], result.bda[5], length, result.rssi);
// Flush if we have reached FLUSH_BATCH_SIZE // Flush if we have reached BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE
if (this->advertisement_count_ >= FLUSH_BATCH_SIZE) { if (this->response_.advertisements_len >= BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE) {
this->flush_pending_advertisements(); this->flush_pending_advertisements();
} }
} }
@@ -103,54 +100,31 @@ bool BluetoothProxy::parse_devices(const esp32_ble::BLEScanResult *scan_results,
} }
void BluetoothProxy::flush_pending_advertisements() { void BluetoothProxy::flush_pending_advertisements() {
if (this->advertisement_count_ == 0 || !api::global_api_server->is_connected() || this->api_connection_ == nullptr) if (this->response_.advertisements_len == 0 || !api::global_api_server->is_connected() ||
this->api_connection_ == nullptr)
return; return;
auto &advertisements = this->response_->advertisements;
// Return any items beyond advertisement_count_ to the pool
if (advertisements.size() > this->advertisement_count_) {
// Move unused items back to pool
this->advertisement_pool_.insert(this->advertisement_pool_.end(),
std::make_move_iterator(advertisements.begin() + this->advertisement_count_),
std::make_move_iterator(advertisements.end()));
// Resize to actual count
advertisements.resize(this->advertisement_count_);
}
// Send the message // Send the message
this->api_connection_->send_message(*this->response_, api::BluetoothLERawAdvertisementsResponse::MESSAGE_TYPE); this->api_connection_->send_message(this->response_, api::BluetoothLERawAdvertisementsResponse::MESSAGE_TYPE);
// Reset count - existing items will be overwritten in next batch ESP_LOGV(TAG, "Sent batch of %u BLE advertisements", this->response_.advertisements_len);
this->advertisement_count_ = 0;
// Reset the length for the next batch
this->response_.advertisements_len = 0;
} }
void BluetoothProxy::dump_config() { void BluetoothProxy::dump_config() {
ESP_LOGCONFIG(TAG, "Bluetooth Proxy:");
ESP_LOGCONFIG(TAG, ESP_LOGCONFIG(TAG,
"Bluetooth Proxy:\n"
" Active: %s\n" " Active: %s\n"
" Connections: %d", " Connections: %d",
YESNO(this->active_), this->connections_.size()); YESNO(this->active_), this->connection_count_);
}
int BluetoothProxy::get_bluetooth_connections_free() {
int free = 0;
for (auto *connection : this->connections_) {
if (connection->address_ == 0) {
free++;
ESP_LOGV(TAG, "[%d] Free connection", connection->get_connection_index());
} else {
ESP_LOGV(TAG, "[%d] Used connection by [%s]", connection->get_connection_index(),
connection->address_str().c_str());
}
}
return free;
} }
void BluetoothProxy::loop() { void BluetoothProxy::loop() {
if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr) { if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr) {
for (auto *connection : this->connections_) { for (uint8_t i = 0; i < this->connection_count_; i++) {
auto *connection = this->connections_[i];
if (connection->get_address() != 0 && !connection->disconnect_pending()) { if (connection->get_address() != 0 && !connection->disconnect_pending()) {
connection->disconnect(); connection->disconnect();
} }
@@ -173,7 +147,8 @@ esp32_ble_tracker::AdvertisementParserType BluetoothProxy::get_advertisement_par
} }
BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool reserve) { BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool reserve) {
for (auto *connection : this->connections_) { for (uint8_t i = 0; i < this->connection_count_; i++) {
auto *connection = this->connections_[i];
if (connection->get_address() == address) if (connection->get_address() == address)
return connection; return connection;
} }
@@ -181,9 +156,10 @@ BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool rese
if (!reserve) if (!reserve)
return nullptr; return nullptr;
for (auto *connection : this->connections_) { for (uint8_t i = 0; i < this->connection_count_; i++) {
auto *connection = this->connections_[i];
if (connection->get_address() == 0) { if (connection->get_address() == 0) {
connection->send_service_ = DONE_SENDING_SERVICES; connection->send_service_ = INIT_SENDING_SERVICES;
connection->set_address(address); connection->set_address(address);
// All connections must start at INIT // All connections must start at INIT
// We only set the state if we allocate the connection // We only set the state if we allocate the connection
@@ -200,8 +176,7 @@ BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool rese
void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest &msg) { void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest &msg) {
switch (msg.request_type) { switch (msg.request_type) {
case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITH_CACHE: case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITH_CACHE:
case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE: case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE: {
case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT: {
auto *connection = this->get_connection_(msg.address, true); auto *connection = this->get_connection_(msg.address, true);
if (connection == nullptr) { if (connection == nullptr) {
ESP_LOGW(TAG, "No free connections available"); ESP_LOGW(TAG, "No free connections available");
@@ -210,23 +185,10 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
} }
if (connection->state() == espbt::ClientState::CONNECTED || if (connection->state() == espbt::ClientState::CONNECTED ||
connection->state() == espbt::ClientState::ESTABLISHED) { connection->state() == espbt::ClientState::ESTABLISHED) {
ESP_LOGW(TAG, "[%d] [%s] Connection already established", connection->get_connection_index(), this->log_connection_request_ignored_(connection, connection->state());
connection->address_str().c_str());
this->send_device_connection(msg.address, true); this->send_device_connection(msg.address, true);
this->send_connections_free(); this->send_connections_free();
return; return;
} else if (connection->state() == espbt::ClientState::SEARCHING) {
ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, already searching for device",
connection->get_connection_index(), connection->address_str().c_str());
return;
} else if (connection->state() == espbt::ClientState::DISCOVERED) {
ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, device already discovered",
connection->get_connection_index(), connection->address_str().c_str());
return;
} else if (connection->state() == espbt::ClientState::READY_TO_CONNECT) {
ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, waiting in line to connect",
connection->get_connection_index(), connection->address_str().c_str());
return;
} else if (connection->state() == espbt::ClientState::CONNECTING) { } else if (connection->state() == espbt::ClientState::CONNECTING) {
if (connection->disconnect_pending()) { if (connection->disconnect_pending()) {
ESP_LOGW(TAG, "[%d] [%s] Connection request while pending disconnect, cancelling pending disconnect", ESP_LOGW(TAG, "[%d] [%s] Connection request while pending disconnect, cancelling pending disconnect",
@@ -234,29 +196,18 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
connection->cancel_pending_disconnect(); connection->cancel_pending_disconnect();
return; return;
} }
ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, already connecting", connection->get_connection_index(), this->log_connection_request_ignored_(connection, connection->state());
connection->address_str().c_str());
return;
} else if (connection->state() == espbt::ClientState::DISCONNECTING) {
ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, device is disconnecting",
connection->get_connection_index(), connection->address_str().c_str());
return; return;
} else if (connection->state() != espbt::ClientState::INIT) { } else if (connection->state() != espbt::ClientState::INIT) {
ESP_LOGW(TAG, "[%d] [%s] Connection already in progress", connection->get_connection_index(), this->log_connection_request_ignored_(connection, connection->state());
connection->address_str().c_str());
return; return;
} }
if (msg.request_type == api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITH_CACHE) { if (msg.request_type == api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITH_CACHE) {
connection->set_connection_type(espbt::ConnectionType::V3_WITH_CACHE); connection->set_connection_type(espbt::ConnectionType::V3_WITH_CACHE);
ESP_LOGI(TAG, "[%d] [%s] Connecting v3 with cache", connection->get_connection_index(), this->log_connection_info_(connection, "v3 with cache");
connection->address_str().c_str()); } else { // BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE
} else if (msg.request_type == api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE) {
connection->set_connection_type(espbt::ConnectionType::V3_WITHOUT_CACHE); connection->set_connection_type(espbt::ConnectionType::V3_WITHOUT_CACHE);
ESP_LOGI(TAG, "[%d] [%s] Connecting v3 without cache", connection->get_connection_index(), this->log_connection_info_(connection, "v3 without cache");
connection->address_str().c_str());
} else {
connection->set_connection_type(espbt::ConnectionType::V1);
ESP_LOGI(TAG, "[%d] [%s] Connecting v1", connection->get_connection_index(), connection->address_str().c_str());
} }
if (msg.has_address_type) { if (msg.has_address_type) {
uint64_to_bd_addr(msg.address, connection->remote_bda_); uint64_to_bd_addr(msg.address, connection->remote_bda_);
@@ -318,14 +269,18 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
break; break;
} }
case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT: {
ESP_LOGE(TAG, "V1 connections removed");
this->send_device_connection(msg.address, false);
break;
}
} }
} }
void BluetoothProxy::bluetooth_gatt_read(const api::BluetoothGATTReadRequest &msg) { void BluetoothProxy::bluetooth_gatt_read(const api::BluetoothGATTReadRequest &msg) {
auto *connection = this->get_connection_(msg.address, false); auto *connection = this->get_connection_(msg.address, false);
if (connection == nullptr) { if (connection == nullptr) {
ESP_LOGW(TAG, "Cannot read GATT characteristic, not connected"); this->handle_gatt_not_connected_(msg.address, msg.handle, "read", "characteristic");
this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
return; return;
} }
@@ -338,8 +293,7 @@ void BluetoothProxy::bluetooth_gatt_read(const api::BluetoothGATTReadRequest &ms
void BluetoothProxy::bluetooth_gatt_write(const api::BluetoothGATTWriteRequest &msg) { void BluetoothProxy::bluetooth_gatt_write(const api::BluetoothGATTWriteRequest &msg) {
auto *connection = this->get_connection_(msg.address, false); auto *connection = this->get_connection_(msg.address, false);
if (connection == nullptr) { if (connection == nullptr) {
ESP_LOGW(TAG, "Cannot write GATT characteristic, not connected"); this->handle_gatt_not_connected_(msg.address, msg.handle, "write", "characteristic");
this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
return; return;
} }
@@ -352,8 +306,7 @@ void BluetoothProxy::bluetooth_gatt_write(const api::BluetoothGATTWriteRequest &
void BluetoothProxy::bluetooth_gatt_read_descriptor(const api::BluetoothGATTReadDescriptorRequest &msg) { void BluetoothProxy::bluetooth_gatt_read_descriptor(const api::BluetoothGATTReadDescriptorRequest &msg) {
auto *connection = this->get_connection_(msg.address, false); auto *connection = this->get_connection_(msg.address, false);
if (connection == nullptr) { if (connection == nullptr) {
ESP_LOGW(TAG, "Cannot read GATT descriptor, not connected"); this->handle_gatt_not_connected_(msg.address, msg.handle, "read", "descriptor");
this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
return; return;
} }
@@ -366,8 +319,7 @@ void BluetoothProxy::bluetooth_gatt_read_descriptor(const api::BluetoothGATTRead
void BluetoothProxy::bluetooth_gatt_write_descriptor(const api::BluetoothGATTWriteDescriptorRequest &msg) { void BluetoothProxy::bluetooth_gatt_write_descriptor(const api::BluetoothGATTWriteDescriptorRequest &msg) {
auto *connection = this->get_connection_(msg.address, false); auto *connection = this->get_connection_(msg.address, false);
if (connection == nullptr) { if (connection == nullptr) {
ESP_LOGW(TAG, "Cannot write GATT descriptor, not connected"); this->handle_gatt_not_connected_(msg.address, msg.handle, "write", "descriptor");
this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
return; return;
} }
@@ -380,8 +332,7 @@ void BluetoothProxy::bluetooth_gatt_write_descriptor(const api::BluetoothGATTWri
void BluetoothProxy::bluetooth_gatt_send_services(const api::BluetoothGATTGetServicesRequest &msg) { void BluetoothProxy::bluetooth_gatt_send_services(const api::BluetoothGATTGetServicesRequest &msg) {
auto *connection = this->get_connection_(msg.address, false); auto *connection = this->get_connection_(msg.address, false);
if (connection == nullptr || !connection->connected()) { if (connection == nullptr || !connection->connected()) {
ESP_LOGW(TAG, "Cannot get GATT services, not connected"); this->handle_gatt_not_connected_(msg.address, 0, "get", "services");
this->send_gatt_error(msg.address, 0, ESP_GATT_NOT_CONNECTED);
return; return;
} }
if (!connection->service_count_) { if (!connection->service_count_) {
@@ -389,16 +340,14 @@ void BluetoothProxy::bluetooth_gatt_send_services(const api::BluetoothGATTGetSer
this->send_gatt_services_done(msg.address); this->send_gatt_services_done(msg.address);
return; return;
} }
if (connection->send_service_ == if (connection->send_service_ == INIT_SENDING_SERVICES) // Start sending services if not started yet
DONE_SENDING_SERVICES) // Only start sending services if we're not already sending them
connection->send_service_ = 0; connection->send_service_ = 0;
} }
void BluetoothProxy::bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest &msg) { void BluetoothProxy::bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest &msg) {
auto *connection = this->get_connection_(msg.address, false); auto *connection = this->get_connection_(msg.address, false);
if (connection == nullptr) { if (connection == nullptr) {
ESP_LOGW(TAG, "Cannot notify GATT characteristic, not connected"); this->handle_gatt_not_connected_(msg.address, msg.handle, "notify", "characteristic");
this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
return; return;
} }
@@ -439,17 +388,13 @@ void BluetoothProxy::send_device_connection(uint64_t address, bool connected, ui
this->api_connection_->send_message(call, api::BluetoothDeviceConnectionResponse::MESSAGE_TYPE); this->api_connection_->send_message(call, api::BluetoothDeviceConnectionResponse::MESSAGE_TYPE);
} }
void BluetoothProxy::send_connections_free() { void BluetoothProxy::send_connections_free() {
if (this->api_connection_ == nullptr) if (this->api_connection_ != nullptr) {
return; this->send_connections_free(this->api_connection_);
api::BluetoothConnectionsFreeResponse call;
call.free = this->get_bluetooth_connections_free();
call.limit = this->get_bluetooth_connections_limit();
for (auto *connection : this->connections_) {
if (connection->address_ != 0) {
call.allocated.push_back(connection->address_);
}
} }
this->api_connection_->send_message(call, api::BluetoothConnectionsFreeResponse::MESSAGE_TYPE); }
void BluetoothProxy::send_connections_free(api::APIConnection *api_connection) {
api_connection->send_message(this->connections_free_response_, api::BluetoothConnectionsFreeResponse::MESSAGE_TYPE);
} }
void BluetoothProxy::send_gatt_services_done(uint64_t address) { void BluetoothProxy::send_gatt_services_done(uint64_t address) {

View File

@@ -2,6 +2,7 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
#include <array>
#include <map> #include <map>
#include <vector> #include <vector>
@@ -22,6 +23,7 @@ namespace esphome::bluetooth_proxy {
static const esp_err_t ESP_GATT_NOT_CONNECTED = -1; static const esp_err_t ESP_GATT_NOT_CONNECTED = -1;
static const int DONE_SENDING_SERVICES = -2; static const int DONE_SENDING_SERVICES = -2;
static const int INIT_SENDING_SERVICES = -3;
using namespace esp32_ble_client; using namespace esp32_ble_client;
@@ -49,6 +51,7 @@ enum BluetoothProxySubscriptionFlag : uint32_t {
}; };
class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Component { class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Component {
friend class BluetoothConnection; // Allow connection to update connections_free_response_
public: public:
BluetoothProxy(); BluetoothProxy();
#ifdef USE_ESP32_BLE_DEVICE #ifdef USE_ESP32_BLE_DEVICE
@@ -62,8 +65,10 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override; esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override;
void register_connection(BluetoothConnection *connection) { void register_connection(BluetoothConnection *connection) {
this->connections_.push_back(connection); if (this->connection_count_ < BLUETOOTH_PROXY_MAX_CONNECTIONS) {
connection->proxy_ = this; this->connections_[this->connection_count_++] = connection;
connection->proxy_ = this;
}
} }
void bluetooth_device_request(const api::BluetoothDeviceRequest &msg); void bluetooth_device_request(const api::BluetoothDeviceRequest &msg);
@@ -74,15 +79,13 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
void bluetooth_gatt_send_services(const api::BluetoothGATTGetServicesRequest &msg); void bluetooth_gatt_send_services(const api::BluetoothGATTGetServicesRequest &msg);
void bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest &msg); void bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest &msg);
int get_bluetooth_connections_free();
int get_bluetooth_connections_limit() { return this->connections_.size(); }
void subscribe_api_connection(api::APIConnection *api_connection, uint32_t flags); void subscribe_api_connection(api::APIConnection *api_connection, uint32_t flags);
void unsubscribe_api_connection(api::APIConnection *api_connection); void unsubscribe_api_connection(api::APIConnection *api_connection);
api::APIConnection *get_api_connection() { return this->api_connection_; } api::APIConnection *get_api_connection() { return this->api_connection_; }
void send_device_connection(uint64_t address, bool connected, uint16_t mtu = 0, esp_err_t error = ESP_OK); void send_device_connection(uint64_t address, bool connected, uint16_t mtu = 0, esp_err_t error = ESP_OK);
void send_connections_free(); void send_connections_free();
void send_connections_free(api::APIConnection *api_connection);
void send_gatt_services_done(uint64_t address); void send_gatt_services_done(uint64_t address);
void send_gatt_error(uint64_t address, uint16_t handle, esp_err_t error); void send_gatt_error(uint64_t address, uint16_t handle, esp_err_t error);
void send_device_pairing(uint64_t address, bool paired, esp_err_t error = ESP_OK); void send_device_pairing(uint64_t address, bool paired, esp_err_t error = ESP_OK);
@@ -134,24 +137,30 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
void send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerState state); void send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerState state);
BluetoothConnection *get_connection_(uint64_t address, bool reserve); BluetoothConnection *get_connection_(uint64_t address, bool reserve);
void log_connection_request_ignored_(BluetoothConnection *connection, espbt::ClientState state);
void log_connection_info_(BluetoothConnection *connection, const char *message);
void log_not_connected_gatt_(const char *action, const char *type);
void handle_gatt_not_connected_(uint64_t address, uint16_t handle, const char *action, const char *type);
// Memory optimized layout for 32-bit systems // Memory optimized layout for 32-bit systems
// Group 1: Pointers (4 bytes each, naturally aligned) // Group 1: Pointers (4 bytes each, naturally aligned)
api::APIConnection *api_connection_{nullptr}; api::APIConnection *api_connection_{nullptr};
// Group 2: Container types (typically 12 bytes on 32-bit) // Group 2: Fixed-size array of connection pointers
std::vector<BluetoothConnection *> connections_{}; std::array<BluetoothConnection *, BLUETOOTH_PROXY_MAX_CONNECTIONS> connections_{};
// BLE advertisement batching // BLE advertisement batching
std::vector<api::BluetoothLERawAdvertisement> advertisement_pool_; api::BluetoothLERawAdvertisementsResponse response_;
std::unique_ptr<api::BluetoothLERawAdvertisementsResponse> response_;
// Group 3: 4-byte types // Group 3: 4-byte types
uint32_t last_advertisement_flush_time_{0}; uint32_t last_advertisement_flush_time_{0};
// Pre-allocated response message - always ready to send
api::BluetoothConnectionsFreeResponse connections_free_response_;
// Group 4: 1-byte types grouped together // Group 4: 1-byte types grouped together
bool active_; bool active_;
uint8_t advertisement_count_{0}; uint8_t connection_count_{0};
// 2 bytes used, 2 bytes padding // 2 bytes used, 2 bytes padding
}; };

View File

@@ -7,6 +7,8 @@
#include <esphome/components/sensor/sensor.h> #include <esphome/components/sensor/sensor.h>
#include <esphome/core/component.h> #include <esphome/core/component.h>
#define BME280_ERROR_WRONG_CHIP_ID "Wrong chip ID"
namespace esphome { namespace esphome {
namespace bme280_base { namespace bme280_base {
@@ -98,18 +100,18 @@ void BME280Component::setup() {
if (!this->read_byte(BME280_REGISTER_CHIPID, &chip_id)) { if (!this->read_byte(BME280_REGISTER_CHIPID, &chip_id)) {
this->error_code_ = COMMUNICATION_FAILED; this->error_code_ = COMMUNICATION_FAILED;
this->mark_failed(); this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
return; return;
} }
if (chip_id != 0x60) { if (chip_id != 0x60) {
this->error_code_ = WRONG_CHIP_ID; this->error_code_ = WRONG_CHIP_ID;
this->mark_failed(); this->mark_failed(BME280_ERROR_WRONG_CHIP_ID);
return; return;
} }
// Send a soft reset. // Send a soft reset.
if (!this->write_byte(BME280_REGISTER_RESET, BME280_SOFT_RESET)) { if (!this->write_byte(BME280_REGISTER_RESET, BME280_SOFT_RESET)) {
this->mark_failed(); this->mark_failed("Reset failed");
return; return;
} }
// Wait until the NVM data has finished loading. // Wait until the NVM data has finished loading.
@@ -118,14 +120,12 @@ void BME280Component::setup() {
do { // NOLINT do { // NOLINT
delay(2); delay(2);
if (!this->read_byte(BME280_REGISTER_STATUS, &status)) { if (!this->read_byte(BME280_REGISTER_STATUS, &status)) {
ESP_LOGW(TAG, "Error reading status register."); this->mark_failed("Error reading status register");
this->mark_failed();
return; return;
} }
} while ((status & BME280_STATUS_IM_UPDATE) && (--retry)); } while ((status & BME280_STATUS_IM_UPDATE) && (--retry));
if (status & BME280_STATUS_IM_UPDATE) { if (status & BME280_STATUS_IM_UPDATE) {
ESP_LOGW(TAG, "Timeout loading NVM."); this->mark_failed("Timeout loading NVM");
this->mark_failed();
return; return;
} }
@@ -153,26 +153,26 @@ void BME280Component::setup() {
uint8_t humid_control_val = 0; uint8_t humid_control_val = 0;
if (!this->read_byte(BME280_REGISTER_CONTROLHUMID, &humid_control_val)) { if (!this->read_byte(BME280_REGISTER_CONTROLHUMID, &humid_control_val)) {
this->mark_failed(); this->mark_failed("Read humidity control");
return; return;
} }
humid_control_val &= ~0b00000111; humid_control_val &= ~0b00000111;
humid_control_val |= this->humidity_oversampling_ & 0b111; humid_control_val |= this->humidity_oversampling_ & 0b111;
if (!this->write_byte(BME280_REGISTER_CONTROLHUMID, humid_control_val)) { if (!this->write_byte(BME280_REGISTER_CONTROLHUMID, humid_control_val)) {
this->mark_failed(); this->mark_failed("Write humidity control");
return; return;
} }
uint8_t config_register = 0; uint8_t config_register = 0;
if (!this->read_byte(BME280_REGISTER_CONFIG, &config_register)) { if (!this->read_byte(BME280_REGISTER_CONFIG, &config_register)) {
this->mark_failed(); this->mark_failed("Read config");
return; return;
} }
config_register &= ~0b11111100; config_register &= ~0b11111100;
config_register |= 0b101 << 5; // 1000 ms standby time config_register |= 0b101 << 5; // 1000 ms standby time
config_register |= (this->iir_filter_ & 0b111) << 2; config_register |= (this->iir_filter_ & 0b111) << 2;
if (!this->write_byte(BME280_REGISTER_CONFIG, config_register)) { if (!this->write_byte(BME280_REGISTER_CONFIG, config_register)) {
this->mark_failed(); this->mark_failed("Write config");
return; return;
} }
} }
@@ -183,7 +183,7 @@ void BME280Component::dump_config() {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
break; break;
case WRONG_CHIP_ID: case WRONG_CHIP_ID:
ESP_LOGE(TAG, "BME280 has wrong chip ID! Is it a BME280?"); ESP_LOGE(TAG, BME280_ERROR_WRONG_CHIP_ID);
break; break;
case NONE: case NONE:
default: default:
@@ -223,21 +223,21 @@ void BME280Component::update() {
this->set_timeout("data", uint32_t(ceilf(meas_time)), [this]() { this->set_timeout("data", uint32_t(ceilf(meas_time)), [this]() {
uint8_t data[8]; uint8_t data[8];
if (!this->read_bytes(BME280_REGISTER_MEASUREMENTS, data, 8)) { if (!this->read_bytes(BME280_REGISTER_MEASUREMENTS, data, 8)) {
ESP_LOGW(TAG, "Error reading registers."); ESP_LOGW(TAG, "Error reading registers");
this->status_set_warning(); this->status_set_warning();
return; return;
} }
int32_t t_fine = 0; int32_t t_fine = 0;
float const temperature = this->read_temperature_(data, &t_fine); float const temperature = this->read_temperature_(data, &t_fine);
if (std::isnan(temperature)) { if (std::isnan(temperature)) {
ESP_LOGW(TAG, "Invalid temperature, cannot read pressure & humidity values."); ESP_LOGW(TAG, "Invalid temperature");
this->status_set_warning(); this->status_set_warning();
return; return;
} }
float const pressure = this->read_pressure_(data, t_fine); float const pressure = this->read_pressure_(data, t_fine);
float const humidity = this->read_humidity_(data, t_fine); float const humidity = this->read_humidity_(data, t_fine);
ESP_LOGV(TAG, "Got temperature=%.1f°C pressure=%.1fhPa humidity=%.1f%%", temperature, pressure, humidity); ESP_LOGV(TAG, "Temperature=%.1f°C Pressure=%.1fhPa Humidity=%.1f%%", temperature, pressure, humidity);
if (this->temperature_sensor_ != nullptr) if (this->temperature_sensor_ != nullptr)
this->temperature_sensor_->publish_state(temperature); this->temperature_sensor_->publish_state(temperature);
if (this->pressure_sensor_ != nullptr) if (this->pressure_sensor_ != nullptr)

View File

@@ -28,7 +28,7 @@ const float BME680_GAS_LOOKUP_TABLE_1[16] PROGMEM = {0.0, 0.0, 0.0, 0.0, 0.0,
const float BME680_GAS_LOOKUP_TABLE_2[16] PROGMEM = {0.0, 0.0, 0.0, 0.0, 0.1, 0.7, 0.0, -0.8, const float BME680_GAS_LOOKUP_TABLE_2[16] PROGMEM = {0.0, 0.0, 0.0, 0.0, 0.1, 0.7, 0.0, -0.8,
-0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; -0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0};
static const char *oversampling_to_str(BME680Oversampling oversampling) { [[maybe_unused]] static const char *oversampling_to_str(BME680Oversampling oversampling) {
switch (oversampling) { switch (oversampling) {
case BME680_OVERSAMPLING_NONE: case BME680_OVERSAMPLING_NONE:
return "None"; return "None";
@@ -47,7 +47,7 @@ static const char *oversampling_to_str(BME680Oversampling oversampling) {
} }
} }
static const char *iir_filter_to_str(BME680IIRFilter filter) { [[maybe_unused]] static const char *iir_filter_to_str(BME680IIRFilter filter) {
switch (filter) { switch (filter) {
case BME680_IIR_FILTER_OFF: case BME680_IIR_FILTER_OFF:
return "OFF"; return "OFF";

View File

@@ -2,6 +2,8 @@
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#define BMP280_ERROR_WRONG_CHIP_ID "Wrong chip ID"
namespace esphome { namespace esphome {
namespace bmp280_base { namespace bmp280_base {
@@ -63,23 +65,23 @@ void BMP280Component::setup() {
// https://community.st.com/t5/stm32-mcus-products/issue-with-reading-bmp280-chip-id-using-spi/td-p/691855 // https://community.st.com/t5/stm32-mcus-products/issue-with-reading-bmp280-chip-id-using-spi/td-p/691855
if (!this->read_byte(0xD0, &chip_id)) { if (!this->read_byte(0xD0, &chip_id)) {
this->error_code_ = COMMUNICATION_FAILED; this->error_code_ = COMMUNICATION_FAILED;
this->mark_failed(); this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
return; return;
} }
if (!this->read_byte(0xD0, &chip_id)) { if (!this->read_byte(0xD0, &chip_id)) {
this->error_code_ = COMMUNICATION_FAILED; this->error_code_ = COMMUNICATION_FAILED;
this->mark_failed(); this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
return; return;
} }
if (chip_id != 0x58) { if (chip_id != 0x58) {
this->error_code_ = WRONG_CHIP_ID; this->error_code_ = WRONG_CHIP_ID;
this->mark_failed(); this->mark_failed(BMP280_ERROR_WRONG_CHIP_ID);
return; return;
} }
// Send a soft reset. // Send a soft reset.
if (!this->write_byte(BMP280_REGISTER_RESET, BMP280_SOFT_RESET)) { if (!this->write_byte(BMP280_REGISTER_RESET, BMP280_SOFT_RESET)) {
this->mark_failed(); this->mark_failed("Reset failed");
return; return;
} }
// Wait until the NVM data has finished loading. // Wait until the NVM data has finished loading.
@@ -88,14 +90,12 @@ void BMP280Component::setup() {
do { do {
delay(2); delay(2);
if (!this->read_byte(BMP280_REGISTER_STATUS, &status)) { if (!this->read_byte(BMP280_REGISTER_STATUS, &status)) {
ESP_LOGW(TAG, "Error reading status register."); this->mark_failed("Error reading status register");
this->mark_failed();
return; return;
} }
} while ((status & BMP280_STATUS_IM_UPDATE) && (--retry)); } while ((status & BMP280_STATUS_IM_UPDATE) && (--retry));
if (status & BMP280_STATUS_IM_UPDATE) { if (status & BMP280_STATUS_IM_UPDATE) {
ESP_LOGW(TAG, "Timeout loading NVM."); this->mark_failed("Timeout loading NVM");
this->mark_failed();
return; return;
} }
@@ -116,14 +116,14 @@ void BMP280Component::setup() {
uint8_t config_register = 0; uint8_t config_register = 0;
if (!this->read_byte(BMP280_REGISTER_CONFIG, &config_register)) { if (!this->read_byte(BMP280_REGISTER_CONFIG, &config_register)) {
this->mark_failed(); this->mark_failed("Read config");
return; return;
} }
config_register &= ~0b11111100; config_register &= ~0b11111100;
config_register |= 0b000 << 5; // 0.5 ms standby time config_register |= 0b000 << 5; // 0.5 ms standby time
config_register |= (this->iir_filter_ & 0b111) << 2; config_register |= (this->iir_filter_ & 0b111) << 2;
if (!this->write_byte(BMP280_REGISTER_CONFIG, config_register)) { if (!this->write_byte(BMP280_REGISTER_CONFIG, config_register)) {
this->mark_failed(); this->mark_failed("Write config");
return; return;
} }
} }
@@ -134,7 +134,7 @@ void BMP280Component::dump_config() {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
break; break;
case WRONG_CHIP_ID: case WRONG_CHIP_ID:
ESP_LOGE(TAG, "BMP280 has wrong chip ID! Is it a BME280?"); ESP_LOGE(TAG, BMP280_ERROR_WRONG_CHIP_ID);
break; break;
case NONE: case NONE:
default: default:
@@ -172,13 +172,13 @@ void BMP280Component::update() {
int32_t t_fine = 0; int32_t t_fine = 0;
float temperature = this->read_temperature_(&t_fine); float temperature = this->read_temperature_(&t_fine);
if (std::isnan(temperature)) { if (std::isnan(temperature)) {
ESP_LOGW(TAG, "Invalid temperature, cannot read pressure values."); ESP_LOGW(TAG, "Invalid temperature");
this->status_set_warning(); this->status_set_warning();
return; return;
} }
float pressure = this->read_pressure_(t_fine); float pressure = this->read_pressure_(t_fine);
ESP_LOGD(TAG, "Got temperature=%.1f°C pressure=%.1fhPa", temperature, pressure); ESP_LOGV(TAG, "Temperature=%.1f°C Pressure=%.1fhPa", temperature, pressure);
if (this->temperature_sensor_ != nullptr) if (this->temperature_sensor_ != nullptr)
this->temperature_sensor_->publish_state(temperature); this->temperature_sensor_->publish_state(temperature);
if (this->pressure_sensor_ != nullptr) if (this->pressure_sensor_ != nullptr)

View File

@@ -137,4 +137,3 @@ async def button_press_to_code(config, action_id, template_arg, args):
@coroutine_with_priority(100.0) @coroutine_with_priority(100.0)
async def to_code(config): async def to_code(config):
cg.add_global(button_ns.using) cg.add_global(button_ns.using)
cg.add_define("USE_BUTTON")

View File

@@ -14,7 +14,7 @@ from esphome.core import CORE, coroutine_with_priority
AUTO_LOAD = ["web_server_base", "ota.web_server"] AUTO_LOAD = ["web_server_base", "ota.web_server"]
DEPENDENCIES = ["wifi"] DEPENDENCIES = ["wifi"]
CODEOWNERS = ["@OttoWinter"] CODEOWNERS = ["@esphome/core"]
captive_portal_ns = cg.esphome_ns.namespace("captive_portal") captive_portal_ns = cg.esphome_ns.namespace("captive_portal")
CaptivePortal = captive_portal_ns.class_("CaptivePortal", cg.Component) CaptivePortal = captive_portal_ns.class_("CaptivePortal", cg.Component)

View File

@@ -519,5 +519,4 @@ async def climate_control_to_code(config, action_id, template_arg, args):
@coroutine_with_priority(100.0) @coroutine_with_priority(100.0)
async def to_code(config): async def to_code(config):
cg.add_define("USE_CLIMATE")
cg.add_global(climate_ns.using) cg.add_global(climate_ns.using)

View File

@@ -5,6 +5,13 @@
#include <set> #include <set>
namespace esphome { namespace esphome {
#ifdef USE_API
namespace api {
class APIConnection;
} // namespace api
#endif
namespace climate { namespace climate {
/** This class contains all static data for climate devices. /** This class contains all static data for climate devices.
@@ -173,6 +180,23 @@ class ClimateTraits {
void set_visual_max_humidity(float visual_max_humidity) { this->visual_max_humidity_ = visual_max_humidity; } void set_visual_max_humidity(float visual_max_humidity) { this->visual_max_humidity_ = visual_max_humidity; }
protected: protected:
#ifdef USE_API
// The API connection is a friend class to access internal methods
friend class api::APIConnection;
// These methods return references to internal data structures.
// They are used by the API to avoid copying data when encoding messages.
// Warning: Do not use these methods outside of the API connection code.
// They return references to internal data that can be invalidated.
const std::set<ClimateMode> &get_supported_modes_for_api_() const { return this->supported_modes_; }
const std::set<ClimateFanMode> &get_supported_fan_modes_for_api_() const { return this->supported_fan_modes_; }
const std::set<std::string> &get_supported_custom_fan_modes_for_api_() const {
return this->supported_custom_fan_modes_;
}
const std::set<climate::ClimatePreset> &get_supported_presets_for_api_() const { return this->supported_presets_; }
const std::set<std::string> &get_supported_custom_presets_for_api_() const { return this->supported_custom_presets_; }
const std::set<ClimateSwingMode> &get_supported_swing_modes_for_api_() const { return this->supported_swing_modes_; }
#endif
void set_mode_support_(climate::ClimateMode mode, bool supported) { void set_mode_support_(climate::ClimateMode mode, bool supported) {
if (supported) { if (supported) {
this->supported_modes_.insert(mode); this->supported_modes_.insert(mode);

View File

@@ -265,5 +265,4 @@ async def cover_control_to_code(config, action_id, template_arg, args):
@coroutine_with_priority(100.0) @coroutine_with_priority(100.0)
async def to_code(config): async def to_code(config):
cg.add_define("USE_COVER")
cg.add_global(cover_ns.using) cg.add_global(cover_ns.using)

View File

@@ -99,43 +99,39 @@ const optional<float> &CoverCall::get_tilt() const { return this->tilt_; }
const optional<bool> &CoverCall::get_toggle() const { return this->toggle_; } const optional<bool> &CoverCall::get_toggle() const { return this->toggle_; }
void CoverCall::validate_() { void CoverCall::validate_() {
auto traits = this->parent_->get_traits(); auto traits = this->parent_->get_traits();
const char *name = this->parent_->get_name().c_str();
if (this->position_.has_value()) { if (this->position_.has_value()) {
auto pos = *this->position_; auto pos = *this->position_;
if (!traits.get_supports_position() && pos != COVER_OPEN && pos != COVER_CLOSED) { if (!traits.get_supports_position() && pos != COVER_OPEN && pos != COVER_CLOSED) {
ESP_LOGW(TAG, "'%s' - This cover device does not support setting position!", this->parent_->get_name().c_str()); ESP_LOGW(TAG, "'%s': position unsupported", name);
this->position_.reset(); this->position_.reset();
} else if (pos < 0.0f || pos > 1.0f) { } else if (pos < 0.0f || pos > 1.0f) {
ESP_LOGW(TAG, "'%s' - Position %.2f is out of range [0.0 - 1.0]", this->parent_->get_name().c_str(), pos); ESP_LOGW(TAG, "'%s': position %.2f out of range", name, pos);
this->position_ = clamp(pos, 0.0f, 1.0f); this->position_ = clamp(pos, 0.0f, 1.0f);
} }
} }
if (this->tilt_.has_value()) { if (this->tilt_.has_value()) {
auto tilt = *this->tilt_; auto tilt = *this->tilt_;
if (!traits.get_supports_tilt()) { if (!traits.get_supports_tilt()) {
ESP_LOGW(TAG, "'%s' - This cover device does not support tilt!", this->parent_->get_name().c_str()); ESP_LOGW(TAG, "'%s': tilt unsupported", name);
this->tilt_.reset(); this->tilt_.reset();
} else if (tilt < 0.0f || tilt > 1.0f) { } else if (tilt < 0.0f || tilt > 1.0f) {
ESP_LOGW(TAG, "'%s' - Tilt %.2f is out of range [0.0 - 1.0]", this->parent_->get_name().c_str(), tilt); ESP_LOGW(TAG, "'%s': tilt %.2f out of range", name, tilt);
this->tilt_ = clamp(tilt, 0.0f, 1.0f); this->tilt_ = clamp(tilt, 0.0f, 1.0f);
} }
} }
if (this->toggle_.has_value()) { if (this->toggle_.has_value()) {
if (!traits.get_supports_toggle()) { if (!traits.get_supports_toggle()) {
ESP_LOGW(TAG, "'%s' - This cover device does not support toggle!", this->parent_->get_name().c_str()); ESP_LOGW(TAG, "'%s': toggle unsupported", name);
this->toggle_.reset(); this->toggle_.reset();
} }
} }
if (this->stop_) { if (this->stop_) {
if (this->position_.has_value()) { if (this->position_.has_value() || this->tilt_.has_value() || this->toggle_.has_value()) {
ESP_LOGW(TAG, "Cannot set position when stopping a cover!"); ESP_LOGW(TAG, "'%s': cannot position/tilt/toggle when stopping", name);
this->position_.reset(); this->position_.reset();
}
if (this->tilt_.has_value()) {
ESP_LOGW(TAG, "Cannot set tilt when stopping a cover!");
this->tilt_.reset(); this->tilt_.reset();
}
if (this->toggle_.has_value()) {
ESP_LOGW(TAG, "Cannot set toggle when stopping a cover!");
this->toggle_.reset(); this->toggle_.reset();
} }
} }

View File

@@ -164,7 +164,6 @@ async def register_datetime(var, config):
cg.add(getattr(cg.App, f"register_{entity_type}")(var)) cg.add(getattr(cg.App, f"register_{entity_type}")(var))
CORE.register_platform_component(entity_type, var) CORE.register_platform_component(entity_type, var)
await setup_datetime_core_(var, config) await setup_datetime_core_(var, config)
cg.add_define(f"USE_DATETIME_{config[CONF_TYPE]}")
async def new_datetime(config, *args): async def new_datetime(config, *args):
@@ -175,7 +174,6 @@ async def new_datetime(config, *args):
@coroutine_with_priority(100.0) @coroutine_with_priority(100.0)
async def to_code(config): async def to_code(config):
cg.add_define("USE_DATETIME")
cg.add_global(datetime_ns.using) cg.add_global(datetime_ns.using)

View File

@@ -1,4 +1,5 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components.zephyr import zephyr_add_prj_conf
from esphome.config_helpers import filter_source_files_from_platform from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
@@ -10,8 +11,9 @@ from esphome.const import (
CONF_LOOP_TIME, CONF_LOOP_TIME,
PlatformFramework, PlatformFramework,
) )
from esphome.core import CORE
CODEOWNERS = ["@OttoWinter"] CODEOWNERS = ["@esphome/core"]
DEPENDENCIES = ["logger"] DEPENDENCIES = ["logger"]
CONF_DEBUG_ID = "debug_id" CONF_DEBUG_ID = "debug_id"
@@ -44,6 +46,17 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config): async def to_code(config):
if CORE.using_zephyr:
zephyr_add_prj_conf("HWINFO", True)
# gdb thread support
zephyr_add_prj_conf("DEBUG_THREAD_INFO", True)
# RTT
zephyr_add_prj_conf("USE_SEGGER_RTT", True)
zephyr_add_prj_conf("RTT_CONSOLE", True)
zephyr_add_prj_conf("LOG", True)
zephyr_add_prj_conf("LOG_BLOCK_IN_THREAD", True)
zephyr_add_prj_conf("LOG_BUFFER_SIZE", 4096)
zephyr_add_prj_conf("SEGGER_RTT_MODE_BLOCK_IF_FIFO_FULL", True)
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)
@@ -62,5 +75,6 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
PlatformFramework.RTL87XX_ARDUINO, PlatformFramework.RTL87XX_ARDUINO,
PlatformFramework.LN882X_ARDUINO, PlatformFramework.LN882X_ARDUINO,
}, },
"debug_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR},
} }
) )

View File

@@ -0,0 +1,281 @@
#include "debug_component.h"
#ifdef USE_ZEPHYR
#include <climits>
#include "esphome/core/log.h"
#include <zephyr/drivers/hwinfo.h>
#include <hal/nrf_power.h>
#include <cstdint>
#define BOOTLOADER_VERSION_REGISTER NRF_TIMER2->CC[0]
namespace esphome {
namespace debug {
static const char *const TAG = "debug";
constexpr std::uintptr_t MBR_PARAM_PAGE_ADDR = 0xFFC;
constexpr std::uintptr_t MBR_BOOTLOADER_ADDR = 0xFF8;
static void show_reset_reason(std::string &reset_reason, bool set, const char *reason) {
if (!set) {
return;
}
if (!reset_reason.empty()) {
reset_reason += ", ";
}
reset_reason += reason;
}
inline uint32_t read_mem_u32(uintptr_t addr) {
return *reinterpret_cast<volatile uint32_t *>(addr); // NOLINT(performance-no-int-to-ptr)
}
std::string DebugComponent::get_reset_reason_() {
uint32_t cause;
auto ret = hwinfo_get_reset_cause(&cause);
if (ret) {
ESP_LOGE(TAG, "Unable to get reset cause: %d", ret);
return "";
}
std::string reset_reason;
show_reset_reason(reset_reason, cause & RESET_PIN, "External pin");
show_reset_reason(reset_reason, cause & RESET_SOFTWARE, "Software reset");
show_reset_reason(reset_reason, cause & RESET_BROWNOUT, "Brownout (drop in voltage)");
show_reset_reason(reset_reason, cause & RESET_POR, "Power-on reset (POR)");
show_reset_reason(reset_reason, cause & RESET_WATCHDOG, "Watchdog timer expiration");
show_reset_reason(reset_reason, cause & RESET_DEBUG, "Debug event");
show_reset_reason(reset_reason, cause & RESET_SECURITY, "Security violation");
show_reset_reason(reset_reason, cause & RESET_LOW_POWER_WAKE, "Waking up from low power mode");
show_reset_reason(reset_reason, cause & RESET_CPU_LOCKUP, "CPU lock-up detected");
show_reset_reason(reset_reason, cause & RESET_PARITY, "Parity error");
show_reset_reason(reset_reason, cause & RESET_PLL, "PLL error");
show_reset_reason(reset_reason, cause & RESET_CLOCK, "Clock error");
show_reset_reason(reset_reason, cause & RESET_HARDWARE, "Hardware reset");
show_reset_reason(reset_reason, cause & RESET_USER, "User reset");
show_reset_reason(reset_reason, cause & RESET_TEMPERATURE, "Temperature reset");
ESP_LOGD(TAG, "Reset Reason: %s", reset_reason.c_str());
return reset_reason;
}
uint32_t DebugComponent::get_free_heap_() { return INT_MAX; }
void DebugComponent::get_device_info_(std::string &device_info) {
std::string supply = "Main supply status: ";
if (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_NORMAL) {
supply += "Normal voltage.";
} else {
supply += "High voltage.";
}
ESP_LOGD(TAG, "%s", supply.c_str());
device_info += "|" + supply;
std::string reg0 = "Regulator stage 0: ";
if (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_HIGH) {
reg0 += nrf_power_dcdcen_vddh_get(NRF_POWER) ? "DC/DC" : "LDO";
reg0 += ", ";
switch (NRF_UICR->REGOUT0 & UICR_REGOUT0_VOUT_Msk) {
case (UICR_REGOUT0_VOUT_DEFAULT << UICR_REGOUT0_VOUT_Pos):
reg0 += "1.8V (default)";
break;
case (UICR_REGOUT0_VOUT_1V8 << UICR_REGOUT0_VOUT_Pos):
reg0 += "1.8V";
break;
case (UICR_REGOUT0_VOUT_2V1 << UICR_REGOUT0_VOUT_Pos):
reg0 += "2.1V";
break;
case (UICR_REGOUT0_VOUT_2V4 << UICR_REGOUT0_VOUT_Pos):
reg0 += "2.4V";
break;
case (UICR_REGOUT0_VOUT_2V7 << UICR_REGOUT0_VOUT_Pos):
reg0 += "2.7V";
break;
case (UICR_REGOUT0_VOUT_3V0 << UICR_REGOUT0_VOUT_Pos):
reg0 += "3.0V";
break;
case (UICR_REGOUT0_VOUT_3V3 << UICR_REGOUT0_VOUT_Pos):
reg0 += "3.3V";
break;
default:
reg0 += "???V";
}
} else {
reg0 += "disabled";
}
ESP_LOGD(TAG, "%s", reg0.c_str());
device_info += "|" + reg0;
std::string reg1 = "Regulator stage 1: ";
reg1 += nrf_power_dcdcen_get(NRF_POWER) ? "DC/DC" : "LDO";
ESP_LOGD(TAG, "%s", reg1.c_str());
device_info += "|" + reg1;
std::string usb_power = "USB power state: ";
if (nrf_power_usbregstatus_vbusdet_get(NRF_POWER)) {
if (nrf_power_usbregstatus_outrdy_get(NRF_POWER)) {
/**< From the power viewpoint, USB is ready for working. */
usb_power += "ready";
} else {
/**< The USB power is detected, but USB power regulator is not ready. */
usb_power += "connected (regulator is not ready)";
}
} else {
/**< No power on USB lines detected. */
usb_power += "disconected";
}
ESP_LOGD(TAG, "%s", usb_power.c_str());
device_info += "|" + usb_power;
bool enabled;
nrf_power_pof_thr_t pof_thr;
pof_thr = nrf_power_pofcon_get(NRF_POWER, &enabled);
std::string pof = "Power-fail comparator: ";
if (enabled) {
switch (pof_thr) {
case POWER_POFCON_THRESHOLD_V17:
pof += "1.7V";
break;
case POWER_POFCON_THRESHOLD_V18:
pof += "1.8V";
break;
case POWER_POFCON_THRESHOLD_V19:
pof += "1.9V";
break;
case POWER_POFCON_THRESHOLD_V20:
pof += "2.0V";
break;
case POWER_POFCON_THRESHOLD_V21:
pof += "2.1V";
break;
case POWER_POFCON_THRESHOLD_V22:
pof += "2.2V";
break;
case POWER_POFCON_THRESHOLD_V23:
pof += "2.3V";
break;
case POWER_POFCON_THRESHOLD_V24:
pof += "2.4V";
break;
case POWER_POFCON_THRESHOLD_V25:
pof += "2.5V";
break;
case POWER_POFCON_THRESHOLD_V26:
pof += "2.6V";
break;
case POWER_POFCON_THRESHOLD_V27:
pof += "2.7V";
break;
case POWER_POFCON_THRESHOLD_V28:
pof += "2.8V";
break;
}
if (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_HIGH) {
pof += ", VDDH: ";
switch (nrf_power_pofcon_vddh_get(NRF_POWER)) {
case NRF_POWER_POFTHRVDDH_V27:
pof += "2.7V";
break;
case NRF_POWER_POFTHRVDDH_V28:
pof += "2.8V";
break;
case NRF_POWER_POFTHRVDDH_V29:
pof += "2.9V";
break;
case NRF_POWER_POFTHRVDDH_V30:
pof += "3.0V";
break;
case NRF_POWER_POFTHRVDDH_V31:
pof += "3.1V";
break;
case NRF_POWER_POFTHRVDDH_V32:
pof += "3.2V";
break;
case NRF_POWER_POFTHRVDDH_V33:
pof += "3.3V";
break;
case NRF_POWER_POFTHRVDDH_V34:
pof += "3.4V";
break;
case NRF_POWER_POFTHRVDDH_V35:
pof += "3.5V";
break;
case NRF_POWER_POFTHRVDDH_V36:
pof += "3.6V";
break;
case NRF_POWER_POFTHRVDDH_V37:
pof += "3.7V";
break;
case NRF_POWER_POFTHRVDDH_V38:
pof += "3.8V";
break;
case NRF_POWER_POFTHRVDDH_V39:
pof += "3.9V";
break;
case NRF_POWER_POFTHRVDDH_V40:
pof += "4.0V";
break;
case NRF_POWER_POFTHRVDDH_V41:
pof += "4.1V";
break;
case NRF_POWER_POFTHRVDDH_V42:
pof += "4.2V";
break;
}
}
} else {
pof += "disabled";
}
ESP_LOGD(TAG, "%s", pof.c_str());
device_info += "|" + pof;
auto package = [](uint32_t value) {
switch (value) {
case 0x2004:
return "QIxx - 7x7 73-pin aQFN";
case 0x2000:
return "QFxx - 6x6 48-pin QFN";
case 0x2005:
return "CKxx - 3.544 x 3.607 WLCSP";
}
return "Unspecified";
};
ESP_LOGD(TAG, "Code page size: %u, code size: %u, device id: 0x%08x%08x", NRF_FICR->CODEPAGESIZE, NRF_FICR->CODESIZE,
NRF_FICR->DEVICEID[1], NRF_FICR->DEVICEID[0]);
ESP_LOGD(TAG, "Encryption root: 0x%08x%08x%08x%08x, Identity Root: 0x%08x%08x%08x%08x", NRF_FICR->ER[0],
NRF_FICR->ER[1], NRF_FICR->ER[2], NRF_FICR->ER[3], NRF_FICR->IR[0], NRF_FICR->IR[1], NRF_FICR->IR[2],
NRF_FICR->IR[3]);
ESP_LOGD(TAG, "Device address type: %s, address: %s", (NRF_FICR->DEVICEADDRTYPE & 0x1 ? "Random" : "Public"),
get_mac_address_pretty().c_str());
ESP_LOGD(TAG, "Part code: nRF%x, version: %c%c%c%c, package: %s", NRF_FICR->INFO.PART,
NRF_FICR->INFO.VARIANT >> 24 & 0xFF, NRF_FICR->INFO.VARIANT >> 16 & 0xFF, NRF_FICR->INFO.VARIANT >> 8 & 0xFF,
NRF_FICR->INFO.VARIANT & 0xFF, package(NRF_FICR->INFO.PACKAGE));
ESP_LOGD(TAG, "RAM: %ukB, Flash: %ukB, production test: %sdone", NRF_FICR->INFO.RAM, NRF_FICR->INFO.FLASH,
(NRF_FICR->PRODTEST[0] == 0xBB42319F ? "" : "not "));
ESP_LOGD(
TAG, "GPIO as NFC pins: %s, GPIO as nRESET pin: %s",
YESNO((NRF_UICR->NFCPINS & UICR_NFCPINS_PROTECT_Msk) == (UICR_NFCPINS_PROTECT_NFC << UICR_NFCPINS_PROTECT_Pos)),
YESNO(((NRF_UICR->PSELRESET[0] & UICR_PSELRESET_CONNECT_Msk) !=
(UICR_PSELRESET_CONNECT_Connected << UICR_PSELRESET_CONNECT_Pos)) ||
((NRF_UICR->PSELRESET[1] & UICR_PSELRESET_CONNECT_Msk) !=
(UICR_PSELRESET_CONNECT_Connected << UICR_PSELRESET_CONNECT_Pos))));
#ifdef USE_BOOTLOADER_MCUBOOT
ESP_LOGD(TAG, "bootloader: mcuboot");
#else
ESP_LOGD(TAG, "bootloader: Adafruit, version %u.%u.%u", (BOOTLOADER_VERSION_REGISTER >> 16) & 0xFF,
(BOOTLOADER_VERSION_REGISTER >> 8) & 0xFF, BOOTLOADER_VERSION_REGISTER & 0xFF);
ESP_LOGD(TAG, "MBR bootloader addr 0x%08x, UICR bootloader addr 0x%08x", read_mem_u32(MBR_BOOTLOADER_ADDR),
NRF_UICR->NRFFW[0]);
ESP_LOGD(TAG, "MBR param page addr 0x%08x, UICR param page addr 0x%08x", read_mem_u32(MBR_PARAM_PAGE_ADDR),
NRF_UICR->NRFFW[1]);
#endif
}
void DebugComponent::update_platform_() {}
} // namespace debug
} // namespace esphome
#endif

View File

@@ -1,6 +1,7 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import sensor from esphome.components import sensor
from esphome.components.esp32 import CONF_CPU_FREQUENCY from esphome.components.esp32 import CONF_CPU_FREQUENCY
from esphome.components.psram import DOMAIN as PSRAM_DOMAIN
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_BLOCK, CONF_BLOCK,
@@ -54,7 +55,7 @@ CONFIG_SCHEMA = {
), ),
cv.Optional(CONF_PSRAM): cv.All( cv.Optional(CONF_PSRAM): cv.All(
cv.only_on_esp32, cv.only_on_esp32,
cv.requires_component("psram"), cv.requires_component(PSRAM_DOMAIN),
sensor.sensor_schema( sensor.sensor_schema(
unit_of_measurement=UNIT_BYTES, unit_of_measurement=UNIT_BYTES,
icon=ICON_COUNTER, icon=ICON_COUNTER,

View File

@@ -1,4 +1,5 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
#include "driver/gpio.h"
#include "deep_sleep_component.h" #include "deep_sleep_component.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
@@ -74,11 +75,20 @@ void DeepSleepComponent::deep_sleep_() {
if (this->sleep_duration_.has_value()) if (this->sleep_duration_.has_value())
esp_sleep_enable_timer_wakeup(*this->sleep_duration_); esp_sleep_enable_timer_wakeup(*this->sleep_duration_);
if (this->wakeup_pin_ != nullptr) { if (this->wakeup_pin_ != nullptr) {
const auto gpio_pin = gpio_num_t(this->wakeup_pin_->get_pin());
if (this->wakeup_pin_->get_flags() & gpio::FLAG_PULLUP) {
gpio_sleep_set_pull_mode(gpio_pin, GPIO_PULLUP_ONLY);
} else if (this->wakeup_pin_->get_flags() & gpio::FLAG_PULLDOWN) {
gpio_sleep_set_pull_mode(gpio_pin, GPIO_PULLDOWN_ONLY);
}
gpio_sleep_set_direction(gpio_pin, GPIO_MODE_INPUT);
gpio_hold_en(gpio_pin);
gpio_deep_sleep_hold_en();
bool level = !this->wakeup_pin_->is_inverted(); bool level = !this->wakeup_pin_->is_inverted();
if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) { if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) {
level = !level; level = !level;
} }
esp_sleep_enable_ext0_wakeup(gpio_num_t(this->wakeup_pin_->get_pin()), level); esp_sleep_enable_ext0_wakeup(gpio_pin, level);
} }
if (this->ext1_wakeup_.has_value()) { if (this->ext1_wakeup_.has_value()) {
esp_sleep_enable_ext1_wakeup(this->ext1_wakeup_->mask, this->ext1_wakeup_->wakeup_mode); esp_sleep_enable_ext1_wakeup(this->ext1_wakeup_->mask, this->ext1_wakeup_->wakeup_mode);
@@ -102,6 +112,15 @@ void DeepSleepComponent::deep_sleep_() {
if (this->sleep_duration_.has_value()) if (this->sleep_duration_.has_value())
esp_sleep_enable_timer_wakeup(*this->sleep_duration_); esp_sleep_enable_timer_wakeup(*this->sleep_duration_);
if (this->wakeup_pin_ != nullptr) { if (this->wakeup_pin_ != nullptr) {
const auto gpio_pin = gpio_num_t(this->wakeup_pin_->get_pin());
if (this->wakeup_pin_->get_flags() && gpio::FLAG_PULLUP) {
gpio_sleep_set_pull_mode(gpio_pin, GPIO_PULLUP_ONLY);
} else if (this->wakeup_pin_->get_flags() && gpio::FLAG_PULLDOWN) {
gpio_sleep_set_pull_mode(gpio_pin, GPIO_PULLDOWN_ONLY);
}
gpio_sleep_set_direction(gpio_pin, GPIO_MODE_INPUT);
gpio_hold_en(gpio_pin);
gpio_deep_sleep_hold_en();
bool level = !this->wakeup_pin_->is_inverted(); bool level = !this->wakeup_pin_->is_inverted();
if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) { if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) {
level = !level; level = !level;

View File

@@ -12,6 +12,8 @@ from esphome.const import (
CONF_ROTATION, CONF_ROTATION,
CONF_TO, CONF_TO,
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
CONF_UPDATE_INTERVAL,
SCHEDULER_DONT_RUN,
) )
from esphome.core import coroutine_with_priority from esphome.core import coroutine_with_priority
@@ -67,6 +69,18 @@ BASIC_DISPLAY_SCHEMA = cv.Schema(
} }
).extend(cv.polling_component_schema("1s")) ).extend(cv.polling_component_schema("1s"))
def _validate_test_card(config):
if (
config.get(CONF_SHOW_TEST_CARD, False)
and config.get(CONF_UPDATE_INTERVAL, False) == SCHEDULER_DONT_RUN
):
raise cv.Invalid(
f"`{CONF_SHOW_TEST_CARD}: True` cannot be used with `{CONF_UPDATE_INTERVAL}: never` because this combination will not show a test_card."
)
return config
FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend( FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend(
{ {
cv.Optional(CONF_ROTATION): validate_rotation, cv.Optional(CONF_ROTATION): validate_rotation,
@@ -94,6 +108,7 @@ FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend(
cv.Optional(CONF_SHOW_TEST_CARD): cv.boolean, cv.Optional(CONF_SHOW_TEST_CARD): cv.boolean,
} }
) )
FULL_DISPLAY_SCHEMA.add_extra(_validate_test_card)
async def setup_display_core_(var, config): async def setup_display_core_(var, config):
@@ -200,7 +215,6 @@ async def display_is_displaying_page_to_code(config, condition_id, template_arg,
page = await cg.get_variable(config[CONF_PAGE_ID]) page = await cg.get_variable(config[CONF_PAGE_ID])
var = cg.new_Pvariable(condition_id, template_arg, paren) var = cg.new_Pvariable(condition_id, template_arg, paren)
cg.add(var.set_page(page)) cg.add(var.set_page(page))
return var return var

View File

@@ -15,6 +15,7 @@ from esphome.const import (
CONF_FRAMEWORK, CONF_FRAMEWORK,
CONF_IGNORE_EFUSE_CUSTOM_MAC, CONF_IGNORE_EFUSE_CUSTOM_MAC,
CONF_IGNORE_EFUSE_MAC_CRC, CONF_IGNORE_EFUSE_MAC_CRC,
CONF_LOG_LEVEL,
CONF_NAME, CONF_NAME,
CONF_PATH, CONF_PATH,
CONF_PLATFORM_VERSION, CONF_PLATFORM_VERSION,
@@ -76,8 +77,18 @@ CONF_ASSERTION_LEVEL = "assertion_level"
CONF_COMPILER_OPTIMIZATION = "compiler_optimization" CONF_COMPILER_OPTIMIZATION = "compiler_optimization"
CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES = "enable_idf_experimental_features" CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES = "enable_idf_experimental_features"
CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert" CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert"
CONF_EXECUTE_FROM_PSRAM = "execute_from_psram"
CONF_RELEASE = "release" CONF_RELEASE = "release"
LOG_LEVELS_IDF = [
"NONE",
"ERROR",
"WARN",
"INFO",
"DEBUG",
"VERBOSE",
]
ASSERTION_LEVELS = { ASSERTION_LEVELS = {
"DISABLE": "CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_DISABLE", "DISABLE": "CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_DISABLE",
"ENABLE": "CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_ENABLE", "ENABLE": "CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_ENABLE",
@@ -313,7 +324,7 @@ def _format_framework_espidf_version(
RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 2, 1) RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 2, 1)
# The platform-espressif32 version to use for arduino frameworks # The platform-espressif32 version to use for arduino frameworks
# - https://github.com/pioarduino/platform-espressif32/releases # - https://github.com/pioarduino/platform-espressif32/releases
ARDUINO_PLATFORM_VERSION = cv.Version(54, 3, 21) ARDUINO_PLATFORM_VERSION = cv.Version(54, 3, 21, "2")
# 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
@@ -322,7 +333,7 @@ RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(5, 4, 2)
# The platformio/espressif32 version to use for esp-idf frameworks # The platformio/espressif32 version to use for esp-idf frameworks
# - https://github.com/platformio/platform-espressif32/releases # - https://github.com/platformio/platform-espressif32/releases
# - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32 # - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32
ESP_IDF_PLATFORM_VERSION = cv.Version(54, 3, 21) ESP_IDF_PLATFORM_VERSION = cv.Version(54, 3, 21, "2")
# List based on https://registry.platformio.org/tools/platformio/framework-espidf/versions # List based on https://registry.platformio.org/tools/platformio/framework-espidf/versions
SUPPORTED_PLATFORMIO_ESP_IDF_5X = [ SUPPORTED_PLATFORMIO_ESP_IDF_5X = [
@@ -468,10 +479,10 @@ def _parse_platform_version(value):
try: try:
ver = cv.Version.parse(cv.version_number(value)) ver = cv.Version.parse(cv.version_number(value))
if ver.major >= 50: # a pioarduino version if ver.major >= 50: # a pioarduino version
if "-" in value: release = f"{ver.major}.{ver.minor:02d}.{ver.patch:02d}"
# maybe a release candidate?...definitely not our default, just use it as-is... if ver.extra:
return f"https://github.com/pioarduino/platform-espressif32/releases/download/{value}/platform-espressif32.zip" release += f"-{ver.extra}"
return f"https://github.com/pioarduino/platform-espressif32/releases/download/{ver.major}.{ver.minor:02d}.{ver.patch:02d}/platform-espressif32.zip" return f"https://github.com/pioarduino/platform-espressif32/releases/download/{release}/platform-espressif32.zip"
# if platform version is a valid version constraint, prefix the default package # if platform version is a valid version constraint, prefix the default package
cv.platformio_version_constraint(value) cv.platformio_version_constraint(value)
return f"platformio/espressif32@{value}" return f"platformio/espressif32@{value}"
@@ -519,32 +530,59 @@ def _detect_variant(value):
def final_validate(config): def final_validate(config):
if not ( # Imported locally to avoid circular import issues
pio_options := fv.full_config.get()[CONF_ESPHOME].get(CONF_PLATFORMIO_OPTIONS) from esphome.components.psram import DOMAIN as PSRAM_DOMAIN
):
# Not specified or empty
return config
pio_flash_size_key = "board_upload.flash_size"
pio_partitions_key = "board_build.partitions"
if CONF_PARTITIONS in config and pio_partitions_key in pio_options:
raise cv.Invalid(
f"Do not specify '{pio_partitions_key}' in '{CONF_PLATFORMIO_OPTIONS}' with '{CONF_PARTITIONS}' in esp32"
)
if pio_flash_size_key in pio_options:
raise cv.Invalid(
f"Please specify {CONF_FLASH_SIZE} within esp32 configuration only"
)
errs = []
full_config = fv.full_config.get()
if pio_options := full_config[CONF_ESPHOME].get(CONF_PLATFORMIO_OPTIONS):
pio_flash_size_key = "board_upload.flash_size"
pio_partitions_key = "board_build.partitions"
if CONF_PARTITIONS in config and pio_partitions_key in pio_options:
errs.append(
cv.Invalid(
f"Do not specify '{pio_partitions_key}' in '{CONF_PLATFORMIO_OPTIONS}' with '{CONF_PARTITIONS}' in esp32"
)
)
if pio_flash_size_key in pio_options:
errs.append(
cv.Invalid(
f"Please specify {CONF_FLASH_SIZE} within esp32 configuration only"
)
)
if ( if (
config[CONF_VARIANT] != VARIANT_ESP32 config[CONF_VARIANT] != VARIANT_ESP32
and CONF_ADVANCED in (conf_fw := config[CONF_FRAMEWORK]) and CONF_ADVANCED in (conf_fw := config[CONF_FRAMEWORK])
and CONF_IGNORE_EFUSE_MAC_CRC in conf_fw[CONF_ADVANCED] and CONF_IGNORE_EFUSE_MAC_CRC in conf_fw[CONF_ADVANCED]
): ):
raise cv.Invalid( errs.append(
f"{CONF_IGNORE_EFUSE_MAC_CRC} is not supported on {config[CONF_VARIANT]}" cv.Invalid(
f"'{CONF_IGNORE_EFUSE_MAC_CRC}' is not supported on {config[CONF_VARIANT]}",
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_IGNORE_EFUSE_MAC_CRC],
)
) )
if (
config.get(CONF_FRAMEWORK, {})
.get(CONF_ADVANCED, {})
.get(CONF_EXECUTE_FROM_PSRAM)
):
if config[CONF_VARIANT] != VARIANT_ESP32S3:
errs.append(
cv.Invalid(
f"'{CONF_EXECUTE_FROM_PSRAM}' is only supported on {VARIANT_ESP32S3} variant",
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_EXECUTE_FROM_PSRAM],
)
)
if PSRAM_DOMAIN not in full_config:
errs.append(
cv.Invalid(
f"'{CONF_EXECUTE_FROM_PSRAM}' requires PSRAM to be configured",
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_EXECUTE_FROM_PSRAM],
)
)
if errs:
raise cv.MultipleInvalid(errs)
return config return config
@@ -571,6 +609,8 @@ CONF_SDKCONFIG_OPTIONS = "sdkconfig_options"
CONF_ENABLE_LWIP_DHCP_SERVER = "enable_lwip_dhcp_server" CONF_ENABLE_LWIP_DHCP_SERVER = "enable_lwip_dhcp_server"
CONF_ENABLE_LWIP_MDNS_QUERIES = "enable_lwip_mdns_queries" CONF_ENABLE_LWIP_MDNS_QUERIES = "enable_lwip_mdns_queries"
CONF_ENABLE_LWIP_BRIDGE_INTERFACE = "enable_lwip_bridge_interface" CONF_ENABLE_LWIP_BRIDGE_INTERFACE = "enable_lwip_bridge_interface"
CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING = "enable_lwip_tcpip_core_locking"
CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY = "enable_lwip_check_thread_safety"
def _validate_idf_component(config: ConfigType) -> ConfigType: def _validate_idf_component(config: ConfigType) -> ConfigType:
@@ -593,6 +633,9 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All(
cv.Optional(CONF_SDKCONFIG_OPTIONS, default={}): { cv.Optional(CONF_SDKCONFIG_OPTIONS, default={}): {
cv.string_strict: cv.string_strict cv.string_strict: cv.string_strict
}, },
cv.Optional(CONF_LOG_LEVEL, default="ERROR"): cv.one_of(
*LOG_LEVELS_IDF, upper=True
),
cv.Optional(CONF_ADVANCED, default={}): cv.Schema( cv.Optional(CONF_ADVANCED, default={}): cv.Schema(
{ {
cv.Optional(CONF_ASSERTION_LEVEL): cv.one_of( cv.Optional(CONF_ASSERTION_LEVEL): cv.one_of(
@@ -619,6 +662,13 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All(
cv.Optional( cv.Optional(
CONF_ENABLE_LWIP_BRIDGE_INTERFACE, default=False CONF_ENABLE_LWIP_BRIDGE_INTERFACE, default=False
): cv.boolean, ): cv.boolean,
cv.Optional(
CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING, default=True
): cv.boolean,
cv.Optional(
CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, default=True
): cv.boolean,
cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean,
} }
), ),
cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list( cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list(
@@ -643,6 +693,64 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All(
) )
class _FrameworkMigrationWarning:
shown = False
def _show_framework_migration_message(name: str, variant: str) -> None:
"""Show a friendly message about framework migration when defaulting to Arduino."""
if _FrameworkMigrationWarning.shown:
return
_FrameworkMigrationWarning.shown = True
from esphome.log import AnsiFore, color
message = (
color(
AnsiFore.BOLD_CYAN,
f"💡 IMPORTANT: {name} doesn't have a framework specified!",
)
+ "\n\n"
+ f"Currently, {variant} defaults to the Arduino framework.\n"
+ color(AnsiFore.YELLOW, "This will change to ESP-IDF in ESPHome 2026.1.0.\n")
+ "\n"
+ "Note: Newer ESP32 variants (C6, H2, P4, etc.) already use ESP-IDF by default.\n"
+ "\n"
+ "Why change? ESP-IDF offers:\n"
+ color(AnsiFore.GREEN, " ✨ Up to 40% smaller binaries\n")
+ color(AnsiFore.GREEN, " 🚀 Better performance and optimization\n")
+ color(AnsiFore.GREEN, " 📦 Custom-built firmware for your exact needs\n")
+ color(
AnsiFore.GREEN,
" 🔧 Active development and testing by ESPHome developers\n",
)
+ "\n"
+ "Trade-offs:\n"
+ color(AnsiFore.YELLOW, " ⏱️ Compile times are ~25% longer\n")
+ color(AnsiFore.YELLOW, " 🔄 Some components need migration\n")
+ "\n"
+ "What should I do?\n"
+ color(AnsiFore.CYAN, " Option 1")
+ ": Migrate to ESP-IDF (recommended)\n"
+ " Add this to your YAML under 'esp32:':\n"
+ color(AnsiFore.WHITE, " framework:\n")
+ color(AnsiFore.WHITE, " type: esp-idf\n")
+ "\n"
+ color(AnsiFore.CYAN, " Option 2")
+ ": Keep using Arduino (still supported)\n"
+ " Add this to your YAML under 'esp32:':\n"
+ color(AnsiFore.WHITE, " framework:\n")
+ color(AnsiFore.WHITE, " type: arduino\n")
+ "\n"
+ "Need help? Check out the migration guide:\n"
+ color(
AnsiFore.BLUE,
"https://esphome.io/guides/esp32_arduino_to_idf.html",
)
)
_LOGGER.warning(message)
def _set_default_framework(config): def _set_default_framework(config):
if CONF_FRAMEWORK not in config: if CONF_FRAMEWORK not in config:
config = config.copy() config = config.copy()
@@ -651,6 +759,10 @@ def _set_default_framework(config):
if variant in ARDUINO_ALLOWED_VARIANTS: if variant in ARDUINO_ALLOWED_VARIANTS:
config[CONF_FRAMEWORK] = ARDUINO_FRAMEWORK_SCHEMA({}) config[CONF_FRAMEWORK] = ARDUINO_FRAMEWORK_SCHEMA({})
config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ARDUINO config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ARDUINO
# Show the migration message
_show_framework_migration_message(
config.get(CONF_NAME, "This device"), variant
)
else: else:
config[CONF_FRAMEWORK] = ESP_IDF_FRAMEWORK_SCHEMA({}) config[CONF_FRAMEWORK] = ESP_IDF_FRAMEWORK_SCHEMA({})
config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ESP_IDF config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ESP_IDF
@@ -784,6 +896,21 @@ async def to_code(config):
add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False) add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False)
if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False): if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False):
add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0) add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0)
if advanced.get(CONF_EXECUTE_FROM_PSRAM, False):
add_idf_sdkconfig_option("CONFIG_SPIRAM_FETCH_INSTRUCTIONS", True)
add_idf_sdkconfig_option("CONFIG_SPIRAM_RODATA", True)
# Apply LWIP core locking for better socket performance
# This is already enabled by default in Arduino framework, where it provides
# significant performance benefits. Our benchmarks show socket operations are
# 24-200% faster with core locking enabled:
# - select() on 4 sockets: ~190μs (Arduino/core locking) vs ~235μs (ESP-IDF default)
# - Up to 200% slower under load when all operations queue through tcpip_thread
# Enabling this makes ESP-IDF socket performance match Arduino framework.
if advanced.get(CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING, True):
add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_CORE_LOCKING", True)
if advanced.get(CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, True):
add_idf_sdkconfig_option("CONFIG_LWIP_CHECK_THREAD_SAFETY", True)
cg.add_platformio_option("board_build.partitions", "partitions.csv") cg.add_platformio_option("board_build.partitions", "partitions.csv")
if CONF_PARTITIONS in config: if CONF_PARTITIONS in config:
@@ -823,6 +950,10 @@ async def to_code(config):
), ),
) )
add_idf_sdkconfig_option(
f"CONFIG_LOG_DEFAULT_LEVEL_{conf[CONF_LOG_LEVEL]}", True
)
for name, value in conf[CONF_SDKCONFIG_OPTIONS].items(): for name, value in conf[CONF_SDKCONFIG_OPTIONS].items():
add_idf_sdkconfig_option(name, RawSdkconfigValue(value)) add_idf_sdkconfig_option(name, RawSdkconfigValue(value))
@@ -872,7 +1003,7 @@ def get_arduino_partition_csv(flash_size):
eeprom_partition_start = app1_partition_start + app_partition_size eeprom_partition_start = app1_partition_start + app_partition_size
spiffs_partition_start = eeprom_partition_start + eeprom_partition_size spiffs_partition_start = eeprom_partition_start + eeprom_partition_size
partition_csv = f"""\ return f"""\
nvs, data, nvs, 0x9000, 0x5000, nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xE000, 0x2000, otadata, data, ota, 0xE000, 0x2000,
app0, app, ota_0, 0x{app0_partition_start:X}, 0x{app_partition_size:X}, app0, app, ota_0, 0x{app0_partition_start:X}, 0x{app_partition_size:X},
@@ -880,20 +1011,18 @@ app1, app, ota_1, 0x{app1_partition_start:X}, 0x{app_partition_size:X},
eeprom, data, 0x99, 0x{eeprom_partition_start:X}, 0x{eeprom_partition_size:X}, eeprom, data, 0x99, 0x{eeprom_partition_start:X}, 0x{eeprom_partition_size:X},
spiffs, data, spiffs, 0x{spiffs_partition_start:X}, 0x{spiffs_partition_size:X} spiffs, data, spiffs, 0x{spiffs_partition_start:X}, 0x{spiffs_partition_size:X}
""" """
return partition_csv
def get_idf_partition_csv(flash_size): def get_idf_partition_csv(flash_size):
app_partition_size = APP_PARTITION_SIZES[flash_size] app_partition_size = APP_PARTITION_SIZES[flash_size]
partition_csv = f"""\ return f"""\
otadata, data, ota, , 0x2000, otadata, data, ota, , 0x2000,
phy_init, data, phy, , 0x1000, phy_init, data, phy, , 0x1000,
app0, app, ota_0, , 0x{app_partition_size:X}, app0, app, ota_0, , 0x{app_partition_size:X},
app1, app, ota_1, , 0x{app_partition_size:X}, app1, app, ota_1, , 0x{app_partition_size:X},
nvs, data, nvs, , 0x6D000, nvs, data, nvs, , 0x6D000,
""" """
return partition_csv
def _format_sdkconfig_val(value: SdkconfigValueType) -> str: def _format_sdkconfig_val(value: SdkconfigValueType) -> str:

View File

@@ -187,8 +187,7 @@ def validate_supports(value):
"Open-drain only works with output mode", [CONF_MODE, CONF_OPEN_DRAIN] "Open-drain only works with output mode", [CONF_MODE, CONF_OPEN_DRAIN]
) )
value = _esp32_validations[variant].usage_validation(value) return _esp32_validations[variant].usage_validation(value)
return value
# https://docs.espressif.com/projects/esp-idf/en/v3.3.5/api-reference/peripherals/gpio.html#_CPPv416gpio_drive_cap_t # https://docs.espressif.com/projects/esp-idf/en/v3.3.5/api-reference/peripherals/gpio.html#_CPPv416gpio_drive_cap_t

View File

@@ -2,6 +2,7 @@ import logging
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
from esphome.pins import check_strapping_pin
_ESP32H2_SPI_FLASH_PINS = {6, 7, 15, 16, 17, 18, 19, 20, 21} _ESP32H2_SPI_FLASH_PINS = {6, 7, 15, 16, 17, 18, 19, 20, 21}
@@ -15,13 +16,6 @@ _LOGGER = logging.getLogger(__name__)
def esp32_h2_validate_gpio_pin(value): def esp32_h2_validate_gpio_pin(value):
if value < 0 or value > 27: if value < 0 or value > 27:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-27)") raise cv.Invalid(f"Invalid pin number: {value} (must be 0-27)")
if value in _ESP32H2_STRAPPING_PINS:
_LOGGER.warning(
"GPIO%d is a Strapping PIN and should be avoided.\n"
"Attaching external pullup/down resistors to strapping pins can cause unexpected failures.\n"
"See https://esphome.io/guides/faq.html#why-am-i-getting-a-warning-about-strapping-pins",
value,
)
if value in _ESP32H2_SPI_FLASH_PINS: if value in _ESP32H2_SPI_FLASH_PINS:
_LOGGER.warning( _LOGGER.warning(
"GPIO%d is reserved for SPI Flash communication on some ESP32-H2 chip variants.\n" "GPIO%d is reserved for SPI Flash communication on some ESP32-H2 chip variants.\n"
@@ -49,4 +43,5 @@ def esp32_h2_validate_supports(value):
if is_input: if is_input:
# All ESP32 pins support input mode # All ESP32 pins support input mode
pass pass
check_strapping_pin(value, _ESP32H2_STRAPPING_PINS, _LOGGER)
return value return value

View File

@@ -2,6 +2,7 @@ import logging
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
from esphome.pins import check_strapping_pin
_ESP32P4_USB_JTAG_PINS = {24, 25} _ESP32P4_USB_JTAG_PINS = {24, 25}
@@ -13,13 +14,6 @@ _LOGGER = logging.getLogger(__name__)
def esp32_p4_validate_gpio_pin(value): def esp32_p4_validate_gpio_pin(value):
if value < 0 or value > 54: if value < 0 or value > 54:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-54)") raise cv.Invalid(f"Invalid pin number: {value} (must be 0-54)")
if value in _ESP32P4_STRAPPING_PINS:
_LOGGER.warning(
"GPIO%d is a Strapping PIN and should be avoided.\n"
"Attaching external pullup/down resistors to strapping pins can cause unexpected failures.\n"
"See https://esphome.io/guides/faq.html#why-am-i-getting-a-warning-about-strapping-pins",
value,
)
if value in _ESP32P4_USB_JTAG_PINS: if value in _ESP32P4_USB_JTAG_PINS:
_LOGGER.warning( _LOGGER.warning(
"GPIO%d is reserved for the USB-Serial-JTAG interface.\n" "GPIO%d is reserved for the USB-Serial-JTAG interface.\n"
@@ -40,4 +34,5 @@ def esp32_p4_validate_supports(value):
if is_input: if is_input:
# All ESP32 pins support input mode # All ESP32 pins support input mode
pass pass
check_strapping_pin(value, _ESP32P4_STRAPPING_PINS, _LOGGER)
return value return value

View File

@@ -1,10 +1,11 @@
Import("env") Import("env") # noqa: F821
import itertools # noqa: E402
import json # noqa: E402
import os # noqa: E402
import pathlib # noqa: E402
import shutil # noqa: E402
import os
import json
import shutil
import pathlib
import itertools
def merge_factory_bin(source, target, env): def merge_factory_bin(source, target, env):
""" """
@@ -25,7 +26,9 @@ def merge_factory_bin(source, target, env):
try: try:
with flasher_args_path.open() as f: with flasher_args_path.open() as f:
flash_data = json.load(f) flash_data = json.load(f)
for addr, fname in sorted(flash_data["flash_files"].items(), key=lambda kv: int(kv[0], 16)): for addr, fname in sorted(
flash_data["flash_files"].items(), key=lambda kv: int(kv[0], 16)
):
file_path = pathlib.Path(fname) file_path = pathlib.Path(fname)
if file_path.exists(): if file_path.exists():
sections.append((addr, str(file_path))) sections.append((addr, str(file_path)))
@@ -40,20 +43,27 @@ def merge_factory_bin(source, target, env):
if flash_images: if flash_images:
print("Using FLASH_EXTRA_IMAGES from PlatformIO environment") print("Using FLASH_EXTRA_IMAGES from PlatformIO environment")
# flatten any nested lists # flatten any nested lists
flat = list(itertools.chain.from_iterable( flat = list(
x if isinstance(x, (list, tuple)) else [x] for x in flash_images itertools.chain.from_iterable(
)) x if isinstance(x, (list, tuple)) else [x] for x in flash_images
)
)
entries = [env.subst(x) for x in flat] entries = [env.subst(x) for x in flat]
for i in range(0, len(entries) - 1, 2): for i in range(0, len(entries) - 1, 2):
addr, fname = entries[i], entries[i + 1] addr, fname = entries[i], entries[i + 1]
if isinstance(fname, (list, tuple)): if isinstance(fname, (list, tuple)):
print(f"Warning: Skipping malformed FLASH_EXTRA_IMAGES entry: {fname}") print(
f"Warning: Skipping malformed FLASH_EXTRA_IMAGES entry: {fname}"
)
continue continue
file_path = pathlib.Path(str(fname)) file_path = pathlib.Path(str(fname))
if file_path.exists(): if file_path.exists():
sections.append((addr, str(file_path))) sections.append((addr, file_path))
else: else:
print(f"Info: {file_path.name} not found — skipping") print(f"Info: {file_path.name} not found — skipping")
if sections:
# Append main firmware to sections
sections.append(("0x10000", firmware_path))
# 3. Final fallback: guess standard image locations # 3. Final fallback: guess standard image locations
if not sections: if not sections:
@@ -62,11 +72,11 @@ def merge_factory_bin(source, target, env):
("0x0", build_dir / "bootloader" / "bootloader.bin"), ("0x0", build_dir / "bootloader" / "bootloader.bin"),
("0x8000", build_dir / "partition_table" / "partition-table.bin"), ("0x8000", build_dir / "partition_table" / "partition-table.bin"),
("0xe000", build_dir / "ota_data_initial.bin"), ("0xe000", build_dir / "ota_data_initial.bin"),
("0x10000", firmware_path) ("0x10000", firmware_path),
] ]
for addr, file_path in guesses: for addr, file_path in guesses:
if file_path.exists(): if file_path.exists():
sections.append((addr, str(file_path))) sections.append((addr, file_path))
else: else:
print(f"Info: {file_path.name} not found — skipping") print(f"Info: {file_path.name} not found — skipping")
@@ -76,27 +86,32 @@ def merge_factory_bin(source, target, env):
return return
output_path = firmware_path.with_suffix(".factory.bin") output_path = firmware_path.with_suffix(".factory.bin")
python_exe = f'"{env.subst("$PYTHONEXE")}"'
cmd = [ cmd = [
"--chip", chip, python_exe,
"merge_bin", "-m",
"--flash_size", flash_size, "esptool",
"--output", str(output_path) "--chip",
chip,
"merge-bin",
"--flash-size",
flash_size,
"--output",
str(output_path),
] ]
for addr, file_path in sections: for addr, file_path in sections:
cmd += [addr, file_path] cmd += [addr, str(file_path)]
print(f"Merging binaries into {output_path}") print(f"Merging binaries into {output_path}")
result = env.Execute( result = env.Execute(
env.VerboseAction( env.VerboseAction(" ".join(cmd), "Merging binaries with esptool")
f"{env.subst('$PYTHONEXE')} -m esptool " + " ".join(cmd),
"Merging binaries with esptool"
)
) )
if result == 0: if result == 0:
print(f"Successfully created {output_path}") print(f"Successfully created {output_path}")
else: else:
print(f"Error: esptool merge_bin failed with code {result}") print(f"Error: esptool merge-bin failed with code {result}")
def esp32_copy_ota_bin(source, target, env): def esp32_copy_ota_bin(source, target, env):
""" """
@@ -107,6 +122,7 @@ def esp32_copy_ota_bin(source, target, env):
shutil.copyfile(firmware_name, new_file_name) shutil.copyfile(firmware_name, new_file_name)
print(f"Copied firmware to {new_file_name}") print(f"Copied firmware to {new_file_name}")
# Run merge first, then ota copy second # Run merge first, then ota copy second
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", merge_factory_bin) env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", merge_factory_bin) # noqa: F821
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_copy_ota_bin) env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_copy_ota_bin) # noqa: F821

View File

@@ -6,12 +6,13 @@ import esphome.codegen as cg
from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ENABLE_ON_BOOT, CONF_ESPHOME, CONF_ID, CONF_NAME from esphome.const import CONF_ENABLE_ON_BOOT, CONF_ESPHOME, CONF_ID, CONF_NAME
from esphome.core import CORE from esphome.core import CORE, TimePeriod
from esphome.core.config import CONF_NAME_ADD_MAC_SUFFIX from esphome.core.config import CONF_NAME_ADD_MAC_SUFFIX
import esphome.final_validate as fv import esphome.final_validate as fv
DEPENDENCIES = ["esp32"] DEPENDENCIES = ["esp32"]
CODEOWNERS = ["@jesserockz", "@Rapsssito"] CODEOWNERS = ["@jesserockz", "@Rapsssito", "@bdraco"]
DOMAIN = "esp32_ble"
class BTLoggers(Enum): class BTLoggers(Enum):
@@ -115,8 +116,11 @@ def register_bt_logger(*loggers: BTLoggers) -> None:
CONF_BLE_ID = "ble_id" CONF_BLE_ID = "ble_id"
CONF_IO_CAPABILITY = "io_capability" CONF_IO_CAPABILITY = "io_capability"
CONF_ADVERTISING = "advertising"
CONF_ADVERTISING_CYCLE_TIME = "advertising_cycle_time" CONF_ADVERTISING_CYCLE_TIME = "advertising_cycle_time"
CONF_DISABLE_BT_LOGS = "disable_bt_logs" CONF_DISABLE_BT_LOGS = "disable_bt_logs"
CONF_CONNECTION_TIMEOUT = "connection_timeout"
CONF_MAX_NOTIFICATIONS = "max_notifications"
NO_BLUETOOTH_VARIANTS = [const.VARIANT_ESP32S2] NO_BLUETOOTH_VARIANTS = [const.VARIANT_ESP32S2]
@@ -161,12 +165,23 @@ CONFIG_SCHEMA = cv.Schema(
IO_CAPABILITY, lower=True IO_CAPABILITY, lower=True
), ),
cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean, cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean,
cv.Optional(CONF_ADVERTISING, default=False): cv.boolean,
cv.Optional( cv.Optional(
CONF_ADVERTISING_CYCLE_TIME, default="10s" CONF_ADVERTISING_CYCLE_TIME, default="10s"
): cv.positive_time_period_milliseconds, ): cv.positive_time_period_milliseconds,
cv.SplitDefault(CONF_DISABLE_BT_LOGS, esp32_idf=True): cv.All( cv.SplitDefault(CONF_DISABLE_BT_LOGS, esp32_idf=True): cv.All(
cv.only_with_esp_idf, cv.boolean cv.only_with_esp_idf, cv.boolean
), ),
cv.SplitDefault(CONF_CONNECTION_TIMEOUT, esp32_idf="20s"): cv.All(
cv.only_with_esp_idf,
cv.positive_time_period_seconds,
cv.Range(min=TimePeriod(seconds=10), max=TimePeriod(seconds=180)),
),
cv.SplitDefault(CONF_MAX_NOTIFICATIONS, esp32_idf=12): cv.All(
cv.only_with_esp_idf,
cv.positive_int,
cv.Range(min=1, max=64),
),
} }
).extend(cv.COMPONENT_SCHEMA) ).extend(cv.COMPONENT_SCHEMA)
@@ -255,8 +270,31 @@ async def to_code(config):
if logger not in _required_loggers: if logger not in _required_loggers:
add_idf_sdkconfig_option(f"{logger.value}_NONE", True) add_idf_sdkconfig_option(f"{logger.value}_NONE", True)
# Set BLE connection establishment timeout to match aioesphomeapi/bleak-retry-connector
# Default is 20 seconds instead of ESP-IDF's 30 seconds. Because there is no way to
# cancel a BLE connection in progress, when aioesphomeapi times out at 20 seconds,
# the connection slot remains occupied for the remaining time, preventing new connection
# attempts and wasting valuable connection slots.
if CONF_CONNECTION_TIMEOUT in config:
timeout_seconds = int(config[CONF_CONNECTION_TIMEOUT].total_seconds)
add_idf_sdkconfig_option(
"CONFIG_BT_BLE_ESTAB_LINK_CONN_TOUT", timeout_seconds
)
# Set the maximum number of notification registrations
# This controls how many BLE characteristics can have notifications enabled
# across all connections for a single GATT client interface
# https://github.com/esphome/issues/issues/6808
if CONF_MAX_NOTIFICATIONS in config:
add_idf_sdkconfig_option(
"CONFIG_BT_GATTC_NOTIF_REG_MAX", config[CONF_MAX_NOTIFICATIONS]
)
cg.add_define("USE_ESP32_BLE") cg.add_define("USE_ESP32_BLE")
if config[CONF_ADVERTISING]:
cg.add_define("USE_ESP32_BLE_ADVERTISING")
@automation.register_condition("ble.enabled", BLEEnabledCondition, cv.Schema({})) @automation.register_condition("ble.enabled", BLEEnabledCondition, cv.Schema({}))
async def ble_enabled_to_code(config, condition_id, template_arg, args): async def ble_enabled_to_code(config, condition_id, template_arg, args):

View File

@@ -1,7 +1,7 @@
#ifdef USE_ESP32
#include "ble.h" #include "ble.h"
#ifdef USE_ESP32
#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"
@@ -53,6 +53,7 @@ void ESP32BLE::disable() {
bool ESP32BLE::is_active() { return this->state_ == BLE_COMPONENT_STATE_ACTIVE; } bool ESP32BLE::is_active() { return this->state_ == BLE_COMPONENT_STATE_ACTIVE; }
#ifdef USE_ESP32_BLE_ADVERTISING
void ESP32BLE::advertising_start() { void ESP32BLE::advertising_start() {
this->advertising_init_(); this->advertising_init_();
if (!this->is_active()) if (!this->is_active())
@@ -88,6 +89,7 @@ void ESP32BLE::advertising_remove_service_uuid(ESPBTUUID uuid) {
this->advertising_->remove_service_uuid(uuid); this->advertising_->remove_service_uuid(uuid);
this->advertising_start(); this->advertising_start();
} }
#endif
bool ESP32BLE::ble_pre_setup_() { bool ESP32BLE::ble_pre_setup_() {
esp_err_t err = nvs_flash_init(); esp_err_t err = nvs_flash_init();
@@ -98,6 +100,7 @@ bool ESP32BLE::ble_pre_setup_() {
return true; return true;
} }
#ifdef USE_ESP32_BLE_ADVERTISING
void ESP32BLE::advertising_init_() { void ESP32BLE::advertising_init_() {
if (this->advertising_ != nullptr) if (this->advertising_ != nullptr)
return; return;
@@ -107,6 +110,7 @@ void ESP32BLE::advertising_init_() {
this->advertising_->set_min_preferred_interval(0x06); this->advertising_->set_min_preferred_interval(0x06);
this->advertising_->set_appearance(this->appearance_); this->advertising_->set_appearance(this->appearance_);
} }
#endif
bool ESP32BLE::ble_setup_() { bool ESP32BLE::ble_setup_() {
esp_err_t err; esp_err_t err;
@@ -394,9 +398,11 @@ void ESP32BLE::loop() {
this->ble_event_pool_.release(ble_event); this->ble_event_pool_.release(ble_event);
ble_event = this->ble_events_.pop(); ble_event = this->ble_events_.pop();
} }
#ifdef USE_ESP32_BLE_ADVERTISING
if (this->advertising_ != nullptr) { if (this->advertising_ != nullptr) {
this->advertising_->loop(); this->advertising_->loop();
} }
#endif
// Log dropped events periodically // Log dropped events periodically
uint16_t dropped = this->ble_events_.get_and_reset_dropped_count(); uint16_t dropped = this->ble_events_.get_and_reset_dropped_count();
@@ -468,6 +474,8 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa
// Ignore these GAP events as they are not relevant for our use case // Ignore these GAP events as they are not relevant for our use case
case ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT: case ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT:
case ESP_GAP_BLE_SET_PKT_LENGTH_COMPLETE_EVT: case ESP_GAP_BLE_SET_PKT_LENGTH_COMPLETE_EVT:
case ESP_GAP_BLE_PHY_UPDATE_COMPLETE_EVT: // BLE 5.0 PHY update complete
case ESP_GAP_BLE_CHANNEL_SELECT_ALGORITHM_EVT: // BLE 5.0 channel selection algorithm
return; return;
default: default:

View File

@@ -1,14 +1,17 @@
#pragma once #pragma once
#include "ble_advertising.h" #include "esphome/core/defines.h" // Must be included before conditional includes
#include "ble_uuid.h" #include "ble_uuid.h"
#include "ble_scan_result.h" #include "ble_scan_result.h"
#ifdef USE_ESP32_BLE_ADVERTISING
#include "ble_advertising.h"
#endif
#include <functional> #include <functional>
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "ble_event.h" #include "ble_event.h"
@@ -23,21 +26,14 @@
namespace esphome::esp32_ble { namespace esphome::esp32_ble {
// Maximum number of BLE scan results to buffer // Maximum size of the BLE event queue
// Sized to handle bursts of advertisements while allowing for processing delays // Increased to absorb the ring buffer capacity from esp32_ble_tracker
// With 16 advertisements per batch and some safety margin:
// - Without PSRAM: 24 entries (1.5× batch size)
// - With PSRAM: 36 entries (2.25× batch size)
// The reduced structure size (~80 bytes vs ~400 bytes) allows for larger buffers
#ifdef USE_PSRAM #ifdef USE_PSRAM
static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 36; static constexpr uint8_t MAX_BLE_QUEUE_SIZE = 100; // 64 + 36 (ring buffer size with PSRAM)
#else #else
static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 24; static constexpr uint8_t MAX_BLE_QUEUE_SIZE = 88; // 64 + 24 (ring buffer size without PSRAM)
#endif #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)
@@ -113,6 +109,7 @@ class ESP32BLE : public Component {
float get_setup_priority() const override; float get_setup_priority() const override;
void set_name(const std::string &name) { this->name_ = name; } void set_name(const std::string &name) { this->name_ = name; }
#ifdef USE_ESP32_BLE_ADVERTISING
void advertising_start(); void advertising_start();
void advertising_set_service_data(const std::vector<uint8_t> &data); void advertising_set_service_data(const std::vector<uint8_t> &data);
void advertising_set_manufacturer_data(const std::vector<uint8_t> &data); void advertising_set_manufacturer_data(const std::vector<uint8_t> &data);
@@ -120,6 +117,7 @@ class ESP32BLE : public Component {
void advertising_add_service_uuid(ESPBTUUID uuid); void advertising_add_service_uuid(ESPBTUUID uuid);
void advertising_remove_service_uuid(ESPBTUUID uuid); void advertising_remove_service_uuid(ESPBTUUID uuid);
void advertising_register_raw_advertisement_callback(std::function<void(bool)> &&callback); void advertising_register_raw_advertisement_callback(std::function<void(bool)> &&callback);
#endif
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) { void register_gap_scan_event_handler(GAPScanEventHandler *handler) {
@@ -140,7 +138,9 @@ class ESP32BLE : public Component {
bool ble_setup_(); bool ble_setup_();
bool ble_dismantle_(); bool ble_dismantle_();
bool ble_pre_setup_(); bool ble_pre_setup_();
#ifdef USE_ESP32_BLE_ADVERTISING
void advertising_init_(); void advertising_init_();
#endif
private: private:
template<typename... Args> friend void enqueue_ble_event(Args... args); template<typename... Args> friend void enqueue_ble_event(Args... args);
@@ -160,7 +160,9 @@ class ESP32BLE : public Component {
optional<std::string> name_; optional<std::string> name_;
// 4-byte aligned members // 4-byte aligned members
BLEAdvertising *advertising_{}; // 4 bytes (pointer) #ifdef USE_ESP32_BLE_ADVERTISING
BLEAdvertising *advertising_{}; // 4 bytes (pointer)
#endif
esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; // 4 bytes (enum) esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; // 4 bytes (enum)
uint32_t advertising_cycle_time_{}; // 4 bytes uint32_t advertising_cycle_time_{}; // 4 bytes

View File

@@ -1,6 +1,7 @@
#include "ble_advertising.h" #include "ble_advertising.h"
#ifdef USE_ESP32 #ifdef USE_ESP32
#ifdef USE_ESP32_BLE_ADVERTISING
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
@@ -161,4 +162,5 @@ void BLEAdvertising::register_raw_advertisement_callback(std::function<void(bool
} // namespace esphome::esp32_ble } // namespace esphome::esp32_ble
#endif #endif // USE_ESP32_BLE_ADVERTISING
#endif // USE_ESP32

View File

@@ -1,10 +1,13 @@
#pragma once #pragma once
#include "esphome/core/defines.h"
#include <array> #include <array>
#include <functional> #include <functional>
#include <vector> #include <vector>
#ifdef USE_ESP32 #ifdef USE_ESP32
#ifdef USE_ESP32_BLE_ADVERTISING
#include <esp_bt.h> #include <esp_bt.h>
#include <esp_gap_ble_api.h> #include <esp_gap_ble_api.h>
@@ -56,4 +59,5 @@ class BLEAdvertising {
} // namespace esphome::esp32_ble } // namespace esphome::esp32_ble
#endif #endif // USE_ESP32_BLE_ADVERTISING
#endif // USE_ESP32

View File

@@ -1,6 +1,7 @@
#include "ble_uuid.h" #include "ble_uuid.h"
#ifdef USE_ESP32 #ifdef USE_ESP32
#ifdef USE_ESP32_BLE_UUID
#include <cstring> #include <cstring>
#include <cstdio> #include <cstdio>
@@ -190,4 +191,5 @@ std::string ESPBTUUID::to_string() const {
} // namespace esphome::esp32_ble } // namespace esphome::esp32_ble
#endif #endif // USE_ESP32_BLE_UUID
#endif // USE_ESP32

View File

@@ -1,9 +1,11 @@
#pragma once #pragma once
#include "esphome/core/defines.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#ifdef USE_ESP32 #ifdef USE_ESP32
#ifdef USE_ESP32_BLE_UUID
#include <string> #include <string>
#include <esp_bt_defs.h> #include <esp_bt_defs.h>
@@ -42,4 +44,5 @@ class ESPBTUUID {
} // namespace esphome::esp32_ble } // namespace esphome::esp32_ble
#endif #endif // USE_ESP32_BLE_UUID
#endif // USE_ESP32

View File

@@ -65,6 +65,8 @@ FINAL_VALIDATE_SCHEMA = esp32_ble.validate_variant
async def to_code(config): async def to_code(config):
cg.add_define("USE_ESP32_BLE_UUID")
uuid = config[CONF_UUID].hex uuid = config[CONF_UUID].hex
uuid_arr = [ uuid_arr = [
cg.RawExpression(f"0x{uuid[i : i + 2]}") for i in range(0, len(uuid), 2) cg.RawExpression(f"0x{uuid[i : i + 2]}") for i in range(0, len(uuid), 2)
@@ -82,6 +84,8 @@ async def to_code(config):
cg.add(var.set_measured_power(config[CONF_MEASURED_POWER])) cg.add(var.set_measured_power(config[CONF_MEASURED_POWER]))
cg.add(var.set_tx_power(config[CONF_TX_POWER])) cg.add(var.set_tx_power(config[CONF_TX_POWER]))
cg.add_define("USE_ESP32_BLE_ADVERTISING")
if CORE.using_esp_idf: if CORE.using_esp_idf:
add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True) add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True)
add_idf_sdkconfig_option("CONFIG_BT_BLE_42_FEATURES_SUPPORTED", True) add_idf_sdkconfig_option("CONFIG_BT_BLE_42_FEATURES_SUPPORTED", True)

View File

@@ -2,7 +2,7 @@ import esphome.codegen as cg
from esphome.components import esp32_ble_tracker from esphome.components import esp32_ble_tracker
AUTO_LOAD = ["esp32_ble_tracker"] AUTO_LOAD = ["esp32_ble_tracker"]
CODEOWNERS = ["@jesserockz"] CODEOWNERS = ["@jesserockz", "@bdraco"]
DEPENDENCIES = ["esp32"] DEPENDENCIES = ["esp32"]
esp32_ble_client_ns = cg.esphome_ns.namespace("esp32_ble_client") esp32_ble_client_ns = cg.esphome_ns.namespace("esp32_ble_client")

View File

@@ -5,9 +5,9 @@
#include "esphome/core/log.h" #include "esphome/core/log.h"
#ifdef USE_ESP32 #ifdef USE_ESP32
#ifdef USE_ESP32_BLE_DEVICE
namespace esphome { namespace esphome::esp32_ble_client {
namespace esp32_ble_client {
static const char *const TAG = "esp32_ble_client"; static const char *const TAG = "esp32_ble_client";
@@ -93,7 +93,7 @@ esp_err_t BLECharacteristic::write_value(uint8_t *new_val, int16_t new_val_size)
return write_value(new_val, new_val_size, ESP_GATT_WRITE_TYPE_NO_RSP); return write_value(new_val, new_val_size, ESP_GATT_WRITE_TYPE_NO_RSP);
} }
} // namespace esp32_ble_client } // namespace esphome::esp32_ble_client
} // namespace esphome
#endif // USE_ESP32_BLE_DEVICE
#endif // USE_ESP32 #endif // USE_ESP32

View File

@@ -1,6 +1,9 @@
#pragma once #pragma once
#include "esphome/core/defines.h"
#ifdef USE_ESP32 #ifdef USE_ESP32
#ifdef USE_ESP32_BLE_DEVICE
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
@@ -8,8 +11,7 @@
#include <vector> #include <vector>
namespace esphome { namespace esphome::esp32_ble_client {
namespace esp32_ble_client {
namespace espbt = esphome::esp32_ble_tracker; namespace espbt = esphome::esp32_ble_tracker;
@@ -33,7 +35,7 @@ class BLECharacteristic {
BLEService *service; BLEService *service;
}; };
} // namespace esp32_ble_client } // namespace esphome::esp32_ble_client
} // namespace esphome
#endif // USE_ESP32_BLE_DEVICE
#endif // USE_ESP32 #endif // USE_ESP32

View File

@@ -5,10 +5,27 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
namespace esphome { #include <esp_gap_ble_api.h>
namespace esp32_ble_client { #include <esp_gatt_defs.h>
namespace esphome::esp32_ble_client {
static const char *const TAG = "esp32_ble_client"; static const char *const TAG = "esp32_ble_client";
// Intermediate connection parameters for standard operation
// ESP-IDF defaults (12.5-15ms) are too slow for stable connections through WiFi-based BLE proxies,
// causing disconnections. These medium parameters balance responsiveness with bandwidth usage.
static const uint16_t MEDIUM_MIN_CONN_INTERVAL = 0x07; // 7 * 1.25ms = 8.75ms
static const uint16_t MEDIUM_MAX_CONN_INTERVAL = 0x09; // 9 * 1.25ms = 11.25ms
// The timeout value was increased from 6s to 8s to address stability issues observed
// in certain BLE devices when operating through WiFi-based BLE proxies. The longer
// timeout reduces the likelihood of disconnections during periods of high latency.
static const uint16_t MEDIUM_CONN_TIMEOUT = 800; // 800 * 10ms = 8s
// Fastest connection parameters for devices with short discovery timeouts
static const uint16_t FAST_MIN_CONN_INTERVAL = 0x06; // 6 * 1.25ms = 7.5ms (BLE minimum)
static const uint16_t FAST_MAX_CONN_INTERVAL = 0x06; // 6 * 1.25ms = 7.5ms
static const uint16_t FAST_CONN_TIMEOUT = 1000; // 1000 * 10ms = 10s
static const esp_bt_uuid_t NOTIFY_DESC_UUID = { static const esp_bt_uuid_t NOTIFY_DESC_UUID = {
.len = ESP_UUID_LEN_16, .len = ESP_UUID_LEN_16,
.uuid = .uuid =
@@ -27,8 +44,10 @@ void BLEClientBase::set_state(espbt::ClientState st) {
ESPBTClient::set_state(st); ESPBTClient::set_state(st);
if (st == espbt::ClientState::READY_TO_CONNECT) { if (st == espbt::ClientState::READY_TO_CONNECT) {
// Enable loop when we need to connect // Enable loop for state processing
this->enable_loop(); this->enable_loop();
// Connect immediately instead of waiting for next loop
this->connect();
} }
} }
@@ -45,11 +64,6 @@ void BLEClientBase::loop() {
} }
this->set_state(espbt::ClientState::IDLE); this->set_state(espbt::ClientState::IDLE);
} }
// READY_TO_CONNECT means we have discovered the device
// and the scanner has been stopped by the tracker.
else if (this->state_ == espbt::ClientState::READY_TO_CONNECT) {
this->connect();
}
// If its idle, we can disable the loop as set_state // If its idle, we can disable the loop as set_state
// will enable it again when we need to connect. // will enable it again when we need to connect.
else if (this->state_ == espbt::ClientState::IDLE) { else if (this->state_ == espbt::ClientState::IDLE) {
@@ -64,40 +78,7 @@ void BLEClientBase::dump_config() {
" Address: %s\n" " Address: %s\n"
" Auto-Connect: %s", " Auto-Connect: %s",
this->address_str().c_str(), TRUEFALSE(this->auto_connect_)); this->address_str().c_str(), TRUEFALSE(this->auto_connect_));
std::string state_name; ESP_LOGCONFIG(TAG, " State: %s", espbt::client_state_to_string(this->state()));
switch (this->state()) {
case espbt::ClientState::INIT:
state_name = "INIT";
break;
case espbt::ClientState::DISCONNECTING:
state_name = "DISCONNECTING";
break;
case espbt::ClientState::IDLE:
state_name = "IDLE";
break;
case espbt::ClientState::SEARCHING:
state_name = "SEARCHING";
break;
case espbt::ClientState::DISCOVERED:
state_name = "DISCOVERED";
break;
case espbt::ClientState::READY_TO_CONNECT:
state_name = "READY_TO_CONNECT";
break;
case espbt::ClientState::CONNECTING:
state_name = "CONNECTING";
break;
case espbt::ClientState::CONNECTED:
state_name = "CONNECTED";
break;
case espbt::ClientState::ESTABLISHED:
state_name = "ESTABLISHED";
break;
default:
state_name = "UNKNOWN_STATE";
break;
}
ESP_LOGCONFIG(TAG, " State: %s", state_name.c_str());
if (this->status_ == ESP_GATT_NO_RESOURCES) { if (this->status_ == ESP_GATT_NO_RESOURCES) {
ESP_LOGE(TAG, " Failed due to no resources. Try to reduce number of BLE clients in config."); ESP_LOGE(TAG, " Failed due to no resources. Try to reduce number of BLE clients in config.");
} else if (this->status_ != ESP_GATT_OK) { } else if (this->status_ != ESP_GATT_OK) {
@@ -126,13 +107,43 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) {
#endif #endif
void BLEClientBase::connect() { void BLEClientBase::connect() {
ESP_LOGI(TAG, "[%d] [%s] 0x%02x Attempting BLE connection", this->connection_index_, this->address_str_.c_str(), ESP_LOGI(TAG, "[%d] [%s] 0x%02x Connecting", this->connection_index_, this->address_str_.c_str(),
this->remote_addr_type_); this->remote_addr_type_);
this->paired_ = false; this->paired_ = false;
// Set preferred connection parameters before connecting
// Use FAST for all V3 connections (better latency and reliability)
// Use MEDIUM for V1/legacy connections (balanced performance)
uint16_t min_interval, max_interval, timeout;
const char *param_type;
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE ||
this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
min_interval = FAST_MIN_CONN_INTERVAL;
max_interval = FAST_MAX_CONN_INTERVAL;
timeout = FAST_CONN_TIMEOUT;
param_type = "fast";
} else {
min_interval = MEDIUM_MIN_CONN_INTERVAL;
max_interval = MEDIUM_MAX_CONN_INTERVAL;
timeout = MEDIUM_CONN_TIMEOUT;
param_type = "medium";
}
auto param_ret = esp_ble_gap_set_prefer_conn_params(this->remote_bda_, min_interval, max_interval,
0, // latency: 0
timeout);
if (param_ret != ESP_OK) {
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gap_set_prefer_conn_params failed: %d", this->connection_index_,
this->address_str_.c_str(), param_ret);
} else {
this->log_connection_params_(param_type);
}
// Now open the connection
auto ret = esp_ble_gattc_open(this->gattc_if_, this->remote_bda_, this->remote_addr_type_, true); auto ret = esp_ble_gattc_open(this->gattc_if_, this->remote_bda_, this->remote_addr_type_, true);
if (ret) { if (ret) {
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_open error, status=%d", this->connection_index_, this->address_str_.c_str(), this->log_gattc_warning_("esp_ble_gattc_open", ret);
ret);
this->set_state(espbt::ClientState::IDLE); this->set_state(espbt::ClientState::IDLE);
} else { } else {
this->set_state(espbt::ClientState::CONNECTING); this->set_state(espbt::ClientState::CONNECTING);
@@ -142,14 +153,9 @@ void BLEClientBase::connect() {
esp_err_t BLEClientBase::pair() { return esp_ble_set_encryption(this->remote_bda_, ESP_BLE_SEC_ENCRYPT); } esp_err_t BLEClientBase::pair() { return esp_ble_set_encryption(this->remote_bda_, ESP_BLE_SEC_ENCRYPT); }
void BLEClientBase::disconnect() { void BLEClientBase::disconnect() {
if (this->state_ == espbt::ClientState::IDLE) { if (this->state_ == espbt::ClientState::IDLE || this->state_ == espbt::ClientState::DISCONNECTING) {
ESP_LOGI(TAG, "[%d] [%s] Disconnect requested, but already idle.", this->connection_index_, ESP_LOGI(TAG, "[%d] [%s] Disconnect requested, but already %s", this->connection_index_, this->address_str_.c_str(),
this->address_str_.c_str()); espbt::client_state_to_string(this->state_));
return;
}
if (this->state_ == espbt::ClientState::DISCONNECTING) {
ESP_LOGI(TAG, "[%d] [%s] Disconnect requested, but already disconnecting.", this->connection_index_,
this->address_str_.c_str());
return; return;
} }
if (this->state_ == espbt::ClientState::CONNECTING || this->conn_id_ == UNSET_CONN_ID) { if (this->state_ == espbt::ClientState::CONNECTING || this->conn_id_ == UNSET_CONN_ID) {
@@ -184,8 +190,7 @@ void BLEClientBase::unconditional_disconnect() {
// In the future we might consider App.reboot() here since // In the future we might consider App.reboot() here since
// the BLE stack is in an indeterminate state. // the BLE stack is in an indeterminate state.
// //
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_close error, err=%d", this->connection_index_, this->address_str_.c_str(), this->log_gattc_warning_("esp_ble_gattc_close", err);
err);
} }
if (this->state_ == espbt::ClientState::SEARCHING || this->state_ == espbt::ClientState::READY_TO_CONNECT || if (this->state_ == espbt::ClientState::SEARCHING || this->state_ == espbt::ClientState::READY_TO_CONNECT ||
@@ -198,9 +203,11 @@ void BLEClientBase::unconditional_disconnect() {
} }
void BLEClientBase::release_services() { void BLEClientBase::release_services() {
#ifdef USE_ESP32_BLE_DEVICE
for (auto &svc : this->services_) for (auto &svc : this->services_)
delete svc; // NOLINT(cppcoreguidelines-owning-memory) delete svc; // NOLINT(cppcoreguidelines-owning-memory)
this->services_.clear(); this->services_.clear();
#endif
#ifndef CONFIG_BT_GATTC_CACHE_NVS_FLASH #ifndef CONFIG_BT_GATTC_CACHE_NVS_FLASH
esp_ble_gattc_cache_clean(this->remote_bda_); esp_ble_gattc_cache_clean(this->remote_bda_);
#endif #endif
@@ -210,6 +217,36 @@ void BLEClientBase::log_event_(const char *name) {
ESP_LOGD(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_.c_str(), name); ESP_LOGD(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_.c_str(), name);
} }
void BLEClientBase::log_gattc_event_(const char *name) {
ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_%s_EVT", this->connection_index_, this->address_str_.c_str(), name);
}
void BLEClientBase::log_gattc_warning_(const char *operation, esp_gatt_status_t status) {
ESP_LOGW(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str_.c_str(), operation,
status);
}
void BLEClientBase::log_gattc_warning_(const char *operation, esp_err_t err) {
ESP_LOGW(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str_.c_str(), operation, err);
}
void BLEClientBase::log_connection_params_(const char *param_type) {
ESP_LOGD(TAG, "[%d] [%s] %s conn params", this->connection_index_, this->address_str_.c_str(), param_type);
}
void BLEClientBase::restore_medium_conn_params_() {
// Restore to medium connection parameters after initial connection phase
// This balances performance with bandwidth usage for normal operation
esp_ble_conn_update_params_t conn_params = {{0}};
memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t));
conn_params.min_int = MEDIUM_MIN_CONN_INTERVAL;
conn_params.max_int = MEDIUM_MAX_CONN_INTERVAL;
conn_params.latency = 0;
conn_params.timeout = MEDIUM_CONN_TIMEOUT;
this->log_connection_params_("medium");
esp_ble_gap_update_conn_params(&conn_params);
}
bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if, bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if,
esp_ble_gattc_cb_param_t *param) { esp_ble_gattc_cb_param_t *param) {
if (event == ESP_GATTC_REG_EVT && this->app_id != param->reg.app_id) if (event == ESP_GATTC_REG_EVT && this->app_id != param->reg.app_id)
@@ -237,30 +274,18 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
case ESP_GATTC_OPEN_EVT: { case ESP_GATTC_OPEN_EVT: {
if (!this->check_addr(param->open.remote_bda)) if (!this->check_addr(param->open.remote_bda))
return false; return false;
this->log_event_("ESP_GATTC_OPEN_EVT"); this->log_gattc_event_("OPEN");
this->conn_id_ = param->open.conn_id; // conn_id was already set in ESP_GATTC_CONNECT_EVT
this->service_count_ = 0; this->service_count_ = 0;
if (this->state_ != espbt::ClientState::CONNECTING) { if (this->state_ != espbt::ClientState::CONNECTING) {
// This should not happen but lets log it in case it does // This should not happen but lets log it in case it does
// because it means we have a bad assumption about how the // because it means we have a bad assumption about how the
// ESP BT stack works. // ESP BT stack works.
if (this->state_ == espbt::ClientState::CONNECTED) { ESP_LOGE(TAG, "[%d] [%s] Got ESP_GATTC_OPEN_EVT while in %s state, status=%d", this->connection_index_,
ESP_LOGE(TAG, "[%d] [%s] Got ESP_GATTC_OPEN_EVT while already connected, status=%d", this->connection_index_, this->address_str_.c_str(), espbt::client_state_to_string(this->state_), param->open.status);
this->address_str_.c_str(), param->open.status);
} else if (this->state_ == espbt::ClientState::ESTABLISHED) {
ESP_LOGE(TAG, "[%d] [%s] Got ESP_GATTC_OPEN_EVT while already established, status=%d",
this->connection_index_, this->address_str_.c_str(), param->open.status);
} else if (this->state_ == espbt::ClientState::DISCONNECTING) {
ESP_LOGE(TAG, "[%d] [%s] Got ESP_GATTC_OPEN_EVT while disconnecting, status=%d", this->connection_index_,
this->address_str_.c_str(), param->open.status);
} else {
ESP_LOGE(TAG, "[%d] [%s] Got ESP_GATTC_OPEN_EVT while not in connecting state, status=%d",
this->connection_index_, this->address_str_.c_str(), param->open.status);
}
} }
if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) { if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) {
ESP_LOGW(TAG, "[%d] [%s] Connection failed, status=%d", this->connection_index_, this->address_str_.c_str(), this->log_gattc_warning_("Connection open", param->open.status);
param->open.status);
this->set_state(espbt::ClientState::IDLE); this->set_state(espbt::ClientState::IDLE);
break; break;
} }
@@ -272,32 +297,47 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
this->conn_id_ = UNSET_CONN_ID; this->conn_id_ = UNSET_CONN_ID;
break; break;
} }
auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if_, param->open.conn_id); // MTU negotiation already started in ESP_GATTC_CONNECT_EVT
if (ret) {
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_send_mtu_req failed, status=%x", this->connection_index_,
this->address_str_.c_str(), ret);
}
this->set_state(espbt::ClientState::CONNECTED); this->set_state(espbt::ClientState::CONNECTED);
ESP_LOGI(TAG, "[%d] [%s] Connection open", this->connection_index_, this->address_str_.c_str());
if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) { if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
ESP_LOGI(TAG, "[%d] [%s] Connected", this->connection_index_, this->address_str_.c_str()); // Restore to medium connection parameters for cached connections too
this->restore_medium_conn_params_();
// only set our state, subclients might have more stuff to do yet. // only set our state, subclients might have more stuff to do yet.
this->state_ = espbt::ClientState::ESTABLISHED; this->state_ = espbt::ClientState::ESTABLISHED;
break; break;
} }
ESP_LOGD(TAG, "[%d] [%s] Searching for services", this->connection_index_, this->address_str_.c_str());
esp_ble_gattc_search_service(esp_gattc_if, param->cfg_mtu.conn_id, nullptr); esp_ble_gattc_search_service(esp_gattc_if, param->cfg_mtu.conn_id, nullptr);
break; break;
} }
case ESP_GATTC_CONNECT_EVT: { case ESP_GATTC_CONNECT_EVT: {
if (!this->check_addr(param->connect.remote_bda)) if (!this->check_addr(param->connect.remote_bda))
return false; return false;
this->log_event_("ESP_GATTC_CONNECT_EVT"); this->log_gattc_event_("CONNECT");
this->conn_id_ = param->connect.conn_id;
// Start MTU negotiation immediately as recommended by ESP-IDF examples
// (gatt_client, ble_throughput) which call esp_ble_gattc_send_mtu_req in
// ESP_GATTC_CONNECT_EVT instead of waiting for ESP_GATTC_OPEN_EVT.
// This saves ~3ms in the connection process.
auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if_, param->connect.conn_id);
if (ret) {
this->log_gattc_warning_("esp_ble_gattc_send_mtu_req", ret);
}
break; break;
} }
case ESP_GATTC_DISCONNECT_EVT: { case ESP_GATTC_DISCONNECT_EVT: {
if (!this->check_addr(param->disconnect.remote_bda)) if (!this->check_addr(param->disconnect.remote_bda))
return false; return false;
ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason %d", this->connection_index_, // Check if we were disconnected while waiting for service discovery
this->address_str_.c_str(), param->disconnect.reason); if (param->disconnect.reason == ESP_GATT_CONN_TERMINATE_PEER_USER &&
this->state_ == espbt::ClientState::CONNECTED) {
ESP_LOGW(TAG, "[%d] [%s] Disconnected by remote during service discovery", this->connection_index_,
this->address_str_.c_str());
} else {
ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason 0x%02x", this->connection_index_,
this->address_str_.c_str(), param->disconnect.reason);
}
this->release_services(); this->release_services();
this->set_state(espbt::ClientState::IDLE); this->set_state(espbt::ClientState::IDLE);
break; break;
@@ -320,7 +360,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
case ESP_GATTC_CLOSE_EVT: { case ESP_GATTC_CLOSE_EVT: {
if (this->conn_id_ != param->close.conn_id) if (this->conn_id_ != param->close.conn_id)
return false; return false;
this->log_event_("ESP_GATTC_CLOSE_EVT"); this->log_gattc_event_("CLOSE");
this->release_services(); this->release_services();
this->set_state(espbt::ClientState::IDLE); this->set_state(espbt::ClientState::IDLE);
this->conn_id_ = UNSET_CONN_ID; this->conn_id_ = UNSET_CONN_ID;
@@ -332,63 +372,74 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
this->service_count_++; this->service_count_++;
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
// V3 clients don't need services initialized since // V3 clients don't need services initialized since
// they only request by handle after receiving the services. // as they use the ESP APIs to get services.
break; break;
} }
#ifdef USE_ESP32_BLE_DEVICE
BLEService *ble_service = new BLEService(); // NOLINT(cppcoreguidelines-owning-memory) BLEService *ble_service = new BLEService(); // NOLINT(cppcoreguidelines-owning-memory)
ble_service->uuid = espbt::ESPBTUUID::from_uuid(param->search_res.srvc_id.uuid); ble_service->uuid = espbt::ESPBTUUID::from_uuid(param->search_res.srvc_id.uuid);
ble_service->start_handle = param->search_res.start_handle; ble_service->start_handle = param->search_res.start_handle;
ble_service->end_handle = param->search_res.end_handle; ble_service->end_handle = param->search_res.end_handle;
ble_service->client = this; ble_service->client = this;
this->services_.push_back(ble_service); this->services_.push_back(ble_service);
#endif
break; break;
} }
case ESP_GATTC_SEARCH_CMPL_EVT: { case ESP_GATTC_SEARCH_CMPL_EVT: {
if (this->conn_id_ != param->search_cmpl.conn_id) if (this->conn_id_ != param->search_cmpl.conn_id)
return false; return false;
this->log_event_("ESP_GATTC_SEARCH_CMPL_EVT"); this->log_gattc_event_("SEARCH_CMPL");
for (auto &svc : this->services_) { // For V3 connections, restore to medium connection parameters after service discovery
ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_.c_str(), // This balances performance with bandwidth usage after the critical discovery phase
svc->uuid.to_string().c_str()); if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE ||
ESP_LOGV(TAG, "[%d] [%s] start_handle: 0x%x end_handle: 0x%x", this->connection_index_, this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
this->address_str_.c_str(), svc->start_handle, svc->end_handle); this->restore_medium_conn_params_();
} else {
#ifdef USE_ESP32_BLE_DEVICE
for (auto &svc : this->services_) {
ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_.c_str(),
svc->uuid.to_string().c_str());
ESP_LOGV(TAG, "[%d] [%s] start_handle: 0x%x end_handle: 0x%x", this->connection_index_,
this->address_str_.c_str(), svc->start_handle, svc->end_handle);
}
#endif
} }
ESP_LOGI(TAG, "[%d] [%s] Connected", this->connection_index_, this->address_str_.c_str()); ESP_LOGI(TAG, "[%d] [%s] Service discovery complete", this->connection_index_, this->address_str_.c_str());
this->state_ = espbt::ClientState::ESTABLISHED; this->state_ = espbt::ClientState::ESTABLISHED;
break; break;
} }
case ESP_GATTC_READ_DESCR_EVT: { case ESP_GATTC_READ_DESCR_EVT: {
if (this->conn_id_ != param->write.conn_id) if (this->conn_id_ != param->write.conn_id)
return false; return false;
this->log_event_("ESP_GATTC_READ_DESCR_EVT"); this->log_gattc_event_("READ_DESCR");
break; break;
} }
case ESP_GATTC_WRITE_DESCR_EVT: { case ESP_GATTC_WRITE_DESCR_EVT: {
if (this->conn_id_ != param->write.conn_id) if (this->conn_id_ != param->write.conn_id)
return false; return false;
this->log_event_("ESP_GATTC_WRITE_DESCR_EVT"); this->log_gattc_event_("WRITE_DESCR");
break; break;
} }
case ESP_GATTC_WRITE_CHAR_EVT: { case ESP_GATTC_WRITE_CHAR_EVT: {
if (this->conn_id_ != param->write.conn_id) if (this->conn_id_ != param->write.conn_id)
return false; return false;
this->log_event_("ESP_GATTC_WRITE_CHAR_EVT"); this->log_gattc_event_("WRITE_CHAR");
break; break;
} }
case ESP_GATTC_READ_CHAR_EVT: { case ESP_GATTC_READ_CHAR_EVT: {
if (this->conn_id_ != param->read.conn_id) if (this->conn_id_ != param->read.conn_id)
return false; return false;
this->log_event_("ESP_GATTC_READ_CHAR_EVT"); this->log_gattc_event_("READ_CHAR");
break; break;
} }
case ESP_GATTC_NOTIFY_EVT: { case ESP_GATTC_NOTIFY_EVT: {
if (this->conn_id_ != param->notify.conn_id) if (this->conn_id_ != param->notify.conn_id)
return false; return false;
this->log_event_("ESP_GATTC_NOTIFY_EVT"); this->log_gattc_event_("NOTIFY");
break; break;
} }
case ESP_GATTC_REG_FOR_NOTIFY_EVT: { case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
this->log_event_("ESP_GATTC_REG_FOR_NOTIFY_EVT"); this->log_gattc_event_("REG_FOR_NOTIFY");
if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE || if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
// Client is responsible for flipping the descriptor value // Client is responsible for flipping the descriptor value
@@ -400,8 +451,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
esp_gatt_status_t descr_status = esp_ble_gattc_get_descr_by_char_handle( esp_gatt_status_t descr_status = esp_ble_gattc_get_descr_by_char_handle(
this->gattc_if_, this->conn_id_, param->reg_for_notify.handle, NOTIFY_DESC_UUID, &desc_result, &count); this->gattc_if_, this->conn_id_, param->reg_for_notify.handle, NOTIFY_DESC_UUID, &desc_result, &count);
if (descr_status != ESP_GATT_OK) { if (descr_status != ESP_GATT_OK) {
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_descr_by_char_handle error, status=%d", this->connection_index_, this->log_gattc_warning_("esp_ble_gattc_get_descr_by_char_handle", descr_status);
this->address_str_.c_str(), descr_status);
break; break;
} }
esp_gattc_char_elem_t char_result; esp_gattc_char_elem_t char_result;
@@ -409,8 +459,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, param->reg_for_notify.handle, esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, param->reg_for_notify.handle,
param->reg_for_notify.handle, &char_result, &count, 0); param->reg_for_notify.handle, &char_result, &count, 0);
if (char_status != ESP_GATT_OK) { if (char_status != ESP_GATT_OK) {
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", this->connection_index_, this->log_gattc_warning_("esp_ble_gattc_get_all_char", char_status);
this->address_str_.c_str(), char_status);
break; break;
} }
@@ -424,8 +473,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
(uint8_t *) &notify_en, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE); (uint8_t *) &notify_en, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE);
ESP_LOGD(TAG, "Wrote notify descriptor %d, properties=%d", notify_en, char_result.properties); ESP_LOGD(TAG, "Wrote notify descriptor %d, properties=%d", notify_en, char_result.properties);
if (status) { if (status) {
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_write_char_descr error, status=%d", this->connection_index_, this->log_gattc_warning_("esp_ble_gattc_write_char_descr", status);
this->address_str_.c_str(), status);
} }
break; break;
} }
@@ -533,6 +581,7 @@ float BLEClientBase::parse_char_value(uint8_t *value, uint16_t length) {
return NAN; return NAN;
} }
#ifdef USE_ESP32_BLE_DEVICE
BLEService *BLEClientBase::get_service(espbt::ESPBTUUID uuid) { BLEService *BLEClientBase::get_service(espbt::ESPBTUUID uuid) {
for (auto *svc : this->services_) { for (auto *svc : this->services_) {
if (svc->uuid == uuid) if (svc->uuid == uuid)
@@ -609,8 +658,8 @@ BLEDescriptor *BLEClientBase::get_descriptor(uint16_t handle) {
} }
return nullptr; return nullptr;
} }
#endif // USE_ESP32_BLE_DEVICE
} // namespace esp32_ble_client } // namespace esphome::esp32_ble_client
} // namespace esphome
#endif // USE_ESP32 #endif // USE_ESP32

View File

@@ -5,7 +5,9 @@
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#ifdef USE_ESP32_BLE_DEVICE
#include "ble_service.h" #include "ble_service.h"
#endif
#include <array> #include <array>
#include <string> #include <string>
@@ -16,8 +18,7 @@
#include <esp_gatt_common_api.h> #include <esp_gatt_common_api.h>
#include <esp_gattc_api.h> #include <esp_gattc_api.h>
namespace esphome { namespace esphome::esp32_ble_client {
namespace esp32_ble_client {
namespace espbt = esphome::esp32_ble_tracker; namespace espbt = esphome::esp32_ble_tracker;
@@ -48,7 +49,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
void set_auto_connect(bool auto_connect) { this->auto_connect_ = auto_connect; } void set_auto_connect(bool auto_connect) { this->auto_connect_ = auto_connect; }
void set_address(uint64_t address) { virtual void set_address(uint64_t address) {
this->address_ = address; this->address_ = address;
this->remote_bda_[0] = (address >> 40) & 0xFF; this->remote_bda_[0] = (address >> 40) & 0xFF;
this->remote_bda_[1] = (address >> 32) & 0xFF; this->remote_bda_[1] = (address >> 32) & 0xFF;
@@ -66,8 +67,9 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
(uint8_t) (this->address_ >> 0) & 0xff); (uint8_t) (this->address_ >> 0) & 0xff);
} }
} }
std::string address_str() const { return this->address_str_; } const std::string &address_str() const { return this->address_str_; }
#ifdef USE_ESP32_BLE_DEVICE
BLEService *get_service(espbt::ESPBTUUID uuid); BLEService *get_service(espbt::ESPBTUUID uuid);
BLEService *get_service(uint16_t uuid); BLEService *get_service(uint16_t uuid);
BLECharacteristic *get_characteristic(espbt::ESPBTUUID service, espbt::ESPBTUUID chr); BLECharacteristic *get_characteristic(espbt::ESPBTUUID service, espbt::ESPBTUUID chr);
@@ -78,6 +80,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
BLEDescriptor *get_descriptor(uint16_t handle); BLEDescriptor *get_descriptor(uint16_t handle);
// Get the configuration descriptor for the given characteristic handle. // Get the configuration descriptor for the given characteristic handle.
BLEDescriptor *get_config_descriptor(uint16_t handle); BLEDescriptor *get_config_descriptor(uint16_t handle);
#endif
float parse_char_value(uint8_t *value, uint16_t length); float parse_char_value(uint8_t *value, uint16_t length);
@@ -104,7 +107,9 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
// Group 2: Container types (grouped for memory optimization) // Group 2: Container types (grouped for memory optimization)
std::string address_str_{}; std::string address_str_{};
#ifdef USE_ESP32_BLE_DEVICE
std::vector<BLEService *> services_; std::vector<BLEService *> services_;
#endif
// Group 3: 4-byte types // Group 3: 4-byte types
int gattc_if_; int gattc_if_;
@@ -127,9 +132,13 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
// 6 bytes used, 2 bytes padding // 6 bytes used, 2 bytes padding
void log_event_(const char *name); void log_event_(const char *name);
void log_gattc_event_(const char *name);
void restore_medium_conn_params_();
void log_gattc_warning_(const char *operation, esp_gatt_status_t status);
void log_gattc_warning_(const char *operation, esp_err_t err);
void log_connection_params_(const char *param_type);
}; };
} // namespace esp32_ble_client } // namespace esphome::esp32_ble_client
} // namespace esphome
#endif // USE_ESP32 #endif // USE_ESP32

View File

@@ -1,11 +1,13 @@
#pragma once #pragma once
#include "esphome/core/defines.h"
#ifdef USE_ESP32 #ifdef USE_ESP32
#ifdef USE_ESP32_BLE_DEVICE
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
namespace esphome { namespace esphome::esp32_ble_client {
namespace esp32_ble_client {
namespace espbt = esphome::esp32_ble_tracker; namespace espbt = esphome::esp32_ble_tracker;
@@ -19,7 +21,7 @@ class BLEDescriptor {
BLECharacteristic *characteristic; BLECharacteristic *characteristic;
}; };
} // namespace esp32_ble_client } // namespace esphome::esp32_ble_client
} // namespace esphome
#endif // USE_ESP32_BLE_DEVICE
#endif // USE_ESP32 #endif // USE_ESP32

View File

@@ -4,9 +4,9 @@
#include "esphome/core/log.h" #include "esphome/core/log.h"
#ifdef USE_ESP32 #ifdef USE_ESP32
#ifdef USE_ESP32_BLE_DEVICE
namespace esphome { namespace esphome::esp32_ble_client {
namespace esp32_ble_client {
static const char *const TAG = "esp32_ble_client"; static const char *const TAG = "esp32_ble_client";
@@ -71,7 +71,7 @@ void BLEService::parse_characteristics() {
} }
} }
} // namespace esp32_ble_client } // namespace esphome::esp32_ble_client
} // namespace esphome
#endif // USE_ESP32_BLE_DEVICE
#endif // USE_ESP32 #endif // USE_ESP32

View File

@@ -1,6 +1,9 @@
#pragma once #pragma once
#include "esphome/core/defines.h"
#ifdef USE_ESP32 #ifdef USE_ESP32
#ifdef USE_ESP32_BLE_DEVICE
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
@@ -8,8 +11,7 @@
#include <vector> #include <vector>
namespace esphome { namespace esphome::esp32_ble_client {
namespace esp32_ble_client {
namespace espbt = esphome::esp32_ble_tracker; namespace espbt = esphome::esp32_ble_tracker;
@@ -30,7 +32,7 @@ class BLEService {
BLECharacteristic *get_characteristic(uint16_t uuid); BLECharacteristic *get_characteristic(uint16_t uuid);
}; };
} // namespace esp32_ble_client } // namespace esphome::esp32_ble_client
} // namespace esphome
#endif // USE_ESP32_BLE_DEVICE
#endif // USE_ESP32 #endif // USE_ESP32

View File

@@ -529,6 +529,7 @@ async def to_code_characteristic(service_var, char_conf):
async def to_code(config): async def to_code(config):
# Register the loggers this component needs # Register the loggers this component needs
esp32_ble.register_bt_logger(BTLoggers.GATT, BTLoggers.SMP) esp32_ble.register_bt_logger(BTLoggers.GATT, BTLoggers.SMP)
cg.add_define("USE_ESP32_BLE_UUID")
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
@@ -571,6 +572,7 @@ async def to_code(config):
config[CONF_ON_DISCONNECT], config[CONF_ON_DISCONNECT],
) )
cg.add_define("USE_ESP32_BLE_SERVER") cg.add_define("USE_ESP32_BLE_SERVER")
cg.add_define("USE_ESP32_BLE_ADVERTISING")
if CORE.using_esp_idf: if CORE.using_esp_idf:
add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True) add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True)
@@ -628,5 +630,4 @@ async def ble_server_descriptor_set_value(config, action_id, template_arg, args)
) )
async def ble_server_characteristic_notify(config, action_id, template_arg, args): async def ble_server_characteristic_notify(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID]) paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren) return cg.new_Pvariable(action_id, template_arg, paren)
return var

View File

@@ -36,6 +36,7 @@ from esphome.types import ConfigType
AUTO_LOAD = ["esp32_ble"] AUTO_LOAD = ["esp32_ble"]
DEPENDENCIES = ["esp32"] DEPENDENCIES = ["esp32"]
CODEOWNERS = ["@bdraco"]
KEY_ESP32_BLE_TRACKER = "esp32_ble_tracker" KEY_ESP32_BLE_TRACKER = "esp32_ble_tracker"
KEY_USED_CONNECTION_SLOTS = "used_connection_slots" KEY_USED_CONNECTION_SLOTS = "used_connection_slots"
@@ -354,11 +355,6 @@ async def to_code(config):
add_idf_sdkconfig_option( add_idf_sdkconfig_option(
"CONFIG_BTDM_CTRL_BLE_MAX_CONN", config[CONF_MAX_CONNECTIONS] "CONFIG_BTDM_CTRL_BLE_MAX_CONN", config[CONF_MAX_CONNECTIONS]
) )
# CONFIG_BT_GATTC_NOTIF_REG_MAX controls the number of
# max notifications in 5.x, setting CONFIG_BT_ACL_CONNECTIONS
# is enough in 4.x
# https://github.com/esphome/issues/issues/6808
add_idf_sdkconfig_option("CONFIG_BT_GATTC_NOTIF_REG_MAX", 9)
cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts
cg.add_define("USE_ESP32_BLE_CLIENT") cg.add_define("USE_ESP32_BLE_CLIENT")
@@ -377,6 +373,7 @@ async def _add_ble_features():
# Add feature-specific defines based on what's needed # Add feature-specific defines based on what's needed
if BLEFeatures.ESP_BT_DEVICE in _required_features: if BLEFeatures.ESP_BT_DEVICE in _required_features:
cg.add_define("USE_ESP32_BLE_DEVICE") cg.add_define("USE_ESP32_BLE_DEVICE")
cg.add_define("USE_ESP32_BLE_UUID")
ESP32_BLE_START_SCAN_ACTION_SCHEMA = cv.Schema( ESP32_BLE_START_SCAN_ACTION_SCHEMA = cv.Schema(

View File

@@ -41,6 +41,31 @@ static const char *const TAG = "esp32_ble_tracker";
ESP32BLETracker *global_esp32_ble_tracker = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) ESP32BLETracker *global_esp32_ble_tracker = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
const char *client_state_to_string(ClientState state) {
switch (state) {
case ClientState::INIT:
return "INIT";
case ClientState::DISCONNECTING:
return "DISCONNECTING";
case ClientState::IDLE:
return "IDLE";
case ClientState::SEARCHING:
return "SEARCHING";
case ClientState::DISCOVERED:
return "DISCOVERED";
case ClientState::READY_TO_CONNECT:
return "READY_TO_CONNECT";
case ClientState::CONNECTING:
return "CONNECTING";
case ClientState::CONNECTED:
return "CONNECTED";
case ClientState::ESTABLISHED:
return "ESTABLISHED";
default:
return "UNKNOWN";
}
}
float ESP32BLETracker::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; } float ESP32BLETracker::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; }
void ESP32BLETracker::setup() { void ESP32BLETracker::setup() {
@@ -49,13 +74,6 @@ 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;
} }
RAMAllocator<BLEScanResult> allocator;
this->scan_ring_buffer_ = allocator.allocate(SCAN_RESULT_BUFFER_SIZE);
if (this->scan_ring_buffer_ == nullptr) {
ESP_LOGE(TAG, "Could not allocate ring buffer for BLE Tracker!");
this->mark_failed();
}
global_esp32_ble_tracker = this; global_esp32_ble_tracker = this;
@@ -83,127 +101,49 @@ void ESP32BLETracker::loop() {
this->start_scan(); this->start_scan();
} }
} }
int connecting = 0;
int discovered = 0;
int searching = 0;
int disconnecting = 0;
for (auto *client : this->clients_) {
switch (client->state()) {
case ClientState::DISCONNECTING:
disconnecting++;
break;
case ClientState::DISCOVERED:
discovered++;
break;
case ClientState::SEARCHING:
searching++;
break;
case ClientState::CONNECTING:
case ClientState::READY_TO_CONNECT:
connecting++;
break;
default:
break;
}
}
if (connecting != connecting_ || discovered != discovered_ || searching != searching_ ||
disconnecting != disconnecting_) {
connecting_ = connecting;
discovered_ = discovered;
searching_ = searching;
disconnecting_ = disconnecting;
ESP_LOGD(TAG, "connecting: %d, discovered: %d, searching: %d, disconnecting: %d", connecting_, discovered_,
searching_, disconnecting_);
}
bool promote_to_connecting = discovered && !searching && !connecting;
// Process scan results from lock-free SPSC ring buffer // Check for scan timeout - moved here from scheduler to avoid false reboots
// Consumer side: This runs in the main loop thread // when the loop is blocked
if (this->scanner_state_ == ScannerState::RUNNING) { if (this->scanner_state_ == ScannerState::RUNNING) {
// Load our own index with relaxed ordering (we're the only writer) switch (this->scan_timeout_state_) {
uint8_t read_idx = this->ring_read_index_.load(std::memory_order_relaxed); case ScanTimeoutState::MONITORING: {
uint32_t now = App.get_loop_component_start_time();
// Load producer's index with acquire to see their latest writes uint32_t timeout_ms = this->scan_duration_ * 2000;
uint8_t write_idx = this->ring_write_index_.load(std::memory_order_acquire); // Robust time comparison that handles rollover correctly
// This works because unsigned arithmetic wraps around predictably
while (read_idx != write_idx) { if ((now - this->scan_start_time_) > timeout_ms) {
// Calculate how many contiguous results we can process in one batch // First time we've seen the timeout exceeded - wait one more loop iteration
// If write > read: process all results from read to write // This ensures all components have had a chance to process pending events
// If write <= read (wraparound): process from read to end of buffer first // This is because esp32_ble may not have run yet and called
size_t batch_size = (write_idx > read_idx) ? (write_idx - read_idx) : (SCAN_RESULT_BUFFER_SIZE - read_idx); // gap_scan_event_handler yet when the loop unblocks
ESP_LOGW(TAG, "Scan timeout exceeded");
// Process the batch for raw advertisements this->scan_timeout_state_ = ScanTimeoutState::EXCEEDED_WAIT;
if (this->raw_advertisements_) {
for (auto *listener : this->listeners_) {
listener->parse_devices(&this->scan_ring_buffer_[read_idx], batch_size);
}
for (auto *client : this->clients_) {
client->parse_devices(&this->scan_ring_buffer_[read_idx], batch_size);
} }
break;
} }
case ScanTimeoutState::EXCEEDED_WAIT:
// We've waited at least one full loop iteration, and scan is still running
ESP_LOGE(TAG, "Scan never terminated, rebooting");
App.reboot();
break;
// Process individual results for parsed advertisements case ScanTimeoutState::INACTIVE:
if (this->parse_advertisements_) { // This case should be unreachable - scanner and timeout states are always synchronized
#ifdef USE_ESP32_BLE_DEVICE break;
for (size_t i = 0; i < batch_size; i++) {
BLEScanResult &scan_result = this->scan_ring_buffer_[read_idx + i];
ESPBTDevice device;
device.parse_scan_rst(scan_result);
bool found = false;
for (auto *listener : this->listeners_) {
if (listener->parse_device(device))
found = true;
}
for (auto *client : this->clients_) {
if (client->parse_device(device)) {
found = true;
if (!connecting && client->state() == ClientState::DISCOVERED) {
promote_to_connecting = true;
}
}
}
if (!found && !this->scan_continuous_) {
this->print_bt_device_info(device);
}
}
#endif // USE_ESP32_BLE_DEVICE
}
// Update read index for entire batch
read_idx = (read_idx + batch_size) % 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);
} }
} }
if (this->scanner_state_ == ScannerState::STOPPED) {
this->end_of_scan_(); // Change state to IDLE ClientStateCounts counts = this->count_client_states_();
if (counts != this->client_state_counts_) {
this->client_state_counts_ = counts;
ESP_LOGD(TAG, "connecting: %d, discovered: %d, searching: %d, disconnecting: %d",
this->client_state_counts_.connecting, this->client_state_counts_.discovered,
this->client_state_counts_.searching, this->client_state_counts_.disconnecting);
} }
if (this->scanner_state_ == ScannerState::FAILED || if (this->scanner_state_ == ScannerState::FAILED ||
(this->scan_set_param_failed_ && this->scanner_state_ == ScannerState::RUNNING)) { (this->scan_set_param_failed_ && this->scanner_state_ == ScannerState::RUNNING)) {
this->stop_scan_(); this->handle_scanner_failure_();
if (this->scan_start_fail_count_ == std::numeric_limits<uint8_t>::max()) {
ESP_LOGE(TAG, "Scan could not restart after %d attempts, rebooting to restore stack (IDF)",
std::numeric_limits<uint8_t>::max());
App.reboot();
}
if (this->scan_start_failed_) {
ESP_LOGE(TAG, "Scan start failed: %d", this->scan_start_failed_);
this->scan_start_failed_ = ESP_BT_STATUS_SUCCESS;
}
if (this->scan_set_param_failed_) {
ESP_LOGE(TAG, "Scan set param failed: %d", this->scan_set_param_failed_);
this->scan_set_param_failed_ = ESP_BT_STATUS_SUCCESS;
}
} }
/* /*
@@ -218,13 +158,12 @@ void ESP32BLETracker::loop() {
https://github.com/espressif/esp-idf/issues/6688 https://github.com/espressif/esp-idf/issues/6688
*/ */
if (this->scanner_state_ == ScannerState::IDLE && !connecting && !disconnecting && !promote_to_connecting) { bool promote_to_connecting = counts.discovered && !counts.searching && !counts.connecting;
if (this->scanner_state_ == ScannerState::IDLE && !counts.connecting && !counts.disconnecting &&
!promote_to_connecting) {
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE #ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
if (this->coex_prefer_ble_) { this->update_coex_preference_(false);
this->coex_prefer_ble_ = false;
ESP_LOGD(TAG, "Setting coexistence preference to balanced.");
esp_coex_preference_set(ESP_COEX_PREFER_BALANCE); // Reset to default
}
#endif #endif
if (this->scan_continuous_) { if (this->scan_continuous_) {
this->start_scan_(false); // first = false this->start_scan_(false); // first = false
@@ -232,31 +171,13 @@ void ESP32BLETracker::loop() {
} }
// If there is a discovered client and no connecting // If there is a discovered client and no connecting
// clients and no clients using the scanner to search for // clients and no clients using the scanner to search for
// devices, then stop scanning and promote the discovered // devices, then promote the discovered client to ready to connect.
// client to ready to connect. // We check both RUNNING and IDLE states because:
// - RUNNING: gap_scan_event_handler initiates stop_scan_() but promotion can happen immediately
// - IDLE: Scanner has already stopped (naturally or by gap_scan_event_handler)
if (promote_to_connecting && if (promote_to_connecting &&
(this->scanner_state_ == ScannerState::RUNNING || this->scanner_state_ == ScannerState::IDLE)) { (this->scanner_state_ == ScannerState::RUNNING || this->scanner_state_ == ScannerState::IDLE)) {
for (auto *client : this->clients_) { this->try_promote_discovered_clients_();
if (client->state() == ClientState::DISCOVERED) {
if (this->scanner_state_ == ScannerState::RUNNING) {
ESP_LOGD(TAG, "Stopping scan to make connection");
this->stop_scan_();
} else if (this->scanner_state_ == ScannerState::IDLE) {
ESP_LOGD(TAG, "Promoting client to connect");
// We only want to promote one client at a time.
// once the scanner is fully stopped.
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
ESP_LOGD(TAG, "Setting coexistence to Bluetooth to make connection.");
if (!this->coex_prefer_ble_) {
this->coex_prefer_ble_ = true;
esp_coex_preference_set(ESP_COEX_PREFER_BT); // Prioritize Bluetooth
}
#endif
client->set_state(ClientState::READY_TO_CONNECT);
}
break;
}
}
} }
} }
@@ -272,18 +193,11 @@ void ESP32BLETracker::ble_before_disabled_event_handler() { this->stop_scan_();
void ESP32BLETracker::stop_scan_() { void ESP32BLETracker::stop_scan_() {
if (this->scanner_state_ != ScannerState::RUNNING && this->scanner_state_ != ScannerState::FAILED) { if (this->scanner_state_ != ScannerState::RUNNING && this->scanner_state_ != ScannerState::FAILED) {
if (this->scanner_state_ == ScannerState::IDLE) { ESP_LOGE(TAG, "Cannot stop scan: %s", this->scanner_state_to_string_(this->scanner_state_));
ESP_LOGE(TAG, "Scan is already stopped while trying to stop.");
} else if (this->scanner_state_ == ScannerState::STARTING) {
ESP_LOGE(TAG, "Scan is starting while trying to stop.");
} else if (this->scanner_state_ == ScannerState::STOPPING) {
ESP_LOGE(TAG, "Scan is already stopping while trying to stop.");
} else if (this->scanner_state_ == ScannerState::STOPPED) {
ESP_LOGE(TAG, "Scan is already stopped while trying to stop.");
}
return; return;
} }
this->cancel_timeout("scan"); // Reset timeout state machine when stopping scan
this->scan_timeout_state_ = ScanTimeoutState::INACTIVE;
this->set_scanner_state_(ScannerState::STOPPING); this->set_scanner_state_(ScannerState::STOPPING);
esp_err_t err = esp_ble_gap_stop_scanning(); esp_err_t err = esp_ble_gap_stop_scanning();
if (err != ESP_OK) { if (err != ESP_OK) {
@@ -298,17 +212,7 @@ void ESP32BLETracker::start_scan_(bool first) {
return; return;
} }
if (this->scanner_state_ != ScannerState::IDLE) { if (this->scanner_state_ != ScannerState::IDLE) {
if (this->scanner_state_ == ScannerState::STARTING) { this->log_unexpected_state_("start scan", ScannerState::IDLE);
ESP_LOGE(TAG, "Cannot start scan while already starting.");
} else if (this->scanner_state_ == ScannerState::RUNNING) {
ESP_LOGE(TAG, "Cannot start scan while already running.");
} else if (this->scanner_state_ == ScannerState::STOPPING) {
ESP_LOGE(TAG, "Cannot start scan while already stopping.");
} else if (this->scanner_state_ == ScannerState::FAILED) {
ESP_LOGE(TAG, "Cannot start scan while already failed.");
} else if (this->scanner_state_ == ScannerState::STOPPED) {
ESP_LOGE(TAG, "Cannot start scan while already stopped.");
}
return; return;
} }
this->set_scanner_state_(ScannerState::STARTING); this->set_scanner_state_(ScannerState::STARTING);
@@ -317,18 +221,19 @@ void ESP32BLETracker::start_scan_(bool first) {
for (auto *listener : this->listeners_) for (auto *listener : this->listeners_)
listener->on_scan_end(); listener->on_scan_end();
} }
#ifdef USE_ESP32_BLE_DEVICE
this->already_discovered_.clear(); this->already_discovered_.clear();
#endif
this->scan_params_.scan_type = this->scan_active_ ? BLE_SCAN_TYPE_ACTIVE : BLE_SCAN_TYPE_PASSIVE; this->scan_params_.scan_type = this->scan_active_ ? BLE_SCAN_TYPE_ACTIVE : BLE_SCAN_TYPE_PASSIVE;
this->scan_params_.own_addr_type = BLE_ADDR_TYPE_PUBLIC; this->scan_params_.own_addr_type = BLE_ADDR_TYPE_PUBLIC;
this->scan_params_.scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL; this->scan_params_.scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL;
this->scan_params_.scan_interval = this->scan_interval_; this->scan_params_.scan_interval = this->scan_interval_;
this->scan_params_.scan_window = this->scan_window_; this->scan_params_.scan_window = this->scan_window_;
// Start timeout before scan is started. Otherwise scan never starts if any error. // Start timeout monitoring in loop() instead of using scheduler
this->set_timeout("scan", this->scan_duration_ * 2000, []() { // This prevents false reboots when the loop is blocked
ESP_LOGE(TAG, "Scan never terminated, rebooting to restore stack (IDF)"); this->scan_start_time_ = App.get_loop_component_start_time();
App.reboot(); this->scan_timeout_state_ = ScanTimeoutState::MONITORING;
});
esp_err_t err = esp_ble_gap_set_scan_params(&this->scan_params_); esp_err_t err = esp_ble_gap_set_scan_params(&this->scan_params_);
if (err != ESP_OK) { if (err != ESP_OK) {
@@ -342,21 +247,6 @@ void ESP32BLETracker::start_scan_(bool first) {
} }
} }
void ESP32BLETracker::end_of_scan_() {
// The lock must be held when calling this function.
if (this->scanner_state_ != ScannerState::STOPPED) {
ESP_LOGE(TAG, "end_of_scan_ called while scanner is not stopped.");
return;
}
ESP_LOGD(TAG, "End of scan, set scanner state to IDLE.");
this->already_discovered_.clear();
this->cancel_timeout("scan");
for (auto *listener : this->listeners_)
listener->on_scan_end();
this->set_scanner_state_(ScannerState::IDLE);
}
void ESP32BLETracker::register_client(ESPBTClient *client) { void ESP32BLETracker::register_client(ESPBTClient *client) {
client->app_id = ++this->app_id_; client->app_id = ++this->app_id_;
this->clients_.push_back(client); this->clients_.push_back(client);
@@ -389,6 +279,8 @@ 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) {
// Note: This handler is called from the main loop context, not directly from the BT task.
// The esp32_ble component queues events via enqueue_ble_event() and processes them in loop().
switch (event) { switch (event) {
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);
@@ -409,51 +301,32 @@ void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_ga
} }
void ESP32BLETracker::gap_scan_event_handler(const BLEScanResult &scan_result) { void ESP32BLETracker::gap_scan_event_handler(const BLEScanResult &scan_result) {
// Note: This handler is called from the main loop context via esp32_ble's event queue.
// We process advertisements immediately instead of buffering them.
ESP_LOGV(TAG, "gap_scan_result - event %d", scan_result.search_evt); ESP_LOGV(TAG, "gap_scan_result - event %d", scan_result.search_evt);
if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) { if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) {
// Lock-free SPSC ring buffer write (Producer side) // Process the scan result immediately
// This runs in the ESP-IDF Bluetooth stack callback thread bool found_discovered_client = this->process_scan_result_(scan_result);
// IMPORTANT: Only this thread writes to ring_write_index_
// Load our own index with relaxed ordering (we're the only writer) // If we found a discovered client that needs promotion, stop scanning
uint8_t write_idx = this->ring_write_index_.load(std::memory_order_relaxed); // This replaces the promote_to_connecting logic from loop()
uint8_t next_write_idx = (write_idx + 1) % SCAN_RESULT_BUFFER_SIZE; if (found_discovered_client && this->scanner_state_ == ScannerState::RUNNING) {
ESP_LOGD(TAG, "Found discovered client, stopping scan for connection");
// Load consumer's index with acquire to see their latest updates this->stop_scan_();
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) { } else if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_CMPL_EVT) {
// Scan finished on its own // Scan finished on its own
if (this->scanner_state_ != ScannerState::RUNNING) { if (this->scanner_state_ != ScannerState::RUNNING) {
if (this->scanner_state_ == ScannerState::STOPPING) { this->log_unexpected_state_("scan complete", ScannerState::RUNNING);
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); // Scan completed naturally, perform cleanup and transition to IDLE
this->cleanup_scan_state_(false);
} }
} }
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) {
// Called from main loop context via gap_event_handler after being queued from BT task
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) {
this->scan_set_param_failed_ = ESP_BT_STATUS_SUCCESS; this->scan_set_param_failed_ = ESP_BT_STATUS_SUCCESS;
@@ -463,20 +336,11 @@ void ESP32BLETracker::gap_scan_set_param_complete_(const esp_ble_gap_cb_param_t:
} }
void ESP32BLETracker::gap_scan_start_complete_(const esp_ble_gap_cb_param_t::ble_scan_start_cmpl_evt_param &param) { void ESP32BLETracker::gap_scan_start_complete_(const esp_ble_gap_cb_param_t::ble_scan_start_cmpl_evt_param &param) {
// Called from main loop context via gap_event_handler after being queued from BT task
ESP_LOGV(TAG, "gap_scan_start_complete - status %d", param.status); ESP_LOGV(TAG, "gap_scan_start_complete - status %d", param.status);
this->scan_start_failed_ = param.status; this->scan_start_failed_ = param.status;
if (this->scanner_state_ != ScannerState::STARTING) { if (this->scanner_state_ != ScannerState::STARTING) {
if (this->scanner_state_ == ScannerState::RUNNING) { this->log_unexpected_state_("start complete", ScannerState::STARTING);
ESP_LOGE(TAG, "Scan was already running when start complete.");
} else if (this->scanner_state_ == ScannerState::STOPPING) {
ESP_LOGE(TAG, "Scan was stopping when start complete.");
} else if (this->scanner_state_ == ScannerState::FAILED) {
ESP_LOGE(TAG, "Scan was in failed state when start complete.");
} else if (this->scanner_state_ == ScannerState::IDLE) {
ESP_LOGE(TAG, "Scan was idle when start complete.");
} else if (this->scanner_state_ == ScannerState::STOPPED) {
ESP_LOGE(TAG, "Scan was stopped when start complete.");
}
} }
if (param.status == ESP_BT_STATUS_SUCCESS) { if (param.status == ESP_BT_STATUS_SUCCESS) {
this->scan_start_fail_count_ = 0; this->scan_start_fail_count_ = 0;
@@ -490,21 +354,15 @@ void ESP32BLETracker::gap_scan_start_complete_(const esp_ble_gap_cb_param_t::ble
} }
void ESP32BLETracker::gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param &param) { void ESP32BLETracker::gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param &param) {
// Called from main loop context via gap_event_handler after being queued from BT task
// This allows us to safely transition to IDLE state and perform cleanup without race conditions
ESP_LOGV(TAG, "gap_scan_stop_complete - status %d", param.status); ESP_LOGV(TAG, "gap_scan_stop_complete - status %d", param.status);
if (this->scanner_state_ != ScannerState::STOPPING) { if (this->scanner_state_ != ScannerState::STOPPING) {
if (this->scanner_state_ == ScannerState::RUNNING) { this->log_unexpected_state_("stop complete", ScannerState::STOPPING);
ESP_LOGE(TAG, "Scan was not running when stop complete.");
} else if (this->scanner_state_ == ScannerState::STARTING) {
ESP_LOGE(TAG, "Scan was not started when stop complete.");
} else if (this->scanner_state_ == ScannerState::FAILED) {
ESP_LOGE(TAG, "Scan was in failed state when stop complete.");
} else if (this->scanner_state_ == ScannerState::IDLE) {
ESP_LOGE(TAG, "Scan was idle when stop complete.");
} else if (this->scanner_state_ == ScannerState::STOPPED) {
ESP_LOGE(TAG, "Scan was stopped when stop complete.");
}
} }
this->set_scanner_state_(ScannerState::STOPPED);
// Perform cleanup and transition to IDLE
this->cleanup_scan_state_(true);
} }
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,
@@ -781,28 +639,10 @@ void ESP32BLETracker::dump_config() {
" Continuous Scanning: %s", " Continuous Scanning: %s",
this->scan_duration_, this->scan_interval_ * 0.625f, this->scan_window_ * 0.625f, this->scan_duration_, this->scan_interval_ * 0.625f, this->scan_window_ * 0.625f,
this->scan_active_ ? "ACTIVE" : "PASSIVE", YESNO(this->scan_continuous_)); this->scan_active_ ? "ACTIVE" : "PASSIVE", YESNO(this->scan_continuous_));
switch (this->scanner_state_) { ESP_LOGCONFIG(TAG, " Scanner State: %s", this->scanner_state_to_string_(this->scanner_state_));
case ScannerState::IDLE: ESP_LOGCONFIG(TAG, " Connecting: %d, discovered: %d, searching: %d, disconnecting: %d",
ESP_LOGCONFIG(TAG, " Scanner State: IDLE"); this->client_state_counts_.connecting, this->client_state_counts_.discovered,
break; this->client_state_counts_.searching, this->client_state_counts_.disconnecting);
case ScannerState::STARTING:
ESP_LOGCONFIG(TAG, " Scanner State: STARTING");
break;
case ScannerState::RUNNING:
ESP_LOGCONFIG(TAG, " Scanner State: RUNNING");
break;
case ScannerState::STOPPING:
ESP_LOGCONFIG(TAG, " Scanner State: STOPPING");
break;
case ScannerState::STOPPED:
ESP_LOGCONFIG(TAG, " Scanner State: STOPPED");
break;
case ScannerState::FAILED:
ESP_LOGCONFIG(TAG, " Scanner State: FAILED");
break;
}
ESP_LOGCONFIG(TAG, " Connecting: %d, discovered: %d, searching: %d, disconnecting: %d", connecting_, discovered_,
searching_, disconnecting_);
if (this->scan_start_fail_count_) { if (this->scan_start_fail_count_) {
ESP_LOGCONFIG(TAG, " Scan Start Fail Count: %d", this->scan_start_fail_count_); ESP_LOGCONFIG(TAG, " Scan Start Fail Count: %d", this->scan_start_fail_count_);
} }
@@ -879,8 +719,158 @@ bool ESPBTDevice::resolve_irk(const uint8_t *irk) const {
return ecb_ciphertext[15] == (addr64 & 0xff) && ecb_ciphertext[14] == ((addr64 >> 8) & 0xff) && return ecb_ciphertext[15] == (addr64 & 0xff) && ecb_ciphertext[14] == ((addr64 >> 8) & 0xff) &&
ecb_ciphertext[13] == ((addr64 >> 16) & 0xff); ecb_ciphertext[13] == ((addr64 >> 16) & 0xff);
} }
bool ESP32BLETracker::has_connecting_clients_() const {
for (auto *client : this->clients_) {
auto state = client->state();
if (state == ClientState::CONNECTING || state == ClientState::READY_TO_CONNECT) {
return true;
}
}
return false;
}
#endif // USE_ESP32_BLE_DEVICE #endif // USE_ESP32_BLE_DEVICE
bool ESP32BLETracker::process_scan_result_(const BLEScanResult &scan_result) {
bool found_discovered_client = false;
// Process raw advertisements
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);
}
}
// Process parsed advertisements
if (this->parse_advertisements_) {
#ifdef USE_ESP32_BLE_DEVICE
ESPBTDevice device;
device.parse_scan_rst(scan_result);
bool found = false;
for (auto *listener : this->listeners_) {
if (listener->parse_device(device))
found = true;
}
for (auto *client : this->clients_) {
if (client->parse_device(device)) {
found = true;
// Check if this client is discovered and needs promotion
if (client->state() == ClientState::DISCOVERED) {
// Only check for connecting clients if we found a discovered client
// This matches the original logic: !connecting && client->state() == DISCOVERED
if (!this->has_connecting_clients_()) {
found_discovered_client = true;
}
}
}
}
if (!found && !this->scan_continuous_) {
this->print_bt_device_info(device);
}
#endif // USE_ESP32_BLE_DEVICE
}
return found_discovered_client;
}
void ESP32BLETracker::cleanup_scan_state_(bool is_stop_complete) {
ESP_LOGD(TAG, "Scan %scomplete, set scanner state to IDLE.", is_stop_complete ? "stop " : "");
#ifdef USE_ESP32_BLE_DEVICE
this->already_discovered_.clear();
#endif
// Reset timeout state machine instead of cancelling scheduler timeout
this->scan_timeout_state_ = ScanTimeoutState::INACTIVE;
for (auto *listener : this->listeners_)
listener->on_scan_end();
this->set_scanner_state_(ScannerState::IDLE);
}
void ESP32BLETracker::handle_scanner_failure_() {
this->stop_scan_();
if (this->scan_start_fail_count_ == std::numeric_limits<uint8_t>::max()) {
ESP_LOGE(TAG, "Scan could not restart after %d attempts, rebooting to restore stack (IDF)",
std::numeric_limits<uint8_t>::max());
App.reboot();
}
if (this->scan_start_failed_) {
ESP_LOGE(TAG, "Scan start failed: %d", this->scan_start_failed_);
this->scan_start_failed_ = ESP_BT_STATUS_SUCCESS;
}
if (this->scan_set_param_failed_) {
ESP_LOGE(TAG, "Scan set param failed: %d", this->scan_set_param_failed_);
this->scan_set_param_failed_ = ESP_BT_STATUS_SUCCESS;
}
}
void ESP32BLETracker::try_promote_discovered_clients_() {
// Only promote the first discovered client to avoid multiple simultaneous connections
for (auto *client : this->clients_) {
if (client->state() != ClientState::DISCOVERED) {
continue;
}
if (this->scanner_state_ == ScannerState::RUNNING) {
ESP_LOGD(TAG, "Stopping scan to make connection");
this->stop_scan_();
// Don't wait for scan stop complete - promote immediately.
// This is safe because ESP-IDF processes BLE commands sequentially through its internal mailbox queue.
// This guarantees that the stop scan command will be fully processed before any subsequent connect command,
// preventing race conditions or overlapping operations.
}
ESP_LOGD(TAG, "Promoting client to connect");
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
this->update_coex_preference_(true);
#endif
client->set_state(ClientState::READY_TO_CONNECT);
break;
}
}
const char *ESP32BLETracker::scanner_state_to_string_(ScannerState state) const {
switch (state) {
case ScannerState::IDLE:
return "IDLE";
case ScannerState::STARTING:
return "STARTING";
case ScannerState::RUNNING:
return "RUNNING";
case ScannerState::STOPPING:
return "STOPPING";
case ScannerState::FAILED:
return "FAILED";
default:
return "UNKNOWN";
}
}
void ESP32BLETracker::log_unexpected_state_(const char *operation, ScannerState expected_state) const {
ESP_LOGE(TAG, "Unexpected state: %s on %s, expected: %s", this->scanner_state_to_string_(this->scanner_state_),
operation, this->scanner_state_to_string_(expected_state));
}
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
void ESP32BLETracker::update_coex_preference_(bool force_ble) {
if (force_ble && !this->coex_prefer_ble_) {
ESP_LOGD(TAG, "Setting coexistence to Bluetooth to make connection.");
this->coex_prefer_ble_ = true;
esp_coex_preference_set(ESP_COEX_PREFER_BT); // Prioritize Bluetooth
} else if (!force_ble && this->coex_prefer_ble_) {
ESP_LOGD(TAG, "Setting coexistence preference to balanced.");
this->coex_prefer_ble_ = false;
esp_coex_preference_set(ESP_COEX_PREFER_BALANCE); // Reset to default
}
}
#endif
} // namespace esphome::esp32_ble_tracker } // namespace esphome::esp32_ble_tracker
#endif // USE_ESP32 #endif // USE_ESP32

View File

@@ -6,7 +6,6 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include <array> #include <array>
#include <atomic>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -21,6 +20,7 @@
#include "esphome/components/esp32_ble/ble.h" #include "esphome/components/esp32_ble/ble.h"
#include "esphome/components/esp32_ble/ble_uuid.h" #include "esphome/components/esp32_ble/ble_uuid.h"
#include "esphome/components/esp32_ble/ble_scan_result.h"
namespace esphome::esp32_ble_tracker { namespace esphome::esp32_ble_tracker {
@@ -33,10 +33,12 @@ enum AdvertisementParserType {
RAW_ADVERTISEMENTS, RAW_ADVERTISEMENTS,
}; };
#ifdef USE_ESP32_BLE_UUID
struct ServiceData { struct ServiceData {
ESPBTUUID uuid; ESPBTUUID uuid;
adv_data_t data; adv_data_t data;
}; };
#endif
#ifdef USE_ESP32_BLE_DEVICE #ifdef USE_ESP32_BLE_DEVICE
class ESPBLEiBeacon { class ESPBLEiBeacon {
@@ -136,6 +138,20 @@ class ESPBTDeviceListener {
ESP32BLETracker *parent_{nullptr}; ESP32BLETracker *parent_{nullptr};
}; };
struct ClientStateCounts {
uint8_t connecting = 0;
uint8_t discovered = 0;
uint8_t searching = 0;
uint8_t disconnecting = 0;
bool operator==(const ClientStateCounts &other) const {
return connecting == other.connecting && discovered == other.discovered && searching == other.searching &&
disconnecting == other.disconnecting;
}
bool operator!=(const ClientStateCounts &other) const { return !(*this == other); }
};
enum class ClientState : uint8_t { enum class ClientState : uint8_t {
// Connection is allocated // Connection is allocated
INIT, INIT,
@@ -158,20 +174,21 @@ enum class ClientState : uint8_t {
}; };
enum class ScannerState { enum class ScannerState {
// Scanner is idle, init state, set from the main loop when processing STOPPED // Scanner is idle, init state
IDLE, IDLE,
// Scanner is starting, set from the main loop only // Scanner is starting
STARTING, STARTING,
// Scanner is running, set from the ESP callback only // Scanner is running
RUNNING, RUNNING,
// Scanner failed to start, set from the ESP callback only // Scanner failed to start
FAILED, FAILED,
// Scanner is stopping, set from the main loop only // Scanner is stopping
STOPPING, STOPPING,
// Scanner is stopped, set from the ESP callback only
STOPPED,
}; };
// Helper function to convert ClientState to string
const char *client_state_to_string(ClientState state);
enum class ConnectionType : uint8_t { 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.
@@ -262,8 +279,6 @@ class ESP32BLETracker : public Component,
void stop_scan_(); void stop_scan_();
/// Start a single scan by setting up the parameters and doing some esp-idf calls. /// Start a single scan by setting up the parameters and doing some esp-idf calls.
void start_scan_(bool first); void start_scan_(bool first);
/// Called when a scan ends
void end_of_scan_();
/// Called when a `ESP_GAP_BLE_SCAN_RESULT_EVT` event is received. /// Called when a `ESP_GAP_BLE_SCAN_RESULT_EVT` event is received.
void gap_scan_result_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param &param); void gap_scan_result_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param &param);
/// Called when a `ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT` event is received. /// Called when a `ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT` event is received.
@@ -274,47 +289,94 @@ class ESP32BLETracker : public Component,
void gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param &param); void gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param &param);
/// 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);
/// Common cleanup logic when transitioning scanner to IDLE state
void cleanup_scan_state_(bool is_stop_complete);
/// Process a single scan result immediately
/// Returns true if a discovered client needs promotion to READY_TO_CONNECT
bool process_scan_result_(const BLEScanResult &scan_result);
#ifdef USE_ESP32_BLE_DEVICE
/// Check if any clients are in connecting or ready to connect state
bool has_connecting_clients_() const;
#endif
/// Handle scanner failure states
void handle_scanner_failure_();
/// Try to promote discovered clients to ready to connect
void try_promote_discovered_clients_();
/// Convert scanner state enum to string for logging
const char *scanner_state_to_string_(ScannerState state) const;
/// Log an unexpected scanner state
void log_unexpected_state_(const char *operation, ScannerState expected_state) const;
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
/// Update BLE coexistence preference
void update_coex_preference_(bool force_ble);
#endif
/// Count clients in each state
ClientStateCounts count_client_states_() const {
ClientStateCounts counts;
for (auto *client : this->clients_) {
switch (client->state()) {
case ClientState::DISCONNECTING:
counts.disconnecting++;
break;
case ClientState::DISCOVERED:
counts.discovered++;
break;
case ClientState::SEARCHING:
counts.searching++;
break;
case ClientState::CONNECTING:
case ClientState::READY_TO_CONNECT:
counts.connecting++;
break;
default:
break;
}
}
return counts;
}
uint8_t app_id_{0}; // Group 1: Large objects (12+ bytes) - vectors and callback manager
std::vector<ESPBTDeviceListener *> listeners_;
std::vector<ESPBTClient *> clients_;
CallbackManager<void(ScannerState)> scanner_state_callbacks_;
#ifdef USE_ESP32_BLE_DEVICE
/// 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_;
std::vector<ESPBTDeviceListener *> listeners_; #endif
/// Client parameters.
std::vector<ESPBTClient *> clients_; // Group 2: Structs (aligned to 4 bytes)
/// A structure holding the ESP BLE scan parameters. /// A structure holding the ESP BLE scan parameters.
esp_ble_scan_params_t scan_params_; esp_ble_scan_params_t scan_params_;
ClientStateCounts client_state_counts_;
// Group 3: 4-byte types
/// The interval in seconds to perform scans. /// The interval in seconds to perform scans.
uint32_t scan_duration_; uint32_t scan_duration_;
uint32_t scan_interval_; uint32_t scan_interval_;
uint32_t scan_window_; uint32_t scan_window_;
esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS};
esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS};
// Group 4: 1-byte types (enums, uint8_t, bool)
uint8_t app_id_{0};
uint8_t scan_start_fail_count_{0}; uint8_t scan_start_fail_count_{0};
ScannerState scanner_state_{ScannerState::IDLE};
bool scan_continuous_; bool scan_continuous_;
bool scan_active_; bool scan_active_;
ScannerState scanner_state_{ScannerState::IDLE};
CallbackManager<void(ScannerState)> scanner_state_callbacks_;
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};
// Lock-free Single-Producer Single-Consumer (SPSC) ring buffer for scan results
// Producer: ESP-IDF Bluetooth stack callback (gap_scan_event_handler)
// Consumer: ESPHome main loop (loop() method)
// This design ensures zero blocking in the BT callback and prevents scan result loss
BLEScanResult *scan_ring_buffer_;
std::atomic<uint8_t> ring_write_index_{0}; // Written only by BT callback (producer)
std::atomic<uint8_t> ring_read_index_{0}; // Written only by main loop (consumer)
std::atomic<uint16_t> scan_results_dropped_{0}; // Tracks buffer overflow events
esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS};
esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS};
int connecting_{0};
int discovered_{0};
int searching_{0};
int disconnecting_{0};
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE #ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
bool coex_prefer_ble_{false}; bool coex_prefer_ble_{false};
#endif #endif
// Scan timeout state machine
enum class ScanTimeoutState : uint8_t {
INACTIVE, // No timeout monitoring
MONITORING, // Actively monitoring for timeout
EXCEEDED_WAIT, // Timeout exceeded, waiting one loop before reboot
};
uint32_t scan_start_time_{0};
ScanTimeoutState scan_timeout_state_{ScanTimeoutState::INACTIVE};
}; };
// NOLINTNEXTLINE // NOLINTNEXTLINE

View File

@@ -345,7 +345,7 @@ async def to_code(config):
cg.add_define("USE_CAMERA") cg.add_define("USE_CAMERA")
if CORE.using_esp_idf: if CORE.using_esp_idf:
add_idf_component(name="espressif/esp32-camera", ref="2.0.15") add_idf_component(name="espressif/esp32-camera", ref="2.1.1")
for conf in config.get(CONF_ON_STREAM_START, []): for conf in config.get(CONF_ON_STREAM_START, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)

View File

@@ -2,11 +2,7 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#ifdef USE_ESP32 #if defined(USE_ESP32_VARIANT_ESP32) || defined(USE_ESP32_VARIANT_ESP32S2)
#ifdef USE_ARDUINO
#include <esp32-hal-dac.h>
#endif
namespace esphome { namespace esphome {
namespace esp32_dac { namespace esp32_dac {
@@ -23,18 +19,12 @@ void ESP32DAC::setup() {
this->pin_->setup(); this->pin_->setup();
this->turn_off(); this->turn_off();
#ifdef USE_ESP_IDF
const dac_channel_t channel = this->pin_->get_pin() == DAC0_PIN ? DAC_CHAN_0 : DAC_CHAN_1; const dac_channel_t channel = this->pin_->get_pin() == DAC0_PIN ? DAC_CHAN_0 : DAC_CHAN_1;
const dac_oneshot_config_t oneshot_cfg{channel}; const dac_oneshot_config_t oneshot_cfg{channel};
dac_oneshot_new_channel(&oneshot_cfg, &this->dac_handle_); dac_oneshot_new_channel(&oneshot_cfg, &this->dac_handle_);
#endif
} }
void ESP32DAC::on_safe_shutdown() { void ESP32DAC::on_safe_shutdown() { dac_oneshot_del_channel(this->dac_handle_); }
#ifdef USE_ESP_IDF
dac_oneshot_del_channel(this->dac_handle_);
#endif
}
void ESP32DAC::dump_config() { void ESP32DAC::dump_config() {
ESP_LOGCONFIG(TAG, "ESP32 DAC:"); ESP_LOGCONFIG(TAG, "ESP32 DAC:");
@@ -48,15 +38,10 @@ void ESP32DAC::write_state(float state) {
state = state * 255; state = state * 255;
#ifdef USE_ESP_IDF
dac_oneshot_output_voltage(this->dac_handle_, state); dac_oneshot_output_voltage(this->dac_handle_, state);
#endif
#ifdef USE_ARDUINO
dacWrite(this->pin_->get_pin(), state);
#endif
} }
} // namespace esp32_dac } // namespace esp32_dac
} // namespace esphome } // namespace esphome
#endif #endif // USE_ESP32_VARIANT_ESP32 || USE_ESP32_VARIANT_ESP32S2

View File

@@ -1,15 +1,13 @@
#pragma once #pragma once
#include "esphome/components/output/float_output.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/automation.h"
#include "esphome/components/output/float_output.h"
#ifdef USE_ESP32 #if defined(USE_ESP32_VARIANT_ESP32) || defined(USE_ESP32_VARIANT_ESP32S2)
#ifdef USE_ESP_IDF
#include <driver/dac_oneshot.h> #include <driver/dac_oneshot.h>
#endif
namespace esphome { namespace esphome {
namespace esp32_dac { namespace esp32_dac {
@@ -29,12 +27,10 @@ class ESP32DAC : public output::FloatOutput, public Component {
void write_state(float state) override; void write_state(float state) override;
InternalGPIOPin *pin_; InternalGPIOPin *pin_;
#ifdef USE_ESP_IDF
dac_oneshot_handle_t dac_handle_; dac_oneshot_handle_t dac_handle_;
#endif
}; };
} // namespace esp32_dac } // namespace esp32_dac
} // namespace esphome } // namespace esphome
#endif #endif // USE_ESP32_VARIANT_ESP32 || USE_ESP32_VARIANT_ESP32S2

View File

@@ -42,9 +42,6 @@ static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size
symbols[i] = params->bit0; symbols[i] = params->bit0;
} }
} }
if ((index + 1) >= size && params->reset.duration0 == 0 && params->reset.duration1 == 0) {
*done = true;
}
return RMT_SYMBOLS_PER_BYTE; return RMT_SYMBOLS_PER_BYTE;
} }
@@ -110,7 +107,7 @@ void ESP32RMTLEDStripLightOutput::setup() {
memset(&encoder, 0, sizeof(encoder)); memset(&encoder, 0, sizeof(encoder));
encoder.callback = encoder_callback; encoder.callback = encoder_callback;
encoder.arg = &this->params_; encoder.arg = &this->params_;
encoder.min_chunk_size = 8; encoder.min_chunk_size = RMT_SYMBOLS_PER_BYTE;
if (rmt_new_simple_encoder(&encoder, &this->encoder_) != ESP_OK) { if (rmt_new_simple_encoder(&encoder, &this->encoder_) != ESP_OK) {
ESP_LOGE(TAG, "Encoder creation failed"); ESP_LOGE(TAG, "Encoder creation failed");
this->mark_failed(); this->mark_failed();

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