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

Compare commits

...

249 Commits

Author SHA1 Message Date
J. Nick Koston
635cb08e63 Set ble tx power 2025-08-28 14:41:18 -05:00
J. Nick Koston
a92a08c2de [api] Fix string lifetime issue in fill_and_encode_entity_info for dynamic object_id (#10482) 2025-08-28 18:40:36 +00:00
Vinicius Fortuna
75595b08be [rtttl] Fix RTTTL for speakers (#10381) 2025-08-28 13:53:57 +12:00
J. Nick Koston
3c7aba0681 Fix AttributeError when uploading OTA to offline OpenThread devices (#10459) 2025-08-28 09:23:43 +12:00
Clyde Stubbs
e5d1c30797 [wifi] Fix retry with hidden networks. (#10445) 2025-08-28 09:16:26 +12:00
Clyde Stubbs
c171d13c8c [i2c] Perform register reads as single transactions (#10389)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-08-27 19:30:33 +10:00
Clyde Stubbs
65d63de9b6 [mipi_spi] Fix dimensions (#10443) 2025-08-27 19:30:01 +10:00
J. Nick Koston
9e712e4127 [wifi] Fix reconnection failures after adapter restart by not clearing netif pointers (#10458) 2025-08-26 23:49:47 -05:00
Clyde Stubbs
9007621fd7 Revert "[core] Dont copy platform source files if there are no entities of that type" (#10441) 2025-08-26 09:15:44 +10:00
Thomas Rupprecht
c01a26607e improve const imports of esphome.const (#10438) 2025-08-26 09:45:03 +12:00
Jesse Hills
f6ca70970f Merge branch 'release' into dev 2025-08-26 08:48:51 +12:00
Jesse Hills
4dc11f05a7 Merge pull request #10427 from esphome/bump-2025.8.1
2025.8.1
2025-08-26 08:48:10 +12:00
Jesse Hills
5e508f7461 [core] Dont copy platform source files if there are no entities of that type (#10436) 2025-08-25 14:46:54 -05:00
Jonathan Rascher
2aceb56606 Merge commit from fork
Ensures auth check doesn't pass erroneously when the client-supplied
digest is shorter than the correct digest, but happens to match a
prefix of the correct value (e.g., same username + certain substrings of
the password).
2025-08-25 16:00:04 +12:00
Jesse Hills
d071a074ef Bump version to 2025.8.1 2025-08-25 15:59:35 +12:00
Clyde Stubbs
7a459c8c20 [web_server] Use oi.esphome.io for css and js assets (#10296) 2025-08-25 15:59:35 +12:00
J. Nick Koston
aebd21958a [test] Add integration test for light effect memory corruption fix (#10417) 2025-08-25 15:59:35 +12:00
J. Nick Koston
c542db8bfe [esp32_ble_tracker] Fix on_scan_end trigger compilation without USE_ESP32_BLE_DEVICE (#10399) 2025-08-25 15:59:35 +12:00
Clyde Stubbs
d9dcfe66ec [lvgl] Fix meter rotation (#10342) 2025-08-25 15:59:35 +12:00
J. Nick Koston
8517c2e903 [esp32_ble_client] Reduce log level for harmless BLE timeout race conditions (#10339)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-25 15:59:34 +12:00
J. Nick Koston
684384892a [deep_sleep] Fix ESP32-C6 compilation error with gpio_deep_sleep_hold_en() (#10345) 2025-08-25 15:59:34 +12:00
J. Nick Koston
d560831d79 [script] Fix parallel mode scripts with delays cancelling each other (#10324) 2025-08-25 15:59:34 +12:00
J. Nick Koston
fcc3c8e1b6 [esp32_ble] Increase GATT connection retry count to use full timeout window (#10376) 2025-08-25 15:59:34 +12:00
J. Nick Koston
959ffde60e [esp32_ble_client] Optimize BLE connection parameters for different connection types (#10356) 2025-08-25 15:59:34 +12:00
J. Nick Koston
07715dd50f [pvvx_mithermometer] Fix race condition with BLE authentication (#10327) 2025-08-25 15:59:34 +12:00
J. Nick Koston
03836ee2d2 [core] Improve error reporting for entity name conflicts with non-ASCII characters (#10329) 2025-08-25 15:59:34 +12:00
Clyde Stubbs
50408d9abb [http_request] Fix for host after ArduinoJson library bump (#10348) 2025-08-25 15:59:34 +12:00
Jesse Hills
0de7259428 [api] Add `USE_API_HOMEASSISTANT_SERVICES if using tag_scanned` action (#10316) 2025-08-25 15:59:34 +12:00
J. Nick Koston
d054709c2d [esp32_ble_client] Add log helper functions to reduce flash usage by 120 bytes (#10243) 2025-08-25 15:59:34 +12:00
J. Nick Koston
da16887915 [api] Add zero-copy StringRef methods for compilation_time and effect_name (#10257) 2025-08-25 15:59:34 +12:00
Jonathan Rascher
6da8ec8d55 Merge commit from fork
Ensures auth check doesn't pass erroneously when the client-supplied
digest is shorter than the correct digest, but happens to match a
prefix of the correct value (e.g., same username + certain substrings of
the password).
2025-08-25 15:40:19 +12:00
J. Nick Koston
d2752b38c9 [core] Fix preference storage to account for device_id (#10333)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-25 12:22:16 +12:00
J. Nick Koston
6004367ee2 [esp32_ble_client] Add missing ESP_GATTC_UNREG_FOR_NOTIFY_EVT logging (#10347) 2025-08-25 12:07:04 +12:00
Thomas Rupprecht
ecfeb8e4d3 improve AI instructions (#10416) 2025-08-25 11:51:28 +12:00
Clyde Stubbs
456c31262d [web_server] Use oi.esphome.io for css and js assets (#10296) 2025-08-25 09:04:32 +12:00
J. Nick Koston
9f02575287 [test] Add integration test for light effect memory corruption fix (#10417) 2025-08-25 08:58:46 +12:00
J. Nick Koston
07bca6103f [esp32_ble_tracker] Fix on_scan_end trigger compilation without USE_ESP32_BLE_DEVICE (#10399) 2025-08-25 08:57:09 +12:00
Clyde Stubbs
a58c3950bc [lvgl] Fix meter rotation (#10342) 2025-08-25 06:52:37 +10:00
J. Nick Koston
8fe582309e [esp32_ble_client] Reduce log level for harmless BLE timeout race conditions (#10339)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-25 08:51:54 +12:00
J. Nick Koston
b41a61c76e [deep_sleep] Fix ESP32-C6 compilation error with gpio_deep_sleep_hold_en() (#10345) 2025-08-25 08:51:23 +12:00
J. Nick Koston
61a5023888 [script] Fix parallel mode scripts with delays cancelling each other (#10324) 2025-08-25 08:49:52 +12:00
J. Nick Koston
4396bc0d1a [esp32_ble] Increase GATT connection retry count to use full timeout window (#10376) 2025-08-25 08:49:37 +12:00
J. Nick Koston
acfce581fa [esp32_ble_client] Optimize BLE connection parameters for different connection types (#10356) 2025-08-25 08:17:26 +12:00
J. Nick Koston
88303f39fa [pvvx_mithermometer] Fix race condition with BLE authentication (#10327) 2025-08-25 08:16:12 +12:00
J. Nick Koston
ca19959d7c [core] Improve error reporting for entity name conflicts with non-ASCII characters (#10329) 2025-08-25 08:11:54 +12:00
Clyde Stubbs
9737b35579 [http_request] Fix for host after ArduinoJson library bump (#10348) 2025-08-25 07:55:44 +12:00
Clyde Stubbs
be9c20c357 [mipi_spi] Add model (#10392) 2025-08-25 07:52:52 +12:00
Thomas Rupprecht
12ba4b142e Update Python to 3.11 in AI instructions (#10407) 2025-08-24 21:03:14 +12:00
Thomas Rupprecht
c096c6934d fix temperature config validation regex (#9575)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-08-24 08:56:06 +00:00
tomaszduda23
17f787fc36 [nrf52] fix build in dashboard (#10323) 2025-08-23 12:17:42 +00:00
tomaszduda23
5cd9a86dcb [nrf52] update toolchain to v0.17.4, support mac (#10391) 2025-08-23 16:20:16 +10:00
dependabot[bot]
83fe4b4ff3 Bump ruff from 0.12.9 to 0.12.10 (#10362)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-08-21 15:36:06 -05:00
Jesse Hills
94accd5abe [ld2420] Rename c++ files for predictable doxygen generation (#10315) 2025-08-21 18:49:26 +12:00
Jesse Hills
3ca0015284 [opentherm] Rename c++ files for predictable doxygen generation (#10314) 2025-08-21 18:48:48 +12:00
dependabot[bot]
33eddb6035 Bump codecov/codecov-action from 5.4.3 to 5.5.0 (#10336)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-20 16:46:04 -05:00
Jesse Hills
72c58ae36d [core] Add idf-tidy env for esp32-c6 (#10270) 2025-08-20 10:13:50 -04:00
J. Nick Koston
35411d199f [homeassistant] Add compilation test for homeassistant.tag_scanned action (#10319) 2025-08-20 10:10:20 -04:00
Jesse Hills
d45944a9e2 [api] Add `USE_API_HOMEASSISTANT_SERVICES if using tag_scanned` action (#10316) 2025-08-20 06:47:20 -05:00
Jesse Hills
86f306ba9e [CI] Also require tests for `new-features` (#10311) 2025-08-20 22:02:14 +12:00
Jesse Hills
1b3b2f6e6f Merge branch 'release' into dev 2025-08-20 19:58:48 +12:00
Jesse Hills
2adb993242 Merge pull request #10309 from esphome/bump-2025.8.0
2025.8.0
2025-08-20 19:58:01 +12:00
J. Nick Koston
3ff5b4773b [bluetooth_proxy] Mark BluetoothConnection and BluetoothProxy as final for compiler optimizations (#10280) 2025-08-20 14:48:40 +12:00
J. Nick Koston
2cbf4f30f9 [libretiny] Optimize preferences is_changed() by replacing temporary vector with unique_ptr (#10272) 2025-08-20 14:48:04 +12:00
J. Nick Koston
56b6dd31f1 [core] Eliminate heap allocation in teardown_components by using StaticVector (#10256)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-20 14:45:13 +12:00
dependabot[bot]
fc1b49e87d Bump ruamel-yaml from 0.18.14 to 0.18.15 (#10310)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-20 14:42:33 +12:00
J. Nick Koston
0089619518 [web_server] Reduce flash usage by consolidating defer calls in switch and lock handlers (#10297) 2025-08-19 21:41:34 -05:00
Jesse Hills
5a6db28f1d [CI] Base `too-big` label on new additions only (#10307) 2025-08-20 14:39:29 +12:00
J. Nick Koston
6819bbd8f8 [esp32_ble_client] Add log helper functions to reduce flash usage by 120 bytes (#10243) 2025-08-20 14:38:32 +12:00
Edward Firmo
634f687c3e [light] Add support for querying effects by index (#10195)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-08-20 14:38:13 +12:00
J. Nick Koston
e2a9b85924 [number] Convert LOG_NUMBER macro to function to reduce flash usage (#10293) 2025-08-20 14:36:05 +12:00
J. Nick Koston
4ccc6aee09 [button] Convert LOG_BUTTON macro to function to reduce flash usage (#10295)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-20 14:35:53 +12:00
J. Nick Koston
0eab908b0e [sensor] Convert LOG_SENSOR macro to function to reduce flash usage (#10290) 2025-08-20 14:35:45 +12:00
J. Nick Koston
3964f9794b [binary_sensor] Convert LOG_BINARY_SENSOR macro to function to reduce flash usage (#10294) 2025-08-20 14:35:09 +12:00
Jesse Hills
a45137434b [quality] Convert remaining `to_code to async` (#10271) 2025-08-20 14:34:45 +12:00
J. Nick Koston
9b1ebdb6da [mdns] Reduce flash usage and prevent RAM over-allocation in service compilation (#10287) 2025-08-20 14:34:34 +12:00
J. Nick Koston
5a1533bea9 [api] Avoid object_id string allocations for all entity info messages (#10260) 2025-08-20 14:28:13 +12:00
Jesse Hills
0b50ef227b [helper] Make crc8 function more flexible to avoid reimplementation in individual components (#10201) 2025-08-20 14:27:08 +12:00
J. Nick Koston
0e31bc1a67 [api] Add zero-copy StringRef methods for compilation_time and effect_name (#10257) 2025-08-20 14:26:53 +12:00
Jesse Hills
8e67df8059 Bump version to 2025.8.0 2025-08-20 10:45:57 +12:00
Jesse Hills
e1a0949ddb Merge branch 'beta' into dev 2025-08-20 10:31:10 +12:00
Jesse Hills
c5b2c8d971 Merge pull request #10308 from esphome/bump-2025.8.0b4
2025.8.0b4
2025-08-20 10:30:37 +12:00
J. Nick Koston
a8775ba60b [safe_mode] Reduce flash usage by 184 bytes through code optimization (#10284) 2025-08-19 16:57:24 -05:00
Jesse Hills
104906ca11 Bump version to 2025.8.0b4 2025-08-20 09:40:19 +12:00
J. Nick Koston
ad5f6f0cfe [bluetooth_proxy] Fix connection slot race by deferring slot release until GATT close (#10303) 2025-08-20 09:40:19 +12:00
Patrick
8356f7fcd3 [pipsolar] fix faults_present, fix update interval (#10289) 2025-08-20 09:40:19 +12:00
Ben Winslow
225de226b0 [atm90e32] Only read 1 register per SPI transaction per datasheet. (#10258) 2025-08-20 09:40:19 +12:00
J. Nick Koston
2aaf951357 [bluetooth_proxy] Fix connection slot race by deferring slot release until GATT close (#10303) 2025-08-20 07:27:22 +12:00
Jesse Hills
82718e62e7 Merge branch 'beta' into dev 2025-08-19 20:40:45 +12:00
Jesse Hills
fd07e1d979 Merge pull request #10298 from esphome/bump-2025.8.0b3
2025.8.0b3
2025-08-19 20:40:12 +12:00
Patrick
4dab9c4400 [pipsolar] fix faults_present, fix update interval (#10289) 2025-08-19 15:52:01 +12:00
Ben Winslow
7e23d865e6 [atm90e32] Only read 1 register per SPI transaction per datasheet. (#10258) 2025-08-19 15:45:30 +12:00
Jesse Hills
8f118232e4 [CI] Rename and expand needs-docs workflow (#10299) 2025-08-19 15:35:48 +12:00
Jesse Hills
23554cda06 Bump version to 2025.8.0b3 2025-08-19 13:09:22 +12:00
Ben Winslow
064385eac6 [nextion] Don't include terminating NUL in nextion text_sensor states (#10273) 2025-08-19 13:09:22 +12:00
Jesse Hills
6502ed70de [esp32] Write variant to sdkconfig file (#10267) 2025-08-19 13:09:22 +12:00
J. Nick Koston
bb894c3e32 [core] Fix scheduler race condition where cancelled items still execute (#10268) 2025-08-19 13:09:22 +12:00
Ben Winslow
c5858b7032 [core] Fix post-OTA logs display when using esphome run and MQTT (#10274) 2025-08-19 13:09:22 +12:00
Ben Winslow
99f57ecb73 [senseair] Discard 0 ppm readings with "Out Of Range" bit set. (#10275) 2025-08-19 13:09:22 +12:00
J. Nick Koston
cc6c892678 [esp32_ble] Store GATTC/GATTS param and small data inline to nearly eliminate heap allocations (#10249)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-19 13:09:22 +12:00
RFDarter
07a98d2525 [web_server] fix cover_all_json_generator wrong detail (#10252) 2025-08-19 13:09:22 +12:00
J. Nick Koston
e80f616366 [esp32_ble] Optimize BLE event memory usage by eliminating std::vector overhead (#10247)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-19 13:09:22 +12:00
J. Nick Koston
46be877594 [bluetooth_proxy] Remove redundant connection type check after V1 removal (#10208) 2025-08-19 13:09:21 +12:00
J. Nick Koston
ac8b48a53c [core] Trigger clean build when components are removed from configuration (#10235)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-19 13:09:21 +12:00
J. Nick Koston
7fdbd8528a [wifi] Automatically disable Enterprise WiFi support when EAP is not configured (#10242) 2025-08-19 13:09:21 +12:00
Katherine Whitlock
80970f972b Improve error reporting for add_library (#10226)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-19 13:09:21 +12:00
Jesse Hills
3c7865cd6f [esp32_ble] Add `USE_ESP32_BLE_UUID` when advertising is desired (#10230) 2025-08-19 13:09:21 +12:00
Ben Winslow
3a6a66537c [nextion] Don't include terminating NUL in nextion text_sensor states (#10273) 2025-08-18 19:20:13 -05:00
Jesse Hills
7118bea031 [esp32] Write variant to sdkconfig file (#10267) 2025-08-19 12:17:34 +12:00
J. Nick Koston
44bd8e5b54 [api] Optimize protobuf decode loop for better performance and maintainability (#10277) 2025-08-18 16:14:20 -05:00
J. Nick Koston
efaeb91803 [api] Mark APIConnection as final for compiler optimizations (#10279) 2025-08-18 16:01:45 -05:00
J. Nick Koston
761c6c6685 [api] Mark protobuf message classes as final to enable compiler optimizations (#10276) 2025-08-18 15:55:30 -05:00
J. Nick Koston
1f55486896 [api] Optimize APIFrameHelper virtual methods and mark implementations as final (#10278) 2025-08-18 15:55:11 -05:00
J. Nick Koston
6818439109 [core] Fix scheduler race condition where cancelled items still execute (#10268) 2025-08-18 11:14:41 -04:00
J. Nick Koston
0a77423073 [esp8266] Replace std::vector with std::unique_ptr in preferences to save flash (#10245) 2025-08-18 09:01:39 -04:00
Ben Winslow
c29f8d0187 [core] Fix post-OTA logs display when using esphome run and MQTT (#10274) 2025-08-17 21:36:35 -05:00
Ben Winslow
2a3f80a82c [senseair] Discard 0 ppm readings with "Out Of Range" bit set. (#10275) 2025-08-18 14:09:42 +12:00
J. Nick Koston
75f3adcd95 [esp32_ble] Store GATTC/GATTS param and small data inline to nearly eliminate heap allocations (#10249)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-18 07:49:50 +12:00
J. Nick Koston
daf8ec36ab [core] Remove unnecessary FD_SETSIZE check on ESP32 and improve logging (#10255) 2025-08-15 21:26:48 -05:00
J. Nick Koston
6c5632a0b3 [esp32] Optimize preferences is_changed() by replacing temporary vector with unique_ptr (#10246)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-15 10:11:49 -05:00
RFDarter
abecc0e8d8 [web_server] fix cover_all_json_generator wrong detail (#10252) 2025-08-15 09:44:24 -05:00
J. Nick Koston
af9ecf3429 [esp32_ble] Optimize BLE event memory usage by eliminating std::vector overhead (#10247)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-15 07:38:27 +00:00
J. Nick Koston
5fa84439c2 [api] Optimize message buffer allocation and eliminate redundant methods (#10231) 2025-08-14 20:26:09 -05:00
dependabot[bot]
5d18afcd99 Bump ruff from 0.12.8 to 0.12.9 (#10239)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-08-14 22:54:35 +00:00
J. Nick Koston
117cffd2b0 [bluetooth_proxy] Remove redundant connection type check after V1 removal (#10208) 2025-08-15 10:51:15 +12:00
J. Nick Koston
8ea1a3ed64 [core] Trigger clean build when components are removed from configuration (#10235)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-15 10:50:03 +12:00
J. Nick Koston
4f29b3c7aa [wifi] Automatically disable Enterprise WiFi support when EAP is not configured (#10242) 2025-08-15 10:43:45 +12:00
Jesse Hills
3325592d67 Merge branch 'beta' into dev 2025-08-15 08:46:48 +12:00
Jesse Hills
0a3ee7d84e Merge pull request #10228 from esphome/bump-2025.8.0b2
2025.8.0b2
2025-08-15 08:46:15 +12:00
Katherine Whitlock
882237120e Improve error reporting for add_library (#10226)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-15 08:14:53 +12:00
Jesse Hills
71efaf097b [esp32_ble] Add `USE_ESP32_BLE_UUID` when advertising is desired (#10230) 2025-08-14 08:49:14 -05:00
Jesse Hills
bd60dbb746 [quality] Remove period from audio related Invalid raises (#10229) 2025-08-14 08:48:25 -05:00
Jesse Hills
6b5e43ca72 [qm6988] Clean up code (#10216) 2025-08-13 21:19:03 -05: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
dependabot[bot]
46d433775b 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 12:40:20 +12:00
dependabot[bot]
7c4a54de90 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-13 17:42:54 -05:00
Samuel Sieb
c3f1596498 [psram] allow disabling (#10224)
Co-authored-by: Samuel Sieb <samuel@sieb.net>
2025-08-14 10:40:12 +12:00
Jesse Hills
0d1949a61b [espnow] Set state to enabled before adding initial peers (#10225) 2025-08-14 10:30:28 +12:00
Jesse Hills
6a8722f33e [entity] Allow `device_id` to be blank on entities (#10217) 2025-08-14 09:42:11 +12:00
Jesse Hills
fff66072d4 Merge branch 'beta' into dev 2025-08-14 00:02:17 +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
J. Nick Koston
0dda3faed5 [CI] Fix CI job failures for PRs with >300 changed files (#10215) 2025-08-13 15:46:56 +12:00
Jesse Hills
40c0c36179 Bump version to 2025.9.0-dev 2025-08-13 14:46:51 +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
419 changed files with 9756 additions and 3575 deletions

View File

@@ -9,7 +9,7 @@ This document provides essential context for AI models interacting with this pro
## 2. Core Technologies & Stack ## 2. Core Technologies & Stack
* **Languages:** Python (>=3.10), C++ (gnu++20) * **Languages:** Python (>=3.11), C++ (gnu++20)
* **Frameworks & Runtimes:** PlatformIO, Arduino, ESP-IDF. * **Frameworks & Runtimes:** PlatformIO, Arduino, ESP-IDF.
* **Build Systems:** PlatformIO is the primary build system. CMake is used as an alternative. * **Build Systems:** PlatformIO is the primary build system. CMake is used as an alternative.
* **Configuration:** YAML. * **Configuration:** YAML.
@@ -38,7 +38,7 @@ This document provides essential context for AI models interacting with this pro
5. **Dashboard** (`esphome/dashboard/`): A web-based interface for device configuration, management, and OTA updates. 5. **Dashboard** (`esphome/dashboard/`): A web-based interface for device configuration, management, and OTA updates.
* **Platform Support:** * **Platform Support:**
1. **ESP32** (`components/esp32/`): Espressif ESP32 family. Supports multiple variants (S2, S3, C3, etc.) and both IDF and Arduino frameworks. 1. **ESP32** (`components/esp32/`): Espressif ESP32 family. Supports multiple variants (Original, C2, C3, C5, C6, H2, P4, S2, S3) with ESP-IDF framework. Arduino framework supports only a subset of the variants (Original, C3, S2, S3).
2. **ESP8266** (`components/esp8266/`): Espressif ESP8266. Arduino framework only, with memory constraints. 2. **ESP8266** (`components/esp8266/`): Espressif ESP8266. Arduino framework only, with memory constraints.
3. **RP2040** (`components/rp2040/`): Raspberry Pi Pico/RP2040. Arduino framework with PIO (Programmable I/O) support. 3. **RP2040** (`components/rp2040/`): Raspberry Pi Pico/RP2040. Arduino framework with PIO (Programmable I/O) support.
4. **LibreTiny** (`components/libretiny/`): Realtek and Beken chips. Supports multiple chip families and auto-generated components. 4. **LibreTiny** (`components/libretiny/`): Realtek and Beken chips. Supports multiple chip families and auto-generated components.
@@ -60,7 +60,7 @@ This document provides essential context for AI models interacting with this pro
├── __init__.py # Component configuration schema and code generation ├── __init__.py # Component configuration schema and code generation
├── [component].h # C++ header file (if needed) ├── [component].h # C++ header file (if needed)
├── [component].cpp # C++ implementation (if needed) ├── [component].cpp # C++ implementation (if needed)
└── [platform]/ # Platform-specific implementations └── [platform]/ # Platform-specific implementations
├── __init__.py # Platform-specific configuration ├── __init__.py # Platform-specific configuration
├── [platform].h # Platform C++ header ├── [platform].h # Platform C++ header
└── [platform].cpp # Platform C++ implementation └── [platform].cpp # Platform C++ implementation
@@ -150,7 +150,8 @@ This document provides essential context for AI models interacting with this pro
* **Configuration Validation:** * **Configuration Validation:**
* **Common Validators:** `cv.int_`, `cv.float_`, `cv.string`, `cv.boolean`, `cv.int_range(min=0, max=100)`, `cv.positive_int`, `cv.percentage`. * **Common Validators:** `cv.int_`, `cv.float_`, `cv.string`, `cv.boolean`, `cv.int_range(min=0, max=100)`, `cv.positive_int`, `cv.percentage`.
* **Complex Validation:** `cv.All(cv.string, cv.Length(min=1, max=50))`, `cv.Any(cv.int_, cv.string)`. * **Complex Validation:** `cv.All(cv.string, cv.Length(min=1, max=50))`, `cv.Any(cv.int_, cv.string)`.
* **Platform-Specific:** `cv.only_on(["esp32", "esp8266"])`, `cv.only_with_arduino`. * **Platform-Specific:** `cv.only_on(["esp32", "esp8266"])`, `esp32.only_on_variant(...)`, `cv.only_on_esp32`, `cv.only_on_esp8266`, `cv.only_on_rp2040`.
* **Framework-Specific:** `cv.only_with_framework(...)`, `cv.only_with_arduino`, `cv.only_with_esp_idf`.
* **Schema Extensions:** * **Schema Extensions:**
```python ```python
CONFIG_SCHEMA = cv.Schema({ ... }) CONFIG_SCHEMA = cv.Schema({ ... })

View File

@@ -1 +1 @@
6af8b429b94191fe8e239fcb3b73f7982d0266cb5b05ffbc81edaeac1bc8c273 4368db58e8f884aff245996b1e8b644cc0796c0bb2fa706d5740d40b823d3ac9

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
@@ -105,7 +105,9 @@ jobs:
// Calculate data from PR files // Calculate data from PR files
const changedFiles = prFiles.map(file => file.filename); const changedFiles = prFiles.map(file => file.filename);
const totalChanges = prFiles.reduce((sum, file) => sum + (file.additions || 0) + (file.deletions || 0), 0); const totalAdditions = prFiles.reduce((sum, file) => sum + (file.additions || 0), 0);
const totalDeletions = prFiles.reduce((sum, file) => sum + (file.deletions || 0), 0);
const totalChanges = totalAdditions + totalDeletions;
console.log('Current labels:', currentLabels.join(', ')); console.log('Current labels:', currentLabels.join(', '));
console.log('Changed files:', changedFiles.length); console.log('Changed files:', changedFiles.length);
@@ -231,16 +233,21 @@ jobs:
// Strategy: PR size detection // Strategy: PR size detection
async function detectPRSize() { async function detectPRSize() {
const labels = new Set(); const labels = new Set();
const testChanges = prFiles
.filter(file => file.filename.startsWith('tests/'))
.reduce((sum, file) => sum + (file.additions || 0) + (file.deletions || 0), 0);
const nonTestChanges = totalChanges - testChanges;
if (totalChanges <= SMALL_PR_THRESHOLD) { if (totalChanges <= SMALL_PR_THRESHOLD) {
labels.add('small-pr'); labels.add('small-pr');
return labels;
} }
const testAdditions = prFiles
.filter(file => file.filename.startsWith('tests/'))
.reduce((sum, file) => sum + (file.additions || 0), 0);
const testDeletions = prFiles
.filter(file => file.filename.startsWith('tests/'))
.reduce((sum, file) => sum + (file.deletions || 0), 0);
const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions);
// Don't add too-big if mega-pr label is already present // Don't add too-big if mega-pr label is already present
if (nonTestChanges > TOO_BIG_THRESHOLD && !isMegaPR) { if (nonTestChanges > TOO_BIG_THRESHOLD && !isMegaPR) {
labels.add('too-big'); labels.add('too-big');
@@ -375,7 +382,7 @@ jobs:
const labels = new Set(); const labels = new Set();
// Check for missing tests // Check for missing tests
if ((allLabels.has('new-component') || allLabels.has('new-platform')) && !allLabels.has('has-tests')) { if ((allLabels.has('new-component') || allLabels.has('new-platform') || allLabels.has('new-feature')) && !allLabels.has('has-tests')) {
labels.add('needs-tests'); labels.add('needs-tests');
} }
@@ -412,10 +419,13 @@ jobs:
// Too big message // Too big message
if (finalLabels.includes('too-big')) { if (finalLabels.includes('too-big')) {
const testChanges = prFiles const testAdditions = prFiles
.filter(file => file.filename.startsWith('tests/')) .filter(file => file.filename.startsWith('tests/'))
.reduce((sum, file) => sum + (file.additions || 0) + (file.deletions || 0), 0); .reduce((sum, file) => sum + (file.additions || 0), 0);
const nonTestChanges = totalChanges - testChanges; const testDeletions = prFiles
.filter(file => file.filename.startsWith('tests/'))
.reduce((sum, file) => sum + (file.deletions || 0), 0);
const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions);
const tooManyLabels = finalLabels.length > MAX_LABELS; const tooManyLabels = finalLabels.length > MAX_LABELS;
const tooManyChanges = nonTestChanges > TOO_BIG_THRESHOLD; const tooManyChanges = nonTestChanges > TOO_BIG_THRESHOLD;

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
@@ -156,12 +156,12 @@ jobs:
. venv/bin/activate . venv/bin/activate
pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/ pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@v5.4.3 uses: codecov/codecov-action@v5.5.0
with: with:
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 }}
@@ -287,7 +287,7 @@ jobs:
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

@@ -1,24 +0,0 @@
name: Needs Docs
on:
pull_request:
types: [labeled, unlabeled]
jobs:
check:
name: Check
runs-on: ubuntu-latest
steps:
- name: Check for needs-docs label
uses: actions/github-script@v7.0.1
with:
script: |
const { data: labels } = await github.rest.issues.listLabelsOnIssue({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
});
const needsDocs = labels.find(label => label.name === 'needs-docs');
if (needsDocs) {
core.setFailed('Pull request needs docs');
}

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

@@ -0,0 +1,30 @@
name: Status check labels
on:
pull_request:
types: [labeled, unlabeled]
jobs:
check:
name: Check ${{ matrix.label }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
label:
- needs-docs
- merge-after-release
steps:
- name: Check for ${{ matrix.label }} label
uses: actions/github-script@v7.0.1
with:
script: |
const { data: labels } = await github.rest.issues.listLabelsOnIssue({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
});
const hasLabel = labels.find(label => label.name === '${{ matrix.label }}');
if (hasLabel) {
core.setFailed('Pull request cannot be merged, it is labeled as ${{ matrix.label }}');
}

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.7 rev: v0.12.10
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
@@ -237,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
@@ -245,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
@@ -467,7 +469,7 @@ 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
@@ -511,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.9.0-dev
# 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,57 @@ 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 CORE.address and (
(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)
if not resolved:
_LOGGER.error("All specified devices: %s could not be resolved.", defaults)
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 +169,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 +184,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 +261,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 +269,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 +280,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 +291,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 +302,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(
@@ -314,7 +369,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 +378,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 +396,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 +411,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 +434,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,24 +457,29 @@ 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) in ("NETWORK", "MQTT") and "mqtt" in config:
from esphome import mqtt from esphome import mqtt
return mqtt.show_logs( return mqtt.show_logs(
@@ -426,7 +489,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 +497,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 +518,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 +526,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 +540,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 +550,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 +574,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 +584,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 +604,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 +613,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 +669,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 +722,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 +738,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 +855,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 +948,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 +971,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 +1001,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 +1129,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

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

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

@@ -89,7 +89,7 @@ void AGS10Component::dump_config() {
bool AGS10Component::new_i2c_address(uint8_t newaddress) { bool AGS10Component::new_i2c_address(uint8_t newaddress) {
uint8_t rev_newaddress = ~newaddress; uint8_t rev_newaddress = ~newaddress;
std::array<uint8_t, 5> data{newaddress, rev_newaddress, newaddress, rev_newaddress, 0}; std::array<uint8_t, 5> data{newaddress, rev_newaddress, newaddress, rev_newaddress, 0};
data[4] = calc_crc8_(data, 4); data[4] = crc8(data.data(), 4, 0xFF, 0x31, true);
if (!this->write_bytes(REG_ADDRESS, data)) { if (!this->write_bytes(REG_ADDRESS, data)) {
this->error_code_ = COMMUNICATION_FAILED; this->error_code_ = COMMUNICATION_FAILED;
this->status_set_warning(); this->status_set_warning();
@@ -109,7 +109,7 @@ bool AGS10Component::set_zero_point_with_current_resistance() { return this->set
bool AGS10Component::set_zero_point_with(uint16_t value) { bool AGS10Component::set_zero_point_with(uint16_t value) {
std::array<uint8_t, 5> data{0x00, 0x0C, (uint8_t) ((value >> 8) & 0xFF), (uint8_t) (value & 0xFF), 0}; std::array<uint8_t, 5> data{0x00, 0x0C, (uint8_t) ((value >> 8) & 0xFF), (uint8_t) (value & 0xFF), 0};
data[4] = calc_crc8_(data, 4); data[4] = crc8(data.data(), 4, 0xFF, 0x31, true);
if (!this->write_bytes(REG_CALIBRATION, data)) { if (!this->write_bytes(REG_CALIBRATION, data)) {
this->error_code_ = COMMUNICATION_FAILED; this->error_code_ = COMMUNICATION_FAILED;
this->status_set_warning(); this->status_set_warning();
@@ -184,7 +184,7 @@ template<size_t N> optional<std::array<uint8_t, N>> AGS10Component::read_and_che
auto res = *data; auto res = *data;
auto crc_byte = res[len]; auto crc_byte = res[len];
if (crc_byte != calc_crc8_(res, len)) { if (crc_byte != crc8(res.data(), len, 0xFF, 0x31, true)) {
this->error_code_ = CRC_CHECK_FAILED; this->error_code_ = CRC_CHECK_FAILED;
ESP_LOGE(TAG, "Reading AGS10 version failed: crc error!"); ESP_LOGE(TAG, "Reading AGS10 version failed: crc error!");
return optional<std::array<uint8_t, N>>(); return optional<std::array<uint8_t, N>>();
@@ -192,20 +192,5 @@ template<size_t N> optional<std::array<uint8_t, N>> AGS10Component::read_and_che
return data; return data;
} }
template<size_t N> uint8_t AGS10Component::calc_crc8_(std::array<uint8_t, N> dat, uint8_t num) {
uint8_t i, byte1, crc = 0xFF;
for (byte1 = 0; byte1 < num; byte1++) {
crc ^= (dat[byte1]);
for (i = 0; i < 8; i++) {
if (crc & 0x80) {
crc = (crc << 1) ^ 0x31;
} else {
crc = (crc << 1);
}
}
}
return crc;
}
} // namespace ags10 } // namespace ags10
} // namespace esphome } // namespace esphome

View File

@@ -1,9 +1,9 @@
#pragma once #pragma once
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome { namespace esphome {
namespace ags10 { namespace ags10 {
@@ -99,16 +99,6 @@ class AGS10Component : public PollingComponent, public i2c::I2CDevice {
* Read, checks and returns data from the sensor. * Read, checks and returns data from the sensor.
*/ */
template<size_t N> optional<std::array<uint8_t, N>> read_and_check_(uint8_t a_register); template<size_t N> optional<std::array<uint8_t, N>> read_and_check_(uint8_t a_register);
/**
* Calculates CRC8 value.
*
* CRC8 calculation, initial value: 0xFF, polynomial: 0x31 (x8+ x5+ x4+1)
*
* @param[in] dat the data buffer
* @param num number of bytes in the buffer
*/
template<size_t N> uint8_t calc_crc8_(std::array<uint8_t, N> dat, uint8_t num);
}; };
template<typename... Ts> class AGS10NewI2cAddressAction : public Action<Ts...>, public Parented<AGS10Component> { template<typename... Ts> class AGS10NewI2cAddressAction : public Action<Ts...>, public Parented<AGS10Component> {

View File

@@ -18,6 +18,6 @@ CONFIG_SCHEMA = cv.Schema(
).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) ).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
yield esp32_ble_tracker.register_ble_device(var, config) await esp32_ble_tracker.register_ble_device(var, config)

View File

@@ -29,22 +29,6 @@ namespace am2315c {
static const char *const TAG = "am2315c"; static const char *const TAG = "am2315c";
uint8_t AM2315C::crc8_(uint8_t *data, uint8_t len) {
uint8_t crc = 0xFF;
while (len--) {
crc ^= *data++;
for (uint8_t i = 0; i < 8; i++) {
if (crc & 0x80) {
crc <<= 1;
crc ^= 0x31;
} else {
crc <<= 1;
}
}
}
return crc;
}
bool AM2315C::reset_register_(uint8_t reg) { bool AM2315C::reset_register_(uint8_t reg) {
// code based on demo code sent by www.aosong.com // code based on demo code sent by www.aosong.com
// no further documentation. // no further documentation.
@@ -86,7 +70,7 @@ bool AM2315C::convert_(uint8_t *data, float &humidity, float &temperature) {
humidity = raw * 9.5367431640625e-5; humidity = raw * 9.5367431640625e-5;
raw = ((data[3] & 0x0F) << 16) | (data[4] << 8) | data[5]; raw = ((data[3] & 0x0F) << 16) | (data[4] << 8) | data[5];
temperature = raw * 1.9073486328125e-4 - 50; temperature = raw * 1.9073486328125e-4 - 50;
return this->crc8_(data, 6) == data[6]; return crc8(data, 6, 0xFF, 0x31, true) == data[6];
} }
void AM2315C::setup() { void AM2315C::setup() {

View File

@@ -21,9 +21,9 @@
// SOFTWARE. // SOFTWARE.
#pragma once #pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h" #include "esphome/components/i2c/i2c.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/component.h"
namespace esphome { namespace esphome {
namespace am2315c { namespace am2315c {
@@ -39,7 +39,6 @@ class AM2315C : public PollingComponent, public i2c::I2CDevice {
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
protected: protected:
uint8_t crc8_(uint8_t *data, uint8_t len);
bool convert_(uint8_t *data, float &humidity, float &temperature); bool convert_(uint8_t *data, float &humidity, float &temperature);
bool reset_register_(uint8_t reg); bool reset_register_(uint8_t reg);

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)
@@ -321,6 +321,7 @@ HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA = cv.maybe_simple_value(
HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA, HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA,
) )
async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, args): async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, args):
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
serv = await cg.get_variable(config[CONF_ID]) serv = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, serv, True) var = cg.new_Pvariable(action_id, template_arg, serv, True)
cg.add(var.set_service("esphome.tag_scanned")) cg.add(var.set_service("esphome.tag_scanned"))

View File

@@ -1438,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;

View File

@@ -289,16 +289,26 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint8_t mess
return 0; // Doesn't fit return 0; // Doesn't fit
} }
// Allocate buffer space - pass payload size, allocation functions add header/footer space
ProtoWriteBuffer buffer = is_single ? conn->allocate_single_message_buffer(calculated_size)
: conn->allocate_batch_message_buffer(calculated_size);
// Get buffer size after allocation (which includes header padding) // Get buffer size after allocation (which includes header padding)
std::vector<uint8_t> &shared_buf = conn->parent_->get_shared_buffer_ref(); std::vector<uint8_t> &shared_buf = conn->parent_->get_shared_buffer_ref();
size_t size_before_encode = shared_buf.size();
if (is_single || conn->flags_.batch_first_message) {
// Single message or first batch message
conn->prepare_first_message_buffer(shared_buf, header_padding, total_calculated_size);
if (conn->flags_.batch_first_message) {
conn->flags_.batch_first_message = false;
}
} else {
// Batch message second or later
// Add padding for previous message footer + this message header
size_t current_size = shared_buf.size();
shared_buf.reserve(current_size + total_calculated_size);
shared_buf.resize(current_size + footer_size + header_padding);
}
// Encode directly into buffer // Encode directly into buffer
msg.encode(buffer); size_t size_before_encode = shared_buf.size();
msg.encode({&shared_buf});
// Calculate actual encoded size (not including header that was already added) // Calculate actual encoded size (not including header that was already added)
size_t actual_payload_size = shared_buf.size() - size_before_encode; size_t actual_payload_size = shared_buf.size() - size_before_encode;
@@ -455,9 +465,7 @@ uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *
resp.cold_white = values.get_cold_white(); resp.cold_white = values.get_cold_white();
resp.warm_white = values.get_warm_white(); resp.warm_white = values.get_warm_white();
if (light->supports_effects()) { if (light->supports_effects()) {
// get_effect_name() returns temporary std::string - must store it resp.set_effect(light->get_effect_name_ref());
std::string effect_name = light->get_effect_name();
resp.set_effect(StringRef(effect_name));
} }
return fill_and_encode_entity_state(light, resp, LightStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return fill_and_encode_entity_state(light, resp, LightStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
@@ -1415,9 +1423,7 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
static constexpr auto ESPHOME_VERSION_REF = StringRef::from_lit(ESPHOME_VERSION); static constexpr auto ESPHOME_VERSION_REF = StringRef::from_lit(ESPHOME_VERSION);
resp.set_esphome_version(ESPHOME_VERSION_REF); resp.set_esphome_version(ESPHOME_VERSION_REF);
// get_compilation_time() returns temporary std::string - must store it resp.set_compilation_time(App.get_compilation_time_ref());
std::string compilation_time = App.get_compilation_time();
resp.set_compilation_time(StringRef(compilation_time));
// Compile-time StringRef constants for manufacturers // Compile-time StringRef constants for manufacturers
#if defined(USE_ESP8266) || defined(USE_ESP32) #if defined(USE_ESP8266) || defined(USE_ESP32)
@@ -1620,14 +1626,6 @@ bool APIConnection::schedule_batch_() {
return true; return true;
} }
ProtoWriteBuffer APIConnection::allocate_single_message_buffer(uint16_t size) { return this->create_buffer(size); }
ProtoWriteBuffer APIConnection::allocate_batch_message_buffer(uint16_t size) {
ProtoWriteBuffer result = this->prepare_message_buffer(size, this->flags_.batch_first_message);
this->flags_.batch_first_message = false;
return result;
}
void APIConnection::process_batch_() { void APIConnection::process_batch_() {
// Ensure PacketInfo remains trivially destructible for our placement new approach // Ensure PacketInfo remains trivially destructible for our placement new approach
static_assert(std::is_trivially_destructible<PacketInfo>::value, static_assert(std::is_trivially_destructible<PacketInfo>::value,
@@ -1735,7 +1733,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 for this message
current_offset = shared_buf.size() + footer_size; current_offset = shared_buf.size() + footer_size;
} }

View File

@@ -44,7 +44,7 @@ static constexpr size_t MAX_PACKETS_PER_BATCH = 64; // ESP32 has 8KB+ stack, HO
static constexpr size_t MAX_PACKETS_PER_BATCH = 32; // ESP8266/RP2040/etc have smaller stacks static constexpr size_t MAX_PACKETS_PER_BATCH = 32; // ESP8266/RP2040/etc have smaller stacks
#endif #endif
class APIConnection : public APIServerConnection { class APIConnection final : public APIServerConnection {
public: public:
friend class APIServer; friend class APIServer;
friend class ListEntitiesIterator; friend class ListEntitiesIterator;
@@ -252,44 +252,21 @@ class APIConnection : public APIServerConnection {
// Get header padding size - used for both reserve and insert // Get header padding size - used for both reserve and insert
uint8_t header_padding = this->helper_->frame_header_padding(); uint8_t header_padding = this->helper_->frame_header_padding();
// Get shared buffer from parent server // Get shared buffer from parent server
std::vector<uint8_t> &shared_buf = this->parent_->get_shared_buffer_ref(); std::vector<uint8_t> &shared_buf = this->parent_->get_shared_buffer_ref();
this->prepare_first_message_buffer(shared_buf, header_padding,
reserve_size + header_padding + this->helper_->frame_footer_size());
return {&shared_buf};
}
void prepare_first_message_buffer(std::vector<uint8_t> &shared_buf, size_t header_padding, size_t total_size) {
shared_buf.clear(); shared_buf.clear();
// Reserve space for header padding + message + footer // Reserve space for header padding + message + footer
// - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext) // - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext)
// - Footer: space for MAC (16 bytes for Noise, 0 for Plaintext) // - Footer: space for MAC (16 bytes for Noise, 0 for Plaintext)
shared_buf.reserve(reserve_size + header_padding + this->helper_->frame_footer_size()); shared_buf.reserve(total_size);
// Resize to add header padding so message encoding starts at the correct position // Resize to add header padding so message encoding starts at the correct position
shared_buf.resize(header_padding); shared_buf.resize(header_padding);
return {&shared_buf};
}
// Prepare buffer for next message in batch
ProtoWriteBuffer prepare_message_buffer(uint16_t message_size, bool is_first_message) {
// Get reference to shared buffer (it maintains state between batch messages)
std::vector<uint8_t> &shared_buf = this->parent_->get_shared_buffer_ref();
if (is_first_message) {
shared_buf.clear();
}
size_t current_size = shared_buf.size();
// Calculate padding to add:
// - First message: just header padding
// - Subsequent messages: footer for previous message + header padding for this message
size_t padding_to_add = is_first_message
? this->helper_->frame_header_padding()
: this->helper_->frame_header_padding() + this->helper_->frame_footer_size();
// Reserve space for padding + message
shared_buf.reserve(current_size + padding_to_add + message_size);
// Resize to add the padding bytes
shared_buf.resize(current_size + padding_to_add);
return {&shared_buf};
} }
bool try_to_clear_buffer(bool log_out_of_space); bool try_to_clear_buffer(bool log_out_of_space);
@@ -297,10 +274,6 @@ class APIConnection : public APIServerConnection {
std::string get_client_combined_info() const { return this->client_info_.get_combined_info(); } std::string get_client_combined_info() const { return this->client_info_.get_combined_info(); }
// Buffer allocator methods for batch processing
ProtoWriteBuffer allocate_single_message_buffer(uint16_t size);
ProtoWriteBuffer allocate_batch_message_buffer(uint16_t size);
protected: protected:
// Helper function to handle authentication completion // Helper function to handle authentication completion
void complete_authentication_(); void complete_authentication_();
@@ -328,9 +301,17 @@ class APIConnection : public APIServerConnection {
APIConnection *conn, uint32_t remaining_size, bool is_single) { APIConnection *conn, uint32_t remaining_size, bool is_single) {
// Set common fields that are shared by all entity types // Set common fields that are shared by all entity types
msg.key = entity->get_object_id_hash(); msg.key = entity->get_object_id_hash();
// IMPORTANT: get_object_id() may return a temporary std::string // Try to use static reference first to avoid allocation
std::string object_id = entity->get_object_id(); StringRef static_ref = entity->get_object_id_ref_for_api_();
msg.set_object_id(StringRef(object_id)); // Store dynamic string outside the if-else to maintain lifetime
std::string object_id;
if (!static_ref.empty()) {
msg.set_object_id(static_ref);
} else {
// Dynamic case - need to allocate
object_id = entity->get_object_id();
msg.set_object_id(StringRef(object_id));
}
if (entity->has_own_name()) { if (entity->has_own_name()) {
msg.set_name(entity->get_name()); msg.set_name(entity->get_name());

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

@@ -104,9 +104,9 @@ class APIFrameHelper {
// The buffer contains all messages with appropriate padding before each // The buffer contains all messages with appropriate padding before each
virtual APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) = 0; virtual APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) = 0;
// Get the frame header padding required by this protocol // Get the frame header padding required by this protocol
virtual uint8_t frame_header_padding() = 0; uint8_t frame_header_padding() const { return frame_header_padding_; }
// Get the frame footer size required by this protocol // Get the frame footer size required by this protocol
virtual uint8_t frame_footer_size() = 0; uint8_t frame_footer_size() const { return frame_footer_size_; }
// Check if socket has data ready to read // Check if socket has data ready to read
bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); } bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); }

View File

@@ -7,7 +7,7 @@
namespace esphome::api { namespace esphome::api {
class APINoiseFrameHelper : public APIFrameHelper { class APINoiseFrameHelper final : public APIFrameHelper {
public: public:
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx, APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx,
const ClientInfo *client_info) const ClientInfo *client_info)
@@ -25,10 +25,6 @@ class APINoiseFrameHelper : public APIFrameHelper {
APIError read_packet(ReadPacketBuffer *buffer) override; APIError read_packet(ReadPacketBuffer *buffer) override;
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override; APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override; APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override;
// Get the frame header padding required by this protocol
uint8_t frame_header_padding() override { return frame_header_padding_; }
// Get the frame footer size required by this protocol
uint8_t frame_footer_size() override { return frame_footer_size_; }
protected: protected:
APIError state_action_(); APIError state_action_();

View File

@@ -5,7 +5,7 @@
namespace esphome::api { namespace esphome::api {
class APIPlaintextFrameHelper : public APIFrameHelper { class APIPlaintextFrameHelper final : public APIFrameHelper {
public: public:
APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket, const ClientInfo *client_info) APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket, const ClientInfo *client_info)
: APIFrameHelper(std::move(socket), client_info) { : APIFrameHelper(std::move(socket), client_info) {
@@ -22,9 +22,6 @@ class APIPlaintextFrameHelper : public APIFrameHelper {
APIError read_packet(ReadPacketBuffer *buffer) override; APIError read_packet(ReadPacketBuffer *buffer) override;
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override; APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override; APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override;
uint8_t frame_header_padding() override { return frame_header_padding_; }
// Get the frame footer size required by this protocol
uint8_t frame_footer_size() override { return frame_footer_size_; }
protected: protected:
APIError try_read_frame_(std::vector<uint8_t> *frame); APIError try_read_frame_(std::vector<uint8_t> *frame);

View File

@@ -30,6 +30,7 @@ extend google.protobuf.FieldOptions {
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 bool fixed_array_skip_zero = 50009 [default=false];
optional string fixed_array_size_define = 50010; optional string fixed_array_size_define = 50010;
optional string fixed_array_with_length_define = 50011;
// container_pointer: Zero-copy optimization for repeated fields. // container_pointer: Zero-copy optimization for repeated fields.
// //

View File

@@ -1843,12 +1843,14 @@ void BluetoothLERawAdvertisement::calculate_size(ProtoSize &size) const {
size.add_length(1, this->data_len); size.add_length(1, this->data_len);
} }
void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer buffer) const { void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer buffer) const {
for (auto &it : this->advertisements) { for (uint16_t i = 0; i < this->advertisements_len; i++) {
buffer.encode_message(1, it, true); buffer.encode_message(1, this->advertisements[i], true);
} }
} }
void BluetoothLERawAdvertisementsResponse::calculate_size(ProtoSize &size) const { void BluetoothLERawAdvertisementsResponse::calculate_size(ProtoSize &size) const {
size.add_repeated_message(1, this->advertisements); for (uint16_t i = 0; i < this->advertisements_len; i++) {
size.add_message_object_force(1, this->advertisements[i]);
}
} }
bool BluetoothDeviceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { bool BluetoothDeviceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) { switch (field_id) {

File diff suppressed because it is too large Load Diff

View File

@@ -1534,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");
} }
} }

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

@@ -8,74 +8,70 @@ namespace esphome::api {
static const char *const TAG = "api.proto"; static const char *const TAG = "api.proto";
void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) { void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
uint32_t i = 0; const uint8_t *ptr = buffer;
bool error = false; const uint8_t *end = buffer + length;
while (i < length) {
while (ptr < end) {
uint32_t consumed; uint32_t consumed;
auto res = ProtoVarInt::parse(&buffer[i], length - i, &consumed);
// Parse field header
auto res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
if (!res.has_value()) { if (!res.has_value()) {
ESP_LOGV(TAG, "Invalid field start at %" PRIu32, i); ESP_LOGV(TAG, "Invalid field start at offset %ld", (long) (ptr - buffer));
break; return;
} }
uint32_t field_type = (res->as_uint32()) & 0b111; uint32_t tag = res->as_uint32();
uint32_t field_id = (res->as_uint32()) >> 3; uint32_t field_type = tag & 0b111;
i += consumed; uint32_t field_id = tag >> 3;
ptr += consumed;
switch (field_type) { switch (field_type) {
case 0: { // VarInt case 0: { // VarInt
res = ProtoVarInt::parse(&buffer[i], length - i, &consumed); res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
if (!res.has_value()) { if (!res.has_value()) {
ESP_LOGV(TAG, "Invalid VarInt at %" PRIu32, i); ESP_LOGV(TAG, "Invalid VarInt at offset %ld", (long) (ptr - buffer));
error = true; return;
break;
} }
if (!this->decode_varint(field_id, *res)) { if (!this->decode_varint(field_id, *res)) {
ESP_LOGV(TAG, "Cannot decode VarInt field %" PRIu32 " with value %" PRIu32 "!", field_id, res->as_uint32()); ESP_LOGV(TAG, "Cannot decode VarInt field %" PRIu32 " with value %" PRIu32 "!", field_id, res->as_uint32());
} }
i += consumed; ptr += consumed;
break; break;
} }
case 2: { // Length-delimited case 2: { // Length-delimited
res = ProtoVarInt::parse(&buffer[i], length - i, &consumed); res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
if (!res.has_value()) { if (!res.has_value()) {
ESP_LOGV(TAG, "Invalid Length Delimited at %" PRIu32, i); ESP_LOGV(TAG, "Invalid Length Delimited at offset %ld", (long) (ptr - buffer));
error = true; return;
break;
} }
uint32_t field_length = res->as_uint32(); uint32_t field_length = res->as_uint32();
i += consumed; ptr += consumed;
if (field_length > length - i) { if (ptr + field_length > end) {
ESP_LOGV(TAG, "Out-of-bounds Length Delimited at %" PRIu32, i); ESP_LOGV(TAG, "Out-of-bounds Length Delimited at offset %ld", (long) (ptr - buffer));
error = true; return;
break;
} }
if (!this->decode_length(field_id, ProtoLengthDelimited(&buffer[i], field_length))) { if (!this->decode_length(field_id, ProtoLengthDelimited(ptr, field_length))) {
ESP_LOGV(TAG, "Cannot decode Length Delimited field %" PRIu32 "!", field_id); ESP_LOGV(TAG, "Cannot decode Length Delimited field %" PRIu32 "!", field_id);
} }
i += field_length; ptr += field_length;
break; break;
} }
case 5: { // 32-bit case 5: { // 32-bit
if (length - i < 4) { if (ptr + 4 > end) {
ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at %" PRIu32, i); ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer));
error = true; return;
break;
} }
uint32_t val = encode_uint32(buffer[i + 3], buffer[i + 2], buffer[i + 1], buffer[i]); uint32_t val = encode_uint32(ptr[3], ptr[2], ptr[1], ptr[0]);
if (!this->decode_32bit(field_id, Proto32Bit(val))) { if (!this->decode_32bit(field_id, Proto32Bit(val))) {
ESP_LOGV(TAG, "Cannot decode 32-bit field %" PRIu32 " with value %" PRIu32 "!", field_id, val); ESP_LOGV(TAG, "Cannot decode 32-bit field %" PRIu32 " with value %" PRIu32 "!", field_id, val);
} }
i += 4; ptr += 4;
break; break;
} }
default: default:
ESP_LOGV(TAG, "Invalid field type at %" PRIu32, i); ESP_LOGV(TAG, "Invalid field type %u at offset %ld", field_type, (long) (ptr - buffer));
error = true; return;
break;
}
if (error) {
break;
} }
} }
} }

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_; }
@@ -395,7 +392,7 @@ class ProtoSize {
* @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)
@@ -419,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));
@@ -450,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) {
@@ -466,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
@@ -480,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);
} }
@@ -607,9 +604,8 @@ class ProtoSize {
*/ */
inline void add_sint32_force(uint32_t field_id_size, int32_t value) { inline void add_sint32_force(uint32_t field_id_size, int32_t value) {
// Always calculate size when force is true // 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);
} }
/** /**

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; }
@@ -263,19 +383,17 @@ float ATM90E32Component::get_setup_priority() const { return setup_priority::IO;
// 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_(uint16_t a_register) {
this->enable();
delay_microseconds_safe(1); // min delay between CS low and first SCK is 200ns - 1us is plenty
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);
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; return output;
} }
@@ -292,13 +410,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 +565,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 +580,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 +604,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 +629,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 +651,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 +665,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 +813,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 +1103,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 +1125,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 +1148,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 +1174,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; }
@@ -127,7 +141,7 @@ class ATM90E32Component : public PollingComponent,
#endif #endif
uint16_t read16_(uint16_t a_register); uint16_t read16_(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 +173,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 +221,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 +262,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

@@ -41,7 +41,7 @@ void AXS15231Touchscreen::update_touches() {
i2c::ErrorCode err; i2c::ErrorCode err;
uint8_t data[8]{}; uint8_t data[8]{};
err = this->write(AXS_READ_TOUCHPAD, sizeof(AXS_READ_TOUCHPAD), false); err = this->write(AXS_READ_TOUCHPAD, sizeof(AXS_READ_TOUCHPAD));
ERROR_CHECK(err); ERROR_CHECK(err);
err = this->read(data, sizeof(data)); err = this->read(data, sizeof(data));
ERROR_CHECK(err); ERROR_CHECK(err);

View File

@@ -7,6 +7,19 @@ namespace binary_sensor {
static const char *const TAG = "binary_sensor"; static const char *const TAG = "binary_sensor";
// Function implementation of LOG_BINARY_SENSOR macro to reduce code size
void log_binary_sensor(const char *tag, const char *prefix, const char *type, BinarySensor *obj) {
if (obj == nullptr) {
return;
}
ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str());
if (!obj->get_device_class().empty()) {
ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->get_device_class().c_str());
}
}
void BinarySensor::publish_state(bool new_state) { void BinarySensor::publish_state(bool new_state) {
if (this->filter_list_ == nullptr) { if (this->filter_list_ == nullptr) {
this->send_state_internal(new_state); this->send_state_internal(new_state);

View File

@@ -10,13 +10,10 @@ namespace esphome {
namespace binary_sensor { namespace binary_sensor {
#define LOG_BINARY_SENSOR(prefix, type, obj) \ class BinarySensor;
if ((obj) != nullptr) { \ void log_binary_sensor(const char *tag, const char *prefix, const char *type, BinarySensor *obj);
ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \
if (!(obj)->get_device_class().empty()) { \ #define LOG_BINARY_SENSOR(prefix, type, obj) log_binary_sensor(TAG, prefix, LOG_STR_LITERAL(type), obj)
ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class().c_str()); \
} \
}
#define SUB_BINARY_SENSOR(name) \ #define SUB_BINARY_SENSOR(name) \
protected: \ protected: \

View File

@@ -286,6 +286,7 @@ async def remove_bond_to_code(config, action_id, template_arg, args):
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

@@ -27,7 +27,7 @@ CONFIG_SCHEMA = cv.All(
) )
def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format):
cg.add( cg.add(
@@ -63,6 +63,6 @@ def to_code(config):
) )
cg.add(var.set_char_uuid128(uuid128)) cg.add(var.set_char_uuid128(uuid128))
cg.add(var.set_require_response(config[CONF_REQUIRE_RESPONSE])) cg.add(var.set_require_response(config[CONF_REQUIRE_RESPONSE]))
yield output.register_output(var, config) await output.register_output(var, config)
yield ble_client.register_ble_node(var, config) await ble_client.register_ble_node(var, config)
yield 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)],
@@ -91,6 +118,12 @@ async def to_code(config):
connection_count = len(config.get(CONF_CONNECTIONS, [])) connection_count = len(config.get(CONF_CONNECTIONS, []))
cg.add_define("BLUETOOTH_PROXY_MAX_CONNECTIONS", connection_count) 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,30 @@ 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 // Helper to fill UUID in the appropriate format based on client support and UUID type
@@ -80,9 +94,11 @@ void BluetoothConnection::dump_config() {
void BluetoothConnection::update_allocated_slot_(uint64_t find_value, uint64_t set_value) { void BluetoothConnection::update_allocated_slot_(uint64_t find_value, uint64_t set_value) {
auto &allocated = this->proxy_->connections_free_response_.allocated; auto &allocated = this->proxy_->connections_free_response_.allocated;
auto *it = std::find(allocated.begin(), allocated.end(), find_value); for (auto &slot : allocated) {
if (it != allocated.end()) { if (slot == find_value) {
*it = set_value; slot = set_value;
return;
}
} }
} }
@@ -105,13 +121,24 @@ void BluetoothConnection::set_address(uint64_t 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 V3_WITHOUT_CACHE: 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) {
@@ -125,7 +152,7 @@ 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();
} }
@@ -133,10 +160,7 @@ 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 || this->release_services();
this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
this->release_services();
}
return; return;
} }
@@ -185,8 +209,7 @@ void BluetoothConnection::send_service_for_discovery_() {
service_result.start_handle, service_result.end_handle, 0, &total_char_count); service_result.start_handle, service_result.end_handle, 0, &total_char_count);
if (char_count_status != ESP_GATT_OK) { if (char_count_status != ESP_GATT_OK) {
ESP_LOGE(TAG, "[%d] [%s] Error getting characteristic count, status=%d", this->connection_index_, this->log_connection_error_("esp_ble_gattc_get_attr_count", char_count_status);
this->address_str().c_str(), char_count_status);
this->send_service_ = DONE_SENDING_SERVICES; this->send_service_ = DONE_SENDING_SERVICES;
return; return;
} }
@@ -220,8 +243,7 @@ void BluetoothConnection::send_service_for_discovery_() {
break; break;
} }
if (char_status != ESP_GATT_OK) { if (char_status != ESP_GATT_OK) {
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", this->connection_index_, this->log_connection_error_("esp_ble_gattc_get_all_char", char_status);
this->address_str().c_str(), char_status);
this->send_service_ = DONE_SENDING_SERVICES; this->send_service_ = DONE_SENDING_SERVICES;
return; return;
} }
@@ -244,8 +266,7 @@ void BluetoothConnection::send_service_for_discovery_() {
this->gattc_if_, this->conn_id_, ESP_GATT_DB_DESCRIPTOR, 0, 0, char_result.char_handle, &total_desc_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) { if (desc_count_status != ESP_GATT_OK) {
ESP_LOGE(TAG, "[%d] [%s] Error getting descriptor count for char handle %d, status=%d", this->log_connection_error_("esp_ble_gattc_get_attr_count", desc_count_status);
this->connection_index_, this->address_str().c_str(), char_result.char_handle, desc_count_status);
this->send_service_ = DONE_SENDING_SERVICES; this->send_service_ = DONE_SENDING_SERVICES;
return; return;
} }
@@ -266,8 +287,7 @@ void BluetoothConnection::send_service_for_discovery_() {
break; break;
} }
if (desc_status != ESP_GATT_OK) { if (desc_status != ESP_GATT_OK) {
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_descr error, status=%d", this->connection_index_, this->log_connection_error_("esp_ble_gattc_get_all_descr", desc_status);
this->address_str().c_str(), desc_status);
this->send_service_ = DONE_SENDING_SERVICES; this->send_service_ = DONE_SENDING_SERVICES;
return; return;
} }
@@ -321,6 +341,33 @@ void BluetoothConnection::send_service_for_discovery_() {
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))
@@ -328,10 +375,19 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
switch (event) { switch (event) {
case ESP_GATTC_DISCONNECT_EVT: { case ESP_GATTC_DISCONNECT_EVT: {
this->reset_connection_(param->disconnect.reason); // Don't reset connection yet - wait for CLOSE_EVT to ensure controller has freed resources
// This prevents race condition where we mark slot as free before controller cleanup is complete
ESP_LOGD(TAG, "[%d] [%s] Disconnect, reason=0x%02x", this->connection_index_, this->address_str_.c_str(),
param->disconnect.reason);
// Send disconnection notification but don't free the slot yet
this->proxy_->send_device_connection(this->address_, false, 0, param->disconnect.reason);
break; break;
} }
case ESP_GATTC_CLOSE_EVT: { case ESP_GATTC_CLOSE_EVT: {
ESP_LOGD(TAG, "[%d] [%s] Close, reason=0x%02x, freeing slot", this->connection_index_, this->address_str_.c_str(),
param->close.reason);
// Now the GATT connection is fully closed and controller resources are freed
// Safe to mark the connection slot as available
this->reset_connection_(param->close.reason); this->reset_connection_(param->close.reason);
break; break;
} }
@@ -361,8 +417,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;
} }
@@ -376,8 +431,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;
} }
@@ -389,9 +443,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;
} }
@@ -403,8 +456,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;
} }
@@ -450,8 +503,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;
} }
@@ -459,18 +511,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(),
@@ -479,36 +525,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(),
@@ -517,18 +551,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;
} }
@@ -536,22 +564,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

@@ -8,7 +8,7 @@ namespace esphome::bluetooth_proxy {
class BluetoothProxy; class BluetoothProxy;
class BluetoothConnection : public esp32_ble_client::BLEClientBase { class BluetoothConnection final : public esp32_ble_client::BLEClientBase {
public: public:
void dump_config() override; void dump_config() override;
void loop() override; void loop() override;
@@ -33,13 +33,18 @@ class BluetoothConnection : public esp32_ble_client::BLEClientBase {
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 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,18 +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->connections_free_response_.limit = this->connections_.size();
this->connections_free_response_.free = this->connections_.size();
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) {
@@ -53,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
@@ -65,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();
} }
} }
@@ -106,40 +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_);
} }
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();
} }
@@ -162,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;
} }
@@ -170,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
@@ -189,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");
@@ -199,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",
@@ -223,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_);
@@ -307,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;
} }
@@ -327,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;
} }
@@ -341,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;
} }
@@ -355,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;
} }
@@ -369,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_) {
@@ -378,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;
} }

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;
@@ -48,7 +50,7 @@ enum BluetoothProxySubscriptionFlag : uint32_t {
SUBSCRIPTION_RAW_ADVERTISEMENTS = 1 << 0, SUBSCRIPTION_RAW_ADVERTISEMENTS = 1 << 0,
}; };
class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Component { class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, public Component {
friend class BluetoothConnection; // Allow connection to update connections_free_response_ friend class BluetoothConnection; // Allow connection to update connections_free_response_
public: public:
BluetoothProxy(); BluetoothProxy();
@@ -63,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);
@@ -133,17 +137,20 @@ 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};
@@ -153,7 +160,7 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
// 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

@@ -203,7 +203,7 @@ void BMI160Component::dump_config() {
i2c::ErrorCode BMI160Component::read_le_int16_(uint8_t reg, int16_t *value, uint8_t len) { i2c::ErrorCode BMI160Component::read_le_int16_(uint8_t reg, int16_t *value, uint8_t len) {
uint8_t raw_data[len * 2]; uint8_t raw_data[len * 2];
// read using read_register because we have little-endian data, and read_bytes_16 will swap it // read using read_register because we have little-endian data, and read_bytes_16 will swap it
i2c::ErrorCode err = this->read_register(reg, raw_data, len * 2, true); i2c::ErrorCode err = this->read_register(reg, raw_data, len * 2);
if (err != i2c::ERROR_OK) { if (err != i2c::ERROR_OK) {
return err; return err;
} }

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 {
@@ -61,25 +63,25 @@ void BMP280Component::setup() {
// Read the chip id twice, to work around a bug where the first read is 0. // Read the chip id twice, to work around a bug where the first read is 0.
// 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->bmp_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->bmp_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->bmp_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.
@@ -87,15 +89,13 @@ void BMP280Component::setup() {
uint8_t retry = 5; uint8_t retry = 5;
do { do {
delay(2); delay(2);
if (!this->read_byte(BMP280_REGISTER_STATUS, &status)) { if (!this->bmp_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;
} }
@@ -115,15 +115,15 @@ void BMP280Component::setup() {
this->calibration_.p9 = this->read_s16_le_(0x9E); this->calibration_.p9 = this->read_s16_le_(0x9E);
uint8_t config_register = 0; uint8_t config_register = 0;
if (!this->read_byte(BMP280_REGISTER_CONFIG, &config_register)) { if (!this->bmp_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->bmp_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:
@@ -159,7 +159,7 @@ void BMP280Component::update() {
meas_value |= (this->temperature_oversampling_ & 0b111) << 5; meas_value |= (this->temperature_oversampling_ & 0b111) << 5;
meas_value |= (this->pressure_oversampling_ & 0b111) << 2; meas_value |= (this->pressure_oversampling_ & 0b111) << 2;
meas_value |= 0b01; // Forced mode meas_value |= 0b01; // Forced mode
if (!this->write_byte(BMP280_REGISTER_CONTROL, meas_value)) { if (!this->bmp_write_byte(BMP280_REGISTER_CONTROL, meas_value)) {
this->status_set_warning(); this->status_set_warning();
return; return;
} }
@@ -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)
@@ -188,9 +188,10 @@ void BMP280Component::update() {
} }
float BMP280Component::read_temperature_(int32_t *t_fine) { float BMP280Component::read_temperature_(int32_t *t_fine) {
uint8_t data[3]; uint8_t data[3]{};
if (!this->read_bytes(BMP280_REGISTER_TEMPDATA, data, 3)) if (!this->bmp_read_bytes(BMP280_REGISTER_TEMPDATA, data, 3))
return NAN; return NAN;
ESP_LOGV(TAG, "Read temperature data, raw: %02X %02X %02X", data[0], data[1], data[2]);
int32_t adc = ((data[0] & 0xFF) << 16) | ((data[1] & 0xFF) << 8) | (data[2] & 0xFF); int32_t adc = ((data[0] & 0xFF) << 16) | ((data[1] & 0xFF) << 8) | (data[2] & 0xFF);
adc >>= 4; adc >>= 4;
if (adc == 0x80000) { if (adc == 0x80000) {
@@ -212,7 +213,7 @@ float BMP280Component::read_temperature_(int32_t *t_fine) {
float BMP280Component::read_pressure_(int32_t t_fine) { float BMP280Component::read_pressure_(int32_t t_fine) {
uint8_t data[3]; uint8_t data[3];
if (!this->read_bytes(BMP280_REGISTER_PRESSUREDATA, data, 3)) if (!this->bmp_read_bytes(BMP280_REGISTER_PRESSUREDATA, data, 3))
return NAN; return NAN;
int32_t adc = ((data[0] & 0xFF) << 16) | ((data[1] & 0xFF) << 8) | (data[2] & 0xFF); int32_t adc = ((data[0] & 0xFF) << 16) | ((data[1] & 0xFF) << 8) | (data[2] & 0xFF);
adc >>= 4; adc >>= 4;
@@ -258,12 +259,12 @@ void BMP280Component::set_pressure_oversampling(BMP280Oversampling pressure_over
void BMP280Component::set_iir_filter(BMP280IIRFilter iir_filter) { this->iir_filter_ = iir_filter; } void BMP280Component::set_iir_filter(BMP280IIRFilter iir_filter) { this->iir_filter_ = iir_filter; }
uint8_t BMP280Component::read_u8_(uint8_t a_register) { uint8_t BMP280Component::read_u8_(uint8_t a_register) {
uint8_t data = 0; uint8_t data = 0;
this->read_byte(a_register, &data); this->bmp_read_byte(a_register, &data);
return data; return data;
} }
uint16_t BMP280Component::read_u16_le_(uint8_t a_register) { uint16_t BMP280Component::read_u16_le_(uint8_t a_register) {
uint16_t data = 0; uint16_t data = 0;
this->read_byte_16(a_register, &data); this->bmp_read_byte_16(a_register, &data);
return (data >> 8) | (data << 8); return (data >> 8) | (data << 8);
} }
int16_t BMP280Component::read_s16_le_(uint8_t a_register) { return this->read_u16_le_(a_register); } int16_t BMP280Component::read_s16_le_(uint8_t a_register) { return this->read_u16_le_(a_register); }

View File

@@ -67,12 +67,12 @@ class BMP280Component : public PollingComponent {
float get_setup_priority() const override; float get_setup_priority() const override;
void update() override; void update() override;
virtual bool read_byte(uint8_t a_register, uint8_t *data) = 0;
virtual bool write_byte(uint8_t a_register, uint8_t data) = 0;
virtual bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0;
virtual bool read_byte_16(uint8_t a_register, uint16_t *data) = 0;
protected: protected:
virtual bool bmp_read_byte(uint8_t a_register, uint8_t *data) = 0;
virtual bool bmp_write_byte(uint8_t a_register, uint8_t data) = 0;
virtual bool bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0;
virtual bool bmp_read_byte_16(uint8_t a_register, uint16_t *data) = 0;
/// Read the temperature value and store the calculated ambient temperature in t_fine. /// Read the temperature value and store the calculated ambient temperature in t_fine.
float read_temperature_(int32_t *t_fine); float read_temperature_(int32_t *t_fine);
/// Read the pressure value in hPa using the provided t_fine value. /// Read the pressure value in hPa using the provided t_fine value.

View File

@@ -5,19 +5,6 @@
namespace esphome { namespace esphome {
namespace bmp280_i2c { namespace bmp280_i2c {
bool BMP280I2CComponent::read_byte(uint8_t a_register, uint8_t *data) {
return I2CDevice::read_byte(a_register, data);
};
bool BMP280I2CComponent::write_byte(uint8_t a_register, uint8_t data) {
return I2CDevice::write_byte(a_register, data);
};
bool BMP280I2CComponent::read_bytes(uint8_t a_register, uint8_t *data, size_t len) {
return I2CDevice::read_bytes(a_register, data, len);
};
bool BMP280I2CComponent::read_byte_16(uint8_t a_register, uint16_t *data) {
return I2CDevice::read_byte_16(a_register, data);
};
void BMP280I2CComponent::dump_config() { void BMP280I2CComponent::dump_config() {
LOG_I2C_DEVICE(this); LOG_I2C_DEVICE(this);
BMP280Component::dump_config(); BMP280Component::dump_config();

View File

@@ -11,10 +11,12 @@ static const char *const TAG = "bmp280_i2c.sensor";
/// This class implements support for the BMP280 Temperature+Pressure i2c sensor. /// This class implements support for the BMP280 Temperature+Pressure i2c sensor.
class BMP280I2CComponent : public esphome::bmp280_base::BMP280Component, public i2c::I2CDevice { class BMP280I2CComponent : public esphome::bmp280_base::BMP280Component, public i2c::I2CDevice {
public: public:
bool read_byte(uint8_t a_register, uint8_t *data) override; bool bmp_read_byte(uint8_t a_register, uint8_t *data) override { return read_byte(a_register, data); }
bool write_byte(uint8_t a_register, uint8_t data) override; bool bmp_write_byte(uint8_t a_register, uint8_t data) override { return write_byte(a_register, data); }
bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) override; bool bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) override {
bool read_byte_16(uint8_t a_register, uint16_t *data) override; return read_bytes(a_register, data, len);
}
bool bmp_read_byte_16(uint8_t a_register, uint16_t *data) override { return read_byte_16(a_register, data); }
void dump_config() override; void dump_config() override;
}; };

View File

@@ -28,7 +28,7 @@ void BMP280SPIComponent::setup() {
// 0x77 is transferred, for read access, the byte 0xF7 is transferred. // 0x77 is transferred, for read access, the byte 0xF7 is transferred.
// https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bmp280-ds001.pdf // https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bmp280-ds001.pdf
bool BMP280SPIComponent::read_byte(uint8_t a_register, uint8_t *data) { bool BMP280SPIComponent::bmp_read_byte(uint8_t a_register, uint8_t *data) {
this->enable(); this->enable();
this->transfer_byte(set_bit(a_register, 7)); this->transfer_byte(set_bit(a_register, 7));
*data = this->transfer_byte(0); *data = this->transfer_byte(0);
@@ -36,7 +36,7 @@ bool BMP280SPIComponent::read_byte(uint8_t a_register, uint8_t *data) {
return true; return true;
} }
bool BMP280SPIComponent::write_byte(uint8_t a_register, uint8_t data) { bool BMP280SPIComponent::bmp_write_byte(uint8_t a_register, uint8_t data) {
this->enable(); this->enable();
this->transfer_byte(clear_bit(a_register, 7)); this->transfer_byte(clear_bit(a_register, 7));
this->transfer_byte(data); this->transfer_byte(data);
@@ -44,7 +44,7 @@ bool BMP280SPIComponent::write_byte(uint8_t a_register, uint8_t data) {
return true; return true;
} }
bool BMP280SPIComponent::read_bytes(uint8_t a_register, uint8_t *data, size_t len) { bool BMP280SPIComponent::bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) {
this->enable(); this->enable();
this->transfer_byte(set_bit(a_register, 7)); this->transfer_byte(set_bit(a_register, 7));
this->read_array(data, len); this->read_array(data, len);
@@ -52,7 +52,7 @@ bool BMP280SPIComponent::read_bytes(uint8_t a_register, uint8_t *data, size_t le
return true; return true;
} }
bool BMP280SPIComponent::read_byte_16(uint8_t a_register, uint16_t *data) { bool BMP280SPIComponent::bmp_read_byte_16(uint8_t a_register, uint16_t *data) {
this->enable(); this->enable();
this->transfer_byte(set_bit(a_register, 7)); this->transfer_byte(set_bit(a_register, 7));
((uint8_t *) data)[1] = this->transfer_byte(0); ((uint8_t *) data)[1] = this->transfer_byte(0);

View File

@@ -10,10 +10,10 @@ class BMP280SPIComponent : public esphome::bmp280_base::BMP280Component,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_200KHZ> { spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_200KHZ> {
void setup() override; void setup() override;
bool read_byte(uint8_t a_register, uint8_t *data) override; bool bmp_read_byte(uint8_t a_register, uint8_t *data) override;
bool write_byte(uint8_t a_register, uint8_t data) override; bool bmp_write_byte(uint8_t a_register, uint8_t data) override;
bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) override; bool bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) override;
bool read_byte_16(uint8_t a_register, uint16_t *data) override; bool bmp_read_byte_16(uint8_t a_register, uint16_t *data) override;
}; };
} // namespace bmp280_spi } // namespace bmp280_spi

View File

@@ -6,6 +6,19 @@ namespace button {
static const char *const TAG = "button"; static const char *const TAG = "button";
// Function implementation of LOG_BUTTON macro to reduce code size
void log_button(const char *tag, const char *prefix, const char *type, Button *obj) {
if (obj == nullptr) {
return;
}
ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str());
if (!obj->get_icon().empty()) {
ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon().c_str());
}
}
void Button::press() { void Button::press() {
ESP_LOGD(TAG, "'%s' Pressed.", this->get_name().c_str()); ESP_LOGD(TAG, "'%s' Pressed.", this->get_name().c_str());
this->press_action(); this->press_action();

View File

@@ -7,13 +7,10 @@
namespace esphome { namespace esphome {
namespace button { namespace button {
#define LOG_BUTTON(prefix, type, obj) \ class Button;
if ((obj) != nullptr) { \ void log_button(const char *tag, const char *prefix, const char *type, Button *obj);
ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \
if (!(obj)->get_icon().empty()) { \ #define LOG_BUTTON(prefix, type, obj) log_button(TAG, prefix, LOG_STR_LITERAL(type), obj)
ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \
} \
}
#define SUB_BUTTON(name) \ #define SUB_BUTTON(name) \
protected: \ protected: \

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

@@ -153,8 +153,8 @@ void CCS811Component::dump_config() {
ESP_LOGCONFIG(TAG, "CCS811"); ESP_LOGCONFIG(TAG, "CCS811");
LOG_I2C_DEVICE(this) LOG_I2C_DEVICE(this)
LOG_UPDATE_INTERVAL(this) LOG_UPDATE_INTERVAL(this)
LOG_SENSOR(" ", "CO2 Sensor", this->co2_) LOG_SENSOR(" ", "CO2 Sensor", this->co2_);
LOG_SENSOR(" ", "TVOC Sensor", this->tvoc_) LOG_SENSOR(" ", "TVOC Sensor", this->tvoc_);
LOG_TEXT_SENSOR(" ", "Firmware Version Sensor", this->version_) LOG_TEXT_SENSOR(" ", "Firmware Version Sensor", this->version_)
if (this->baseline_) { if (this->baseline_) {
ESP_LOGCONFIG(TAG, " Baseline: %04X", *this->baseline_); ESP_LOGCONFIG(TAG, " Baseline: %04X", *this->baseline_);

View File

@@ -91,7 +91,7 @@ bool CH422GComponent::read_inputs_() {
// Write a register. Can't use the standard write_byte() method because there is no single pre-configured i2c address. // Write a register. Can't use the standard write_byte() method because there is no single pre-configured i2c address.
bool CH422GComponent::write_reg_(uint8_t reg, uint8_t value) { bool CH422GComponent::write_reg_(uint8_t reg, uint8_t value) {
auto err = this->bus_->write(reg, &value, 1); auto err = this->bus_->write_readv(reg, &value, 1, nullptr, 0);
if (err != i2c::ERROR_OK) { if (err != i2c::ERROR_OK) {
this->status_set_warning(str_sprintf("write failed for register 0x%X, error %d", reg, err).c_str()); this->status_set_warning(str_sprintf("write failed for register 0x%X, error %d", reg, err).c_str());
return false; return false;
@@ -102,7 +102,7 @@ bool CH422GComponent::write_reg_(uint8_t reg, uint8_t value) {
uint8_t CH422GComponent::read_reg_(uint8_t reg) { uint8_t CH422GComponent::read_reg_(uint8_t reg) {
uint8_t value; uint8_t value;
auto err = this->bus_->read(reg, &value, 1); auto err = this->bus_->write_readv(reg, nullptr, 0, &value, 1);
if (err != i2c::ERROR_OK) { if (err != i2c::ERROR_OK) {
this->status_set_warning(str_sprintf("read failed for register 0x%X, error %d", reg, err).c_str()); this->status_set_warning(str_sprintf("read failed for register 0x%X, error %d", reg, err).c_str());
return 0; return 0;

View File

@@ -327,7 +327,7 @@ void Climate::add_on_control_callback(std::function<void(ClimateCall &)> &&callb
static const uint32_t RESTORE_STATE_VERSION = 0x848EA6ADUL; static const uint32_t RESTORE_STATE_VERSION = 0x848EA6ADUL;
optional<ClimateDeviceRestoreState> Climate::restore_state_() { optional<ClimateDeviceRestoreState> Climate::restore_state_() {
this->rtc_ = global_preferences->make_preference<ClimateDeviceRestoreState>(this->get_object_id_hash() ^ this->rtc_ = global_preferences->make_preference<ClimateDeviceRestoreState>(this->get_preference_hash() ^
RESTORE_STATE_VERSION); RESTORE_STATE_VERSION);
ClimateDeviceRestoreState recovered{}; ClimateDeviceRestoreState recovered{};
if (!this->rtc_.load(&recovered)) if (!this->rtc_.load(&recovered))

View File

@@ -228,9 +228,9 @@ async def cover_stop_to_code(config, action_id, template_arg, args):
@automation.register_action("cover.toggle", ToggleAction, COVER_ACTION_SCHEMA) @automation.register_action("cover.toggle", ToggleAction, COVER_ACTION_SCHEMA)
def cover_toggle_to_code(config, action_id, template_arg, args): async def cover_toggle_to_code(config, action_id, template_arg, args):
paren = yield cg.get_variable(config[CONF_ID]) paren = await cg.get_variable(config[CONF_ID])
yield cg.new_Pvariable(action_id, template_arg, paren) return cg.new_Pvariable(action_id, template_arg, paren)
COVER_CONTROL_ACTION_SCHEMA = cv.Schema( COVER_CONTROL_ACTION_SCHEMA = cv.Schema(

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();
} }
} }
@@ -198,7 +194,7 @@ void Cover::publish_state(bool save) {
} }
} }
optional<CoverRestoreState> Cover::restore_state_() { optional<CoverRestoreState> Cover::restore_state_() {
this->rtc_ = global_preferences->make_preference<CoverRestoreState>(this->get_object_id_hash()); this->rtc_ = global_preferences->make_preference<CoverRestoreState>(this->get_preference_hash());
CoverRestoreState recovered{}; CoverRestoreState recovered{};
if (!this->rtc_.load(&recovered)) if (!this->rtc_.load(&recovered))
return {}; return {};

View File

@@ -13,7 +13,7 @@ from esphome.const import (
) )
from esphome.core import CORE from esphome.core import CORE
CODEOWNERS = ["@OttoWinter"] CODEOWNERS = ["@esphome/core"]
DEPENDENCIES = ["logger"] DEPENDENCIES = ["logger"]
CONF_DEBUG_ID = "debug_id" CONF_DEBUG_ID = "debug_id"
@@ -48,6 +48,15 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config): async def to_code(config):
if CORE.using_zephyr: if CORE.using_zephyr:
zephyr_add_prj_conf("HWINFO", True) 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)

View File

@@ -1,4 +1,6 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
#include "soc/soc_caps.h"
#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 +76,24 @@ 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);
#if !SOC_GPIO_SUPPORT_HOLD_SINGLE_IO_IN_DSLP
// Some ESP32 variants support holding a single GPIO during deep sleep without this function
// For those variants, gpio_hold_en() is sufficient to hold the pin state during deep sleep
gpio_deep_sleep_hold_en();
#endif
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 +117,19 @@ 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);
#if !SOC_GPIO_SUPPORT_HOLD_SINGLE_IO_IN_DSLP
// Some ESP32 variants support holding a single GPIO during deep sleep without this function
// For those variants, gpio_hold_en() is sufficient to hold the pin state during deep sleep
gpio_deep_sleep_hold_en();
#endif
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

@@ -41,7 +41,7 @@ void DutyTimeSensor::setup() {
uint32_t seconds = 0; uint32_t seconds = 0;
if (this->restore_) { if (this->restore_) {
this->pref_ = global_preferences->make_preference<uint32_t>(this->get_object_id_hash()); this->pref_ = global_preferences->make_preference<uint32_t>(this->get_preference_hash());
this->pref_.load(&seconds); this->pref_.load(&seconds);
} }

View File

@@ -83,7 +83,7 @@ void EE895Component::write_command_(uint16_t addr, uint16_t reg_cnt) {
crc16 = calc_crc16_(address, 6); crc16 = calc_crc16_(address, 6);
address[5] = crc16 & 0xFF; address[5] = crc16 & 0xFF;
address[6] = (crc16 >> 8) & 0xFF; address[6] = (crc16 >> 8) & 0xFF;
this->write(address, 7, true); this->write(address, 7);
} }
float EE895Component::read_float_() { float EE895Component::read_float_() {

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,
@@ -79,6 +80,15 @@ CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert"
CONF_EXECUTE_FROM_PSRAM = "execute_from_psram" 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",
@@ -623,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(
@@ -811,8 +824,9 @@ async def to_code(config):
cg.set_cpp_standard("gnu++20") cg.set_cpp_standard("gnu++20")
cg.add_build_flag("-DUSE_ESP32") cg.add_build_flag("-DUSE_ESP32")
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{config[CONF_VARIANT]}") variant = config[CONF_VARIANT]
cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[config[CONF_VARIANT]]) cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{variant}")
cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[variant])
cg.add_define(ThreadModel.MULTI_ATOMICS) cg.add_define(ThreadModel.MULTI_ATOMICS)
cg.add_platformio_option("lib_ldf_mode", "off") cg.add_platformio_option("lib_ldf_mode", "off")
@@ -846,6 +860,7 @@ async def to_code(config):
cg.add_platformio_option( cg.add_platformio_option(
"platform_packages", ["espressif/toolchain-esp32ulp@2.35.0-20220830"] "platform_packages", ["espressif/toolchain-esp32ulp@2.35.0-20220830"]
) )
add_idf_sdkconfig_option(f"CONFIG_IDF_TARGET_{variant}", True)
add_idf_sdkconfig_option( add_idf_sdkconfig_option(
f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True
) )
@@ -937,6 +952,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))

View File

@@ -8,6 +8,7 @@
#include <cinttypes> #include <cinttypes>
#include <vector> #include <vector>
#include <string> #include <string>
#include <memory>
namespace esphome { namespace esphome {
namespace esp32 { namespace esp32 {
@@ -156,20 +157,23 @@ class ESP32Preferences : public ESPPreferences {
return failed == 0; return failed == 0;
} }
bool is_changed(const uint32_t nvs_handle, const NVSData &to_save) { bool is_changed(const uint32_t nvs_handle, const NVSData &to_save) {
NVSData stored_data{};
size_t actual_len; size_t actual_len;
esp_err_t err = nvs_get_blob(nvs_handle, to_save.key.c_str(), nullptr, &actual_len); esp_err_t err = nvs_get_blob(nvs_handle, to_save.key.c_str(), nullptr, &actual_len);
if (err != 0) { if (err != 0) {
ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", to_save.key.c_str(), esp_err_to_name(err)); ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", to_save.key.c_str(), esp_err_to_name(err));
return true; return true;
} }
stored_data.data.resize(actual_len); // Check size first before allocating memory
err = nvs_get_blob(nvs_handle, to_save.key.c_str(), stored_data.data.data(), &actual_len); if (actual_len != to_save.data.size()) {
return true;
}
auto stored_data = std::make_unique<uint8_t[]>(actual_len);
err = nvs_get_blob(nvs_handle, to_save.key.c_str(), stored_data.get(), &actual_len);
if (err != 0) { if (err != 0) {
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", to_save.key.c_str(), esp_err_to_name(err)); ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", to_save.key.c_str(), esp_err_to_name(err));
return true; return true;
} }
return to_save.data != stored_data.data; return memcmp(to_save.data.data(), stored_data.get(), to_save.data.size()) != 0;
} }
bool reset() override { bool reset() override {

View File

@@ -3,15 +3,33 @@ import re
from esphome import automation from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant from esphome.components.esp32 import (
VARIANT_ESP32C2,
VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6,
VARIANT_ESP32H2,
VARIANT_ESP32S3,
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,
CONF_NAME_ADD_MAC_SUFFIX,
CONF_TX_POWER,
)
from esphome.core import CORE, TimePeriod from esphome.core import CORE, TimePeriod
from esphome.core.config import CONF_NAME_ADD_MAC_SUFFIX from esphome.cpp_types import MockObj
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,9 +133,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_CONNECTION_TIMEOUT = "connection_timeout"
CONF_MAX_NOTIFICATIONS = "max_notifications"
NO_BLUETOOTH_VARIANTS = [const.VARIANT_ESP32S2] NO_BLUETOOTH_VARIANTS = [const.VARIANT_ESP32S2]
@@ -143,7 +163,8 @@ IO_CAPABILITY = {
esp_power_level_t = cg.global_ns.enum("esp_power_level_t") esp_power_level_t = cg.global_ns.enum("esp_power_level_t")
TX_POWER_LEVELS = { # Power level mappings for code generation - ESP32 classic
TX_POWER_LEVELS_ESP32 = {
-12: esp_power_level_t.ESP_PWR_LVL_N12, -12: esp_power_level_t.ESP_PWR_LVL_N12,
-9: esp_power_level_t.ESP_PWR_LVL_N9, -9: esp_power_level_t.ESP_PWR_LVL_N9,
-6: esp_power_level_t.ESP_PWR_LVL_N6, -6: esp_power_level_t.ESP_PWR_LVL_N6,
@@ -154,6 +175,53 @@ TX_POWER_LEVELS = {
9: esp_power_level_t.ESP_PWR_LVL_P9, 9: esp_power_level_t.ESP_PWR_LVL_P9,
} }
# Power level mappings for code generation - Extended variants
TX_POWER_LEVELS_EXT = {
-24: esp_power_level_t.ESP_PWR_LVL_N24,
-21: esp_power_level_t.ESP_PWR_LVL_N21,
-18: esp_power_level_t.ESP_PWR_LVL_N18,
-15: esp_power_level_t.ESP_PWR_LVL_N15,
-12: esp_power_level_t.ESP_PWR_LVL_N12,
-9: esp_power_level_t.ESP_PWR_LVL_N9,
-6: esp_power_level_t.ESP_PWR_LVL_N6,
-3: esp_power_level_t.ESP_PWR_LVL_N3,
0: esp_power_level_t.ESP_PWR_LVL_N0,
3: esp_power_level_t.ESP_PWR_LVL_P3,
6: esp_power_level_t.ESP_PWR_LVL_P6,
9: esp_power_level_t.ESP_PWR_LVL_P9,
12: esp_power_level_t.ESP_PWR_LVL_P12,
15: esp_power_level_t.ESP_PWR_LVL_P15,
18: esp_power_level_t.ESP_PWR_LVL_P18,
20: esp_power_level_t.ESP_PWR_LVL_P20,
}
def _get_tx_power_levels() -> dict[str, MockObj]:
variant = get_esp32_variant()
if variant in [
VARIANT_ESP32C2,
VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6,
VARIANT_ESP32H2,
VARIANT_ESP32S3,
]:
return TX_POWER_LEVELS_EXT
return TX_POWER_LEVELS_ESP32
def validate_tx_power(value: int) -> int:
value = cv.decibel(value)
power_levels = _get_tx_power_levels()
if value not in power_levels:
raise cv.Invalid(
f"TX power {value}dBm is not valid. "
f"Valid values are: {', '.join(str(v) + 'dBm' for v in sorted(power_levels.keys()))}"
)
# Return just the dBm value, we'll map it to enum in to_code
return value
CONFIG_SCHEMA = cv.Schema( CONFIG_SCHEMA = cv.Schema(
{ {
cv.GenerateID(): cv.declare_id(ESP32BLE), cv.GenerateID(): cv.declare_id(ESP32BLE),
@@ -161,7 +229,9 @@ CONFIG_SCHEMA = cv.Schema(
cv.Optional(CONF_IO_CAPABILITY, default="none"): cv.enum( cv.Optional(CONF_IO_CAPABILITY, default="none"): cv.enum(
IO_CAPABILITY, lower=True IO_CAPABILITY, lower=True
), ),
cv.Optional(CONF_TX_POWER): validate_tx_power,
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,
@@ -173,6 +243,11 @@ CONFIG_SCHEMA = cv.Schema(
cv.positive_time_period_seconds, cv.positive_time_period_seconds,
cv.Range(min=TimePeriod(seconds=10), max=TimePeriod(seconds=180)), 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)
@@ -245,6 +320,9 @@ async def to_code(config):
cg.add(var.set_advertising_cycle_time(config[CONF_ADVERTISING_CYCLE_TIME])) cg.add(var.set_advertising_cycle_time(config[CONF_ADVERTISING_CYCLE_TIME]))
if (name := config.get(CONF_NAME)) is not None: if (name := config.get(CONF_NAME)) is not None:
cg.add(var.set_name(name)) cg.add(var.set_name(name))
if (tx_power := config.get(CONF_TX_POWER)) is not None:
# The validation already returned the enum value
cg.add(var.set_tx_power(_get_tx_power_levels()[tx_power]))
await cg.register_component(var, config) await cg.register_component(var, config)
if CORE.using_esp_idf: if CORE.using_esp_idf:
@@ -271,9 +349,26 @@ async def to_code(config):
add_idf_sdkconfig_option( add_idf_sdkconfig_option(
"CONFIG_BT_BLE_ESTAB_LINK_CONN_TOUT", timeout_seconds "CONFIG_BT_BLE_ESTAB_LINK_CONN_TOUT", timeout_seconds
) )
# Increase GATT client connection retry count for problematic devices
# Default in ESP-IDF is 3, we increase to 10 for better reliability with
# low-power/timing-sensitive devices
add_idf_sdkconfig_option("CONFIG_BT_GATTC_CONNECT_RETRY_COUNT", 10)
# 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")
cg.add_define("USE_ESP32_BLE_UUID")
@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;
@@ -208,6 +212,15 @@ bool ESP32BLE::ble_setup_() {
return false; return false;
} }
// Set TX power for all BLE operations (advertising, scanning, connections)
err = esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_DEFAULT, this->tx_power_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "esp_ble_tx_power_set failed: %s", esp_err_to_name(err));
// Continue anyway as this is not critical
} else {
ESP_LOGD(TAG, "BLE TX power set to level %d", this->tx_power_);
}
// BLE takes some time to be fully set up, 200ms should be more than enough // BLE takes some time to be fully set up, 200ms should be more than enough
delay(200); // NOLINT delay(200); // NOLINT
@@ -302,7 +315,7 @@ void ESP32BLE::loop() {
case BLEEvent::GATTS: { case BLEEvent::GATTS: {
esp_gatts_cb_event_t event = ble_event->event_.gatts.gatts_event; esp_gatts_cb_event_t event = ble_event->event_.gatts.gatts_event;
esp_gatt_if_t gatts_if = ble_event->event_.gatts.gatts_if; esp_gatt_if_t gatts_if = ble_event->event_.gatts.gatts_if;
esp_ble_gatts_cb_param_t *param = ble_event->event_.gatts.gatts_param; esp_ble_gatts_cb_param_t *param = &ble_event->event_.gatts.gatts_param;
ESP_LOGV(TAG, "gatts_event [esp_gatt_if: %d] - %d", gatts_if, event); ESP_LOGV(TAG, "gatts_event [esp_gatt_if: %d] - %d", gatts_if, event);
for (auto *gatts_handler : this->gatts_event_handlers_) { for (auto *gatts_handler : this->gatts_event_handlers_) {
gatts_handler->gatts_event_handler(event, gatts_if, param); gatts_handler->gatts_event_handler(event, gatts_if, param);
@@ -312,7 +325,7 @@ void ESP32BLE::loop() {
case BLEEvent::GATTC: { case BLEEvent::GATTC: {
esp_gattc_cb_event_t event = ble_event->event_.gattc.gattc_event; esp_gattc_cb_event_t event = ble_event->event_.gattc.gattc_event;
esp_gatt_if_t gattc_if = ble_event->event_.gattc.gattc_if; esp_gatt_if_t gattc_if = ble_event->event_.gattc.gattc_if;
esp_ble_gattc_cb_param_t *param = ble_event->event_.gattc.gattc_param; esp_ble_gattc_cb_param_t *param = &ble_event->event_.gattc.gattc_param;
ESP_LOGV(TAG, "gattc_event [esp_gatt_if: %d] - %d", gattc_if, event); ESP_LOGV(TAG, "gattc_event [esp_gatt_if: %d] - %d", gattc_if, event);
for (auto *gattc_handler : this->gattc_event_handlers_) { for (auto *gattc_handler : this->gattc_event_handlers_) {
gattc_handler->gattc_event_handler(event, gattc_if, param); gattc_handler->gattc_event_handler(event, gattc_if, param);
@@ -394,9 +407,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();
@@ -514,11 +529,106 @@ void ESP32BLE::dump_config() {
io_capability_s = "invalid"; io_capability_s = "invalid";
break; break;
} }
// Convert TX power level to dBm for display
int tx_power_dbm = 0;
#if defined(CONFIG_IDF_TARGET_ESP32)
// ESP32 classic power levels (0-7)
switch (this->tx_power_) {
case 0:
tx_power_dbm = -12;
break; // ESP_PWR_LVL_N12
case 1:
tx_power_dbm = -9;
break; // ESP_PWR_LVL_N9
case 2:
tx_power_dbm = -6;
break; // ESP_PWR_LVL_N6
case 3:
tx_power_dbm = -3;
break; // ESP_PWR_LVL_N3
case 4:
tx_power_dbm = 0;
break; // ESP_PWR_LVL_N0
case 5:
tx_power_dbm = 3;
break; // ESP_PWR_LVL_P3
case 6:
tx_power_dbm = 6;
break; // ESP_PWR_LVL_P6
case 7:
tx_power_dbm = 9;
break; // ESP_PWR_LVL_P9
default:
tx_power_dbm = 0;
break;
}
#elif defined(CONFIG_IDF_TARGET_ESP32C2) || defined(CONFIG_IDF_TARGET_ESP32C3) || \
defined(CONFIG_IDF_TARGET_ESP32C5) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32H2) || \
defined(CONFIG_IDF_TARGET_ESP32S3)
// Extended power levels for C2/C3/C5/C6/H2/S3 (0-15)
switch (this->tx_power_) {
case 0:
tx_power_dbm = -24;
break; // ESP_PWR_LVL_N24
case 1:
tx_power_dbm = -21;
break; // ESP_PWR_LVL_N21
case 2:
tx_power_dbm = -18;
break; // ESP_PWR_LVL_N18
case 3:
tx_power_dbm = -15;
break; // ESP_PWR_LVL_N15
case 4:
tx_power_dbm = -12;
break; // ESP_PWR_LVL_N12
case 5:
tx_power_dbm = -9;
break; // ESP_PWR_LVL_N9
case 6:
tx_power_dbm = -6;
break; // ESP_PWR_LVL_N6
case 7:
tx_power_dbm = -3;
break; // ESP_PWR_LVL_N3
case 8:
tx_power_dbm = 0;
break; // ESP_PWR_LVL_N0
case 9:
tx_power_dbm = 3;
break; // ESP_PWR_LVL_P3
case 10:
tx_power_dbm = 6;
break; // ESP_PWR_LVL_P6
case 11:
tx_power_dbm = 9;
break; // ESP_PWR_LVL_P9
case 12:
tx_power_dbm = 12;
break; // ESP_PWR_LVL_P12
case 13:
tx_power_dbm = 15;
break; // ESP_PWR_LVL_P15
case 14:
tx_power_dbm = 18;
break; // ESP_PWR_LVL_P18
case 15:
tx_power_dbm = 20;
break; // ESP_PWR_LVL_P20
default:
tx_power_dbm = 0;
break;
}
#else
// Unknown variant
tx_power_dbm = 0;
#endif
ESP_LOGCONFIG(TAG, ESP_LOGCONFIG(TAG,
"BLE:\n" "BLE:\n"
" MAC address: %s\n" " MAC address: %s\n"
" IO Capability: %s", " IO Capability: %s\n"
format_mac_address_pretty(mac_address).c_str(), io_capability_s); " TX Power: %d dBm",
format_mac_address_pretty(mac_address).c_str(), io_capability_s, tx_power_dbm);
} else { } else {
ESP_LOGCONFIG(TAG, "Bluetooth stack is not enabled"); ESP_LOGCONFIG(TAG, "Bluetooth stack is not enabled");
} }

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"
@@ -17,27 +20,21 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
#include <esp_bt.h>
#include <esp_gap_ble_api.h> #include <esp_gap_ble_api.h>
#include <esp_gattc_api.h> #include <esp_gattc_api.h>
#include <esp_gatts_api.h> #include <esp_gatts_api.h>
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)
@@ -98,6 +95,7 @@ class BLEStatusEventHandler {
class ESP32BLE : public Component { class ESP32BLE : public Component {
public: public:
void set_io_capability(IoCapability io_capability) { this->io_cap_ = (esp_ble_io_cap_t) io_capability; } void set_io_capability(IoCapability io_capability) { this->io_cap_ = (esp_ble_io_cap_t) io_capability; }
void set_tx_power(esp_power_level_t tx_power) { this->tx_power_ = tx_power; }
void set_advertising_cycle_time(uint32_t advertising_cycle_time) { void set_advertising_cycle_time(uint32_t advertising_cycle_time) {
this->advertising_cycle_time_ = advertising_cycle_time; this->advertising_cycle_time_ = advertising_cycle_time;
@@ -113,6 +111,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 +119,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 +140,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 +162,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
@@ -170,6 +174,7 @@ class ESP32BLE : public Component {
// 1-byte aligned members (grouped together to minimize padding) // 1-byte aligned members (grouped together to minimize padding)
BLEComponentState state_{BLE_COMPONENT_STATE_OFF}; // 1 byte (uint8_t enum) BLEComponentState state_{BLE_COMPONENT_STATE_OFF}; // 1 byte (uint8_t enum)
bool enable_on_boot_{}; // 1 byte bool enable_on_boot_{}; // 1 byte
esp_power_level_t tx_power_{ESP_PWR_LVL_P9}; // 1 byte (default: +9 dBm)
}; };
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)

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

@@ -3,8 +3,7 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
#include <cstddef> // for offsetof #include <cstddef> // for offsetof
#include <vector> #include <cstring> // for memcpy
#include <esp_gap_ble_api.h> #include <esp_gap_ble_api.h>
#include <esp_gattc_api.h> #include <esp_gattc_api.h>
#include <esp_gatts_api.h> #include <esp_gatts_api.h>
@@ -62,10 +61,24 @@ static_assert(offsetof(esp_ble_gap_cb_param_t, read_rssi_cmpl.rssi) == sizeof(es
static_assert(offsetof(esp_ble_gap_cb_param_t, read_rssi_cmpl.remote_addr) == sizeof(esp_bt_status_t) + sizeof(int8_t), static_assert(offsetof(esp_ble_gap_cb_param_t, read_rssi_cmpl.remote_addr) == sizeof(esp_bt_status_t) + sizeof(int8_t),
"remote_addr must follow rssi in read_rssi_cmpl"); "remote_addr must follow rssi in read_rssi_cmpl");
// Param struct sizes on ESP32
static constexpr size_t GATTC_PARAM_SIZE = 28;
static constexpr size_t GATTS_PARAM_SIZE = 32;
// Maximum size for inline storage of data
// GATTC: 80 - 28 (param) - 8 (other fields) = 44 bytes for data
// GATTS: 80 - 32 (param) - 8 (other fields) = 40 bytes for data
static constexpr size_t GATTC_INLINE_DATA_SIZE = 44;
static constexpr size_t GATTS_INLINE_DATA_SIZE = 40;
// Verify param struct sizes
static_assert(sizeof(esp_ble_gattc_cb_param_t) == GATTC_PARAM_SIZE, "GATTC param size unexpected");
static_assert(sizeof(esp_ble_gatts_cb_param_t) == GATTS_PARAM_SIZE, "GATTS param size unexpected");
// Received GAP, GATTC and GATTS events are only queued, and get processed in the main loop(). // Received GAP, GATTC and GATTS events are only queued, and get processed in the main loop().
// This class stores each event with minimal memory usage. // This class stores each event with minimal memory usage.
// GAP events (99% of traffic) don't have the vector overhead. // GAP events (99% of traffic) don't have the heap allocation overhead.
// GATTC/GATTS events use heap allocation for their param and data. // GATTC/GATTS events use heap allocation for their param and inline storage for small data.
// //
// Event flow: // Event flow:
// 1. ESP-IDF BLE stack calls our static handlers in the BLE task context // 1. ESP-IDF BLE stack calls our static handlers in the BLE task context
@@ -112,21 +125,21 @@ class BLEEvent {
this->init_gap_data_(e, p); this->init_gap_data_(e, p);
} }
// Constructor for GATTC events - uses heap allocation // Constructor for GATTC events - param stored inline, data may use heap
// IMPORTANT: The heap allocation is REQUIRED and must not be removed as an optimization. // IMPORTANT: We MUST copy the param struct because the pointer from ESP-IDF
// The param pointer from ESP-IDF is only valid during the callback execution. // is only valid during the callback execution. Since BLE events are processed
// Since BLE events are processed asynchronously in the main loop, we must create // asynchronously in the main loop, we store our own copy inline to ensure
// our own copy to ensure the data remains valid until the event is processed. // the data remains valid until the event is processed.
BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) {
this->type_ = GATTC; this->type_ = GATTC;
this->init_gattc_data_(e, i, p); this->init_gattc_data_(e, i, p);
} }
// Constructor for GATTS events - uses heap allocation // Constructor for GATTS events - param stored inline, data may use heap
// IMPORTANT: The heap allocation is REQUIRED and must not be removed as an optimization. // IMPORTANT: We MUST copy the param struct because the pointer from ESP-IDF
// The param pointer from ESP-IDF is only valid during the callback execution. // is only valid during the callback execution. Since BLE events are processed
// Since BLE events are processed asynchronously in the main loop, we must create // asynchronously in the main loop, we store our own copy inline to ensure
// our own copy to ensure the data remains valid until the event is processed. // the data remains valid until the event is processed.
BLEEvent(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { BLEEvent(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) {
this->type_ = GATTS; this->type_ = GATTS;
this->init_gatts_data_(e, i, p); this->init_gatts_data_(e, i, p);
@@ -136,25 +149,32 @@ class BLEEvent {
~BLEEvent() { this->release(); } ~BLEEvent() { this->release(); }
// Default constructor for pre-allocation in pool // Default constructor for pre-allocation in pool
BLEEvent() : type_(GAP) {} BLEEvent() : event_{}, type_(GAP) {}
// Invoked on return to EventPool - clean up any heap-allocated data // Invoked on return to EventPool - clean up any heap-allocated data
void release() { void release() {
if (this->type_ == GAP) { switch (this->type_) {
return; case GAP:
} // GAP events don't have heap allocations
if (this->type_ == GATTC) { break;
delete this->event_.gattc.gattc_param; case GATTC:
delete this->event_.gattc.data; // Param is now stored inline, only delete heap data if it was heap-allocated
this->event_.gattc.gattc_param = nullptr; if (!this->event_.gattc.is_inline && this->event_.gattc.data.heap_data != nullptr) {
this->event_.gattc.data = nullptr; delete[] this->event_.gattc.data.heap_data;
return; }
} // Clear critical fields to prevent issues if type changes
if (this->type_ == GATTS) { this->event_.gattc.is_inline = false;
delete this->event_.gatts.gatts_param; this->event_.gattc.data.heap_data = nullptr;
delete this->event_.gatts.data; break;
this->event_.gatts.gatts_param = nullptr; case GATTS:
this->event_.gatts.data = nullptr; // Param is now stored inline, only delete heap data if it was heap-allocated
if (!this->event_.gatts.is_inline && this->event_.gatts.data.heap_data != nullptr) {
delete[] this->event_.gatts.data.heap_data;
}
// Clear critical fields to prevent issues if type changes
this->event_.gatts.is_inline = false;
this->event_.gatts.data.heap_data = nullptr;
break;
} }
} }
@@ -206,20 +226,30 @@ class BLEEvent {
// NOLINTNEXTLINE(readability-identifier-naming) // NOLINTNEXTLINE(readability-identifier-naming)
struct gattc_event { struct gattc_event {
esp_gattc_cb_event_t gattc_event; esp_ble_gattc_cb_param_t gattc_param; // Stored inline (28 bytes)
esp_gatt_if_t gattc_if; esp_gattc_cb_event_t gattc_event; // 4 bytes
esp_ble_gattc_cb_param_t *gattc_param; // Heap-allocated union {
std::vector<uint8_t> *data; // Heap-allocated uint8_t *heap_data; // 4 bytes when heap-allocated
} gattc; // 16 bytes (pointers only) uint8_t inline_data[GATTC_INLINE_DATA_SIZE]; // 44 bytes when stored inline
} data; // 44 bytes total
uint16_t data_len; // 2 bytes
esp_gatt_if_t gattc_if; // 1 byte
bool is_inline; // 1 byte - true when data is stored inline
} gattc; // Total: 80 bytes
// NOLINTNEXTLINE(readability-identifier-naming) // NOLINTNEXTLINE(readability-identifier-naming)
struct gatts_event { struct gatts_event {
esp_gatts_cb_event_t gatts_event; esp_ble_gatts_cb_param_t gatts_param; // Stored inline (32 bytes)
esp_gatt_if_t gatts_if; esp_gatts_cb_event_t gatts_event; // 4 bytes
esp_ble_gatts_cb_param_t *gatts_param; // Heap-allocated union {
std::vector<uint8_t> *data; // Heap-allocated uint8_t *heap_data; // 4 bytes when heap-allocated
} gatts; // 16 bytes (pointers only) uint8_t inline_data[GATTS_INLINE_DATA_SIZE]; // 40 bytes when stored inline
} event_; // 80 bytes } data; // 40 bytes total
uint16_t data_len; // 2 bytes
esp_gatt_if_t gatts_if; // 1 byte
bool is_inline; // 1 byte - true when data is stored inline
} gatts; // Total: 80 bytes
} event_; // 80 bytes
ble_event_t type_; ble_event_t type_;
@@ -233,6 +263,29 @@ class BLEEvent {
const esp_ble_sec_t &security() const { return event_.gap.security; } const esp_ble_sec_t &security() const { return event_.gap.security; }
private: private:
// Helper to copy data with inline storage optimization
template<typename EventStruct, size_t InlineSize>
void copy_data_with_inline_storage_(EventStruct &event, const uint8_t *src_data, uint16_t len,
uint8_t **param_value_ptr) {
event.data_len = len;
if (len > 0) {
if (len <= InlineSize) {
event.is_inline = true;
memcpy(event.data.inline_data, src_data, len);
*param_value_ptr = event.data.inline_data;
} else {
event.is_inline = false;
event.data.heap_data = new uint8_t[len];
memcpy(event.data.heap_data, src_data, len);
*param_value_ptr = event.data.heap_data;
}
} else {
event.is_inline = false;
event.data.heap_data = nullptr;
*param_value_ptr = nullptr;
}
}
// Initialize GAP event data // Initialize GAP event data
void init_gap_data_(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { void init_gap_data_(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
this->event_.gap.gap_event = e; this->event_.gap.gap_event = e;
@@ -317,35 +370,38 @@ class BLEEvent {
this->event_.gattc.gattc_if = i; this->event_.gattc.gattc_if = i;
if (p == nullptr) { if (p == nullptr) {
this->event_.gattc.gattc_param = nullptr; // Zero out the param struct when null
this->event_.gattc.data = nullptr; memset(&this->event_.gattc.gattc_param, 0, sizeof(this->event_.gattc.gattc_param));
this->event_.gattc.is_inline = false;
this->event_.gattc.data.heap_data = nullptr;
this->event_.gattc.data_len = 0;
return; // Invalid event, but we can't log in header file return; // Invalid event, but we can't log in header file
} }
// Heap-allocate param and data // Copy param struct inline (no heap allocation!)
// Heap allocation is used because GATTC/GATTS events are rare (<1% of events) // GATTC/GATTS events are rare (<1% of events) but we can still store them inline
// while GAP events (99%) are stored inline to minimize memory usage // along with small data payloads, eliminating all heap allocations for typical BLE operations
// IMPORTANT: This heap allocation provides clear ownership semantics: // CRITICAL: This copy is REQUIRED for memory safety - the ESP-IDF param pointer
// - The BLEEvent owns the allocated memory for its lifetime // is only valid during the callback and will be reused/freed after we return
// - The data remains valid from the BLE callback context until processed in the main loop this->event_.gattc.gattc_param = *p;
// - Without this copy, we'd have use-after-free bugs as ESP-IDF reuses the callback memory
this->event_.gattc.gattc_param = new esp_ble_gattc_cb_param_t(*p);
// Copy data for events that need it // Copy data for events that need it
// The param struct contains pointers (e.g., notify.value) that point to temporary buffers. // The param struct contains pointers (e.g., notify.value) that point to temporary buffers.
// We must copy this data to ensure it remains valid when the event is processed later. // We must copy this data to ensure it remains valid when the event is processed later.
switch (e) { switch (e) {
case ESP_GATTC_NOTIFY_EVT: case ESP_GATTC_NOTIFY_EVT:
this->event_.gattc.data = new std::vector<uint8_t>(p->notify.value, p->notify.value + p->notify.value_len); copy_data_with_inline_storage_<decltype(this->event_.gattc), GATTC_INLINE_DATA_SIZE>(
this->event_.gattc.gattc_param->notify.value = this->event_.gattc.data->data(); this->event_.gattc, p->notify.value, p->notify.value_len, &this->event_.gattc.gattc_param.notify.value);
break; break;
case ESP_GATTC_READ_CHAR_EVT: case ESP_GATTC_READ_CHAR_EVT:
case ESP_GATTC_READ_DESCR_EVT: case ESP_GATTC_READ_DESCR_EVT:
this->event_.gattc.data = new std::vector<uint8_t>(p->read.value, p->read.value + p->read.value_len); copy_data_with_inline_storage_<decltype(this->event_.gattc), GATTC_INLINE_DATA_SIZE>(
this->event_.gattc.gattc_param->read.value = this->event_.gattc.data->data(); this->event_.gattc, p->read.value, p->read.value_len, &this->event_.gattc.gattc_param.read.value);
break; break;
default: default:
this->event_.gattc.data = nullptr; this->event_.gattc.is_inline = false;
this->event_.gattc.data.heap_data = nullptr;
this->event_.gattc.data_len = 0;
break; break;
} }
} }
@@ -356,30 +412,33 @@ class BLEEvent {
this->event_.gatts.gatts_if = i; this->event_.gatts.gatts_if = i;
if (p == nullptr) { if (p == nullptr) {
this->event_.gatts.gatts_param = nullptr; // Zero out the param struct when null
this->event_.gatts.data = nullptr; memset(&this->event_.gatts.gatts_param, 0, sizeof(this->event_.gatts.gatts_param));
this->event_.gatts.is_inline = false;
this->event_.gatts.data.heap_data = nullptr;
this->event_.gatts.data_len = 0;
return; // Invalid event, but we can't log in header file return; // Invalid event, but we can't log in header file
} }
// Heap-allocate param and data // Copy param struct inline (no heap allocation!)
// Heap allocation is used because GATTC/GATTS events are rare (<1% of events) // GATTC/GATTS events are rare (<1% of events) but we can still store them inline
// while GAP events (99%) are stored inline to minimize memory usage // along with small data payloads, eliminating all heap allocations for typical BLE operations
// IMPORTANT: This heap allocation provides clear ownership semantics: // CRITICAL: This copy is REQUIRED for memory safety - the ESP-IDF param pointer
// - The BLEEvent owns the allocated memory for its lifetime // is only valid during the callback and will be reused/freed after we return
// - The data remains valid from the BLE callback context until processed in the main loop this->event_.gatts.gatts_param = *p;
// - Without this copy, we'd have use-after-free bugs as ESP-IDF reuses the callback memory
this->event_.gatts.gatts_param = new esp_ble_gatts_cb_param_t(*p);
// Copy data for events that need it // Copy data for events that need it
// The param struct contains pointers (e.g., write.value) that point to temporary buffers. // The param struct contains pointers (e.g., write.value) that point to temporary buffers.
// We must copy this data to ensure it remains valid when the event is processed later. // We must copy this data to ensure it remains valid when the event is processed later.
switch (e) { switch (e) {
case ESP_GATTS_WRITE_EVT: case ESP_GATTS_WRITE_EVT:
this->event_.gatts.data = new std::vector<uint8_t>(p->write.value, p->write.value + p->write.len); copy_data_with_inline_storage_<decltype(this->event_.gatts), GATTS_INLINE_DATA_SIZE>(
this->event_.gatts.gatts_param->write.value = this->event_.gatts.data->data(); this->event_.gatts, p->write.value, p->write.len, &this->event_.gatts.gatts_param.write.value);
break; break;
default: default:
this->event_.gatts.data = nullptr; this->event_.gatts.is_inline = false;
this->event_.gatts.data.heap_data = nullptr;
this->event_.gatts.data_len = 0;
break; break;
} }
} }
@@ -389,6 +448,15 @@ class BLEEvent {
// The gap member in the union should be 80 bytes (including the gap_event enum) // The gap member in the union should be 80 bytes (including the gap_event enum)
static_assert(sizeof(decltype(((BLEEvent *) nullptr)->event_.gap)) <= 80, "gap_event struct has grown beyond 80 bytes"); static_assert(sizeof(decltype(((BLEEvent *) nullptr)->event_.gap)) <= 80, "gap_event struct has grown beyond 80 bytes");
// Verify GATTC and GATTS structs don't exceed GAP struct size
// This ensures the union size is determined by GAP (the most common event type)
static_assert(sizeof(decltype(((BLEEvent *) nullptr)->event_.gattc)) <=
sizeof(decltype(((BLEEvent *) nullptr)->event_.gap)),
"gattc_event struct exceeds gap_event size - union size would increase");
static_assert(sizeof(decltype(((BLEEvent *) nullptr)->event_.gatts)) <=
sizeof(decltype(((BLEEvent *) nullptr)->event_.gap)),
"gatts_event struct exceeds gap_event size - union size would increase");
// Verify esp_ble_sec_t fits within our union // Verify esp_ble_sec_t fits within our union
static_assert(sizeof(esp_ble_sec_t) <= 73, "esp_ble_sec_t is larger than BLEScanResult"); static_assert(sizeof(esp_ble_sec_t) <= 73, "esp_ble_sec_t is larger than BLEScanResult");

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

@@ -7,9 +7,9 @@
#include <esp_gap_ble_api.h> #include <esp_gap_ble_api.h>
#include <esp_gatt_defs.h> #include <esp_gatt_defs.h>
#include <esp_gattc_api.h>
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";
@@ -79,40 +79,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) {
@@ -141,64 +108,35 @@ 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;
auto ret = esp_ble_gattc_open(this->gattc_if_, this->remote_bda_, this->remote_addr_type_, true); // Determine connection parameters based on connection type
if (ret) { if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_open error, status=%d", this->connection_index_, this->address_str_.c_str(), // V3 without cache needs fast params for service discovery
ret); this->set_conn_params_(FAST_MIN_CONN_INTERVAL, FAST_MAX_CONN_INTERVAL, 0, FAST_CONN_TIMEOUT, "fast");
this->set_state(espbt::ClientState::IDLE); } else if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
} else { // V3 with cache can use medium params
this->set_state(espbt::ClientState::CONNECTING); this->set_conn_params_(MEDIUM_MIN_CONN_INTERVAL, MEDIUM_MAX_CONN_INTERVAL, 0, MEDIUM_CONN_TIMEOUT, "medium");
// Always set connection parameters to ensure stable operation
// 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 {
ESP_LOGD(TAG, "[%d] [%s] Set %s conn params", this->connection_index_, this->address_str_.c_str(), param_type);
}
} }
// For V1/Legacy, don't set params - use ESP-IDF defaults
// Open the connection
auto ret = esp_ble_gattc_open(this->gattc_if_, this->remote_bda_, this->remote_addr_type_, true);
this->handle_connection_result_(ret);
} }
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) {
ESP_LOGW(TAG, "[%d] [%s] Disconnecting before connected, disconnect scheduled.", this->connection_index_, ESP_LOGD(TAG, "[%d] [%s] Disconnect before connected, disconnect scheduled", this->connection_index_,
this->address_str_.c_str()); this->address_str_.c_str());
this->want_disconnect_ = true; this->want_disconnect_ = true;
return; return;
@@ -211,13 +149,11 @@ void BLEClientBase::unconditional_disconnect() {
ESP_LOGI(TAG, "[%d] [%s] Disconnecting (conn_id: %d).", this->connection_index_, this->address_str_.c_str(), ESP_LOGI(TAG, "[%d] [%s] Disconnecting (conn_id: %d).", this->connection_index_, this->address_str_.c_str(),
this->conn_id_); this->conn_id_);
if (this->state_ == espbt::ClientState::DISCONNECTING) { if (this->state_ == espbt::ClientState::DISCONNECTING) {
ESP_LOGE(TAG, "[%d] [%s] Tried to disconnect while already disconnecting.", this->connection_index_, this->log_error_("Already disconnecting");
this->address_str_.c_str());
return; return;
} }
if (this->conn_id_ == UNSET_CONN_ID) { if (this->conn_id_ == UNSET_CONN_ID) {
ESP_LOGE(TAG, "[%d] [%s] No connection ID set, cannot disconnect.", this->connection_index_, this->log_error_("conn id unset, cannot disconnect");
this->address_str_.c_str());
return; return;
} }
auto err = esp_ble_gattc_close(this->gattc_if_, this->conn_id_); auto err = esp_ble_gattc_close(this->gattc_if_, this->conn_id_);
@@ -229,8 +165,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 ||
@@ -243,9 +178,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
@@ -255,6 +192,70 @@ 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::handle_connection_result_(esp_err_t ret) {
if (ret) {
this->log_gattc_warning_("esp_ble_gattc_open", ret);
this->set_state(espbt::ClientState::IDLE);
} else {
this->set_state(espbt::ClientState::CONNECTING);
}
}
void BLEClientBase::log_error_(const char *message) {
ESP_LOGE(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_.c_str(), message);
}
void BLEClientBase::log_error_(const char *message, int code) {
ESP_LOGE(TAG, "[%d] [%s] %s=%d", this->connection_index_, this->address_str_.c_str(), message, code);
}
void BLEClientBase::log_warning_(const char *message) {
ESP_LOGW(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_.c_str(), message);
}
void BLEClientBase::update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency,
uint16_t timeout, const char *param_type) {
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 = min_interval;
conn_params.max_int = max_interval;
conn_params.latency = latency;
conn_params.timeout = timeout;
this->log_connection_params_(param_type);
esp_err_t err = esp_ble_gap_update_conn_params(&conn_params);
if (err != ESP_OK) {
this->log_gattc_warning_("esp_ble_gap_update_conn_params", err);
}
}
void BLEClientBase::set_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout,
const char *param_type) {
// Set preferred connection parameters before connecting
// These will be used when establishing the connection
this->log_connection_params_(param_type);
esp_err_t err = esp_ble_gap_set_prefer_conn_params(this->remote_bda_, min_interval, max_interval, latency, timeout);
if (err != ESP_OK) {
this->log_gattc_warning_("esp_ble_gap_set_prefer_conn_params", err);
}
}
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)
@@ -272,8 +273,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
this->app_id); this->app_id);
this->gattc_if_ = esp_gattc_if; this->gattc_if_ = esp_gattc_if;
} else { } else {
ESP_LOGE(TAG, "[%d] [%s] gattc app registration failed id=%d code=%d", this->connection_index_, this->log_error_("gattc app registration failed status", param->reg.status);
this->address_str_.c_str(), param->reg.app_id, param->reg.status);
this->status_ = param->reg.status; this->status_ = param->reg.status;
this->mark_failed(); this->mark_failed();
} }
@@ -282,30 +282,28 @@ 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;
// ESP-IDF's BLE stack may send ESP_GATTC_OPEN_EVT after esp_ble_gattc_open() returns an
// error, if the error occurred at the BTA/GATT layer. This can result in the event
// arriving after we've already transitioned to IDLE state.
if (this->state_ == espbt::ClientState::IDLE) {
ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in IDLE state (status=%d), ignoring", this->connection_index_,
this->address_str_.c_str(), param->open.status);
break;
}
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] ESP_GATTC_OPEN_EVT 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;
} }
@@ -317,27 +315,34 @@ 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()); 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] Using cached services", this->connection_index_, this->address_str_.c_str()); // Cached connections already connected with medium parameters, no update needed
// 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()); // For V3_WITHOUT_CACHE, we already set fast params before connecting
// No need to update them again here
this->log_event_("Searching for services");
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: {
@@ -346,8 +351,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
// Check if we were disconnected while waiting for service discovery // Check if we were disconnected while waiting for service discovery
if (param->disconnect.reason == ESP_GATT_CONN_TERMINATE_PEER_USER && if (param->disconnect.reason == ESP_GATT_CONN_TERMINATE_PEER_USER &&
this->state_ == espbt::ClientState::CONNECTED) { this->state_ == espbt::ClientState::CONNECTED) {
ESP_LOGW(TAG, "[%d] [%s] Disconnected by remote during service discovery", this->connection_index_, this->log_warning_("Remote closed during discovery");
this->address_str_.c_str());
} else { } else {
ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason 0x%02x", this->connection_index_, ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason 0x%02x", this->connection_index_,
this->address_str_.c_str(), param->disconnect.reason); this->address_str_.c_str(), param->disconnect.reason);
@@ -374,7 +378,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;
@@ -386,79 +390,73 @@ 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_WITHOUT_CACHE, switch back 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->update_conn_params_(MEDIUM_MIN_CONN_INTERVAL, MEDIUM_MAX_CONN_INTERVAL, 0, MEDIUM_CONN_TIMEOUT, "medium");
this->address_str_.c_str(), svc->start_handle, svc->end_handle); } else if (this->connection_type_ != espbt::ConnectionType::V3_WITH_CACHE) {
#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] Service discovery complete", this->connection_index_, this->address_str_.c_str()); ESP_LOGI(TAG, "[%d] [%s] Service discovery complete", this->connection_index_, this->address_str_.c_str());
// For V3 connections, restore to medium connection parameters after service discovery
// This balances performance with bandwidth usage after the critical discovery phase
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE ||
this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
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;
ESP_LOGD(TAG, "[%d] [%s] Restored medium conn params after service discovery", this->connection_index_,
this->address_str_.c_str());
esp_ble_gap_update_conn_params(&conn_params);
}
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
@@ -470,8 +468,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;
@@ -479,8 +476,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;
} }
@@ -494,12 +490,16 @@ 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;
} }
case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: {
this->log_gattc_event_("UNREG_FOR_NOTIFY");
break;
}
default: default:
// ideally would check all other events for matching conn_id // ideally would check all other events for matching conn_id
ESP_LOGD(TAG, "[%d] [%s] Event %d", this->connection_index_, this->address_str_.c_str(), event); ESP_LOGD(TAG, "[%d] [%s] Event %d", this->connection_index_, this->address_str_.c_str(), event);
@@ -528,16 +528,14 @@ void BLEClientBase::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_
return; return;
esp_bd_addr_t bd_addr; esp_bd_addr_t bd_addr;
memcpy(bd_addr, param->ble_security.auth_cmpl.bd_addr, sizeof(esp_bd_addr_t)); memcpy(bd_addr, param->ble_security.auth_cmpl.bd_addr, sizeof(esp_bd_addr_t));
ESP_LOGI(TAG, "[%d] [%s] auth complete. remote BD_ADDR: %s", this->connection_index_, this->address_str_.c_str(), ESP_LOGI(TAG, "[%d] [%s] auth complete addr: %s", this->connection_index_, this->address_str_.c_str(),
format_hex(bd_addr, 6).c_str()); format_hex(bd_addr, 6).c_str());
if (!param->ble_security.auth_cmpl.success) { if (!param->ble_security.auth_cmpl.success) {
ESP_LOGE(TAG, "[%d] [%s] auth fail reason = 0x%x", this->connection_index_, this->address_str_.c_str(), this->log_error_("auth fail reason", param->ble_security.auth_cmpl.fail_reason);
param->ble_security.auth_cmpl.fail_reason);
} else { } else {
this->paired_ = true; this->paired_ = true;
ESP_LOGD(TAG, "[%d] [%s] auth success. address type = %d auth mode = %d", this->connection_index_, ESP_LOGD(TAG, "[%d] [%s] auth success type = %d mode = %d", this->connection_index_, this->address_str_.c_str(),
this->address_str_.c_str(), param->ble_security.auth_cmpl.addr_type, param->ble_security.auth_cmpl.addr_type, param->ble_security.auth_cmpl.auth_mode);
param->ble_security.auth_cmpl.auth_mode);
} }
break; break;
@@ -603,6 +601,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)
@@ -679,8 +678,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;
@@ -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,21 @@ 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 update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout,
const char *param_type);
void set_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout,
const char *param_type);
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);
void handle_connection_result_(esp_err_t ret);
// Compact error logging helpers to reduce flash usage
void log_error_(const char *message);
void log_error_(const char *message, int code);
void log_warning_(const char *message);
}; };
} // 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)

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

@@ -80,14 +80,17 @@ class BLEManufacturerDataAdvertiseTrigger : public Trigger<const adv_data_t &>,
ESPBTUUID uuid_; ESPBTUUID uuid_;
}; };
#endif // USE_ESP32_BLE_DEVICE
class BLEEndOfScanTrigger : public Trigger<>, public ESPBTDeviceListener { class BLEEndOfScanTrigger : public Trigger<>, public ESPBTDeviceListener {
public: public:
explicit BLEEndOfScanTrigger(ESP32BLETracker *parent) { parent->register_listener(this); } explicit BLEEndOfScanTrigger(ESP32BLETracker *parent) { parent->register_listener(this); }
#ifdef USE_ESP32_BLE_DEVICE
bool parse_device(const ESPBTDevice &device) override { return false; } bool parse_device(const ESPBTDevice &device) override { return false; }
#endif
void on_scan_end() override { this->trigger(); } void on_scan_end() override { this->trigger(); }
}; };
#endif // USE_ESP32_BLE_DEVICE
template<typename... Ts> class ESP32BLEStartScanAction : public Action<Ts...> { template<typename... Ts> class ESP32BLEStartScanAction : public Action<Ts...> {
public: public:

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