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

Compare commits

...

231 Commits

Author SHA1 Message Date
Jesse Hills
c732d602f2 [midea] Allow None as a supported preset 2025-07-19 11:02:18 +12:00
Jesse Hills
19a68dc650 Add core team as codeowner of .github folder (#9663) 2025-07-19 10:55:22 +12:00
J. Nick Koston
6cefe943e9 [gpio] Disable interrupt mode by default for LibreTiny platforms (#9687)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-19 09:32:20 +12:00
Kevin Ahrendt
6f74decd79 [i2s_audio] Bugfix: cast adc_channel_t to adc1_channel_t (#9688) 2025-07-18 16:52:46 -04:00
dependabot[bot]
60350e8abd Bump aioesphomeapi from 37.0.0 to 37.0.1 (#9685)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-18 20:08:29 +00:00
@RubenKelevra
08407706aa esp32cam: add fb location config option (#9630) 2025-07-19 07:28:13 +12:00
Kevin Ahrendt
cb8d9dca2a [voice_assistant] Use media player callbacks to track TTS response status (#9670) 2025-07-19 07:24:55 +12:00
Kevin Ahrendt
3f8494bf8f [speaker] Media player's pipeline properly returns playing state near end of file (#9668) 2025-07-19 07:21:36 +12:00
J. Nick Koston
95a08579f6 Fix AsyncTCP version mismatch between platformio.ini and async_tcp component (#9676) 2025-07-19 07:20:08 +12:00
dependabot[bot]
a11c39bdc9 Bump aioesphomeapi from 36.0.1 to 37.0.0 (#9677)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-18 17:57:40 +00:00
J. Nick Koston
71cc298363 Use message_source_map consistently in proto generation (#9542) 2025-07-19 00:28:08 +12:00
J. Nick Koston
0d422bd74f [scheduler] Add integration tests for set_retry functionality (#9644) 2025-07-19 00:26:54 +12:00
Jesse Hills
ce3a16f03c [lvgl] Prevent keyerror on min/max value widgets with no default (#9660) 2025-07-18 21:49:34 +10:00
J. Nick Koston
72905f5f42 [libretiny] Remove unsupported lock-free queue and event pool implementations (#9653) 2025-07-18 23:40:14 +12:00
Jesse Hills
b5b301f935 [CI] Fix by-code-owner labelling (#9661) 2025-07-18 23:24:06 +12:00
Jesse Hills
afc48812fa [CI] Add codeowners mention workflow (#9651) 2025-07-18 23:21:38 +12:00
Jesse Hills
e189add8a3 [CI] New workflow to mention codeowners on issues (#9658) 2025-07-18 22:57:25 +12:00
@RubenKelevra
f8146bd340 core/schedule: fixup out of sync code comment (#9649) 2025-07-17 18:54:01 -10:00
J. Nick Koston
ec5a517a76 Fix bluetooth_proxy heap allocations during BLE scanning (#9633) 2025-07-18 16:24:29 +12:00
Clyde Stubbs
f7314adff4 [lvgl] Fix meter rotation (#9605)
Co-authored-by: clydeps <U5yx99dok9>
2025-07-18 16:14:21 +12:00
J. Nick Koston
f0f76066f3 [scheduler] Fix DelayAction cancellation in restart mode scripts (#9646) 2025-07-18 04:07:59 +00:00
@RubenKelevra
1ebf157768 esp32_camera: deprecate i2c_pins; throw error if combined with i2c: block (#9615) 2025-07-17 17:09:24 -10:00
Samuel Sieb
4bd0561ba3 [logger] fix on_message (#9642)
Co-authored-by: Samuel Sieb <samuel@sieb.net>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-17 17:08:18 -10:00
J. Nick Koston
a18ddd1169 [scheduler] Fix LibreTiny compilation error due to missing atomic operations (#9643) 2025-07-18 14:21:46 +12:00
J. Nick Koston
158a3b2835 [scheduler] Fix cancellation of timers with empty string names (#9641) 2025-07-18 14:20:35 +12:00
Clyde Stubbs
eb8a241a01 [esp32] Allow variant in place of board (#9427)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-18 13:48:48 +12:00
tomaszduda23
7cdb48b820 [code quality] move const to esphome/const.py (#9632)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-17 23:39:35 +00:00
tomaszduda23
558e175c6b adds nRF52840 to PR templates (#9631) 2025-07-18 11:23:42 +12:00
J. Nick Koston
dfa8c8c77f Fix scheduler rollover detection with concurrent task calls (#9624) 2025-07-17 13:07:36 -10:00
J. Nick Koston
7f807e08b1 [wireguard] Fix boot loop when CONFIG_LWIP_TCPIP_CORE_LOCKING is enabled (#9637) 2025-07-18 11:00:56 +12:00
J. Nick Koston
fc1fd3f897 [api] Fix compilation error with char* lambdas in HomeAssistant services (#9638)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-18 10:55:39 +12:00
J. Nick Koston
f5afe1145e Refactor API send_message from template to non-template implementation (#9561) 2025-07-18 10:28:14 +12:00
dependabot[bot]
91e5bcf787 Bump aioesphomeapi from 36.0.0 to 36.0.1 (#9636)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-17 21:49:03 +00:00
Flo
4378d10f45 Fix template event web_server crash (#9618) 2025-07-17 11:45:07 -10:00
dependabot[bot]
6178e7d6c8 Bump ruff from 0.12.3 to 0.12.4 (#9634)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-07-17 21:27:18 +00:00
dependabot[bot]
b01f42d995 Bump pytest-xdist from 3.7.0 to 3.8.0 (#9287)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-17 11:20:31 -10:00
dependabot[bot]
3f842806ae Bump pytest-asyncio from 1.0.0 to 1.1.0 (#9588)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-17 11:13:01 -10:00
Clyde Stubbs
2347375757 [ci] attempt to fix permission for workflow (#9610)
Co-authored-by: clydeps <U5yx99dok9>
2025-07-18 00:45:08 +12:00
Clyde Stubbs
513908d8a0 [ci] Implement external component PR workflow (#9595)
Co-authored-by: clydeps <U5yx99dok9>
2025-07-18 00:05:26 +12:00
@RubenKelevra
f7acad747f Update Issues / Feature Requests links (#9607) 2025-07-18 00:02:09 +12:00
Jesse Hills
b361b93722 Add some AI instructions (#9606) 2025-07-17 22:40:28 +12:00
Jesse Hills
3713f7004d Merge branch 'release' into dev 2025-07-17 21:55:16 +12:00
Jesse Hills
1a9f02fa63 Merge pull request #9596 from esphome/bump-2025.7.1
2025.7.1
2025-07-17 21:54:35 +12:00
@RubenKelevra
66dd5138b9 Update Issues / Feature Requests links in Readme (#9600) 2025-07-17 21:48:37 +12:00
J. Nick Koston
44979f0840 Skip compilation of web_server_v1.cpp when not using version 1 (#9590)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-17 21:02:51 +12:00
Jesse Hills
7ad1b039f9 Bump version to 2025.7.1 2025-07-17 19:40:03 +12:00
J. Nick Koston
e255d73c29 Fix lwIP thread safety assertion failures on ESP32 (#9570) 2025-07-17 19:39:57 +12:00
Jesse Hills
46f5c44b37 [esp32] Add missing include for helpers (#9579)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-07-17 19:39:39 +12:00
J. Nick Koston
9d80889bc9 Allow disabling OTA for web_server while keeping it enabled for captive_portal (#9583) 2025-07-17 19:39:39 +12:00
J. Nick Koston
08a5ba6ef1 Add helpful error message when ESP32+Arduino runs out of flash space (#9580) 2025-07-17 19:39:39 +12:00
J. Nick Koston
28128c65e5 Fix format string warnings in Web Server OTA component (#9569)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-17 19:39:39 +12:00
J. Nick Koston
efcad565ee Fix compilation error when using string lambdas with homeassistant services (#9543) 2025-07-17 19:39:39 +12:00
Vladimir Kuznetsov
cd987feb5b [lvgl]: fix missing await keyword in meter tick_style width processing (#9538) 2025-07-17 19:39:12 +12:00
Jesse Hills
b2406f9def [CI] Add `needs-docs` labelling (#9591) 2025-07-17 17:15:28 +12:00
J. Nick Koston
b1048d6e25 Fix lwIP thread safety assertion failures on ESP32 (#9570) 2025-07-17 17:06:57 +12:00
Jesse Hills
a8263cb79f [CI] Add `by-code-owner` labelling (#9589)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-17 16:54:00 +12:00
Jesse Hills
7868b2b456 [dependabot] Use specific labels for github-actions updates (#9586) 2025-07-17 15:19:34 +12:00
Jesse Hills
faaaded0b1 Workflow to auto label PRs based on changes (#9585) 2025-07-17 15:19:07 +12:00
Jesse Hills
c14b102776 [esp32] Add missing include for helpers (#9579)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-07-17 15:13:03 +12:00
J. Nick Koston
b1655b3fd4 Allow disabling OTA for web_server while keeping it enabled for captive_portal (#9583) 2025-07-16 17:05:09 -10:00
J. Nick Koston
02999195cd Add helpful error message when ESP32+Arduino runs out of flash space (#9580) 2025-07-17 12:13:55 +12:00
dependabot[bot]
8415467dab Bump aioesphomeapi from 35.0.1 to 36.0.0 (#9567)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-07-16 13:52:16 -10:00
J. Nick Koston
66b6985975 Fix format string warnings in Web Server OTA component (#9569)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-16 23:14:25 +00:00
Jesse Hills
b6e8f6398c Revert "Bump ESP32 IDF version to 5.4.2 and Arduino version to 3.2.1" (#9574) 2025-07-17 10:25:57 +12:00
Big Mike
0958e49965 Move CONF_ALTITUDE_COMPENSATION to const.py (#9563) 2025-07-16 16:06:50 -05:00
J. Nick Koston
f4cd559a0b Fix compilation error when using string lambdas with homeassistant services (#9543) 2025-07-17 09:02:32 +12:00
Jonathan Swoboda
c93b892ccc Bump ESP32 IDF version to 5.4.2 and Arduino version to 3.2.1 (#9305) 2025-07-17 07:50:42 +12:00
Edward Firmo
78c32eac04 [adc] Add ESP32-C5 support (#9486)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Co-authored-by: oxynatOr <98734567+oxynatOr@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-07-16 23:32:44 +12:00
J. Nick Koston
9e621a1769 Update script/helpers.py to use ESPHome YAML parser for integration fixtures (#9544) 2025-07-16 22:19:27 +12:00
esphomebot
d0b45f7cb6 Synchronise Device Classes from Home Assistant (#9513) 2025-07-16 09:55:40 +00:00
J. Nick Koston
e40b45cab1 Add ability to have same entity names on different sub devices (#9355)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-07-16 21:34:51 +12:00
Jesse Hills
b15a09e8bc Merge branch 'release' into dev 2025-07-16 20:47:13 +12:00
Jesse Hills
5707389faa Merge pull request #9534 from esphome/bump-2025.7.0
2025.7.0
2025-07-16 20:46:26 +12:00
J. Nick Koston
15768ec00d Reduce API proto vtable overhead by splitting decode functionality (#9541) 2025-07-16 20:46:04 +12:00
J. Nick Koston
2c478efcba Refactor API connection entity encoding to reduce code duplication (#9505)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-07-16 07:54:49 +00:00
Edward Firmo
9ae8c5b147 [adc] Test platforms on IDF (#9536) 2025-07-16 19:35:33 +12:00
Vladimir Kuznetsov
63e2e2b2a2 [lvgl]: fix missing await keyword in meter tick_style width processing (#9538) 2025-07-16 17:05:19 +10:00
J. Nick Koston
3ab1ee7a04 Reduce binary size with field-level conditional compilation for protobuf messages (#9473)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-07-16 18:36:26 +12:00
J. Nick Koston
f3c0c0c00c Remove legacy unique_id field from entities (#9022)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-16 04:56:49 +00:00
J. Nick Koston
231bcb1f7d Fix CI failures from merge collisions (#9535) 2025-07-16 15:24:20 +12:00
Thomas Rupprecht
9cac1c824e [ssd1306_base] fix typo brighrness (#9491) 2025-07-15 22:22:33 -05:00
J. Nick Koston
c691f01c7f Reduce flash usage by replacing ProtoSize template with specialized methods (#9487) 2025-07-16 01:50:32 +00:00
J. Nick Koston
b648944973 Optimize API connection batch priority message handling to reduce flash usage (#9510) 2025-07-16 13:46:12 +12:00
J. Nick Koston
40935f7ae4 Skip API log message calls for unsubscribed log levels (#9514) 2025-07-16 13:43:55 +12:00
J. Nick Koston
e152690867 Optimize API component LOGCONFIG usage for flash memory savings (#9526) 2025-07-16 13:42:55 +12:00
J. Nick Koston
b1c86fe30e Optimize scheduler timing by reducing millis() calls (#9524) 2025-07-16 13:41:55 +12:00
Jonathan Swoboda
b695f13f86 [i2c] Use new driver with IDF 5.4.2+ (#8483) 2025-07-15 20:40:28 -05:00
J. Nick Koston
ab54a880c1 Optimize MedianFilter memory allocation by adding vector reserve (#9531) 2025-07-16 01:25:41 +00:00
J. Nick Koston
f745135bdc Drop Python 3.10 support, require Python 3.11+ (#9522)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-07-15 15:20:58 -10:00
J. Nick Koston
30c4b91697 Remove parsed advertisement support from bluetooth_proxy to save memory (#9489) 2025-07-16 13:19:03 +12:00
J. Nick Koston
bfaf2547e3 Reduce API component flash usage by consolidating error logging (#9468) 2025-07-16 13:15:23 +12:00
J. Nick Koston
b5be45273f Improve API protobuf decode method readability and reduce code size (#9455) 2025-07-16 13:15:11 +12:00
J. Nick Koston
5c2dea79ef Make API ConnectRequest optional for passwordless connections (#9445) 2025-07-16 13:14:43 +12:00
J. Nick Koston
e012fd5b32 Add runtime_stats component for performance debugging and analysis (#9386)
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
2025-07-16 13:13:51 +12:00
J. Nick Koston
856cb182fc Remove dead code: 64-bit protobuf types never used in 7 years (#9471) 2025-07-15 15:12:12 -10:00
Clyde Stubbs
6486147da1 [mipi_spi] Template code, partial buffer support (#9314)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
2025-07-16 11:05:27 +10:00
Edward Firmo
5480675dd8 [adc] Use new library with ESP-IDF v5 (#9021)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-16 13:03:19 +12:00
tomaszduda23
6ab3de65a6 remove duplication from component_iterator (#7210)
Co-authored-by: Samuel Tardieu <sam@rfc1149.net>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-07-16 13:02:14 +12:00
tomaszduda23
5d9cba3dce [nrf52, core] nrf52 core based on zephyr (#7049)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Co-authored-by: Samuel Sieb <samuel-github@sieb.net>
Co-authored-by: Tomasz Duda <tomaszduda23@gmai.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-07-16 13:00:21 +12:00
Jesse Hills
3f78db5c63 Bump version to 2025.7.0 2025-07-16 12:31:13 +12:00
Jesse Hills
eb81b8a1c8 Merge branch 'beta' into dev 2025-07-16 11:58:43 +12:00
Jesse Hills
de0656a188 Merge pull request #9532 from esphome/bump-2025.7.0b5
2025.7.0b5
2025-07-16 11:58:12 +12:00
Jesse Hills
90a16ffa89 Bump version to 2025.7.0b5 2025-07-16 10:45:20 +12:00
Samuel Sieb
4182076f64 [as3935_spi] remove unnecessary includes (#9528) 2025-07-16 10:45:19 +12:00
J. Nick Koston
8c8c08d40c Fix timing overflow when components disable themselves during loop (#9529)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-16 10:45:19 +12:00
Samuel Sieb
82120bc5d7 [as3935_spi] remove unnecessary includes (#9528) 2025-07-16 10:03:02 +12:00
J. Nick Koston
9769f8a4cc Fix timing overflow when components disable themselves during loop (#9529)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-15 21:51:01 +00:00
Jesse Hills
0968338064 Merge branch 'beta' into dev 2025-07-16 07:35:15 +12:00
Jesse Hills
18e2f41424 Merge pull request #9518 from esphome/bump-2025.7.0b4
2025.7.0b4
2025-07-16 07:34:42 +12:00
Christian Glombek
bd0fe34b14 [ms8607] Fix humidity calc (#9499) 2025-07-16 07:33:49 +12:00
Christian Glombek
6e90feeccf [ms8607] Fix humidity calc (#9499) 2025-07-16 07:33:15 +12:00
Jesse Hills
37982290f7 Bump version to 2025.7.0b4 2025-07-15 23:35:55 +12:00
Jesse Hills
02b7db7311 [component] Fix `is_ready` flag when loop disabled (#9501)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-07-15 23:35:55 +12:00
Clyde Stubbs
9bc3ff5f53 [core] Don't issue -Wno-volatile for host platform (#9511) 2025-07-15 23:35:55 +12:00
J. Nick Koston
786cb7ded5 Add missing clang-tidy NOLINT comments for ArduinoJson v7 in IDF webserver (#9508) 2025-07-15 23:35:55 +12:00
Keith Burzinski
7f01c25782 [servo] Fix `lerp` (#9507) 2025-07-15 23:35:55 +12:00
Keith Burzinski
321f2f87b0 [opentherm.output] Fix `lerp` (#9506) 2025-07-15 23:35:55 +12:00
Clyde Stubbs
11a051401f [captive_portal] Add test case for libretiny (#9457)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-07-15 23:35:55 +12:00
J. Nick Koston
6148dd7e41 Fix LibreTiny compilation error by updating ESPAsyncWebServer and dependencies (#9492)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-15 23:35:55 +12:00
skyegecko
42b6939e90 [fan] Do not save state for fan if configured as NO_RESTORE (#9472) 2025-07-15 23:35:55 +12:00
Kevin Ahrendt
35b3f75f7c [json] Bump ArduinoJson library to 7.4.2 (#8857)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-15 23:35:55 +12:00
Clyde Stubbs
78e8001aa8 [online_image] Support byte_order (#9502) 2025-07-15 23:35:55 +12:00
J. Nick Koston
84fc6ff71a Suppress spurious volatile and Python syntax warnings during builds (#9488) 2025-07-15 23:35:55 +12:00
Jesse Hills
a896190de5 [repo] Fix issue template config.yml (#9516) 2025-07-15 22:13:18 +12:00
Jesse Hills
e599ab1a03 Enable issue tracking (#9515) 2025-07-15 21:55:55 +12:00
Jesse Hills
d3342d6a1a [component] Fix `is_ready` flag when loop disabled (#9501)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-07-15 07:20:18 +00:00
Clyde Stubbs
3f492e3b82 [core] Don't issue -Wno-volatile for host platform (#9511) 2025-07-14 20:59:20 -10:00
J. Nick Koston
b959baf3d6 Add missing clang-tidy NOLINT comments for ArduinoJson v7 in IDF webserver (#9508) 2025-07-15 06:26:54 +00:00
J. Nick Koston
63b8a219e6 Include entire platformio.ini in clang-tidy hash calculation (#9509) 2025-07-15 01:26:39 -05:00
Keith Burzinski
84349b6d05 [servo] Fix `lerp` (#9507) 2025-07-15 03:45:38 +00:00
Keith Burzinski
0f15250f12 [opentherm.output] Fix `lerp` (#9506) 2025-07-15 03:43:00 +00:00
Clyde Stubbs
c2f7dcfa6d [captive_portal] Add test case for libretiny (#9457)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-07-14 17:36:07 -10:00
J. Nick Koston
778b586d78 Fix LibreTiny compilation error by updating ESPAsyncWebServer and dependencies (#9492)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-15 02:49:12 +00:00
J. Nick Koston
d3d1ba553d Fix blocked CI cancellation caused by always() in clang-tidy workflow (#9503) 2025-07-15 14:17:56 +12:00
skyegecko
a572d4eb47 [fan] Do not save state for fan if configured as NO_RESTORE (#9472) 2025-07-15 14:15:47 +12:00
Kevin Ahrendt
9ae45ba8aa [json] Bump ArduinoJson library to 7.4.2 (#8857)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-15 14:11:10 +12:00
Clyde Stubbs
8f58ca3a2a [online_image] Support byte_order (#9502) 2025-07-15 02:09:18 +00:00
J. Nick Koston
e3da197adf Remove yamllint job from CI since its now handled by pre-commit job (#9500) 2025-07-15 12:52:03 +12:00
J. Nick Koston
b2a8b0a22f Add pre-commit hooks to fix common formatting issues causing CI failures (#9494) 2025-07-15 12:25:18 +12:00
J. Nick Koston
619e2d69c0 Remove redundant pyupgrade CI job (follow-up to #9484) (#9493) 2025-07-15 12:20:05 +12:00
J. Nick Koston
f78e71c86a Fix WebServer routes constant naming convention (#9497) 2025-07-14 10:13:24 -10:00
J. Nick Koston
f8c45573f3 Refactor WebServer request handling for improved maintainability (#9470) 2025-07-15 07:24:20 +12:00
Jesse Hills
e231d334a3 Merge branch 'beta' into dev 2025-07-15 06:39:20 +12:00
J. Nick Koston
e7d819a656 Suppress spurious volatile and Python syntax warnings during builds (#9488) 2025-07-14 17:47:52 +10:00
Jesse Hills
16292a9f13 Merge pull request #9483 from esphome/bump-2025.7.0b3
2025.7.0b3
2025-07-14 15:41:59 +12:00
J. Nick Koston
873f4125c5 Fix pre-commit CI issues by switching to lite mode (#9484) 2025-07-13 17:30:34 -10:00
Jesse Hills
90f0ebb22b Dont autofix PR 2025-07-14 14:12:26 +12:00
Jesse Hills
4153380f99 Remove hook that doesnt exist on beta 2025-07-14 13:48:41 +12:00
J. Nick Koston
740c0ef9d7 Fix pre-commit CI failures by skipping local hooks that require virtual environment (#9476) 2025-07-14 13:46:14 +12:00
Jesse Hills
b4521e1d8c Bump version to 2025.7.0b3 2025-07-14 13:08:24 +12:00
J. Nick Koston
10ca7ed85b Fix dormant bug in RAMAllocator::reallocate() manual_size calculation (#9482) 2025-07-14 13:08:23 +12:00
J. Nick Koston
e43efdaaec Follow logging best practices by removing redundant component prefix (#9481) 2025-07-14 13:08:23 +12:00
Clyde Stubbs
9207bf97f3 [esp_ldo] Component schema; default priority (#9479) 2025-07-14 13:08:23 +12:00
Javier Peletier
c13317f807 [substitutions] Fix #7189 (#9469) 2025-07-14 13:08:23 +12:00
J. Nick Koston
77d1d0414d Automatically disable interrupts for ESP8266 GPIO16 binary sensors (#9467) 2025-07-14 13:08:23 +12:00
Peter Zich
8f42bc6aac [lvgl] Post-process size arguments in meter config (#9466)
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2025-07-14 13:08:23 +12:00
Peter Zich
9beb4e2cd4 (Maybe?) fix I2S speaker internal DAC mode (#9435) 2025-07-14 13:08:23 +12:00
Keith Burzinski
097aac2183 [ld2420] Memory optimization, code clean-up (#9426) 2025-07-14 13:08:23 +12:00
J. Nick Koston
d31b8ad2e2 Fix dormant bug in RAMAllocator::reallocate() manual_size calculation (#9482) 2025-07-14 00:58:07 +00:00
J. Nick Koston
f5c8595a46 Follow logging best practices by removing redundant component prefix (#9481) 2025-07-14 00:41:49 +00:00
J. Nick Koston
02d1894a9f Refactor format_hex_pretty functions to eliminate code duplication (#9480) 2025-07-14 00:32:16 +00:00
Clyde Stubbs
fc337aef69 [esp_ldo] Component schema; default priority (#9479) 2025-07-13 23:47:52 +00:00
J. Nick Koston
b21c76a6c6 Fix clang-tidy skipping when Python linters are skipped (#9463) 2025-07-14 11:04:14 +12:00
J. Nick Koston
5416cee2c9 Fix pre-commit CI failures by skipping local hooks that require virtual environment (#9476) 2025-07-14 10:44:21 +12:00
Javier Peletier
9e002cd7a3 [substitutions] Fix #7189 (#9469) 2025-07-14 09:58:52 +12:00
dependabot[bot]
9451781915 Bump aioesphomeapi from 34.2.1 to 35.0.1 (#9474)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-07-13 21:53:04 +00:00
J. Nick Koston
84956b6dc5 Automatically disable interrupts for ESP8266 GPIO16 binary sensors (#9467) 2025-07-13 20:09:55 +12:00
Peter Zich
6f19808eff [lvgl] Post-process size arguments in meter config (#9466)
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2025-07-13 15:43:32 +10:00
Peter Zich
cd8e1548bf (Maybe?) fix I2S speaker internal DAC mode (#9435) 2025-07-13 13:22:07 +12:00
Jesse Hills
48d55a70c0 Merge branch 'beta' into dev 2025-07-13 13:18:50 +12:00
Jesse Hills
18787b0be0 Merge pull request #9462 from esphome/bump-2025.7.0b2
2025.7.0b2
2025-07-13 13:18:17 +12:00
Jesse Hills
39e01c42e1 Bump version to 2025.7.0b2 2025-07-13 11:05:14 +12:00
Jonathan Swoboda
c760f89e46 [libretiny] Set lib_compat_mode to soft for libretiny (#9439) 2025-07-13 11:05:13 +12:00
Clyde Stubbs
01b4e214b9 [usb_uart] Be flexible about descriptor layout for CDC-ACM devices (#9425) 2025-07-13 11:05:13 +12:00
J. Nick Koston
bc7cfeb9cd Only generate protobuf encode/decode methods for the message direction they're used (#9461) 2025-07-13 11:05:13 +12:00
dependabot[bot]
36dd203e74 Bump aioesphomeapi from 34.2.0 to 34.2.1 (#9460)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-13 11:05:13 +12:00
J. Nick Koston
8605994cc6 Apply existing protobuf buffer optimization to nested message encoding (~2.3x speed up) (#9458) 2025-07-13 11:05:13 +12:00
Jonathan Swoboda
80fbe28088 [sx127x, sx126x] Fix preamble_size default and validation (#9454) 2025-07-13 11:05:13 +12:00
Clyde Stubbs
1d9f17a57c [packet_transport] Don't run update if ping_pong not enabled. (#9434) 2025-07-13 11:05:13 +12:00
J. Nick Koston
42947bcf56 Conditionally compile API user services to save 4.3KB flash (follow-up to #9262) (#9451) 2025-07-13 11:05:13 +12:00
J. Nick Koston
3c864b2bca Reduce API flash usage by eliminating unnecessary template instantiations (#9452) 2025-07-13 11:05:13 +12:00
Keith Burzinski
35d88fc0d6 [ld2410] Remove redundant `delay()` calls, minor optimizations (#9453) 2025-07-13 11:05:13 +12:00
J. Nick Koston
7a6894e087 Optimize API proto size calculations by removing redundant force parameter (#9449) 2025-07-13 11:05:13 +12:00
J. Nick Koston
1b222ceca3 Optimize API flash usage by storing message size at compile time (#9447) 2025-07-13 11:05:13 +12:00
Samuel Sieb
bab3deee1b [wizard] use lowercase to match (#9448)
Co-authored-by: Samuel Sieb <samuel@sieb.net>
2025-07-13 11:05:13 +12:00
J. Nick Koston
ccd30110b1 Fix scheduler crash when cancelling items with NULL names (#9444) 2025-07-13 11:05:13 +12:00
J. Nick Koston
904c7b8a3a Sync api.proto from aioesphomeapi (#9393) 2025-07-13 11:05:13 +12:00
DT-art1
fa262673e4 Replace remaining instances of USE_ESP32_CAMERA with USE_CAMERA (#9401) 2025-07-13 11:05:13 +12:00
Adam Liddell
0ef5f1fd65 Handle ESP32 chunked MQTT messages missing topic on non-first chunks, causing panic (#5786)
Co-authored-by: Samuel Sieb <samuel-github@sieb.net>
2025-07-13 11:05:13 +12:00
J. Nick Koston
23dd2d648e Exclude internal entities from name uniqueness validation (#9410) 2025-07-13 11:05:13 +12:00
@RubenKelevra
5ba493acc3 debug: bufferoverflow mitigation in DebugComponent::on_shutdown() (#9422) 2025-07-13 11:05:12 +12:00
Jonathan Swoboda
a5055094d0 [esp32] Set lib_compat_mode to strict (#9408) 2025-07-13 11:05:12 +12:00
Jonathan Swoboda
92d03dd196 [esp32_touch] Fix touch v1 (#9414) 2025-07-13 11:05:12 +12:00
J. Nick Koston
bd75f0dfea Fix another race in the string lifetime scheduler test (#9399) 2025-07-13 11:05:12 +12:00
Jonathan Swoboda
f4ac951b15 [libretiny] Set lib_compat_mode to soft for libretiny (#9439) 2025-07-13 11:00:38 +12:00
Clyde Stubbs
e020110579 [usb_uart] Be flexible about descriptor layout for CDC-ACM devices (#9425) 2025-07-13 10:59:49 +12:00
J. Nick Koston
1fda40f0ce Only generate protobuf encode/decode methods for the message direction they're used (#9461) 2025-07-13 10:58:57 +12:00
dependabot[bot]
a5e42e1bd0 Bump aioesphomeapi from 34.2.0 to 34.2.1 (#9460)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-12 10:07:58 -10:00
J. Nick Koston
8863188dd8 Apply existing protobuf buffer optimization to nested message encoding (~2.3x speed up) (#9458) 2025-07-13 06:55:32 +12:00
Jonathan Swoboda
7747a5aa62 [sx127x, sx126x] Fix preamble_size default and validation (#9454) 2025-07-12 17:03:03 +10:00
Clyde Stubbs
32419645ca [packet_transport] Don't run update if ping_pong not enabled. (#9434) 2025-07-12 17:00:52 +10:00
J. Nick Koston
634aa55364 Disable WiFi when using Ethernet to save memory (#9456) 2025-07-12 05:19:53 +00:00
J. Nick Koston
dd5ba5a90c Conditionally compile API user services to save 4.3KB flash (follow-up to #9262) (#9451) 2025-07-11 19:08:03 -10:00
dependabot[bot]
0138ef36cf Bump ruff from 0.12.2 to 0.12.3 (#9446)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-07-12 04:01:54 +00:00
J. Nick Koston
ca5ee0ce07 Reduce API flash usage by eliminating unnecessary template instantiations (#9452) 2025-07-12 03:56:08 +00:00
Keith Burzinski
79b5fcf31a [ld2420] Memory optimization, code clean-up (#9426) 2025-07-11 22:33:36 -05:00
Keith Burzinski
2243e44750 [ld2410] Remove redundant `delay()` calls, minor optimizations (#9453) 2025-07-11 22:05:06 -05:00
J. Nick Koston
01f949e097 Optimize API proto size calculations by removing redundant force parameter (#9449) 2025-07-11 21:08:52 -05:00
J. Nick Koston
143bf694c7 Optimize API flash usage by storing message size at compile time (#9447) 2025-07-11 19:38:23 -05:00
Samuel Sieb
983db6215f [wizard] use lowercase to match (#9448)
Co-authored-by: Samuel Sieb <samuel@sieb.net>
2025-07-11 18:35:52 -05:00
J. Nick Koston
bef20b60d0 Fix scheduler crash when cancelling items with NULL names (#9444) 2025-07-12 07:11:45 +12:00
J. Nick Koston
475fe60f27 Sync api.proto from aioesphomeapi (#9393) 2025-07-11 08:33:18 -10:00
J. Nick Koston
8953e53a04 CI: Centralize test determination logic to reduce unnecessary job runners (#9432) 2025-07-11 21:54:57 +12:00
DT-art1
143702beef Replace remaining instances of USE_ESP32_CAMERA with USE_CAMERA (#9401) 2025-07-10 20:35:24 +12:00
Adam Liddell
05238b447f Handle ESP32 chunked MQTT messages missing topic on non-first chunks, causing panic (#5786)
Co-authored-by: Samuel Sieb <samuel-github@sieb.net>
2025-07-10 20:34:43 +12:00
J. Nick Koston
0d94246858 Exclude internal entities from name uniqueness validation (#9410) 2025-07-10 20:34:01 +12:00
Samuel Sieb
2be4951ad9 [esp32] remove debug log (#9424) 2025-07-10 08:24:39 +00:00
Clyde Stubbs
16bb81814c [config] Add bitrate validator (#9423) 2025-07-10 04:14:42 +00:00
@RubenKelevra
7d92499e4c debug: bufferoverflow mitigation in DebugComponent::on_shutdown() (#9422) 2025-07-09 17:01:21 -10:00
Jonathan Swoboda
a240f0af90 [esp32] Set lib_compat_mode to strict (#9408) 2025-07-10 14:49:36 +12:00
J. Nick Koston
fc59c08800 Fix clang-tidy not finding changed files on squash-merge commits (#9421) 2025-07-10 14:37:48 +12:00
J. Nick Koston
e2c60f5384 Fix Windows virtual environment activation in CI workflows (#9420) 2025-07-10 14:37:03 +12:00
Andrew Klaus
33d48732aa Adding support for Airthings Wave Gen2 (#8460)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-07-09 15:50:01 -10:00
J. Nick Koston
9a1edaa4f4 Fix Python cache key mismatch for all pytest jobs (#9417) 2025-07-09 15:21:21 -10:00
J. Nick Koston
926e4fa3e1 Fix Python cache for all pytest CI jobs (#9415) 2025-07-09 14:43:49 -10:00
J. Nick Koston
97dd96b60d Implement shared PlatformIO cache for integration tests (#9413)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-10 12:42:24 +12:00
J. Nick Koston
e9c7596e00 Fix clang-tidy triggering full scan on Python-only core file changes (#9412) 2025-07-10 12:41:59 +12:00
J. Nick Koston
ff836a8434 Fix PlatformIO cache in CI by adding platformio.ini hash to cache key (#9411) 2025-07-10 12:40:10 +12:00
Jonathan Swoboda
3d9c977826 [esp32_touch] Fix touch v1 (#9414) 2025-07-09 14:18:01 -10:00
J. Nick Koston
c1a994b1d9 Fix another race in the string lifetime scheduler test (#9399) 2025-07-10 09:11:42 +12:00
J. Nick Koston
6616567b05 Speed up clang-tidy CI by 80%+ with incremental checking (#9396) 2025-07-10 09:00:44 +12:00
414 changed files with 15410 additions and 10006 deletions

222
.ai/instructions.md Normal file
View File

@@ -0,0 +1,222 @@
# ESPHome AI Collaboration Guide
This document provides essential context for AI models interacting with this project. Adhering to these guidelines will ensure consistency and maintain code quality.
## 1. Project Overview & Purpose
* **Primary Goal:** ESPHome is a system to configure microcontrollers (like ESP32, ESP8266, RP2040, and LibreTiny-based chips) using simple yet powerful YAML configuration files. It generates C++ firmware that can be compiled and flashed to these devices, allowing users to control them remotely through home automation systems.
* **Business Domain:** Internet of Things (IoT), Home Automation.
## 2. Core Technologies & Stack
* **Languages:** Python (>=3.10), C++ (gnu++20)
* **Frameworks & Runtimes:** PlatformIO, Arduino, ESP-IDF.
* **Build Systems:** PlatformIO is the primary build system. CMake is used as an alternative.
* **Configuration:** YAML.
* **Key Libraries/Dependencies:**
* **Python:** `voluptuous` (for configuration validation), `PyYAML` (for parsing configuration files), `paho-mqtt` (for MQTT communication), `tornado` (for the web server), `aioesphomeapi` (for the native API).
* **C++:** `ArduinoJson` (for JSON serialization/deserialization), `AsyncMqttClient-esphome` (for MQTT), `ESPAsyncWebServer` (for the web server).
* **Package Manager(s):** `pip` (for Python dependencies), `platformio` (for C++/PlatformIO dependencies).
* **Communication Protocols:** Protobuf (for native API), MQTT, HTTP.
## 3. Architectural Patterns
* **Overall Architecture:** The project follows a code-generation architecture. The Python code parses user-defined YAML configuration files and generates C++ source code. This C++ code is then compiled and flashed to the target microcontroller using PlatformIO.
* **Directory Structure Philosophy:**
* `/esphome`: Contains the core Python source code for the ESPHome application.
* `/esphome/components`: Contains the individual components that can be used in ESPHome configurations. Each component is a self-contained unit with its own C++ and Python code.
* `/tests`: Contains all unit and integration tests for the Python code.
* `/docker`: Contains Docker-related files for building and running ESPHome in a container.
* `/script`: Contains helper scripts for development and maintenance.
* **Core Architectural Components:**
1. **Configuration System** (`esphome/config*.py`): Handles YAML parsing and validation using Voluptuous, schema definitions, and multi-platform configurations.
2. **Code Generation** (`esphome/codegen.py`, `esphome/cpp_generator.py`): Manages Python to C++ code generation, template processing, and build flag management.
3. **Component System** (`esphome/components/`): Contains modular hardware and software components with platform-specific implementations and dependency management.
4. **Core Framework** (`esphome/core/`): Manages the application lifecycle, hardware abstraction, and component registration.
5. **Dashboard** (`esphome/dashboard/`): A web-based interface for device configuration, management, and OTA updates.
* **Platform Support:**
1. **ESP32** (`components/esp32/`): Espressif ESP32 family. Supports multiple variants (S2, S3, C3, etc.) and both IDF and Arduino frameworks.
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.
4. **LibreTiny** (`components/libretiny/`): Realtek and Beken chips. Supports multiple chip families and auto-generated components.
## 4. Coding Conventions & Style Guide
* **Formatting:**
* **Python:** Uses `ruff` and `flake8` for linting and formatting. Configuration is in `pyproject.toml`.
* **C++:** Uses `clang-format` for formatting. Configuration is in `.clang-format`.
* **Naming Conventions:**
* **Python:** Follows PEP 8. Use clear, descriptive names following snake_case.
* **C++:** Follows the Google C++ Style Guide.
* **Component Structure:**
* **Standard Files:**
```
components/[component_name]/
├── __init__.py # Component configuration schema and code generation
├── [component].h # C++ header file (if needed)
├── [component].cpp # C++ implementation (if needed)
└── [platform]/ # Platform-specific implementations
├── __init__.py # Platform-specific configuration
├── [platform].h # Platform C++ header
└── [platform].cpp # Platform C++ implementation
```
* **Component Metadata:**
- `DEPENDENCIES`: List of required components
- `AUTO_LOAD`: Components to automatically load
- `CONFLICTS_WITH`: Incompatible components
- `CODEOWNERS`: GitHub usernames responsible for maintenance
- `MULTI_CONF`: Whether multiple instances are allowed
* **Code Generation & Common Patterns:**
* **Configuration Schema Pattern:**
```python
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_KEY, CONF_ID
CONF_PARAM = "param" # A constant that does not yet exist in esphome/const.py
my_component_ns = cg.esphome_ns.namespace("my_component")
MyComponent = my_component_ns.class_("MyComponent", cg.Component)
CONFIG_SCHEMA = cv.Schema({
cv.GenerateID(): cv.declare_id(MyComponent),
cv.Required(CONF_KEY): cv.string,
cv.Optional(CONF_PARAM, default=42): cv.int_,
}).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
cg.add(var.set_key(config[CONF_KEY]))
cg.add(var.set_param(config[CONF_PARAM]))
```
* **C++ Class Pattern:**
```cpp
namespace esphome {
namespace my_component {
class MyComponent : public Component {
public:
void setup() override;
void loop() override;
void dump_config() override;
void set_key(const std::string &key) { this->key_ = key; }
void set_param(int param) { this->param_ = param; }
protected:
std::string key_;
int param_{0};
};
} // namespace my_component
} // namespace esphome
```
* **Common Component Examples:**
- **Sensor:**
```python
from esphome.components import sensor
CONFIG_SCHEMA = sensor.sensor_schema(MySensor).extend(cv.polling_component_schema("60s"))
async def to_code(config):
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
```
- **Binary Sensor:**
```python
from esphome.components import binary_sensor
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema().extend({ ... })
async def to_code(config):
var = await binary_sensor.new_binary_sensor(config)
```
- **Switch:**
```python
from esphome.components import switch
CONFIG_SCHEMA = switch.switch_schema().extend({ ... })
async def to_code(config):
var = await switch.new_switch(config)
```
* **Configuration Validation:**
* **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)`.
* **Platform-Specific:** `cv.only_on(["esp32", "esp8266"])`, `cv.only_with_arduino`.
* **Schema Extensions:**
```python
CONFIG_SCHEMA = cv.Schema({ ... })
.extend(cv.COMPONENT_SCHEMA)
.extend(uart.UART_DEVICE_SCHEMA)
.extend(i2c.i2c_device_schema(0x48))
.extend(spi.spi_device_schema(cs_pin_required=True))
```
## 5. Key Files & Entrypoints
* **Main Entrypoint(s):** `esphome/__main__.py` is the main entrypoint for the ESPHome command-line interface.
* **Configuration:**
* `pyproject.toml`: Defines the Python project metadata and dependencies.
* `platformio.ini`: Configures the PlatformIO build environments for different microcontrollers.
* `.pre-commit-config.yaml`: Configures the pre-commit hooks for linting and formatting.
* **CI/CD Pipeline:** Defined in `.github/workflows`.
## 6. Development & Testing Workflow
* **Local Development Environment:** Use the provided Docker container or create a Python virtual environment and install dependencies from `requirements_dev.txt`.
* **Running Commands:** Use the `script/run-in-env.py` script to execute commands within the project's virtual environment. For example, to run the linter: `python3 script/run-in-env.py pre-commit run`.
* **Testing:**
* **Python:** Run unit tests with `pytest`.
* **C++:** Use `clang-tidy` for static analysis.
* **Component Tests:** YAML-based compilation tests are located in `tests/`. The structure is as follows:
```
tests/
├── test_build_components/ # Base test configurations
└── components/[component]/ # Component-specific tests
```
Run them using `script/test_build_components`. Use `-c <component>` to test specific components and `-t <target>` for specific platforms.
* **Debugging and Troubleshooting:**
* **Debug Tools:**
- `esphome config <file>.yaml` to validate configuration.
- `esphome compile <file>.yaml` to compile without uploading.
- Check the Dashboard for real-time logs.
- Use component-specific debug logging.
* **Common Issues:**
- **Import Errors**: Check component dependencies and `PYTHONPATH`.
- **Validation Errors**: Review configuration schema definitions.
- **Build Errors**: Check platform compatibility and library versions.
- **Runtime Errors**: Review generated C++ code and component logic.
## 7. Specific Instructions for AI Collaboration
* **Contribution Workflow (Pull Request Process):**
1. **Fork & Branch:** Create a new branch in your fork.
2. **Make Changes:** Adhere to all coding conventions and patterns.
3. **Test:** Create component tests for all supported platforms and run the full test suite locally.
4. **Lint:** Run `pre-commit` to ensure code is compliant.
5. **Commit:** Commit your changes. There is no strict format for commit messages.
6. **Pull Request:** Submit a PR against the `dev` branch. The Pull Request title should have a prefix of the component being worked on (e.g., `[display] Fix bug`, `[abc123] Add new component`). Update documentation, examples, and add `CODEOWNERS` entries as needed. Pull requests should always be made with the PULL_REQUEST_TEMPLATE.md template filled out correctly.
* **Documentation Contributions:**
* Documentation is hosted in the separate `esphome/esphome-docs` repository.
* The contribution workflow is the same as for the codebase.
* **Best Practices:**
* **Component Development:** Keep dependencies minimal, provide clear error messages, and write comprehensive docstrings and tests.
* **Code Generation:** Generate minimal and efficient C++ code. Validate all user inputs thoroughly. Support multiple platform variations.
* **Configuration Design:** Aim for simplicity with sensible defaults, while allowing for advanced customization.
* **Security:** Be mindful of security when making changes to the API, web server, or any other network-related code. Do not hardcode secrets or keys.
* **Dependencies & Build System Integration:**
* **Python:** When adding a new Python dependency, add it to the appropriate `requirements*.txt` file and `pyproject.toml`.
* **C++ / PlatformIO:** When adding a new C++ dependency, add it to `platformio.ini` and use `cg.add_library`.
* **Build Flags:** Use `cg.add_build_flag(...)` to add compiler flags.

1
.clang-tidy.hash Normal file
View File

@@ -0,0 +1 @@
0c2acbc16bfb7d63571dbe7042f94f683be25e4ca8a0f158a960a94adac4b931

View File

@@ -1,4 +1,4 @@
[run]
omit =
omit =
esphome/components/*
tests/integration/*

92
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,92 @@
name: Report an issue with ESPHome
description: Report an issue with ESPHome.
body:
- type: markdown
attributes:
value: |
This issue form is for reporting bugs only!
If you have a feature request or enhancement, please [request them here instead][fr].
[fr]: https://github.com/orgs/esphome/discussions
- type: textarea
validations:
required: true
id: problem
attributes:
label: The problem
description: >-
Describe the issue you are experiencing here to communicate to the
maintainers. Tell us what you were trying to do and what happened.
Provide a clear and concise description of what the problem is.
- type: markdown
attributes:
value: |
## Environment
- type: input
id: version
validations:
required: true
attributes:
label: Which version of ESPHome has the issue?
description: >
ESPHome version like 1.19, 2025.6.0 or 2025.XX.X-dev.
- type: dropdown
validations:
required: true
id: installation
attributes:
label: What type of installation are you using?
options:
- Home Assistant Add-on
- Docker
- pip
- type: dropdown
validations:
required: true
id: platform
attributes:
label: What platform are you using?
options:
- ESP8266
- ESP32
- RP2040
- BK72XX
- RTL87XX
- LN882X
- Host
- Other
- type: input
id: component_name
attributes:
label: Component causing the issue
description: >
The name of the component or platform. For example, api/i2c or ultrasonic.
- type: markdown
attributes:
value: |
# Details
- type: textarea
id: config
attributes:
label: YAML Config
description: |
Include a complete YAML configuration file demonstrating the problem here. Preferably post the *entire* file - don't make assumptions about what is unimportant. However, if it's a large or complicated config then you will need to reduce it to the smallest possible file *that still demonstrates the problem*. If you don't provide enough information to *easily* reproduce the problem, it's unlikely your bug report will get any attention. Logs do not belong here, attach them below.
render: yaml
- type: textarea
id: logs
attributes:
label: Anything in the logs that might be useful for us?
description: For example, error message, or stack traces. Serial or USB logs are much more useful than WiFi logs.
render: txt
- type: textarea
id: additional
attributes:
label: Additional information
description: >
If you have any additional information for us, use the field below.
Please note, you can attach screenshots or screen recordings here, by
dragging and dropping files in the field below.

View File

@@ -1,15 +1,21 @@
---
blank_issues_enabled: false
contact_links:
- name: Issue Tracker
url: https://github.com/esphome/issues
about: Please create bug reports in the dedicated issue tracker.
- name: Feature Request Tracker
url: https://github.com/esphome/feature-requests
about: |
Please create feature requests in the dedicated feature request tracker.
- name: Report an issue with the ESPHome documentation
url: https://github.com/esphome/esphome-docs/issues/new/choose
about: Report an issue with the ESPHome documentation.
- name: Report an issue with the ESPHome web server
url: https://github.com/esphome/esphome-webserver/issues/new/choose
about: Report an issue with the ESPHome web server.
- name: Report an issue with the ESPHome Builder / Dashboard
url: https://github.com/esphome/dashboard/issues/new/choose
about: Report an issue with the ESPHome Builder / Dashboard.
- name: Report an issue with the ESPHome API client
url: https://github.com/esphome/aioesphomeapi/issues/new/choose
about: Report an issue with the ESPHome API client.
- name: Make a Feature Request
url: https://github.com/orgs/esphome/discussions
about: Please create feature requests in the dedicated feature request tracker.
- name: Frequently Asked Question
url: https://esphome.io/guides/faq.html
about: |
Please view the FAQ for common questions and what
to include in a bug report.
about: Please view the FAQ for common questions and what to include in a bug report.

View File

@@ -26,6 +26,7 @@
- [ ] RP2040
- [ ] BK72xx
- [ ] RTL87xx
- [ ] nRF52840
## Example entry for `config.yaml`:

View File

@@ -41,7 +41,7 @@ runs:
shell: bash
run: |
python -m venv venv
./venv/Scripts/activate
source ./venv/Scripts/activate
python --version
pip install -r requirements.txt -r requirements_test.txt
pip install -e .

1
.github/copilot-instructions.md vendored Symbolic link
View File

@@ -0,0 +1 @@
../.ai/instructions.md

View File

@@ -9,6 +9,9 @@ updates:
# Hypotehsis is only used for testing and is updated quite often
- dependency-name: hypothesis
- package-ecosystem: github-actions
labels:
- "dependencies"
- "github-actions"
directory: "/"
schedule:
interval: daily
@@ -20,11 +23,17 @@ updates:
- "docker/login-action"
- "docker/setup-buildx-action"
- package-ecosystem: github-actions
labels:
- "dependencies"
- "github-actions"
directory: "/.github/actions/build-image"
schedule:
interval: daily
open-pull-requests-limit: 10
- package-ecosystem: github-actions
labels:
- "dependencies"
- "github-actions"
directory: "/.github/actions/restore-python"
schedule:
interval: daily

449
.github/workflows/auto-label-pr.yml vendored Normal file
View File

@@ -0,0 +1,449 @@
name: Auto Label PR
on:
# Runs only on pull_request_target due to having access to a App token.
# This means PRs from forks will not be able to alter this workflow to get the tokens
pull_request_target:
types: [labeled, opened, reopened, synchronize, edited]
permissions:
pull-requests: write
contents: read
env:
TARGET_PLATFORMS: |
esp32
esp8266
rp2040
libretiny
bk72xx
rtl87xx
ln882x
nrf52
host
PLATFORM_COMPONENTS: |
alarm_control_panel
audio_adc
audio_dac
binary_sensor
button
canbus
climate
cover
datetime
display
event
fan
light
lock
media_player
microphone
number
one_wire
ota
output
packet_transport
select
sensor
speaker
stepper
switch
text
text_sensor
time
touchscreen
update
valve
SMALL_PR_THRESHOLD: 30
MAX_LABELS: 15
jobs:
label:
runs-on: ubuntu-latest
if: github.event.action != 'labeled' || github.event.sender.type != 'Bot'
steps:
- name: Checkout
uses: actions/checkout@v4.2.2
- name: Get changes
id: changes
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Get PR number
pr_number="${{ github.event.pull_request.number }}"
# Get list of changed files using gh CLI
files=$(gh pr diff $pr_number --name-only)
echo "files<<EOF" >> $GITHUB_OUTPUT
echo "$files" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
# Get file stats (additions + deletions) using gh CLI
stats=$(gh pr view $pr_number --json files --jq '.files | map(.additions + .deletions) | add')
echo "total_changes=${stats:-0}" >> $GITHUB_OUTPUT
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
- name: Auto Label PR
uses: actions/github-script@v7.0.1
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
const fs = require('fs');
const { owner, repo } = context.repo;
const pr_number = context.issue.number;
// Get current labels
const { data: currentLabelsData } = await github.rest.issues.listLabelsOnIssue({
owner,
repo,
issue_number: pr_number
});
const currentLabels = currentLabelsData.map(label => label.name);
// Define managed labels that this workflow controls
const managedLabels = currentLabels.filter(label =>
label.startsWith('component: ') ||
[
'new-component',
'new-platform',
'new-target-platform',
'merging-to-release',
'merging-to-beta',
'core',
'small-pr',
'dashboard',
'github-actions',
'by-code-owner',
'has-tests',
'needs-tests',
'needs-docs',
'too-big',
'labeller-recheck'
].includes(label)
);
console.log('Current labels:', currentLabels.join(', '));
console.log('Managed labels:', managedLabels.join(', '));
// Get changed files
const changedFiles = `${{ steps.changes.outputs.files }}`.split('\n').filter(f => f.length > 0);
const totalChanges = parseInt('${{ steps.changes.outputs.total_changes }}') || 0;
console.log('Changed files:', changedFiles.length);
console.log('Total changes:', totalChanges);
const labels = new Set();
// Get environment variables
const targetPlatforms = `${{ env.TARGET_PLATFORMS }}`.split('\n').filter(p => p.trim().length > 0).map(p => p.trim());
const platformComponents = `${{ env.PLATFORM_COMPONENTS }}`.split('\n').filter(p => p.trim().length > 0).map(p => p.trim());
const smallPrThreshold = parseInt('${{ env.SMALL_PR_THRESHOLD }}');
const maxLabels = parseInt('${{ env.MAX_LABELS }}');
// Strategy: Merge to release or beta branch
const baseRef = context.payload.pull_request.base.ref;
if (baseRef !== 'dev') {
if (baseRef === 'release') {
labels.add('merging-to-release');
} else if (baseRef === 'beta') {
labels.add('merging-to-beta');
}
// When targeting non-dev branches, only use merge warning labels
const finalLabels = Array.from(labels);
console.log('Computed labels (merge branch only):', finalLabels.join(', '));
// Add new labels
if (finalLabels.length > 0) {
console.log(`Adding labels: ${finalLabels.join(', ')}`);
await github.rest.issues.addLabels({
owner,
repo,
issue_number: pr_number,
labels: finalLabels
});
}
// Remove old managed labels that are no longer needed
const labelsToRemove = managedLabels.filter(label =>
!finalLabels.includes(label)
);
for (const label of labelsToRemove) {
console.log(`Removing label: ${label}`);
try {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: pr_number,
name: label
});
} catch (error) {
console.log(`Failed to remove label ${label}:`, error.message);
}
}
return; // Exit early, don't process other strategies
}
// Strategy: Component and Platform labeling
const componentRegex = /^esphome\/components\/([^\/]+)\//;
const targetPlatformRegex = new RegExp(`^esphome\/components\/(${targetPlatforms.join('|')})/`);
for (const file of changedFiles) {
// Check for component changes
const componentMatch = file.match(componentRegex);
if (componentMatch) {
const component = componentMatch[1];
labels.add(`component: ${component}`);
}
// Check for target platform changes
const platformMatch = file.match(targetPlatformRegex);
if (platformMatch) {
const targetPlatform = platformMatch[1];
labels.add(`platform: ${targetPlatform}`);
}
}
// Get PR files for new component/platform detection
const { data: prFiles } = await github.rest.pulls.listFiles({
owner,
repo,
pull_number: pr_number
});
const addedFiles = prFiles.filter(file => file.status === 'added').map(file => file.filename);
// Strategy: New Component detection
for (const file of addedFiles) {
// Check for new component files: esphome/components/{component}/__init__.py
const componentMatch = file.match(/^esphome\/components\/([^\/]+)\/__init__\.py$/);
if (componentMatch) {
try {
// Read the content directly from the filesystem since we have it checked out
const content = fs.readFileSync(file, 'utf8');
// Strategy: New Target Platform detection
if (content.includes('IS_TARGET_PLATFORM = True')) {
labels.add('new-target-platform');
}
labels.add('new-component');
} catch (error) {
console.log(`Failed to read content of ${file}:`, error.message);
// Fallback: assume it's a new component if we can't read the content
labels.add('new-component');
}
}
}
// Strategy: New Platform detection
for (const file of addedFiles) {
// Check for new platform files: esphome/components/{component}/{platform}.py
const platformFileMatch = file.match(/^esphome\/components\/([^\/]+)\/([^\/]+)\.py$/);
if (platformFileMatch) {
const [, component, platform] = platformFileMatch;
if (platformComponents.includes(platform)) {
labels.add('new-platform');
}
}
// Check for new platform files: esphome/components/{component}/{platform}/__init__.py
const platformDirMatch = file.match(/^esphome\/components\/([^\/]+)\/([^\/]+)\/__init__\.py$/);
if (platformDirMatch) {
const [, component, platform] = platformDirMatch;
if (platformComponents.includes(platform)) {
labels.add('new-platform');
}
}
}
const coreFiles = changedFiles.filter(file =>
file.startsWith('esphome/core/') ||
(file.startsWith('esphome/') && file.split('/').length === 2)
);
if (coreFiles.length > 0) {
labels.add('core');
}
// Strategy: Small PR detection
if (totalChanges <= smallPrThreshold) {
labels.add('small-pr');
}
// Strategy: Dashboard changes
const dashboardFiles = changedFiles.filter(file =>
file.startsWith('esphome/dashboard/') ||
file.startsWith('esphome/components/dashboard_import/')
);
if (dashboardFiles.length > 0) {
labels.add('dashboard');
}
// Strategy: GitHub Actions changes
const githubActionsFiles = changedFiles.filter(file =>
file.startsWith('.github/workflows/')
);
if (githubActionsFiles.length > 0) {
labels.add('github-actions');
}
// Strategy: Code Owner detection
try {
// Fetch CODEOWNERS file from the repository (in case it was changed in this PR)
const { data: codeownersFile } = await github.rest.repos.getContent({
owner,
repo,
path: 'CODEOWNERS',
});
const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8');
const prAuthor = context.payload.pull_request.user.login;
// Parse CODEOWNERS file
const codeownersLines = codeownersContent.split('\n')
.map(line => line.trim())
.filter(line => line && !line.startsWith('#'));
let isCodeOwner = false;
// Precompile CODEOWNERS patterns into regex objects
const codeownersRegexes = codeownersLines.map(line => {
const parts = line.split(/\s+/);
const pattern = parts[0];
const owners = parts.slice(1);
let regex;
if (pattern.endsWith('*')) {
// Directory pattern like "esphome/components/api/*"
const dir = pattern.slice(0, -1);
regex = new RegExp(`^${dir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`);
} else if (pattern.includes('*')) {
// Glob pattern
const regexPattern = pattern
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
.replace(/\\*/g, '.*');
regex = new RegExp(`^${regexPattern}$`);
} else {
// Exact match
regex = new RegExp(`^${pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`);
}
return { regex, owners };
});
for (const file of changedFiles) {
for (const { regex, owners } of codeownersRegexes) {
if (regex.test(file)) {
// Check if PR author is in the owners list
if (owners.some(owner => owner === `@${prAuthor}`)) {
isCodeOwner = true;
break;
}
}
}
if (isCodeOwner) break;
}
if (isCodeOwner) {
labels.add('by-code-owner');
}
} catch (error) {
console.log('Failed to read or parse CODEOWNERS file:', error.message);
}
// Strategy: Test detection
const testFiles = changedFiles.filter(file =>
file.startsWith('tests/')
);
if (testFiles.length > 0) {
labels.add('has-tests');
} else {
// Only check for needs-tests if this is a new component or new platform
if (labels.has('new-component') || labels.has('new-platform')) {
labels.add('needs-tests');
}
}
// Strategy: Documentation check for new components/platforms
if (labels.has('new-component') || labels.has('new-platform')) {
const prBody = context.payload.pull_request.body || '';
// Look for documentation PR links
// Patterns to match:
// - https://github.com/esphome/esphome-docs/pull/1234
// - esphome/esphome-docs#1234
const docsPrPatterns = [
/https:\/\/github\.com\/esphome\/esphome-docs\/pull\/\d+/,
/esphome\/esphome-docs#\d+/
];
const hasDocsLink = docsPrPatterns.some(pattern => pattern.test(prBody));
if (!hasDocsLink) {
labels.add('needs-docs');
}
}
// Convert Set to Array
let finalLabels = Array.from(labels);
console.log('Computed labels:', finalLabels.join(', '));
// Don't set more than max labels
if (finalLabels.length > maxLabels) {
const originalLength = finalLabels.length;
console.log(`Not setting ${originalLength} labels because out of range`);
finalLabels = ['too-big'];
// Request changes on the PR
await github.rest.pulls.createReview({
owner,
repo,
pull_number: pr_number,
body: `This PR is too large and affects ${originalLength} different components/areas. Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.`,
event: 'REQUEST_CHANGES'
});
}
// Add new labels
if (finalLabels.length > 0) {
console.log(`Adding labels: ${finalLabels.join(', ')}`);
await github.rest.issues.addLabels({
owner,
repo,
issue_number: pr_number,
labels: finalLabels
});
}
// Remove old managed labels that are no longer needed
const labelsToRemove = managedLabels.filter(label =>
!finalLabels.includes(label)
);
for (const label of labelsToRemove) {
console.log(`Removing label: ${label}`);
try {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: pr_number,
name: label
});
} catch (error) {
console.log(`Failed to remove label ${label}:`, error.message);
}
}

View File

@@ -0,0 +1,75 @@
name: Clang-tidy Hash CI
on:
pull_request:
paths:
- ".clang-tidy"
- "platformio.ini"
- "requirements_dev.txt"
- ".clang-tidy.hash"
- "script/clang_tidy_hash.py"
- ".github/workflows/ci-clang-tidy-hash.yml"
permissions:
contents: read
pull-requests: write
jobs:
verify-hash:
name: Verify clang-tidy hash
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4.2.2
- name: Set up Python
uses: actions/setup-python@v5.6.0
with:
python-version: "3.11"
- name: Verify hash
run: |
python script/clang_tidy_hash.py --verify
- if: failure()
name: Show hash details
run: |
python script/clang_tidy_hash.py
echo "## Job Failed" | tee -a $GITHUB_STEP_SUMMARY
echo "You have modified clang-tidy configuration but have not updated the hash." | tee -a $GITHUB_STEP_SUMMARY
echo "Please run 'script/clang_tidy_hash.py --update' and commit the changes." | tee -a $GITHUB_STEP_SUMMARY
- if: failure()
name: Request changes
uses: actions/github-script@v7.0.1
with:
script: |
await github.rest.pulls.createReview({
pull_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
event: 'REQUEST_CHANGES',
body: 'You have modified clang-tidy configuration but have not updated the hash.\nPlease run `script/clang_tidy_hash.py --update` and commit the changes.'
})
- if: success()
name: Dismiss review
uses: actions/github-script@v7.0.1
with:
script: |
let reviews = await github.rest.pulls.listReviews({
pull_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo
});
for (let review of reviews.data) {
if (review.user.login === 'github-actions[bot]' && review.state === 'CHANGES_REQUESTED') {
await github.rest.pulls.dismissReview({
pull_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
review_id: review.id,
message: 'Clang-tidy hash now matches configuration.'
});
}
}

View File

@@ -47,7 +47,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5.6.0
with:
python-version: "3.10"
python-version: "3.11"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.11.1

View File

@@ -20,8 +20,8 @@ permissions:
contents: read
env:
DEFAULT_PYTHON: "3.10"
PYUPGRADE_TARGET: "--py310-plus"
DEFAULT_PYTHON: "3.11"
PYUPGRADE_TARGET: "--py311-plus"
concurrency:
# yamllint disable-line rule:line-length
@@ -39,7 +39,7 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Generate cache-key
id: cache-key
run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt') }}" >> $GITHUB_OUTPUT
run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.6.0
@@ -58,56 +58,16 @@ jobs:
python -m venv venv
. venv/bin/activate
python --version
pip install -r requirements.txt -r requirements_test.txt
pip install -r requirements.txt -r requirements_test.txt pre-commit
pip install -e .
ruff:
name: Check ruff
runs-on: ubuntu-24.04
needs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Run Ruff
run: |
. venv/bin/activate
ruff format esphome tests
- name: Suggested changes
run: script/ci-suggest-changes
if: always()
flake8:
name: Check flake8
runs-on: ubuntu-24.04
needs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Run flake8
run: |
. venv/bin/activate
flake8 esphome
- name: Suggested changes
run: script/ci-suggest-changes
if: always()
pylint:
name: Check pylint
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
if: needs.determine-jobs.outputs.python-linters == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
@@ -124,27 +84,6 @@ jobs:
run: script/ci-suggest-changes
if: always()
pyupgrade:
name: Check pyupgrade
runs-on: ubuntu-24.04
needs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Run pyupgrade
run: |
. venv/bin/activate
pyupgrade ${{ env.PYUPGRADE_TARGET }} `find esphome -name "*.py" -type f`
- name: Suggested changes
run: script/ci-suggest-changes
if: always()
ci-custom:
name: Run script/ci-custom
runs-on: ubuntu-24.04
@@ -173,7 +112,6 @@ jobs:
fail-fast: false
matrix:
python-version:
- "3.10"
- "3.11"
- "3.12"
- "3.13"
@@ -189,14 +127,10 @@ jobs:
os: windows-latest
- python-version: "3.12"
os: windows-latest
- python-version: "3.10"
os: windows-latest
- python-version: "3.13"
os: macOS-latest
- python-version: "3.12"
os: macOS-latest
- python-version: "3.10"
os: macOS-latest
runs-on: ${{ matrix.os }}
needs:
- common
@@ -204,6 +138,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Restore Python
id: restore-python
uses: ./.github/actions/restore-python
with:
python-version: ${{ matrix.python-version }}
@@ -213,7 +148,7 @@ jobs:
- name: Run pytest
if: matrix.os == 'windows-latest'
run: |
./venv/Scripts/activate
. ./venv/Scripts/activate.ps1
pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/
- name: Run pytest
if: matrix.os == 'ubuntu-latest' || matrix.os == 'macOS-latest'
@@ -224,12 +159,59 @@ jobs:
uses: codecov/codecov-action@v5.4.3
with:
token: ${{ secrets.CODECOV_TOKEN }}
- name: Save Python virtual environment cache
if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@v4.2.3
with:
path: venv
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
determine-jobs:
name: Determine which jobs to run
runs-on: ubuntu-24.04
needs:
- common
outputs:
integration-tests: ${{ steps.determine.outputs.integration-tests }}
clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
python-linters: ${{ steps.determine.outputs.python-linters }}
changed-components: ${{ steps.determine.outputs.changed-components }}
component-test-count: ${{ steps.determine.outputs.component-test-count }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
with:
# Fetch enough history to find the merge base
fetch-depth: 2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Determine which tests to run
id: determine
env:
GH_TOKEN: ${{ github.token }}
run: |
. venv/bin/activate
output=$(python script/determine-jobs.py)
echo "Test determination output:"
echo "$output" | jq
# Extract individual fields
echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT
echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT
echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT
echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT
echo "component-test-count=$(echo "$output" | jq -r '.component_test_count')" >> $GITHUB_OUTPUT
integration-tests:
name: Run integration tests
runs-on: ubuntu-latest
needs:
- common
- determine-jobs
if: needs.determine-jobs.outputs.integration-tests == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
@@ -259,44 +241,15 @@ jobs:
. venv/bin/activate
pytest -vv --no-cov --tb=native -n auto tests/integration/
clang-format:
name: Check clang-format
runs-on: ubuntu-24.04
needs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Install clang-format
run: |
. venv/bin/activate
pip install clang-format -c requirements_dev.txt
- name: Run clang-format
run: |
. venv/bin/activate
script/clang-format -i
git diff-index --quiet HEAD --
- name: Suggested changes
run: script/ci-suggest-changes
if: always()
clang-tidy:
name: ${{ matrix.name }}
runs-on: ubuntu-24.04
needs:
- common
- ruff
- ci-custom
- clang-format
- flake8
- pylint
- pytest
- pyupgrade
- determine-jobs
if: needs.determine-jobs.outputs.clang-tidy == 'true'
env:
GH_TOKEN: ${{ github.token }}
strategy:
fail-fast: false
max-parallel: 2
@@ -335,6 +288,10 @@ jobs:
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -346,14 +303,14 @@ jobs:
uses: actions/cache@v4.2.3
with:
path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@v4.2.3
with:
path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
- name: Register problem matchers
run: |
@@ -367,10 +324,28 @@ jobs:
mkdir -p .temp
pio run --list-targets -e esp32-idf-tidy
- name: Check if full clang-tidy scan needed
id: check_full_scan
run: |
. venv/bin/activate
if python script/clang_tidy_hash.py --check; then
echo "full_scan=true" >> $GITHUB_OUTPUT
echo "reason=hash_changed" >> $GITHUB_OUTPUT
else
echo "full_scan=false" >> $GITHUB_OUTPUT
echo "reason=normal" >> $GITHUB_OUTPUT
fi
- name: Run clang-tidy
run: |
. venv/bin/activate
script/clang-tidy --all-headers --fix ${{ matrix.options }} ${{ matrix.ignore_errors && '|| true' || '' }}
if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then
echo "Running FULL clang-tidy scan (hash changed)"
script/clang-tidy --all-headers --fix ${{ matrix.options }} ${{ matrix.ignore_errors && '|| true' || '' }}
else
echo "Running clang-tidy on changed files only"
script/clang-tidy --all-headers --fix --changed ${{ matrix.options }} ${{ matrix.ignore_errors && '|| true' || '' }}
fi
env:
# Also cache libdeps, store them in a ~/.platformio subfolder
PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
@@ -380,59 +355,18 @@ jobs:
# yamllint disable-line rule:line-length
if: always()
list-components:
runs-on: ubuntu-24.04
needs:
- common
if: github.event_name == 'pull_request'
outputs:
components: ${{ steps.list-components.outputs.components }}
count: ${{ steps.list-components.outputs.count }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
with:
# Fetch enough history so `git merge-base refs/remotes/origin/dev HEAD` works.
fetch-depth: 500
- name: Get target branch
id: target-branch
run: |
echo "branch=${{ github.event.pull_request.base.ref }}" >> $GITHUB_OUTPUT
- name: Fetch ${{ steps.target-branch.outputs.branch }} branch
run: |
git -c protocol.version=2 fetch --no-tags --prune --no-recurse-submodules --depth=1 origin +refs/heads/${{ steps.target-branch.outputs.branch }}:refs/remotes/origin/${{ steps.target-branch.outputs.branch }}
git merge-base refs/remotes/origin/${{ steps.target-branch.outputs.branch }} HEAD
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Find changed components
id: list-components
run: |
. venv/bin/activate
components=$(script/list-components.py --changed --branch ${{ steps.target-branch.outputs.branch }})
output_components=$(echo "$components" | jq -R -s -c 'split("\n")[:-1] | map(select(length > 0))')
count=$(echo "$output_components" | jq length)
echo "components=$output_components" >> $GITHUB_OUTPUT
echo "count=$count" >> $GITHUB_OUTPUT
echo "$count Components:"
echo "$output_components" | jq
test-build-components:
name: Component test ${{ matrix.file }}
runs-on: ubuntu-24.04
needs:
- common
- list-components
if: github.event_name == 'pull_request' && fromJSON(needs.list-components.outputs.count) > 0 && fromJSON(needs.list-components.outputs.count) < 100
- determine-jobs
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0 && fromJSON(needs.determine-jobs.outputs.component-test-count) < 100
strategy:
fail-fast: false
max-parallel: 2
matrix:
file: ${{ fromJson(needs.list-components.outputs.components) }}
file: ${{ fromJson(needs.determine-jobs.outputs.changed-components) }}
steps:
- name: Install dependencies
run: |
@@ -460,8 +394,8 @@ jobs:
runs-on: ubuntu-24.04
needs:
- common
- list-components
if: github.event_name == 'pull_request' && fromJSON(needs.list-components.outputs.count) >= 100
- determine-jobs
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) >= 100
outputs:
matrix: ${{ steps.split.outputs.components }}
steps:
@@ -470,7 +404,7 @@ jobs:
- name: Split components into 20 groups
id: split
run: |
components=$(echo '${{ needs.list-components.outputs.components }}' | jq -c '.[]' | shuf | jq -s -c '[_nwise(20) | join(" ")]')
components=$(echo '${{ needs.determine-jobs.outputs.changed-components }}' | jq -c '.[]' | shuf | jq -s -c '[_nwise(20) | join(" ")]')
echo "components=$components" >> $GITHUB_OUTPUT
test-build-components-split:
@@ -478,9 +412,9 @@ jobs:
runs-on: ubuntu-24.04
needs:
- common
- list-components
- determine-jobs
- test-build-components-splitter
if: github.event_name == 'pull_request' && fromJSON(needs.list-components.outputs.count) >= 100
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) >= 100
strategy:
fail-fast: false
max-parallel: 4
@@ -517,24 +451,41 @@ jobs:
./script/test_build_components -e compile -c $component
done
pre-commit-ci-lite:
name: pre-commit.ci lite
runs-on: ubuntu-latest
needs:
- common
if: github.event_name == 'pull_request' && github.base_ref != 'beta' && github.base_ref != 'release'
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- uses: pre-commit/action@v3.0.1
env:
SKIP: pylint,clang-tidy-hash
- uses: pre-commit-ci/lite-action@v1.1.0
if: always()
ci-status:
name: CI Status
runs-on: ubuntu-24.04
needs:
- common
- ruff
- ci-custom
- clang-format
- flake8
- pylint
- pytest
- integration-tests
- pyupgrade
- clang-tidy
- list-components
- determine-jobs
- test-build-components
- test-build-components-splitter
- test-build-components-split
- pre-commit-ci-lite
if: always()
steps:
- name: Success

View File

@@ -0,0 +1,264 @@
# This workflow automatically requests reviews from codeowners when:
# 1. A PR is opened, reopened, or synchronized (updated)
# 2. A PR is marked as ready for review
#
# It reads the CODEOWNERS file and matches all changed files in the PR against
# the codeowner patterns, then requests reviews from the appropriate owners
# while avoiding duplicate requests for users who have already been requested
# or have already reviewed the PR.
name: Request Codeowner Reviews
on:
# Needs to be pull_request_target to get write permissions
pull_request_target:
types: [opened, reopened, synchronize, ready_for_review]
permissions:
pull-requests: write
contents: read
jobs:
request-codeowner-reviews:
name: Run
if: ${{ !github.event.pull_request.draft }}
runs-on: ubuntu-latest
steps:
- name: Request reviews from component codeowners
uses: actions/github-script@v7.0.1
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const pr_number = context.payload.pull_request.number;
console.log(`Processing PR #${pr_number} for codeowner review requests`);
try {
// Get the list of changed files in this PR
const { data: files } = await github.rest.pulls.listFiles({
owner,
repo,
pull_number: pr_number
});
const changedFiles = files.map(file => file.filename);
console.log(`Found ${changedFiles.length} changed files`);
if (changedFiles.length === 0) {
console.log('No changed files found, skipping codeowner review requests');
return;
}
// Fetch CODEOWNERS file from root
const { data: codeownersFile } = await github.rest.repos.getContent({
owner,
repo,
path: 'CODEOWNERS',
ref: context.payload.pull_request.base.sha
});
const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8');
// Parse CODEOWNERS file to extract all patterns and their owners
const codeownersLines = codeownersContent.split('\n')
.map(line => line.trim())
.filter(line => line && !line.startsWith('#'));
const codeownersPatterns = [];
// Convert CODEOWNERS pattern to regex (robust glob handling)
function globToRegex(pattern) {
// Escape regex special characters except for glob wildcards
let regexStr = pattern
.replace(/([.+^=!:${}()|[\]\\])/g, '\\$1') // escape regex chars
.replace(/\*\*/g, '.*') // globstar
.replace(/\*/g, '[^/]*') // single star
.replace(/\?/g, '.'); // question mark
return new RegExp('^' + regexStr + '$');
}
// Helper function to create comment body
function createCommentBody(reviewersList, teamsList, matchedFileCount, isSuccessful = true) {
const reviewerMentions = reviewersList.map(r => `@${r}`);
const teamMentions = teamsList.map(t => `@${owner}/${t}`);
const allMentions = [...reviewerMentions, ...teamMentions].join(', ');
if (isSuccessful) {
return `👋 Hi there! I've automatically requested reviews from codeowners based on the files changed in this PR.\n\n${allMentions} - You've been requested to review this PR as codeowner(s) of ${matchedFileCount} file(s) that were modified. Thanks for your time! 🙏`;
} else {
return `👋 Hi there! This PR modifies ${matchedFileCount} file(s) with codeowners.\n\n${allMentions} - As codeowner(s) of the affected files, your review would be appreciated! 🙏\n\n_Note: Automatic review request may have failed, but you're still welcome to review._`;
}
}
for (const line of codeownersLines) {
const parts = line.split(/\s+/);
if (parts.length < 2) continue;
const pattern = parts[0];
const owners = parts.slice(1);
// Use robust glob-to-regex conversion
const regex = globToRegex(pattern);
codeownersPatterns.push({ pattern, regex, owners });
}
console.log(`Parsed ${codeownersPatterns.length} codeowner patterns`);
// Match changed files against CODEOWNERS patterns
const matchedOwners = new Set();
const matchedTeams = new Set();
const fileMatches = new Map(); // Track which files matched which patterns
for (const file of changedFiles) {
for (const { pattern, regex, owners } of codeownersPatterns) {
if (regex.test(file)) {
console.log(`File '${file}' matches pattern '${pattern}' with owners: ${owners.join(', ')}`);
if (!fileMatches.has(file)) {
fileMatches.set(file, []);
}
fileMatches.get(file).push({ pattern, owners });
// Add owners to the appropriate set (remove @ prefix)
for (const owner of owners) {
const cleanOwner = owner.startsWith('@') ? owner.slice(1) : owner;
if (cleanOwner.includes('/')) {
// Team mention (org/team-name)
const teamName = cleanOwner.split('/')[1];
matchedTeams.add(teamName);
} else {
// Individual user
matchedOwners.add(cleanOwner);
}
}
}
}
}
if (matchedOwners.size === 0 && matchedTeams.size === 0) {
console.log('No codeowners found for any changed files');
return;
}
// Remove the PR author from reviewers
const prAuthor = context.payload.pull_request.user.login;
matchedOwners.delete(prAuthor);
// Get current reviewers to avoid duplicate requests (but still mention them)
const { data: prData } = await github.rest.pulls.get({
owner,
repo,
pull_number: pr_number
});
const currentReviewers = new Set();
const currentTeams = new Set();
if (prData.requested_reviewers) {
prData.requested_reviewers.forEach(reviewer => {
currentReviewers.add(reviewer.login);
});
}
if (prData.requested_teams) {
prData.requested_teams.forEach(team => {
currentTeams.add(team.slug);
});
}
// Check for completed reviews to avoid re-requesting users who have already reviewed
const { data: reviews } = await github.rest.pulls.listReviews({
owner,
repo,
pull_number: pr_number
});
const reviewedUsers = new Set();
reviews.forEach(review => {
reviewedUsers.add(review.user.login);
});
// Remove only users who have already submitted reviews (not just requested reviewers)
reviewedUsers.forEach(reviewer => {
matchedOwners.delete(reviewer);
});
// For teams, we'll still remove already requested teams to avoid API errors
currentTeams.forEach(team => {
matchedTeams.delete(team);
});
const reviewersList = Array.from(matchedOwners);
const teamsList = Array.from(matchedTeams);
if (reviewersList.length === 0 && teamsList.length === 0) {
console.log('No eligible reviewers found (all may already be requested or reviewed)');
return;
}
const totalReviewers = reviewersList.length + teamsList.length;
console.log(`Requesting reviews from ${reviewersList.length} users and ${teamsList.length} teams for ${fileMatches.size} matched files`);
// Request reviews
try {
const requestParams = {
owner,
repo,
pull_number: pr_number
};
// Filter out users who are already requested reviewers for the API call
const newReviewers = reviewersList.filter(reviewer => !currentReviewers.has(reviewer));
const newTeams = teamsList.filter(team => !currentTeams.has(team));
if (newReviewers.length > 0) {
requestParams.reviewers = newReviewers;
}
if (newTeams.length > 0) {
requestParams.team_reviewers = newTeams;
}
// Only make the API call if there are new reviewers to request
if (newReviewers.length > 0 || newTeams.length > 0) {
await github.rest.pulls.requestReviewers(requestParams);
console.log(`Successfully requested reviews from ${newReviewers.length} new users and ${newTeams.length} new teams`);
} else {
console.log('All codeowners are already requested reviewers or have reviewed');
}
// Add a comment to the PR mentioning what happened (include all matched codeowners)
const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, true);
await github.rest.issues.createComment({
owner,
repo,
issue_number: pr_number,
body: commentBody
});
} catch (error) {
if (error.status === 422) {
console.log('Some reviewers may already be requested or unavailable:', error.message);
// Try to add a comment even if review request failed
const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, false);
try {
await github.rest.issues.createComment({
owner,
repo,
issue_number: pr_number,
body: commentBody
});
} catch (commentError) {
console.log('Failed to add comment:', commentError.message);
}
} else {
throw error;
}
}
} catch (error) {
console.log('Failed to process codeowner review requests:', error.message);
console.error(error);
}

View File

@@ -0,0 +1,147 @@
name: Add External Component Comment
on:
pull_request_target:
types: [opened, synchronize]
permissions:
contents: read # Needed to fetch PR details
issues: write # Needed to create and update comments (PR comments are managed via the issues REST API)
pull-requests: write # also needed?
jobs:
external-comment:
name: External component comment
runs-on: ubuntu-latest
steps:
- name: Add external component comment
uses: actions/github-script@v7.0.1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
// Generate external component usage instructions
function generateExternalComponentInstructions(prNumber, componentNames, owner, repo) {
let source;
if (owner === 'esphome' && repo === 'esphome')
source = `github://pr#${prNumber}`;
else
source = `github://${owner}/${repo}@pull/${prNumber}/head`;
return `To use the changes from this PR as an external component, add the following to your ESPHome configuration YAML file:
\`\`\`yaml
external_components:
- source: ${source}
components: [${componentNames.join(', ')}]
refresh: 1h
\`\`\``;
}
// Generate repo clone instructions
function generateRepoInstructions(prNumber, owner, repo, branch) {
return `To use the changes in this PR:
\`\`\`bash
# Clone the repository:
git clone https://github.com/${owner}/${repo}
cd ${repo}
# Checkout the PR branch:
git fetch origin pull/${prNumber}/head:${branch}
git checkout ${branch}
# Install the development version:
script/setup
# Activate the development version:
source venv/bin/activate
\`\`\`
Now you can run \`esphome\` as usual to test the changes in this PR.
`;
}
async function createComment(octokit, owner, repo, prNumber, esphomeChanges, componentChanges) {
const commentMarker = "<!-- This comment was generated automatically by a GitHub workflow. -->";
let commentBody;
if (esphomeChanges.length === 1) {
commentBody = generateExternalComponentInstructions(prNumber, componentChanges, owner, repo);
} else {
commentBody = generateRepoInstructions(prNumber, owner, repo, context.payload.pull_request.head.ref);
}
commentBody += `\n\n---\n(Added by the PR bot)\n\n${commentMarker}`;
// Check for existing bot comment
const comments = await github.rest.issues.listComments({
owner: owner,
repo: repo,
issue_number: prNumber,
});
const botComment = comments.data.find(comment =>
comment.body.includes(commentMarker)
);
if (botComment && botComment.body === commentBody) {
// No changes in the comment, do nothing
return;
}
if (botComment) {
// Update existing comment
await github.rest.issues.updateComment({
owner: owner,
repo: repo,
comment_id: botComment.id,
body: commentBody,
});
} else {
// Create new comment
await github.rest.issues.createComment({
owner: owner,
repo: repo,
issue_number: prNumber,
body: commentBody,
});
}
}
async function getEsphomeAndComponentChanges(github, owner, repo, prNumber) {
const changedFiles = await github.rest.pulls.listFiles({
owner: owner,
repo: repo,
pull_number: prNumber,
});
const esphomeChanges = changedFiles.data
.filter(file => file.filename !== "esphome/core/defines.h" && file.filename.startsWith('esphome/'))
.map(file => {
const match = file.filename.match(/esphome\/([^/]+)/);
return match ? match[1] : null;
})
.filter(it => it !== null);
if (esphomeChanges.length === 0) {
return {esphomeChanges: [], componentChanges: []};
}
const uniqueEsphomeChanges = [...new Set(esphomeChanges)];
const componentChanges = changedFiles.data
.filter(file => file.filename.startsWith('esphome/components/'))
.map(file => {
const match = file.filename.match(/esphome\/components\/([^/]+)\//);
return match ? match[1] : null;
})
.filter(it => it !== null);
return {esphomeChanges: uniqueEsphomeChanges, componentChanges: [...new Set(componentChanges)]};
}
// Start of main code.
const prNumber = context.payload.pull_request.number;
const {owner, repo} = context.repo;
const {esphomeChanges, componentChanges} = await getEsphomeAndComponentChanges(github, owner, repo, prNumber);
if (componentChanges.length !== 0) {
await createComment(github, owner, repo, prNumber, esphomeChanges, componentChanges);
}

View File

@@ -0,0 +1,119 @@
# This workflow automatically notifies codeowners when an issue is labeled with component labels.
# It reads the CODEOWNERS file to find the maintainers for the labeled components
# and posts a comment mentioning them to ensure they're aware of the issue.
name: Notify Issue Codeowners
on:
issues:
types: [labeled]
permissions:
issues: write
contents: read
jobs:
notify-codeowners:
name: Run
if: ${{ startsWith(github.event.label.name, format('component{0} ', ':')) }}
runs-on: ubuntu-latest
steps:
- name: Notify codeowners for component issues
uses: actions/github-script@v7.0.1
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const issue_number = context.payload.issue.number;
const labelName = context.payload.label.name;
console.log(`Processing issue #${issue_number} with label: ${labelName}`);
// Extract component name from label
const componentName = labelName.replace('component: ', '');
console.log(`Component: ${componentName}`);
try {
// Fetch CODEOWNERS file from root
const { data: codeownersFile } = await github.rest.repos.getContent({
owner,
repo,
path: 'CODEOWNERS'
});
const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8');
// Parse CODEOWNERS file to extract component mappings
const codeownersLines = codeownersContent.split('\n')
.map(line => line.trim())
.filter(line => line && !line.startsWith('#'));
let componentOwners = null;
for (const line of codeownersLines) {
const parts = line.split(/\s+/);
if (parts.length < 2) continue;
const pattern = parts[0];
const owners = parts.slice(1);
// Look for component patterns: esphome/components/{component}/*
const componentMatch = pattern.match(/^esphome\/components\/([^\/]+)\/\*$/);
if (componentMatch && componentMatch[1] === componentName) {
componentOwners = owners;
break;
}
}
if (!componentOwners) {
console.log(`No codeowners found for component: ${componentName}`);
return;
}
console.log(`Found codeowners for '${componentName}': ${componentOwners.join(', ')}`);
// Separate users and teams
const userOwners = [];
const teamOwners = [];
for (const owner of componentOwners) {
const cleanOwner = owner.startsWith('@') ? owner.slice(1) : owner;
if (cleanOwner.includes('/')) {
// Team mention (org/team-name)
teamOwners.push(`@${cleanOwner}`);
} else {
// Individual user
userOwners.push(`@${cleanOwner}`);
}
}
// Remove issue author from mentions to avoid self-notification
const issueAuthor = context.payload.issue.user.login;
const filteredUserOwners = userOwners.filter(mention =>
mention !== `@${issueAuthor}`
);
const allMentions = [...filteredUserOwners, ...teamOwners];
if (allMentions.length === 0) {
console.log('No codeowners to notify (issue author is the only codeowner)');
return;
}
// Create comment body
const mentionString = allMentions.join(', ');
const commentBody = `👋 Hey ${mentionString}!\n\nThis issue has been labeled with \`component: ${componentName}\` and you've been identified as a codeowner of this component. Please take a look when you have a chance!\n\nThanks for maintaining this component! 🙏`;
// Post comment
await github.rest.issues.createComment({
owner,
repo,
issue_number: issue_number,
body: commentBody
});
console.log(`Successfully notified codeowners: ${mentionString}`);
} catch (error) {
console.log('Failed to process codeowner notifications:', error.message);
console.error(error);
}

View File

@@ -96,7 +96,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5.6.0
with:
python-version: "3.10"
python-version: "3.11"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.11.1

View File

@@ -1,25 +0,0 @@
---
name: YAML lint
on:
push:
branches: [dev, beta, release]
paths:
- "**.yaml"
- "**.yml"
pull_request:
paths:
- "**.yaml"
- "**.yml"
jobs:
yamllint:
name: yamllint
runs-on: ubuntu-latest
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Run yamllint
uses: frenck/action-yamllint@v1.5.0
with:
strict: true

View File

@@ -1,10 +1,17 @@
---
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
ci:
autoupdate_commit_msg: 'pre-commit: autoupdate'
autoupdate_schedule: off # Disabled until ruff versions are synced between deps and pre-commit
# Skip hooks that have issues in pre-commit CI environment
skip: [pylint, clang-tidy-hash]
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.12.2
rev: v0.12.4
hooks:
# Run the linter.
- id: ruff
@@ -20,22 +27,25 @@ repos:
- pydocstyle==5.1.1
files: ^(esphome|tests)/.+\.py$
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.4.0
rev: v5.0.0
hooks:
- id: no-commit-to-branch
args:
- --branch=dev
- --branch=release
- --branch=beta
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/asottile/pyupgrade
rev: v3.20.0
hooks:
- id: pyupgrade
args: [--py310-plus]
args: [--py311-plus]
- repo: https://github.com/adrienverge/yamllint.git
rev: v1.37.1
hooks:
- id: yamllint
exclude: ^(\.clang-format|\.clang-tidy)$
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: v13.0.1
hooks:
@@ -48,3 +58,10 @@ repos:
entry: python3 script/run-in-env.py pylint
language: system
types: [python]
- id: clang-tidy-hash
name: Update clang-tidy hash
entry: python script/clang_tidy_hash.py --update-if-changed
language: python
files: ^(\.clang-tidy|platformio\.ini|requirements_dev\.txt)$
pass_filenames: false
additional_dependencies: []

1
CLAUDE.md Symbolic link
View File

@@ -0,0 +1 @@
.ai/instructions.md

View File

@@ -9,6 +9,7 @@
pyproject.toml @esphome/core
esphome/*.py @esphome/core
esphome/core/* @esphome/core
.github/** @esphome/core
# Integrations
esphome/components/a01nyub/* @MrSuicideParrot
@@ -28,7 +29,7 @@ esphome/components/aic3204/* @kbx81
esphome/components/airthings_ble/* @jeromelaban
esphome/components/airthings_wave_base/* @jeromelaban @kpfleming @ncareau
esphome/components/airthings_wave_mini/* @ncareau
esphome/components/airthings_wave_plus/* @jeromelaban
esphome/components/airthings_wave_plus/* @jeromelaban @precurse
esphome/components/alarm_control_panel/* @grahambrown11 @hwstar
esphome/components/alpha3/* @jan-hofmeier
esphome/components/am2315c/* @swoboda1337
@@ -324,6 +325,7 @@ esphome/components/nextion/text_sensor/* @senexcrenshaw
esphome/components/nfc/* @jesserockz @kbx81
esphome/components/noblex/* @AGalfra
esphome/components/npi19/* @bakerkj
esphome/components/nrf52/* @tomaszduda23
esphome/components/number/* @esphome/core
esphome/components/one_wire/* @ssieb
esphome/components/online_image/* @clydebarrow @guillempages
@@ -378,6 +380,7 @@ esphome/components/rp2040_pwm/* @jesserockz
esphome/components/rpi_dpi_rgb/* @clydebarrow
esphome/components/rtl87xx/* @kuba2k2
esphome/components/rtttl/* @glmnet
esphome/components/runtime_stats/* @bdraco
esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti
esphome/components/scd4x/* @martgras @sjtrny
esphome/components/script/* @esphome/core
@@ -535,5 +538,6 @@ esphome/components/xiaomi_xmwsdj04mmc/* @medusalix
esphome/components/xl9535/* @mreditor97
esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68
esphome/components/xxtea/* @clydebarrow
esphome/components/zephyr/* @tomaszduda23
esphome/components/zhlt01/* @cfeenstra1024
esphome/components/zio_ultrasonic/* @kahrendt

View File

@@ -7,7 +7,7 @@ project and be sure to join us on [Discord](https://discord.gg/KhAMKrd).
**See also:**
[Documentation](https://esphome.io) -- [Issues](https://github.com/esphome/issues/issues) -- [Feature requests](https://github.com/esphome/feature-requests/issues)
[Documentation](https://esphome.io) -- [Issues](https://github.com/esphome/esphome/issues) -- [Feature requests](https://github.com/orgs/esphome/discussions)
---

1
GEMINI.md Symbolic link
View File

@@ -0,0 +1 @@
.ai/instructions.md

View File

@@ -9,7 +9,7 @@
---
[Documentation](https://esphome.io) -- [Issues](https://github.com/esphome/issues/issues) -- [Feature requests](https://github.com/esphome/feature-requests/issues)
[Documentation](https://esphome.io) -- [Issues](https://github.com/esphome/esphome/issues) -- [Feature requests](https://github.com/orgs/esphome/discussions)
---

View File

@@ -5,6 +5,7 @@ from esphome.components.esp32.const import (
VARIANT_ESP32,
VARIANT_ESP32C2,
VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6,
VARIANT_ESP32H2,
VARIANT_ESP32S2,
@@ -51,82 +52,93 @@ SAMPLING_MODES = {
"max": sampling_mode.MAX,
}
adc1_channel_t = cg.global_ns.enum("adc1_channel_t")
adc2_channel_t = cg.global_ns.enum("adc2_channel_t")
adc_unit_t = cg.global_ns.enum("adc_unit_t", is_class=True)
adc_channel_t = cg.global_ns.enum("adc_channel_t", is_class=True)
# pin to adc1 channel mapping
# https://github.com/espressif/esp-idf/blob/v4.4.8/components/driver/include/driver/adc.h
ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/adc_channel.h
VARIANT_ESP32: {
36: adc1_channel_t.ADC1_CHANNEL_0,
37: adc1_channel_t.ADC1_CHANNEL_1,
38: adc1_channel_t.ADC1_CHANNEL_2,
39: adc1_channel_t.ADC1_CHANNEL_3,
32: adc1_channel_t.ADC1_CHANNEL_4,
33: adc1_channel_t.ADC1_CHANNEL_5,
34: adc1_channel_t.ADC1_CHANNEL_6,
35: adc1_channel_t.ADC1_CHANNEL_7,
36: adc_channel_t.ADC_CHANNEL_0,
37: adc_channel_t.ADC_CHANNEL_1,
38: adc_channel_t.ADC_CHANNEL_2,
39: adc_channel_t.ADC_CHANNEL_3,
32: adc_channel_t.ADC_CHANNEL_4,
33: adc_channel_t.ADC_CHANNEL_5,
34: adc_channel_t.ADC_CHANNEL_6,
35: adc_channel_t.ADC_CHANNEL_7,
},
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h
VARIANT_ESP32C2: {
0: adc1_channel_t.ADC1_CHANNEL_0,
1: adc1_channel_t.ADC1_CHANNEL_1,
2: adc1_channel_t.ADC1_CHANNEL_2,
3: adc1_channel_t.ADC1_CHANNEL_3,
4: adc1_channel_t.ADC1_CHANNEL_4,
0: adc_channel_t.ADC_CHANNEL_0,
1: adc_channel_t.ADC_CHANNEL_1,
2: adc_channel_t.ADC_CHANNEL_2,
3: adc_channel_t.ADC_CHANNEL_3,
4: adc_channel_t.ADC_CHANNEL_4,
},
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c3/include/soc/adc_channel.h
VARIANT_ESP32C3: {
0: adc1_channel_t.ADC1_CHANNEL_0,
1: adc1_channel_t.ADC1_CHANNEL_1,
2: adc1_channel_t.ADC1_CHANNEL_2,
3: adc1_channel_t.ADC1_CHANNEL_3,
4: adc1_channel_t.ADC1_CHANNEL_4,
0: adc_channel_t.ADC_CHANNEL_0,
1: adc_channel_t.ADC_CHANNEL_1,
2: adc_channel_t.ADC_CHANNEL_2,
3: adc_channel_t.ADC_CHANNEL_3,
4: adc_channel_t.ADC_CHANNEL_4,
},
# ESP32-C5 ADC1 pin mapping - based on official ESP-IDF documentation
# https://docs.espressif.com/projects/esp-idf/en/latest/esp32c5/api-reference/peripherals/gpio.html
VARIANT_ESP32C5: {
1: adc_channel_t.ADC_CHANNEL_0,
2: adc_channel_t.ADC_CHANNEL_1,
3: adc_channel_t.ADC_CHANNEL_2,
4: adc_channel_t.ADC_CHANNEL_3,
5: adc_channel_t.ADC_CHANNEL_4,
6: adc_channel_t.ADC_CHANNEL_5,
},
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h
VARIANT_ESP32C6: {
0: adc1_channel_t.ADC1_CHANNEL_0,
1: adc1_channel_t.ADC1_CHANNEL_1,
2: adc1_channel_t.ADC1_CHANNEL_2,
3: adc1_channel_t.ADC1_CHANNEL_3,
4: adc1_channel_t.ADC1_CHANNEL_4,
5: adc1_channel_t.ADC1_CHANNEL_5,
6: adc1_channel_t.ADC1_CHANNEL_6,
0: adc_channel_t.ADC_CHANNEL_0,
1: adc_channel_t.ADC_CHANNEL_1,
2: adc_channel_t.ADC_CHANNEL_2,
3: adc_channel_t.ADC_CHANNEL_3,
4: adc_channel_t.ADC_CHANNEL_4,
5: adc_channel_t.ADC_CHANNEL_5,
6: adc_channel_t.ADC_CHANNEL_6,
},
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h
VARIANT_ESP32H2: {
1: adc1_channel_t.ADC1_CHANNEL_0,
2: adc1_channel_t.ADC1_CHANNEL_1,
3: adc1_channel_t.ADC1_CHANNEL_2,
4: adc1_channel_t.ADC1_CHANNEL_3,
5: adc1_channel_t.ADC1_CHANNEL_4,
1: adc_channel_t.ADC_CHANNEL_0,
2: adc_channel_t.ADC_CHANNEL_1,
3: adc_channel_t.ADC_CHANNEL_2,
4: adc_channel_t.ADC_CHANNEL_3,
5: adc_channel_t.ADC_CHANNEL_4,
},
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h
VARIANT_ESP32S2: {
1: adc1_channel_t.ADC1_CHANNEL_0,
2: adc1_channel_t.ADC1_CHANNEL_1,
3: adc1_channel_t.ADC1_CHANNEL_2,
4: adc1_channel_t.ADC1_CHANNEL_3,
5: adc1_channel_t.ADC1_CHANNEL_4,
6: adc1_channel_t.ADC1_CHANNEL_5,
7: adc1_channel_t.ADC1_CHANNEL_6,
8: adc1_channel_t.ADC1_CHANNEL_7,
9: adc1_channel_t.ADC1_CHANNEL_8,
10: adc1_channel_t.ADC1_CHANNEL_9,
1: adc_channel_t.ADC_CHANNEL_0,
2: adc_channel_t.ADC_CHANNEL_1,
3: adc_channel_t.ADC_CHANNEL_2,
4: adc_channel_t.ADC_CHANNEL_3,
5: adc_channel_t.ADC_CHANNEL_4,
6: adc_channel_t.ADC_CHANNEL_5,
7: adc_channel_t.ADC_CHANNEL_6,
8: adc_channel_t.ADC_CHANNEL_7,
9: adc_channel_t.ADC_CHANNEL_8,
10: adc_channel_t.ADC_CHANNEL_9,
},
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h
VARIANT_ESP32S3: {
1: adc1_channel_t.ADC1_CHANNEL_0,
2: adc1_channel_t.ADC1_CHANNEL_1,
3: adc1_channel_t.ADC1_CHANNEL_2,
4: adc1_channel_t.ADC1_CHANNEL_3,
5: adc1_channel_t.ADC1_CHANNEL_4,
6: adc1_channel_t.ADC1_CHANNEL_5,
7: adc1_channel_t.ADC1_CHANNEL_6,
8: adc1_channel_t.ADC1_CHANNEL_7,
9: adc1_channel_t.ADC1_CHANNEL_8,
10: adc1_channel_t.ADC1_CHANNEL_9,
1: adc_channel_t.ADC_CHANNEL_0,
2: adc_channel_t.ADC_CHANNEL_1,
3: adc_channel_t.ADC_CHANNEL_2,
4: adc_channel_t.ADC_CHANNEL_3,
5: adc_channel_t.ADC_CHANNEL_4,
6: adc_channel_t.ADC_CHANNEL_5,
7: adc_channel_t.ADC_CHANNEL_6,
8: adc_channel_t.ADC_CHANNEL_7,
9: adc_channel_t.ADC_CHANNEL_8,
10: adc_channel_t.ADC_CHANNEL_9,
},
}
@@ -135,54 +147,56 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/adc_channel.h
VARIANT_ESP32: {
4: adc2_channel_t.ADC2_CHANNEL_0,
0: adc2_channel_t.ADC2_CHANNEL_1,
2: adc2_channel_t.ADC2_CHANNEL_2,
15: adc2_channel_t.ADC2_CHANNEL_3,
13: adc2_channel_t.ADC2_CHANNEL_4,
12: adc2_channel_t.ADC2_CHANNEL_5,
14: adc2_channel_t.ADC2_CHANNEL_6,
27: adc2_channel_t.ADC2_CHANNEL_7,
25: adc2_channel_t.ADC2_CHANNEL_8,
26: adc2_channel_t.ADC2_CHANNEL_9,
4: adc_channel_t.ADC_CHANNEL_0,
0: adc_channel_t.ADC_CHANNEL_1,
2: adc_channel_t.ADC_CHANNEL_2,
15: adc_channel_t.ADC_CHANNEL_3,
13: adc_channel_t.ADC_CHANNEL_4,
12: adc_channel_t.ADC_CHANNEL_5,
14: adc_channel_t.ADC_CHANNEL_6,
27: adc_channel_t.ADC_CHANNEL_7,
25: adc_channel_t.ADC_CHANNEL_8,
26: adc_channel_t.ADC_CHANNEL_9,
},
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h
VARIANT_ESP32C2: {
5: adc2_channel_t.ADC2_CHANNEL_0,
5: adc_channel_t.ADC_CHANNEL_0,
},
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c3/include/soc/adc_channel.h
VARIANT_ESP32C3: {
5: adc2_channel_t.ADC2_CHANNEL_0,
5: adc_channel_t.ADC_CHANNEL_0,
},
# ESP32-C5 has no ADC2 channels
VARIANT_ESP32C5: {}, # no ADC2
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h
VARIANT_ESP32C6: {}, # no ADC2
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h
VARIANT_ESP32H2: {}, # no ADC2
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h
VARIANT_ESP32S2: {
11: adc2_channel_t.ADC2_CHANNEL_0,
12: adc2_channel_t.ADC2_CHANNEL_1,
13: adc2_channel_t.ADC2_CHANNEL_2,
14: adc2_channel_t.ADC2_CHANNEL_3,
15: adc2_channel_t.ADC2_CHANNEL_4,
16: adc2_channel_t.ADC2_CHANNEL_5,
17: adc2_channel_t.ADC2_CHANNEL_6,
18: adc2_channel_t.ADC2_CHANNEL_7,
19: adc2_channel_t.ADC2_CHANNEL_8,
20: adc2_channel_t.ADC2_CHANNEL_9,
11: adc_channel_t.ADC_CHANNEL_0,
12: adc_channel_t.ADC_CHANNEL_1,
13: adc_channel_t.ADC_CHANNEL_2,
14: adc_channel_t.ADC_CHANNEL_3,
15: adc_channel_t.ADC_CHANNEL_4,
16: adc_channel_t.ADC_CHANNEL_5,
17: adc_channel_t.ADC_CHANNEL_6,
18: adc_channel_t.ADC_CHANNEL_7,
19: adc_channel_t.ADC_CHANNEL_8,
20: adc_channel_t.ADC_CHANNEL_9,
},
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h
VARIANT_ESP32S3: {
11: adc2_channel_t.ADC2_CHANNEL_0,
12: adc2_channel_t.ADC2_CHANNEL_1,
13: adc2_channel_t.ADC2_CHANNEL_2,
14: adc2_channel_t.ADC2_CHANNEL_3,
15: adc2_channel_t.ADC2_CHANNEL_4,
16: adc2_channel_t.ADC2_CHANNEL_5,
17: adc2_channel_t.ADC2_CHANNEL_6,
18: adc2_channel_t.ADC2_CHANNEL_7,
19: adc2_channel_t.ADC2_CHANNEL_8,
20: adc2_channel_t.ADC2_CHANNEL_9,
11: adc_channel_t.ADC_CHANNEL_0,
12: adc_channel_t.ADC_CHANNEL_1,
13: adc_channel_t.ADC_CHANNEL_2,
14: adc_channel_t.ADC_CHANNEL_3,
15: adc_channel_t.ADC_CHANNEL_4,
16: adc_channel_t.ADC_CHANNEL_5,
17: adc_channel_t.ADC_CHANNEL_6,
18: adc_channel_t.ADC_CHANNEL_7,
19: adc_channel_t.ADC_CHANNEL_8,
20: adc_channel_t.ADC_CHANNEL_9,
},
}

View File

@@ -3,12 +3,15 @@
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/voltage_sampler/voltage_sampler.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#ifdef USE_ESP32
#include <esp_adc_cal.h>
#include "driver/adc.h"
#endif // USE_ESP32
#include "esp_adc/adc_cali.h"
#include "esp_adc/adc_cali_scheme.h"
#include "esp_adc/adc_oneshot.h"
#include "hal/adc_types.h" // This defines ADC_CHANNEL_MAX
#endif // USE_ESP32
namespace esphome {
namespace adc {
@@ -49,36 +52,72 @@ class Aggregator {
class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler {
public:
#ifdef USE_ESP32
/// Set the attenuation for this pin. Only available on the ESP32.
void set_attenuation(adc_atten_t attenuation) { this->attenuation_ = attenuation; }
void set_channel1(adc1_channel_t channel) {
this->channel1_ = channel;
this->channel2_ = ADC2_CHANNEL_MAX;
}
void set_channel2(adc2_channel_t channel) {
this->channel2_ = channel;
this->channel1_ = ADC1_CHANNEL_MAX;
}
void set_autorange(bool autorange) { this->autorange_ = autorange; }
#endif // USE_ESP32
/// Update ADC values
/// Update the sensor's state by reading the current ADC value.
/// This method is called periodically based on the update interval.
void update() override;
/// Setup ADC
/// Set up the ADC sensor by initializing hardware and calibration parameters.
/// This method is called once during device initialization.
void setup() override;
/// Output the configuration details of the ADC sensor for debugging purposes.
/// This method is called during the ESPHome setup process to log the configuration.
void dump_config() override;
/// `HARDWARE_LATE` setup priority
/// Return the setup priority for this component.
/// Components with higher priority are initialized earlier during setup.
/// @return A float representing the setup priority.
float get_setup_priority() const override;
/// Set the GPIO pin to be used by the ADC sensor.
/// @param pin Pointer to an InternalGPIOPin representing the ADC input pin.
void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; }
/// Enable or disable the output of raw ADC values (unprocessed data).
/// @param output_raw Boolean indicating whether to output raw ADC values (true) or processed values (false).
void set_output_raw(bool output_raw) { this->output_raw_ = output_raw; }
/// Set the number of samples to be taken for ADC readings to improve accuracy.
/// A higher sample count reduces noise but increases the reading time.
/// @param sample_count The number of samples (e.g., 1, 4, 8).
void set_sample_count(uint8_t sample_count);
/// Set the sampling mode for how multiple ADC samples are combined into a single measurement.
///
/// When multiple samples are taken (controlled by set_sample_count), they can be combined
/// in one of three ways:
/// - SamplingMode::AVG: Compute the average (default)
/// - SamplingMode::MIN: Use the lowest sample value
/// - SamplingMode::MAX: Use the highest sample value
/// @param sampling_mode The desired sampling mode to use for aggregating ADC samples.
void set_sampling_mode(SamplingMode sampling_mode);
/// Perform a single ADC sampling operation and return the measured value.
/// This function handles raw readings, calibration, and averaging as needed.
/// @return The sampled value as a float.
float sample() override;
#ifdef USE_ESP8266
std::string unique_id() override;
#endif // USE_ESP8266
#ifdef USE_ESP32
/// Set the ADC attenuation level to adjust the input voltage range.
/// This determines how the ADC interprets input voltages, allowing for greater precision
/// or the ability to measure higher voltages depending on the chosen attenuation level.
/// @param attenuation The desired ADC attenuation level (e.g., ADC_ATTEN_DB_0, ADC_ATTEN_DB_11).
void set_attenuation(adc_atten_t attenuation) { this->attenuation_ = attenuation; }
/// Configure the ADC to use a specific channel on a specific ADC unit.
/// This sets the channel for single-shot or continuous ADC measurements.
/// @param unit The ADC unit to use (ADC_UNIT_1 or ADC_UNIT_2).
/// @param channel The ADC channel to configure, such as ADC_CHANNEL_0, ADC_CHANNEL_3, etc.
void set_channel(adc_unit_t unit, adc_channel_t channel) {
this->adc_unit_ = unit;
this->channel_ = channel;
}
/// Set whether autoranging should be enabled for the ADC.
/// Autoranging automatically adjusts the attenuation level to handle a wide range of input voltages.
/// @param autorange Boolean indicating whether to enable autoranging.
void set_autorange(bool autorange) { this->autorange_ = autorange; }
#endif // USE_ESP32
#ifdef USE_RP2040
void set_is_temperature() { this->is_temperature_ = true; }
@@ -90,17 +129,28 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
InternalGPIOPin *pin_;
SamplingMode sampling_mode_{SamplingMode::AVG};
#ifdef USE_ESP32
float sample_autorange_();
float sample_fixed_attenuation_();
bool autorange_{false};
adc_oneshot_unit_handle_t adc_handle_{nullptr};
adc_cali_handle_t calibration_handle_{nullptr};
adc_atten_t attenuation_{ADC_ATTEN_DB_0};
adc_channel_t channel_;
adc_unit_t adc_unit_;
struct SetupFlags {
uint8_t init_complete : 1;
uint8_t config_complete : 1;
uint8_t handle_init_complete : 1;
uint8_t calibration_complete : 1;
uint8_t reserved : 4;
} setup_flags_{};
static adc_oneshot_unit_handle_t shared_adc_handles[2];
#endif // USE_ESP32
#ifdef USE_RP2040
bool is_temperature_{false};
#endif // USE_RP2040
#ifdef USE_ESP32
adc_atten_t attenuation_{ADC_ATTEN_DB_0};
adc1_channel_t channel1_{ADC1_CHANNEL_MAX};
adc2_channel_t channel2_{ADC2_CHANNEL_MAX};
bool autorange_{false};
esp_adc_cal_characteristics_t cal_characteristics_[SOC_ADC_ATTEN_NUM] = {};
#endif // USE_ESP32
};
} // namespace adc

View File

@@ -8,145 +8,315 @@ namespace adc {
static const char *const TAG = "adc.esp32";
static const adc_bits_width_t ADC_WIDTH_MAX_SOC_BITS = static_cast<adc_bits_width_t>(ADC_WIDTH_MAX - 1);
adc_oneshot_unit_handle_t ADCSensor::shared_adc_handles[2] = {nullptr, nullptr};
#ifndef SOC_ADC_RTC_MAX_BITWIDTH
#if USE_ESP32_VARIANT_ESP32S2
static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 13;
#else
static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 12;
#endif // USE_ESP32_VARIANT_ESP32S2
#endif // SOC_ADC_RTC_MAX_BITWIDTH
static const int ADC_MAX = (1 << SOC_ADC_RTC_MAX_BITWIDTH) - 1;
static const int ADC_HALF = (1 << SOC_ADC_RTC_MAX_BITWIDTH) >> 1;
void ADCSensor::setup() {
ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->get_name().c_str());
if (this->channel1_ != ADC1_CHANNEL_MAX) {
adc1_config_width(ADC_WIDTH_MAX_SOC_BITS);
if (!this->autorange_) {
adc1_config_channel_atten(this->channel1_, this->attenuation_);
}
} else if (this->channel2_ != ADC2_CHANNEL_MAX) {
if (!this->autorange_) {
adc2_config_channel_atten(this->channel2_, this->attenuation_);
}
}
for (int32_t i = 0; i <= ADC_ATTEN_DB_12_COMPAT; i++) {
auto adc_unit = this->channel1_ != ADC1_CHANNEL_MAX ? ADC_UNIT_1 : ADC_UNIT_2;
auto cal_value = esp_adc_cal_characterize(adc_unit, (adc_atten_t) i, ADC_WIDTH_MAX_SOC_BITS,
1100, // default vref
&this->cal_characteristics_[i]);
switch (cal_value) {
case ESP_ADC_CAL_VAL_EFUSE_VREF:
ESP_LOGV(TAG, "Using eFuse Vref for calibration");
break;
case ESP_ADC_CAL_VAL_EFUSE_TP:
ESP_LOGV(TAG, "Using two-point eFuse Vref for calibration");
break;
case ESP_ADC_CAL_VAL_DEFAULT_VREF:
default:
break;
}
const LogString *attenuation_to_str(adc_atten_t attenuation) {
switch (attenuation) {
case ADC_ATTEN_DB_0:
return LOG_STR("0 dB");
case ADC_ATTEN_DB_2_5:
return LOG_STR("2.5 dB");
case ADC_ATTEN_DB_6:
return LOG_STR("6 dB");
case ADC_ATTEN_DB_12_COMPAT:
return LOG_STR("12 dB");
default:
return LOG_STR("Unknown Attenuation");
}
}
void ADCSensor::dump_config() {
static const char *const ATTEN_AUTO_STR = "auto";
static const char *const ATTEN_0DB_STR = "0 db";
static const char *const ATTEN_2_5DB_STR = "2.5 db";
static const char *const ATTEN_6DB_STR = "6 db";
static const char *const ATTEN_12DB_STR = "12 db";
const char *atten_str = ATTEN_AUTO_STR;
const LogString *adc_unit_to_str(adc_unit_t unit) {
switch (unit) {
case ADC_UNIT_1:
return LOG_STR("ADC1");
case ADC_UNIT_2:
return LOG_STR("ADC2");
default:
return LOG_STR("Unknown ADC Unit");
}
}
LOG_SENSOR("", "ADC Sensor", this);
LOG_PIN(" Pin: ", this->pin_);
if (!this->autorange_) {
switch (this->attenuation_) {
case ADC_ATTEN_DB_0:
atten_str = ATTEN_0DB_STR;
break;
case ADC_ATTEN_DB_2_5:
atten_str = ATTEN_2_5DB_STR;
break;
case ADC_ATTEN_DB_6:
atten_str = ATTEN_6DB_STR;
break;
case ADC_ATTEN_DB_12_COMPAT:
atten_str = ATTEN_12DB_STR;
break;
default: // This is to satisfy the unused ADC_ATTEN_MAX
break;
void ADCSensor::setup() {
ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->get_name().c_str());
// Check if another sensor already initialized this ADC unit
if (ADCSensor::shared_adc_handles[this->adc_unit_] == nullptr) {
adc_oneshot_unit_init_cfg_t init_config = {}; // Zero initialize
init_config.unit_id = this->adc_unit_;
init_config.ulp_mode = ADC_ULP_MODE_DISABLE;
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32H2
init_config.clk_src = ADC_DIGI_CLK_SRC_DEFAULT;
#endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 ||
// USE_ESP32_VARIANT_ESP32H2
esp_err_t err = adc_oneshot_new_unit(&init_config, &ADCSensor::shared_adc_handles[this->adc_unit_]);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Error initializing %s: %d", LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)), err);
this->mark_failed();
return;
}
}
this->adc_handle_ = ADCSensor::shared_adc_handles[this->adc_unit_];
this->setup_flags_.handle_init_complete = true;
adc_oneshot_chan_cfg_t config = {
.atten = this->attenuation_,
.bitwidth = ADC_BITWIDTH_DEFAULT,
};
esp_err_t err = adc_oneshot_config_channel(this->adc_handle_, this->channel_, &config);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Error configuring channel: %d", err);
this->mark_failed();
return;
}
this->setup_flags_.config_complete = true;
// Initialize ADC calibration
if (this->calibration_handle_ == nullptr) {
adc_cali_handle_t handle = nullptr;
esp_err_t err;
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2
// RISC-V variants and S3 use curve fitting calibration
adc_cali_curve_fitting_config_t cali_config = {}; // Zero initialize first
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
cali_config.chan = this->channel_;
#endif // ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
cali_config.unit_id = this->adc_unit_;
cali_config.atten = this->attenuation_;
cali_config.bitwidth = ADC_BITWIDTH_DEFAULT;
err = adc_cali_create_scheme_curve_fitting(&cali_config, &handle);
if (err == ESP_OK) {
this->calibration_handle_ = handle;
this->setup_flags_.calibration_complete = true;
ESP_LOGV(TAG, "Using curve fitting calibration");
} else {
ESP_LOGW(TAG, "Curve fitting calibration failed with error %d, will use uncalibrated readings", err);
this->setup_flags_.calibration_complete = false;
}
#else // Other ESP32 variants use line fitting calibration
adc_cali_line_fitting_config_t cali_config = {
.unit_id = this->adc_unit_,
.atten = this->attenuation_,
.bitwidth = ADC_BITWIDTH_DEFAULT,
#if !defined(USE_ESP32_VARIANT_ESP32S2)
.default_vref = 1100, // Default reference voltage in mV
#endif // !defined(USE_ESP32_VARIANT_ESP32S2)
};
err = adc_cali_create_scheme_line_fitting(&cali_config, &handle);
if (err == ESP_OK) {
this->calibration_handle_ = handle;
this->setup_flags_.calibration_complete = true;
ESP_LOGV(TAG, "Using line fitting calibration");
} else {
ESP_LOGW(TAG, "Line fitting calibration failed with error %d, will use uncalibrated readings", err);
this->setup_flags_.calibration_complete = false;
}
#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32S3 || ESP32H2
}
this->setup_flags_.init_complete = true;
}
void ADCSensor::dump_config() {
LOG_SENSOR("", "ADC Sensor", this);
LOG_PIN(" Pin: ", this->pin_);
ESP_LOGCONFIG(TAG,
" Attenuation: %s\n"
" Samples: %i\n"
" Channel: %d\n"
" Unit: %s\n"
" Attenuation: %s\n"
" Samples: %i\n"
" Sampling mode: %s",
atten_str, this->sample_count_, LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_)));
this->channel_, LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)),
this->autorange_ ? "Auto" : LOG_STR_ARG(attenuation_to_str(this->attenuation_)), this->sample_count_,
LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_)));
ESP_LOGCONFIG(
TAG,
" Setup Status:\n"
" Handle Init: %s\n"
" Config: %s\n"
" Calibration: %s\n"
" Overall Init: %s",
this->setup_flags_.handle_init_complete ? "OK" : "FAILED", this->setup_flags_.config_complete ? "OK" : "FAILED",
this->setup_flags_.calibration_complete ? "OK" : "FAILED", this->setup_flags_.init_complete ? "OK" : "FAILED");
LOG_UPDATE_INTERVAL(this);
}
float ADCSensor::sample() {
if (!this->autorange_) {
auto aggr = Aggregator(this->sampling_mode_);
if (this->autorange_) {
return this->sample_autorange_();
} else {
return this->sample_fixed_attenuation_();
}
}
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
int raw = -1;
if (this->channel1_ != ADC1_CHANNEL_MAX) {
raw = adc1_get_raw(this->channel1_);
} else if (this->channel2_ != ADC2_CHANNEL_MAX) {
adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw);
}
if (raw == -1) {
return NAN;
}
float ADCSensor::sample_fixed_attenuation_() {
auto aggr = Aggregator(this->sampling_mode_);
aggr.add_sample(raw);
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
int raw;
esp_err_t err = adc_oneshot_read(this->adc_handle_, this->channel_, &raw);
if (err != ESP_OK) {
ESP_LOGW(TAG, "ADC read failed with error %d", err);
continue;
}
if (this->output_raw_) {
return aggr.aggregate();
if (raw == -1) {
ESP_LOGW(TAG, "Invalid ADC reading");
continue;
}
uint32_t mv =
esp_adc_cal_raw_to_voltage(aggr.aggregate(), &this->cal_characteristics_[(int32_t) this->attenuation_]);
return mv / 1000.0f;
aggr.add_sample(raw);
}
int raw12 = ADC_MAX, raw6 = ADC_MAX, raw2 = ADC_MAX, raw0 = ADC_MAX;
uint32_t final_value = aggr.aggregate();
if (this->channel1_ != ADC1_CHANNEL_MAX) {
adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_12_COMPAT);
raw12 = adc1_get_raw(this->channel1_);
if (raw12 < ADC_MAX) {
adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_6);
raw6 = adc1_get_raw(this->channel1_);
if (raw6 < ADC_MAX) {
adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_2_5);
raw2 = adc1_get_raw(this->channel1_);
if (raw2 < ADC_MAX) {
adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_0);
raw0 = adc1_get_raw(this->channel1_);
}
if (this->output_raw_) {
return final_value;
}
if (this->calibration_handle_ != nullptr) {
int voltage_mv;
esp_err_t err = adc_cali_raw_to_voltage(this->calibration_handle_, final_value, &voltage_mv);
if (err == ESP_OK) {
return voltage_mv / 1000.0f;
} else {
ESP_LOGW(TAG, "ADC calibration conversion failed with error %d, disabling calibration", err);
if (this->calibration_handle_ != nullptr) {
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2
adc_cali_delete_scheme_curve_fitting(this->calibration_handle_);
#else // Other ESP32 variants use line fitting calibration
adc_cali_delete_scheme_line_fitting(this->calibration_handle_);
#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32S3 || ESP32H2
this->calibration_handle_ = nullptr;
}
}
} else if (this->channel2_ != ADC2_CHANNEL_MAX) {
adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_12_COMPAT);
adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw12);
if (raw12 < ADC_MAX) {
adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_6);
adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw6);
if (raw6 < ADC_MAX) {
adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_2_5);
adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw2);
if (raw2 < ADC_MAX) {
adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_0);
adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw0);
}
}
return final_value * 3.3f / 4095.0f;
}
float ADCSensor::sample_autorange_() {
// Auto-range mode
auto read_atten = [this](adc_atten_t atten) -> std::pair<int, float> {
// First reconfigure the attenuation for this reading
adc_oneshot_chan_cfg_t config = {
.atten = atten,
.bitwidth = ADC_BITWIDTH_DEFAULT,
};
esp_err_t err = adc_oneshot_config_channel(this->adc_handle_, this->channel_, &config);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Error configuring ADC channel for autorange: %d", err);
return {-1, 0.0f};
}
// Need to recalibrate for the new attenuation
if (this->calibration_handle_ != nullptr) {
// Delete old calibration handle
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2
adc_cali_delete_scheme_curve_fitting(this->calibration_handle_);
#else
adc_cali_delete_scheme_line_fitting(this->calibration_handle_);
#endif
this->calibration_handle_ = nullptr;
}
// Create new calibration handle for this attenuation
adc_cali_handle_t handle = nullptr;
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2
adc_cali_curve_fitting_config_t cali_config = {};
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
cali_config.chan = this->channel_;
#endif
cali_config.unit_id = this->adc_unit_;
cali_config.atten = atten;
cali_config.bitwidth = ADC_BITWIDTH_DEFAULT;
err = adc_cali_create_scheme_curve_fitting(&cali_config, &handle);
#else
adc_cali_line_fitting_config_t cali_config = {
.unit_id = this->adc_unit_,
.atten = atten,
.bitwidth = ADC_BITWIDTH_DEFAULT,
#if !defined(USE_ESP32_VARIANT_ESP32S2)
.default_vref = 1100,
#endif
};
err = adc_cali_create_scheme_line_fitting(&cali_config, &handle);
#endif
int raw;
err = adc_oneshot_read(this->adc_handle_, this->channel_, &raw);
if (err != ESP_OK) {
ESP_LOGW(TAG, "ADC read failed in autorange with error %d", err);
if (handle != nullptr) {
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2
adc_cali_delete_scheme_curve_fitting(handle);
#else
adc_cali_delete_scheme_line_fitting(handle);
#endif
}
return {-1, 0.0f};
}
float voltage = 0.0f;
if (handle != nullptr) {
int voltage_mv;
err = adc_cali_raw_to_voltage(handle, raw, &voltage_mv);
if (err == ESP_OK) {
voltage = voltage_mv / 1000.0f;
} else {
voltage = raw * 3.3f / 4095.0f;
}
// Clean up calibration handle
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2
adc_cali_delete_scheme_curve_fitting(handle);
#else
adc_cali_delete_scheme_line_fitting(handle);
#endif
} else {
voltage = raw * 3.3f / 4095.0f;
}
return {raw, voltage};
};
auto [raw12, mv12] = read_atten(ADC_ATTEN_DB_12);
if (raw12 == -1) {
ESP_LOGE(TAG, "Failed to read ADC in autorange mode");
return NAN;
}
int raw6 = 4095, raw2 = 4095, raw0 = 4095;
float mv6 = 0, mv2 = 0, mv0 = 0;
if (raw12 < 4095) {
auto [raw6_val, mv6_val] = read_atten(ADC_ATTEN_DB_6);
raw6 = raw6_val;
mv6 = mv6_val;
if (raw6 < 4095 && raw6 != -1) {
auto [raw2_val, mv2_val] = read_atten(ADC_ATTEN_DB_2_5);
raw2 = raw2_val;
mv2 = mv2_val;
if (raw2 < 4095 && raw2 != -1) {
auto [raw0_val, mv0_val] = read_atten(ADC_ATTEN_DB_0);
raw0 = raw0_val;
mv0 = mv0_val;
}
}
}
@@ -155,19 +325,19 @@ float ADCSensor::sample() {
return NAN;
}
uint32_t mv12 = esp_adc_cal_raw_to_voltage(raw12, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_12_COMPAT]);
uint32_t mv6 = esp_adc_cal_raw_to_voltage(raw6, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_6]);
uint32_t mv2 = esp_adc_cal_raw_to_voltage(raw2, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_2_5]);
uint32_t mv0 = esp_adc_cal_raw_to_voltage(raw0, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_0]);
uint32_t c12 = std::min(raw12, ADC_HALF);
uint32_t c6 = ADC_HALF - std::abs(raw6 - ADC_HALF);
uint32_t c2 = ADC_HALF - std::abs(raw2 - ADC_HALF);
uint32_t c0 = std::min(ADC_MAX - raw0, ADC_HALF);
const int adc_half = 2048;
uint32_t c12 = std::min(raw12, adc_half);
uint32_t c6 = adc_half - std::abs(raw6 - adc_half);
uint32_t c2 = adc_half - std::abs(raw2 - adc_half);
uint32_t c0 = std::min(4095 - raw0, adc_half);
uint32_t csum = c12 + c6 + c2 + c0;
uint32_t mv_scaled = (mv12 * c12) + (mv6 * c6) + (mv2 * c2) + (mv0 * c0);
return mv_scaled / (float) (csum * 1000U);
if (csum == 0) {
ESP_LOGE(TAG, "Invalid weight sum in autorange calculation");
return NAN;
}
return (mv12 * c12 + mv6 * c6 + mv2 * c2 + mv0 * c0) / csum;
}
} // namespace adc

View File

@@ -56,8 +56,6 @@ float ADCSensor::sample() {
return aggr.aggregate() / 1024.0f;
}
std::string ADCSensor::unique_id() { return get_mac_address() + "-adc"; }
} // namespace adc
} // namespace esphome

View File

@@ -10,13 +10,11 @@ from esphome.const import (
CONF_NUMBER,
CONF_PIN,
CONF_RAW,
CONF_WIFI,
DEVICE_CLASS_VOLTAGE,
STATE_CLASS_MEASUREMENT,
UNIT_VOLT,
)
from esphome.core import CORE
import esphome.final_validate as fv
from . import (
ATTENUATION_MODES,
@@ -24,6 +22,7 @@ from . import (
ESP32_VARIANT_ADC2_PIN_TO_CHANNEL,
SAMPLING_MODES,
adc_ns,
adc_unit_t,
validate_adc_pin,
)
@@ -57,21 +56,6 @@ def validate_config(config):
return config
def final_validate_config(config):
if CORE.is_esp32:
variant = get_esp32_variant()
if (
CONF_WIFI in fv.full_config.get()
and config[CONF_PIN][CONF_NUMBER]
in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant]
):
raise cv.Invalid(
f"{variant} doesn't support ADC on this pin when Wi-Fi is configured"
)
return config
ADCSensor = adc_ns.class_(
"ADCSensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler
)
@@ -99,8 +83,6 @@ CONFIG_SCHEMA = cv.All(
validate_config,
)
FINAL_VALIDATE_SCHEMA = final_validate_config
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
@@ -119,13 +101,13 @@ async def to_code(config):
cg.add(var.set_sample_count(config[CONF_SAMPLES]))
cg.add(var.set_sampling_mode(config[CONF_SAMPLING_MODE]))
if attenuation := config.get(CONF_ATTENUATION):
if attenuation == "auto":
cg.add(var.set_autorange(cg.global_ns.true))
else:
cg.add(var.set_attenuation(attenuation))
if CORE.is_esp32:
if attenuation := config.get(CONF_ATTENUATION):
if attenuation == "auto":
cg.add(var.set_autorange(cg.global_ns.true))
else:
cg.add(var.set_attenuation(attenuation))
variant = get_esp32_variant()
pin_num = config[CONF_PIN][CONF_NUMBER]
if (
@@ -133,10 +115,10 @@ async def to_code(config):
and pin_num in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant]
):
chan = ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant][pin_num]
cg.add(var.set_channel1(chan))
cg.add(var.set_channel(adc_unit_t.ADC_UNIT_1, chan))
elif (
variant in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL
and pin_num in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant]
):
chan = ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant][pin_num]
cg.add(var.set_channel2(chan))
cg.add(var.set_channel(adc_unit_t.ADC_UNIT_2, chan))

View File

@@ -1 +1 @@
CODEOWNERS = ["@jeromelaban"]
CODEOWNERS = ["@jeromelaban", "@precurse"]

View File

@@ -73,11 +73,29 @@ void AirthingsWavePlus::dump_config() {
LOG_SENSOR(" ", "Illuminance", this->illuminance_sensor_);
}
AirthingsWavePlus::AirthingsWavePlus() {
this->service_uuid_ = espbt::ESPBTUUID::from_raw(SERVICE_UUID);
this->sensors_data_characteristic_uuid_ = espbt::ESPBTUUID::from_raw(CHARACTERISTIC_UUID);
void AirthingsWavePlus::setup() {
const char *service_uuid;
const char *characteristic_uuid;
const char *access_control_point_characteristic_uuid;
// Change UUIDs for Wave Radon Gen2
switch (this->wave_device_type_) {
case WaveDeviceType::WAVE_GEN2:
service_uuid = SERVICE_UUID_WAVE_RADON_GEN2;
characteristic_uuid = CHARACTERISTIC_UUID_WAVE_RADON_GEN2;
access_control_point_characteristic_uuid = ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID_WAVE_RADON_GEN2;
break;
default:
// Wave Plus
service_uuid = SERVICE_UUID;
characteristic_uuid = CHARACTERISTIC_UUID;
access_control_point_characteristic_uuid = ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID;
}
this->service_uuid_ = espbt::ESPBTUUID::from_raw(service_uuid);
this->sensors_data_characteristic_uuid_ = espbt::ESPBTUUID::from_raw(characteristic_uuid);
this->access_control_point_characteristic_uuid_ =
espbt::ESPBTUUID::from_raw(ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID);
espbt::ESPBTUUID::from_raw(access_control_point_characteristic_uuid);
}
} // namespace airthings_wave_plus

View File

@@ -9,13 +9,20 @@ namespace airthings_wave_plus {
namespace espbt = esphome::esp32_ble_tracker;
enum WaveDeviceType : uint8_t { WAVE_PLUS = 0, WAVE_GEN2 = 1 };
static const char *const SERVICE_UUID = "b42e1c08-ade7-11e4-89d3-123b93f75cba";
static const char *const CHARACTERISTIC_UUID = "b42e2a68-ade7-11e4-89d3-123b93f75cba";
static const char *const ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID = "b42e2d06-ade7-11e4-89d3-123b93f75cba";
static const char *const SERVICE_UUID_WAVE_RADON_GEN2 = "b42e4a8e-ade7-11e4-89d3-123b93f75cba";
static const char *const CHARACTERISTIC_UUID_WAVE_RADON_GEN2 = "b42e4dcc-ade7-11e4-89d3-123b93f75cba";
static const char *const ACCESS_CONTROL_POINT_CHARACTERISTIC_UUID_WAVE_RADON_GEN2 =
"b42e50d8-ade7-11e4-89d3-123b93f75cba";
class AirthingsWavePlus : public airthings_wave_base::AirthingsWaveBase {
public:
AirthingsWavePlus();
void setup() override;
void dump_config() override;
@@ -23,12 +30,14 @@ class AirthingsWavePlus : public airthings_wave_base::AirthingsWaveBase {
void set_radon_long_term(sensor::Sensor *radon_long_term) { radon_long_term_sensor_ = radon_long_term; }
void set_co2(sensor::Sensor *co2) { co2_sensor_ = co2; }
void set_illuminance(sensor::Sensor *illuminance) { illuminance_sensor_ = illuminance; }
void set_device_type(WaveDeviceType wave_device_type) { wave_device_type_ = wave_device_type; }
protected:
bool is_valid_radon_value_(uint16_t radon);
bool is_valid_co2_value_(uint16_t co2);
void read_sensors(uint8_t *raw_value, uint16_t value_len) override;
WaveDeviceType wave_device_type_{WaveDeviceType::WAVE_PLUS};
sensor::Sensor *radon_sensor_{nullptr};
sensor::Sensor *radon_long_term_sensor_{nullptr};

View File

@@ -7,6 +7,7 @@ from esphome.const import (
CONF_ILLUMINANCE,
CONF_RADON,
CONF_RADON_LONG_TERM,
CONF_TVOC,
DEVICE_CLASS_CARBON_DIOXIDE,
DEVICE_CLASS_ILLUMINANCE,
ICON_RADIOACTIVE,
@@ -15,6 +16,7 @@ from esphome.const import (
UNIT_LUX,
UNIT_PARTS_PER_MILLION,
)
from esphome.types import ConfigType
DEPENDENCIES = airthings_wave_base.DEPENDENCIES
@@ -25,35 +27,59 @@ AirthingsWavePlus = airthings_wave_plus_ns.class_(
"AirthingsWavePlus", airthings_wave_base.AirthingsWaveBase
)
CONF_DEVICE_TYPE = "device_type"
WaveDeviceType = airthings_wave_plus_ns.enum("WaveDeviceType")
DEVICE_TYPES = {
"WAVE_PLUS": WaveDeviceType.WAVE_PLUS,
"WAVE_GEN2": WaveDeviceType.WAVE_GEN2,
}
CONFIG_SCHEMA = airthings_wave_base.BASE_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(AirthingsWavePlus),
cv.Optional(CONF_RADON): sensor.sensor_schema(
unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER,
icon=ICON_RADIOACTIVE,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_RADON_LONG_TERM): sensor.sensor_schema(
unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER,
icon=ICON_RADIOACTIVE,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_CO2): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_MILLION,
accuracy_decimals=0,
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema(
unit_of_measurement=UNIT_LUX,
accuracy_decimals=0,
device_class=DEVICE_CLASS_ILLUMINANCE,
state_class=STATE_CLASS_MEASUREMENT,
),
}
def validate_wave_gen2_config(config: ConfigType) -> ConfigType:
"""Validate that Wave Gen2 devices don't have CO2 or TVOC sensors."""
if config[CONF_DEVICE_TYPE] == "WAVE_GEN2":
if CONF_CO2 in config:
raise cv.Invalid("Wave Gen2 devices do not support CO2 sensor")
# Check for TVOC in the base schema config
if CONF_TVOC in config:
raise cv.Invalid("Wave Gen2 devices do not support TVOC sensor")
return config
CONFIG_SCHEMA = cv.All(
airthings_wave_base.BASE_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(AirthingsWavePlus),
cv.Optional(CONF_RADON): sensor.sensor_schema(
unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER,
icon=ICON_RADIOACTIVE,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_RADON_LONG_TERM): sensor.sensor_schema(
unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER,
icon=ICON_RADIOACTIVE,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_CO2): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_MILLION,
accuracy_decimals=0,
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema(
unit_of_measurement=UNIT_LUX,
accuracy_decimals=0,
device_class=DEVICE_CLASS_ILLUMINANCE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_DEVICE_TYPE, default="WAVE_PLUS"): cv.enum(
DEVICE_TYPES, upper=True
),
}
),
validate_wave_gen2_config,
)
@@ -73,3 +99,4 @@ async def to_code(config):
if config_illuminance := config.get(CONF_ILLUMINANCE):
sens = await sensor.new_sensor(config_illuminance)
cg.add(var.set_illuminance(sens))
cg.add(var.set_device_type(config[CONF_DEVICE_TYPE]))

View File

@@ -24,8 +24,9 @@ from esphome.const import (
CONF_TRIGGER_ID,
CONF_VARIABLES,
)
from esphome.core import coroutine_with_priority
from esphome.core import CORE, coroutine_with_priority
DOMAIN = "api"
DEPENDENCIES = ["network"]
AUTO_LOAD = ["socket"]
CODEOWNERS = ["@OttoWinter"]
@@ -51,6 +52,7 @@ SERVICE_ARG_NATIVE_TYPES = {
}
CONF_ENCRYPTION = "encryption"
CONF_BATCH_DELAY = "batch_delay"
CONF_CUSTOM_SERVICES = "custom_services"
def validate_encryption_key(value):
@@ -115,6 +117,7 @@ CONFIG_SCHEMA = cv.All(
cv.positive_time_period_milliseconds,
cv.Range(max=cv.TimePeriod(milliseconds=65535)),
),
cv.Optional(CONF_CUSTOM_SERVICES, default=False): cv.boolean,
cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation(
single=True
),
@@ -139,8 +142,11 @@ async def to_code(config):
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY]))
# Set USE_API_SERVICES if any services are enabled
if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]:
cg.add_define("USE_API_SERVICES")
if actions := config.get(CONF_ACTIONS, []):
cg.add_define("USE_API_YAML_SERVICES")
for conf in actions:
template_args = []
func_args = []
@@ -317,7 +323,10 @@ async def api_connected_to_code(config, condition_id, template_arg, args):
def FILTER_SOURCE_FILES() -> list[str]:
"""Filter out api_pb2_dump.cpp when proto message dumping is not enabled."""
"""Filter out api_pb2_dump.cpp when proto message dumping is not enabled
and user_services.cpp when no services are defined."""
files_to_filter = []
# api_pb2_dump.cpp is only needed when HAS_PROTO_MESSAGE_DUMP is defined
# This is a particularly large file that still needs to be opened and read
# all the way to the end even when ifdef'd out
@@ -325,6 +334,11 @@ def FILTER_SOURCE_FILES() -> list[str]:
# HAS_PROTO_MESSAGE_DUMP is defined when ESPHOME_LOG_HAS_VERY_VERBOSE is set,
# which happens when the logger level is VERY_VERBOSE
if get_logger_level() != "VERY_VERBOSE":
return ["api_pb2_dump.cpp"]
files_to_filter.append("api_pb2_dump.cpp")
return []
# user_services.cpp is only needed when services are defined
config = CORE.config.get(DOMAIN, {})
if config and not config.get(CONF_ACTIONS) and not config[CONF_CUSTOM_SERVICES]:
files_to_filter.append("user_services.cpp")
return files_to_filter

View File

@@ -222,37 +222,37 @@ message DeviceInfoResponse {
// The model of the board. For example NodeMCU
string model = 6;
bool has_deep_sleep = 7;
bool has_deep_sleep = 7 [(field_ifdef) = "USE_DEEP_SLEEP"];
// The esphome project details if set
string project_name = 8;
string project_version = 9;
string project_name = 8 [(field_ifdef) = "ESPHOME_PROJECT_NAME"];
string project_version = 9 [(field_ifdef) = "ESPHOME_PROJECT_NAME"];
uint32 webserver_port = 10;
uint32 webserver_port = 10 [(field_ifdef) = "USE_WEBSERVER"];
uint32 legacy_bluetooth_proxy_version = 11;
uint32 bluetooth_proxy_feature_flags = 15;
uint32 legacy_bluetooth_proxy_version = 11 [(field_ifdef) = "USE_BLUETOOTH_PROXY"];
uint32 bluetooth_proxy_feature_flags = 15 [(field_ifdef) = "USE_BLUETOOTH_PROXY"];
string manufacturer = 12;
string friendly_name = 13;
uint32 legacy_voice_assistant_version = 14;
uint32 voice_assistant_feature_flags = 17;
uint32 legacy_voice_assistant_version = 14 [(field_ifdef) = "USE_VOICE_ASSISTANT"];
uint32 voice_assistant_feature_flags = 17 [(field_ifdef) = "USE_VOICE_ASSISTANT"];
string suggested_area = 16;
string suggested_area = 16 [(field_ifdef) = "USE_AREAS"];
// The Bluetooth mac address of the device. For example "AC:BC:32:89:0E:AA"
string bluetooth_mac_address = 18;
string bluetooth_mac_address = 18 [(field_ifdef) = "USE_BLUETOOTH_PROXY"];
// Supports receiving and saving api encryption key
bool api_encryption_supported = 19;
bool api_encryption_supported = 19 [(field_ifdef) = "USE_API_NOISE"];
repeated DeviceInfo devices = 20;
repeated AreaInfo areas = 21;
repeated DeviceInfo devices = 20 [(field_ifdef) = "USE_DEVICES"];
repeated AreaInfo areas = 21 [(field_ifdef) = "USE_AREAS"];
// Top-level area info to phase out suggested_area
AreaInfo area = 22;
AreaInfo area = 22 [(field_ifdef) = "USE_AREAS"];
}
message ListEntitiesRequest {
@@ -290,14 +290,14 @@ message ListEntitiesBinarySensorResponse {
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
reserved 4; // Deprecated: was string unique_id
string device_class = 5;
bool is_status_binary_sensor = 6;
bool disabled_by_default = 7;
string icon = 8;
string icon = 8 [(field_ifdef) = "USE_ENTITY_ICON"];
EntityCategory entity_category = 9;
uint32 device_id = 10;
uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"];
}
message BinarySensorStateResponse {
option (id) = 21;
@@ -311,7 +311,7 @@ message BinarySensorStateResponse {
// If the binary sensor does not have a valid state yet.
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
bool missing_state = 3;
uint32 device_id = 4;
uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"];
}
// ==================== COVER ====================
@@ -324,17 +324,17 @@ message ListEntitiesCoverResponse {
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
reserved 4; // Deprecated: was string unique_id
bool assumed_state = 5;
bool supports_position = 6;
bool supports_tilt = 7;
string device_class = 8;
bool disabled_by_default = 9;
string icon = 10;
string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"];
EntityCategory entity_category = 11;
bool supports_stop = 12;
uint32 device_id = 13;
uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"];
}
enum LegacyCoverState {
@@ -361,7 +361,7 @@ message CoverStateResponse {
float position = 3;
float tilt = 4;
CoverOperation current_operation = 5;
uint32 device_id = 6;
uint32 device_id = 6 [(field_ifdef) = "USE_DEVICES"];
}
enum LegacyCoverCommand {
@@ -374,6 +374,7 @@ message CoverCommandRequest {
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_COVER";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
@@ -387,6 +388,7 @@ message CoverCommandRequest {
bool has_tilt = 6;
float tilt = 7;
bool stop = 8;
uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"];
}
// ==================== FAN ====================
@@ -399,17 +401,17 @@ message ListEntitiesFanResponse {
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
reserved 4; // Deprecated: was string unique_id
bool supports_oscillation = 5;
bool supports_speed = 6;
bool supports_direction = 7;
int32 supported_speed_count = 8;
bool disabled_by_default = 9;
string icon = 10;
string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"];
EntityCategory entity_category = 11;
repeated string supported_preset_modes = 12;
uint32 device_id = 13;
uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"];
}
enum FanSpeed {
FAN_SPEED_LOW = 0;
@@ -434,13 +436,14 @@ message FanStateResponse {
FanDirection direction = 5;
int32 speed_level = 6;
string preset_mode = 7;
uint32 device_id = 8;
uint32 device_id = 8 [(field_ifdef) = "USE_DEVICES"];
}
message FanCommandRequest {
option (id) = 31;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_FAN";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
bool has_state = 2;
@@ -455,6 +458,7 @@ message FanCommandRequest {
int32 speed_level = 11;
bool has_preset_mode = 12;
string preset_mode = 13;
uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"];
}
// ==================== LIGHT ====================
@@ -480,7 +484,7 @@ message ListEntitiesLightResponse {
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
reserved 4; // Deprecated: was string unique_id
repeated ColorMode supported_color_modes = 12;
// next four supports_* are for legacy clients, newer clients should use color modes
@@ -492,9 +496,9 @@ message ListEntitiesLightResponse {
float max_mireds = 10;
repeated string effects = 11;
bool disabled_by_default = 13;
string icon = 14;
string icon = 14 [(field_ifdef) = "USE_ENTITY_ICON"];
EntityCategory entity_category = 15;
uint32 device_id = 16;
uint32 device_id = 16 [(field_ifdef) = "USE_DEVICES"];
}
message LightStateResponse {
option (id) = 24;
@@ -516,13 +520,14 @@ message LightStateResponse {
float cold_white = 12;
float warm_white = 13;
string effect = 9;
uint32 device_id = 14;
uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"];
}
message LightCommandRequest {
option (id) = 32;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_LIGHT";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
bool has_state = 2;
@@ -551,6 +556,7 @@ message LightCommandRequest {
uint32 flash_length = 17;
bool has_effect = 18;
string effect = 19;
uint32 device_id = 28 [(field_ifdef) = "USE_DEVICES"];
}
// ==================== SENSOR ====================
@@ -576,9 +582,9 @@ message ListEntitiesSensorResponse {
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
reserved 4; // Deprecated: was string unique_id
string icon = 5;
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
string unit_of_measurement = 6;
int32 accuracy_decimals = 7;
bool force_update = 8;
@@ -588,7 +594,7 @@ message ListEntitiesSensorResponse {
SensorLastResetType legacy_last_reset_type = 11;
bool disabled_by_default = 12;
EntityCategory entity_category = 13;
uint32 device_id = 14;
uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"];
}
message SensorStateResponse {
option (id) = 25;
@@ -602,7 +608,7 @@ message SensorStateResponse {
// If the sensor does not have a valid state yet.
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
bool missing_state = 3;
uint32 device_id = 4;
uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"];
}
// ==================== SWITCH ====================
@@ -615,14 +621,14 @@ message ListEntitiesSwitchResponse {
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
reserved 4; // Deprecated: was string unique_id
string icon = 5;
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
bool assumed_state = 6;
bool disabled_by_default = 7;
EntityCategory entity_category = 8;
string device_class = 9;
uint32 device_id = 10;
uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"];
}
message SwitchStateResponse {
option (id) = 26;
@@ -633,16 +639,18 @@ message SwitchStateResponse {
fixed32 key = 1;
bool state = 2;
uint32 device_id = 3;
uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
}
message SwitchCommandRequest {
option (id) = 33;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_SWITCH";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
bool state = 2;
uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
}
// ==================== TEXT SENSOR ====================
@@ -655,13 +663,13 @@ message ListEntitiesTextSensorResponse {
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
reserved 4; // Deprecated: was string unique_id
string icon = 5;
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
string device_class = 8;
uint32 device_id = 9;
uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"];
}
message TextSensorStateResponse {
option (id) = 27;
@@ -675,7 +683,7 @@ message TextSensorStateResponse {
// If the text sensor does not have a valid state yet.
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
bool missing_state = 3;
uint32 device_id = 4;
uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"];
}
// ==================== SUBSCRIBE LOGS ====================
@@ -799,18 +807,21 @@ enum ServiceArgType {
SERVICE_ARG_TYPE_STRING_ARRAY = 7;
}
message ListEntitiesServicesArgument {
option (ifdef) = "USE_API_SERVICES";
string name = 1;
ServiceArgType type = 2;
}
message ListEntitiesServicesResponse {
option (id) = 41;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_API_SERVICES";
string name = 1;
fixed32 key = 2;
repeated ListEntitiesServicesArgument args = 3;
}
message ExecuteServiceArgument {
option (ifdef) = "USE_API_SERVICES";
bool bool_ = 1;
int32 legacy_int = 2;
float float_ = 3;
@@ -826,6 +837,7 @@ message ExecuteServiceRequest {
option (id) = 42;
option (source) = SOURCE_CLIENT;
option (no_delay) = true;
option (ifdef) = "USE_API_SERVICES";
fixed32 key = 1;
repeated ExecuteServiceArgument args = 2;
@@ -841,21 +853,23 @@ message ListEntitiesCameraResponse {
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
reserved 4; // Deprecated: was string unique_id
bool disabled_by_default = 5;
string icon = 6;
string icon = 6 [(field_ifdef) = "USE_ENTITY_ICON"];
EntityCategory entity_category = 7;
uint32 device_id = 8;
uint32 device_id = 8 [(field_ifdef) = "USE_DEVICES"];
}
message CameraImageResponse {
option (id) = 44;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_CAMERA";
fixed32 key = 1;
bytes data = 2;
bool done = 3;
uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"];
}
message CameraImageRequest {
option (id) = 45;
@@ -923,7 +937,7 @@ message ListEntitiesClimateResponse {
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
reserved 4; // Deprecated: was string unique_id
bool supports_current_temperature = 5;
bool supports_two_point_target_temperature = 6;
@@ -941,14 +955,14 @@ message ListEntitiesClimateResponse {
repeated ClimatePreset supported_presets = 16;
repeated string supported_custom_presets = 17;
bool disabled_by_default = 18;
string icon = 19;
string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"];
EntityCategory entity_category = 20;
float visual_current_temperature_step = 21;
bool supports_current_humidity = 22;
bool supports_target_humidity = 23;
float visual_min_humidity = 24;
float visual_max_humidity = 25;
uint32 device_id = 26;
uint32 device_id = 26 [(field_ifdef) = "USE_DEVICES"];
}
message ClimateStateResponse {
option (id) = 47;
@@ -973,13 +987,14 @@ message ClimateStateResponse {
string custom_preset = 13;
float current_humidity = 14;
float target_humidity = 15;
uint32 device_id = 16;
uint32 device_id = 16 [(field_ifdef) = "USE_DEVICES"];
}
message ClimateCommandRequest {
option (id) = 48;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_CLIMATE";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
bool has_mode = 2;
@@ -1005,6 +1020,7 @@ message ClimateCommandRequest {
string custom_preset = 21;
bool has_target_humidity = 22;
float target_humidity = 23;
uint32 device_id = 24 [(field_ifdef) = "USE_DEVICES"];
}
// ==================== NUMBER ====================
@@ -1022,9 +1038,9 @@ message ListEntitiesNumberResponse {
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
reserved 4; // Deprecated: was string unique_id
string icon = 5;
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
float min_value = 6;
float max_value = 7;
float step = 8;
@@ -1033,7 +1049,7 @@ message ListEntitiesNumberResponse {
string unit_of_measurement = 11;
NumberMode mode = 12;
string device_class = 13;
uint32 device_id = 14;
uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"];
}
message NumberStateResponse {
option (id) = 50;
@@ -1047,16 +1063,18 @@ message NumberStateResponse {
// If the number does not have a valid state yet.
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
bool missing_state = 3;
uint32 device_id = 4;
uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"];
}
message NumberCommandRequest {
option (id) = 51;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_NUMBER";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
float state = 2;
uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
}
// ==================== SELECT ====================
@@ -1069,13 +1087,13 @@ message ListEntitiesSelectResponse {
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
reserved 4; // Deprecated: was string unique_id
string icon = 5;
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
repeated string options = 6;
bool disabled_by_default = 7;
EntityCategory entity_category = 8;
uint32 device_id = 9;
uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"];
}
message SelectStateResponse {
option (id) = 53;
@@ -1089,16 +1107,18 @@ message SelectStateResponse {
// If the select does not have a valid state yet.
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
bool missing_state = 3;
uint32 device_id = 4;
uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"];
}
message SelectCommandRequest {
option (id) = 54;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_SELECT";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
string state = 2;
uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
}
// ==================== SIREN ====================
@@ -1111,15 +1131,15 @@ message ListEntitiesSirenResponse {
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
reserved 4; // Deprecated: was string unique_id
string icon = 5;
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
bool disabled_by_default = 6;
repeated string tones = 7;
bool supports_duration = 8;
bool supports_volume = 9;
EntityCategory entity_category = 10;
uint32 device_id = 11;
uint32 device_id = 11 [(field_ifdef) = "USE_DEVICES"];
}
message SirenStateResponse {
option (id) = 56;
@@ -1130,13 +1150,14 @@ message SirenStateResponse {
fixed32 key = 1;
bool state = 2;
uint32 device_id = 3;
uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
}
message SirenCommandRequest {
option (id) = 57;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_SIREN";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
bool has_state = 2;
@@ -1147,6 +1168,7 @@ message SirenCommandRequest {
uint32 duration = 7;
bool has_volume = 8;
float volume = 9;
uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"];
}
// ==================== LOCK ====================
@@ -1172,9 +1194,9 @@ message ListEntitiesLockResponse {
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
reserved 4; // Deprecated: was string unique_id
string icon = 5;
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
bool assumed_state = 8;
@@ -1184,7 +1206,7 @@ message ListEntitiesLockResponse {
// Not yet implemented:
string code_format = 11;
uint32 device_id = 12;
uint32 device_id = 12 [(field_ifdef) = "USE_DEVICES"];
}
message LockStateResponse {
option (id) = 59;
@@ -1194,19 +1216,21 @@ message LockStateResponse {
option (no_delay) = true;
fixed32 key = 1;
LockState state = 2;
uint32 device_id = 3;
uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
}
message LockCommandRequest {
option (id) = 60;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_LOCK";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
LockCommand command = 2;
// Not yet implemented:
bool has_code = 3;
string code = 4;
uint32 device_id = 5 [(field_ifdef) = "USE_DEVICES"];
}
// ==================== BUTTON ====================
@@ -1219,21 +1243,23 @@ message ListEntitiesButtonResponse {
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
reserved 4; // Deprecated: was string unique_id
string icon = 5;
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
string device_class = 8;
uint32 device_id = 9;
uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"];
}
message ButtonCommandRequest {
option (id) = 62;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_BUTTON";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
uint32 device_id = 2 [(field_ifdef) = "USE_DEVICES"];
}
// ==================== MEDIA PLAYER ====================
@@ -1272,9 +1298,9 @@ message ListEntitiesMediaPlayerResponse {
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
reserved 4; // Deprecated: was string unique_id
string icon = 5;
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
@@ -1282,7 +1308,7 @@ message ListEntitiesMediaPlayerResponse {
repeated MediaPlayerSupportedFormat supported_formats = 9;
uint32 device_id = 10;
uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"];
}
message MediaPlayerStateResponse {
option (id) = 64;
@@ -1294,13 +1320,14 @@ message MediaPlayerStateResponse {
MediaPlayerState state = 2;
float volume = 3;
bool muted = 4;
uint32 device_id = 5;
uint32 device_id = 5 [(field_ifdef) = "USE_DEVICES"];
}
message MediaPlayerCommandRequest {
option (id) = 65;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_MEDIA_PLAYER";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
@@ -1315,6 +1342,7 @@ message MediaPlayerCommandRequest {
bool has_announcement = 8;
bool announcement = 9;
uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"];
}
// ==================== BLUETOOTH ====================
@@ -1353,7 +1381,7 @@ message BluetoothLERawAdvertisement {
sint32 rssi = 2;
uint32 address_type = 3;
bytes data = 4;
bytes data = 4 [(fixed_array_size) = 62];
}
message BluetoothLERawAdvertisementsResponse {
@@ -1817,14 +1845,14 @@ message ListEntitiesAlarmControlPanelResponse {
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
string icon = 5;
reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
uint32 supported_features = 8;
bool requires_code = 9;
bool requires_code_to_arm = 10;
uint32 device_id = 11;
uint32 device_id = 11 [(field_ifdef) = "USE_DEVICES"];
}
message AlarmControlPanelStateResponse {
@@ -1835,7 +1863,7 @@ message AlarmControlPanelStateResponse {
option (no_delay) = true;
fixed32 key = 1;
AlarmControlPanelState state = 2;
uint32 device_id = 3;
uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
}
message AlarmControlPanelCommandRequest {
@@ -1843,9 +1871,11 @@ message AlarmControlPanelCommandRequest {
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_ALARM_CONTROL_PANEL";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
AlarmControlPanelStateCommand command = 2;
string code = 3;
uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"];
}
// ===================== TEXT =====================
@@ -1862,8 +1892,8 @@ message ListEntitiesTextResponse {
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
string icon = 5;
reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
@@ -1871,7 +1901,7 @@ message ListEntitiesTextResponse {
uint32 max_length = 9;
string pattern = 10;
TextMode mode = 11;
uint32 device_id = 12;
uint32 device_id = 12 [(field_ifdef) = "USE_DEVICES"];
}
message TextStateResponse {
option (id) = 98;
@@ -1885,16 +1915,18 @@ message TextStateResponse {
// If the Text does not have a valid state yet.
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
bool missing_state = 3;
uint32 device_id = 4;
uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"];
}
message TextCommandRequest {
option (id) = 99;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_TEXT";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
string state = 2;
uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
}
@@ -1908,12 +1940,12 @@ message ListEntitiesDateResponse {
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
reserved 4; // Deprecated: was string unique_id
string icon = 5;
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
uint32 device_id = 8;
uint32 device_id = 8 [(field_ifdef) = "USE_DEVICES"];
}
message DateStateResponse {
option (id) = 101;
@@ -1929,18 +1961,20 @@ message DateStateResponse {
uint32 year = 3;
uint32 month = 4;
uint32 day = 5;
uint32 device_id = 6;
uint32 device_id = 6 [(field_ifdef) = "USE_DEVICES"];
}
message DateCommandRequest {
option (id) = 102;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_DATETIME_DATE";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
uint32 year = 2;
uint32 month = 3;
uint32 day = 4;
uint32 device_id = 5 [(field_ifdef) = "USE_DEVICES"];
}
// ==================== DATETIME TIME ====================
@@ -1953,12 +1987,12 @@ message ListEntitiesTimeResponse {
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
reserved 4; // Deprecated: was string unique_id
string icon = 5;
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
uint32 device_id = 8;
uint32 device_id = 8 [(field_ifdef) = "USE_DEVICES"];
}
message TimeStateResponse {
option (id) = 104;
@@ -1974,18 +2008,20 @@ message TimeStateResponse {
uint32 hour = 3;
uint32 minute = 4;
uint32 second = 5;
uint32 device_id = 6;
uint32 device_id = 6 [(field_ifdef) = "USE_DEVICES"];
}
message TimeCommandRequest {
option (id) = 105;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_DATETIME_TIME";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
uint32 hour = 2;
uint32 minute = 3;
uint32 second = 4;
uint32 device_id = 5 [(field_ifdef) = "USE_DEVICES"];
}
// ==================== EVENT ====================
@@ -1998,15 +2034,15 @@ message ListEntitiesEventResponse {
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
reserved 4; // Deprecated: was string unique_id
string icon = 5;
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
string device_class = 8;
repeated string event_types = 9;
uint32 device_id = 10;
uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"];
}
message EventResponse {
option (id) = 108;
@@ -2016,7 +2052,7 @@ message EventResponse {
fixed32 key = 1;
string event_type = 2;
uint32 device_id = 3;
uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
}
// ==================== VALVE ====================
@@ -2029,9 +2065,9 @@ message ListEntitiesValveResponse {
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
reserved 4; // Deprecated: was string unique_id
string icon = 5;
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
string device_class = 8;
@@ -2039,7 +2075,7 @@ message ListEntitiesValveResponse {
bool assumed_state = 9;
bool supports_position = 10;
bool supports_stop = 11;
uint32 device_id = 12;
uint32 device_id = 12 [(field_ifdef) = "USE_DEVICES"];
}
enum ValveOperation {
@@ -2057,7 +2093,7 @@ message ValveStateResponse {
fixed32 key = 1;
float position = 2;
ValveOperation current_operation = 3;
uint32 device_id = 4;
uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"];
}
message ValveCommandRequest {
@@ -2065,11 +2101,13 @@ message ValveCommandRequest {
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_VALVE";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
bool has_position = 2;
float position = 3;
bool stop = 4;
uint32 device_id = 5 [(field_ifdef) = "USE_DEVICES"];
}
// ==================== DATETIME DATETIME ====================
@@ -2082,12 +2120,12 @@ message ListEntitiesDateTimeResponse {
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
reserved 4; // Deprecated: was string unique_id
string icon = 5;
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
uint32 device_id = 8;
uint32 device_id = 8 [(field_ifdef) = "USE_DEVICES"];
}
message DateTimeStateResponse {
option (id) = 113;
@@ -2101,16 +2139,18 @@ message DateTimeStateResponse {
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
bool missing_state = 2;
fixed32 epoch_seconds = 3;
uint32 device_id = 4;
uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"];
}
message DateTimeCommandRequest {
option (id) = 114;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_DATETIME_DATETIME";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
fixed32 epoch_seconds = 2;
uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
}
// ==================== UPDATE ====================
@@ -2123,13 +2163,13 @@ message ListEntitiesUpdateResponse {
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
reserved 4; // Deprecated: was string unique_id
string icon = 5;
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
string device_class = 8;
uint32 device_id = 9;
uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"];
}
message UpdateStateResponse {
option (id) = 117;
@@ -2148,7 +2188,7 @@ message UpdateStateResponse {
string title = 8;
string release_summary = 9;
string release_url = 10;
uint32 device_id = 11;
uint32 device_id = 11 [(field_ifdef) = "USE_DEVICES"];
}
enum UpdateCommand {
UPDATE_COMMAND_NONE = 0;
@@ -2160,7 +2200,9 @@ message UpdateCommandRequest {
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_UPDATE";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
UpdateCommand command = 2;
uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
}

File diff suppressed because it is too large Load Diff

View File

@@ -33,7 +33,7 @@ class APIConnection : public APIServerConnection {
bool send_list_info_done() {
return this->schedule_message_(nullptr, &APIConnection::try_send_list_info_done,
ListEntitiesDoneResponse::MESSAGE_TYPE);
ListEntitiesDoneResponse::MESSAGE_TYPE, ListEntitiesDoneResponse::ESTIMATED_SIZE);
}
#ifdef USE_BINARY_SENSOR
bool send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor);
@@ -111,7 +111,7 @@ class APIConnection : public APIServerConnection {
void send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
if (!this->flags_.service_call_subscription)
return;
this->send_message(call);
this->send_message(call, HomeassistantServiceResponse::MESSAGE_TYPE);
}
#ifdef USE_BLUETOOTH_PROXY
void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
@@ -133,7 +133,7 @@ class APIConnection : public APIServerConnection {
#ifdef USE_HOMEASSISTANT_TIME
void send_time_request() {
GetTimeRequest req;
this->send_message(req);
this->send_message(req, GetTimeRequest::MESSAGE_TYPE);
}
#endif
@@ -195,7 +195,9 @@ class APIConnection : public APIServerConnection {
// TODO
return {};
}
#ifdef USE_API_SERVICES
void execute_service(const ExecuteServiceRequest &msg) override;
#endif
#ifdef USE_API_NOISE
NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) override;
#endif
@@ -207,6 +209,7 @@ class APIConnection : public APIServerConnection {
return static_cast<ConnectionState>(this->flags_.connection_state) == ConnectionState::CONNECTED ||
this->is_authenticated();
}
uint8_t get_log_subscription_level() const { return this->flags_.log_subscription; }
void on_fatal_error() override;
void on_unauthenticated_access() override;
void on_no_setup_connection() override;
@@ -256,7 +259,7 @@ class APIConnection : public APIServerConnection {
}
bool try_to_clear_buffer(bool log_out_of_space);
bool send_buffer(ProtoWriteBuffer buffer, uint16_t message_type) override;
bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override;
std::string get_client_combined_info() const {
if (this->client_info_ == this->client_peername_) {
@@ -271,36 +274,43 @@ class APIConnection : public APIServerConnection {
ProtoWriteBuffer allocate_batch_message_buffer(uint16_t size);
protected:
// Helper function to fill common entity info fields
static void fill_entity_info_base(esphome::EntityBase *entity, InfoResponseProtoMessage &response) {
// Set common fields that are shared by all entity types
response.key = entity->get_object_id_hash();
response.object_id = entity->get_object_id();
if (entity->has_own_name())
response.name = entity->get_name();
// Set common EntityBase properties
response.icon = entity->get_icon();
response.disabled_by_default = entity->is_disabled_by_default();
response.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category());
#ifdef USE_DEVICES
response.device_id = entity->get_device_id();
#endif
}
// Helper function to fill common entity state fields
static void fill_entity_state_base(esphome::EntityBase *entity, StateResponseProtoMessage &response) {
response.key = entity->get_object_id_hash();
#ifdef USE_DEVICES
response.device_id = entity->get_device_id();
#endif
}
// Helper function to handle authentication completion
void complete_authentication_();
// Non-template helper to encode any ProtoMessage
static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint16_t message_type, APIConnection *conn,
static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn,
uint32_t remaining_size, bool is_single);
// Helper to fill entity state base and encode message
static uint16_t fill_and_encode_entity_state(EntityBase *entity, StateResponseProtoMessage &msg, uint8_t message_type,
APIConnection *conn, uint32_t remaining_size, bool is_single) {
msg.key = entity->get_object_id_hash();
#ifdef USE_DEVICES
msg.device_id = entity->get_device_id();
#endif
return encode_message_to_buffer(msg, message_type, conn, remaining_size, is_single);
}
// Helper to fill entity info base and encode message
static uint16_t fill_and_encode_entity_info(EntityBase *entity, InfoResponseProtoMessage &msg, uint8_t message_type,
APIConnection *conn, uint32_t remaining_size, bool is_single) {
// Set common fields that are shared by all entity types
msg.key = entity->get_object_id_hash();
msg.object_id = entity->get_object_id();
if (entity->has_own_name())
msg.name = entity->get_name();
// Set common EntityBase properties
msg.icon = entity->get_icon();
msg.disabled_by_default = entity->is_disabled_by_default();
msg.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category());
#ifdef USE_DEVICES
msg.device_id = entity->get_device_id();
#endif
return encode_message_to_buffer(msg, message_type, conn, remaining_size, is_single);
}
#ifdef USE_VOICE_ASSISTANT
// Helper to check voice assistant validity and connection ownership
inline bool check_voice_assistant_api_connection_() const;
@@ -443,9 +453,6 @@ class APIConnection : public APIServerConnection {
static uint16_t try_send_disconnect_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single);
// Helper function to get estimated message size for buffer pre-allocation
static uint16_t get_estimated_message_size(uint16_t message_type);
// Batch message method for ping requests
static uint16_t try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single);
@@ -505,10 +512,10 @@ class APIConnection : public APIServerConnection {
// Call operator - uses message_type to determine union type
uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single,
uint16_t message_type) const;
uint8_t message_type) const;
// Manual cleanup method - must be called before destruction for string types
void cleanup(uint16_t message_type) {
void cleanup(uint8_t message_type) {
#ifdef USE_EVENT
if (message_type == EventResponse::MESSAGE_TYPE && data_.string_ptr != nullptr) {
delete data_.string_ptr;
@@ -529,11 +536,12 @@ class APIConnection : public APIServerConnection {
struct BatchItem {
EntityBase *entity; // Entity pointer
MessageCreator creator; // Function that creates the message when needed
uint16_t message_type; // Message type for overhead calculation
uint8_t message_type; // Message type for overhead calculation (max 255)
uint8_t estimated_size; // Estimated message size (max 255 bytes)
// Constructor for creating BatchItem
BatchItem(EntityBase *entity, MessageCreator creator, uint16_t message_type)
: entity(entity), creator(std::move(creator)), message_type(message_type) {}
BatchItem(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size)
: entity(entity), creator(std::move(creator)), message_type(message_type), estimated_size(estimated_size) {}
};
std::vector<BatchItem> items;
@@ -559,9 +567,9 @@ class APIConnection : public APIServerConnection {
}
// Add item to the batch
void add_item(EntityBase *entity, MessageCreator creator, uint16_t message_type);
void add_item(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size);
// Add item to the front of the batch (for high priority messages like ping)
void add_item_front(EntityBase *entity, MessageCreator creator, uint16_t message_type);
void add_item_front(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size);
// Clear all items with proper cleanup
void clear() {
@@ -630,7 +638,7 @@ class APIConnection : public APIServerConnection {
// to send in one go. This is the maximum size of a single packet
// that can be sent over the network.
// This is to avoid fragmentation of the packet.
static constexpr size_t MAX_PACKET_SIZE = 1390; // MTU
static constexpr size_t MAX_BATCH_PACKET_SIZE = 1390; // MTU
bool schedule_batch_();
void process_batch_();
@@ -641,9 +649,9 @@ class APIConnection : public APIServerConnection {
#ifdef HAS_PROTO_MESSAGE_DUMP
// Helper to log a proto message from a MessageCreator object
void log_proto_message_(EntityBase *entity, const MessageCreator &creator, uint16_t message_type) {
void log_proto_message_(EntityBase *entity, const MessageCreator &creator, uint8_t message_type) {
this->flags_.log_only_mode = true;
creator(entity, this, MAX_PACKET_SIZE, true, message_type);
creator(entity, this, MAX_BATCH_PACKET_SIZE, true, message_type);
this->flags_.log_only_mode = false;
}
@@ -654,7 +662,8 @@ class APIConnection : public APIServerConnection {
#endif
// Helper method to send a message either immediately or via batching
bool send_message_smart_(EntityBase *entity, MessageCreatorPtr creator, uint16_t message_type) {
bool send_message_smart_(EntityBase *entity, MessageCreatorPtr creator, uint8_t message_type,
uint8_t estimated_size) {
// Try to send immediately if:
// 1. We should try to send immediately (should_try_send_immediately = true)
// 2. Batch delay is 0 (user has opted in to immediate sending)
@@ -662,7 +671,7 @@ class APIConnection : public APIServerConnection {
if (this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0 &&
this->helper_->can_write_without_blocking()) {
// Now actually encode and send
if (creator(entity, this, MAX_PACKET_SIZE, true) &&
if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true) &&
this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) {
#ifdef HAS_PROTO_MESSAGE_DUMP
// Log the message in verbose mode
@@ -675,23 +684,25 @@ class APIConnection : public APIServerConnection {
}
// Fall back to scheduled batching
return this->schedule_message_(entity, creator, message_type);
return this->schedule_message_(entity, creator, message_type, estimated_size);
}
// Helper function to schedule a deferred message with known message type
bool schedule_message_(EntityBase *entity, MessageCreator creator, uint16_t message_type) {
this->deferred_batch_.add_item(entity, std::move(creator), message_type);
bool schedule_message_(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) {
this->deferred_batch_.add_item(entity, std::move(creator), message_type, estimated_size);
return this->schedule_batch_();
}
// Overload for function pointers (for info messages and current state reads)
bool schedule_message_(EntityBase *entity, MessageCreatorPtr function_ptr, uint16_t message_type) {
return schedule_message_(entity, MessageCreator(function_ptr), message_type);
bool schedule_message_(EntityBase *entity, MessageCreatorPtr function_ptr, uint8_t message_type,
uint8_t estimated_size) {
return schedule_message_(entity, MessageCreator(function_ptr), message_type, estimated_size);
}
// Helper function to schedule a high priority message at the front of the batch
bool schedule_message_front_(EntityBase *entity, MessageCreatorPtr function_ptr, uint16_t message_type) {
this->deferred_batch_.add_item_front(entity, MessageCreator(function_ptr), message_type);
bool schedule_message_front_(EntityBase *entity, MessageCreatorPtr function_ptr, uint8_t message_type,
uint8_t estimated_size) {
this->deferred_batch_.add_item_front(entity, MessageCreator(function_ptr), message_type, estimated_size);
return this->schedule_batch_();
}
};

View File

@@ -5,7 +5,6 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "proto.h"
#include "api_pb2_size.h"
#include <cstring>
#include <cinttypes>
@@ -613,7 +612,7 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
buffer->type = type;
return APIError::OK;
}
APIError APINoiseFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) {
APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
// Resize to include MAC space (required for Noise encryption)
buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_);
PacketInfo packet{type, 0,
@@ -1002,7 +1001,7 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
buffer->type = rx_header_parsed_type_;
return APIError::OK;
}
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) {
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
PacketInfo packet{type, 0, static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_)};
return write_protobuf_packets(buffer, std::span<const PacketInfo>(&packet, 1));
}

View File

@@ -30,13 +30,11 @@ struct ReadPacketBuffer {
// Packed packet info structure to minimize memory usage
struct PacketInfo {
uint16_t message_type; // 2 bytes
uint16_t offset; // 2 bytes (sufficient for packet size ~1460 bytes)
uint16_t payload_size; // 2 bytes (up to 65535 bytes)
uint16_t padding; // 2 byte (for alignment)
uint16_t offset; // Offset in buffer where message starts
uint16_t payload_size; // Size of the message payload
uint8_t message_type; // Message type (0-255)
PacketInfo(uint16_t type, uint16_t off, uint16_t size)
: message_type(type), offset(off), payload_size(size), padding(0) {}
PacketInfo(uint8_t type, uint16_t off, uint16_t size) : offset(off), payload_size(size), message_type(type) {}
};
enum class APIError : uint16_t {
@@ -98,7 +96,7 @@ class APIFrameHelper {
}
// Give this helper a name for logging
void set_log_info(std::string info) { info_ = std::move(info); }
virtual APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) = 0;
virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0;
// Write multiple protobuf packets in a single operation
// packets contains (message_type, offset, length) for each message in the buffer
// The buffer contains all messages with appropriate padding before each
@@ -197,7 +195,7 @@ class APINoiseFrameHelper : public APIFrameHelper {
APIError init() override;
APIError loop() override;
APIError read_packet(ReadPacketBuffer *buffer) override;
APIError write_protobuf_packet(uint16_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;
// Get the frame header padding required by this protocol
uint8_t frame_header_padding() override { return frame_header_padding_; }
@@ -251,7 +249,7 @@ class APIPlaintextFrameHelper : public APIFrameHelper {
APIError init() override;
APIError loop() override;
APIError read_packet(ReadPacketBuffer *buffer) override;
APIError write_protobuf_packet(uint16_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;
uint8_t frame_header_padding() override { return frame_header_padding_; }
// Get the frame footer size required by this protocol

View File

@@ -23,3 +23,8 @@ extend google.protobuf.MessageOptions {
optional bool no_delay = 1040 [default=false];
optional string base_class = 1041;
}
extend google.protobuf.FieldOptions {
optional string field_ifdef = 1042;
optional uint32 fixed_array_size = 50007;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -195,6 +195,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
this->on_home_assistant_state_response(msg);
break;
}
#ifdef USE_API_SERVICES
case 42: {
ExecuteServiceRequest msg;
msg.decode(msg_data, msg_size);
@@ -204,6 +205,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
this->on_execute_service_request(msg);
break;
}
#endif
#ifdef USE_CAMERA
case 45: {
CameraImageRequest msg;
@@ -596,32 +598,32 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
void APIServerConnection::on_hello_request(const HelloRequest &msg) {
HelloResponse ret = this->hello(msg);
if (!this->send_message(ret)) {
if (!this->send_message(ret, HelloResponse::MESSAGE_TYPE)) {
this->on_fatal_error();
}
}
void APIServerConnection::on_connect_request(const ConnectRequest &msg) {
ConnectResponse ret = this->connect(msg);
if (!this->send_message(ret)) {
if (!this->send_message(ret, ConnectResponse::MESSAGE_TYPE)) {
this->on_fatal_error();
}
}
void APIServerConnection::on_disconnect_request(const DisconnectRequest &msg) {
DisconnectResponse ret = this->disconnect(msg);
if (!this->send_message(ret)) {
if (!this->send_message(ret, DisconnectResponse::MESSAGE_TYPE)) {
this->on_fatal_error();
}
}
void APIServerConnection::on_ping_request(const PingRequest &msg) {
PingResponse ret = this->ping(msg);
if (!this->send_message(ret)) {
if (!this->send_message(ret, PingResponse::MESSAGE_TYPE)) {
this->on_fatal_error();
}
}
void APIServerConnection::on_device_info_request(const DeviceInfoRequest &msg) {
if (this->check_connection_setup_()) {
DeviceInfoResponse ret = this->device_info(msg);
if (!this->send_message(ret)) {
if (!this->send_message(ret, DeviceInfoResponse::MESSAGE_TYPE)) {
this->on_fatal_error();
}
}
@@ -655,21 +657,23 @@ void APIServerConnection::on_subscribe_home_assistant_states_request(const Subsc
void APIServerConnection::on_get_time_request(const GetTimeRequest &msg) {
if (this->check_connection_setup_()) {
GetTimeResponse ret = this->get_time(msg);
if (!this->send_message(ret)) {
if (!this->send_message(ret, GetTimeResponse::MESSAGE_TYPE)) {
this->on_fatal_error();
}
}
}
#ifdef USE_API_SERVICES
void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) {
if (this->check_authenticated_()) {
this->execute_service(msg);
}
}
#endif
#ifdef USE_API_NOISE
void APIServerConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) {
if (this->check_authenticated_()) {
NoiseEncryptionSetKeyResponse ret = this->noise_encryption_set_key(msg);
if (!this->send_message(ret)) {
if (!this->send_message(ret, NoiseEncryptionSetKeyResponse::MESSAGE_TYPE)) {
this->on_fatal_error();
}
}
@@ -863,7 +867,7 @@ void APIServerConnection::on_subscribe_bluetooth_connections_free_request(
const SubscribeBluetoothConnectionsFreeRequest &msg) {
if (this->check_authenticated_()) {
BluetoothConnectionsFreeResponse ret = this->subscribe_bluetooth_connections_free(msg);
if (!this->send_message(ret)) {
if (!this->send_message(ret, BluetoothConnectionsFreeResponse::MESSAGE_TYPE)) {
this->on_fatal_error();
}
}
@@ -895,7 +899,7 @@ void APIServerConnection::on_subscribe_voice_assistant_request(const SubscribeVo
void APIServerConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) {
if (this->check_authenticated_()) {
VoiceAssistantConfigurationResponse ret = this->voice_assistant_get_configuration(msg);
if (!this->send_message(ret)) {
if (!this->send_message(ret, VoiceAssistantConfigurationResponse::MESSAGE_TYPE)) {
this->on_fatal_error();
}
}

View File

@@ -18,11 +18,11 @@ class APIServerConnectionBase : public ProtoService {
public:
#endif
template<typename T> bool send_message(const T &msg) {
bool send_message(const ProtoMessage &msg, uint8_t message_type) {
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_send_message_(msg.message_name(), msg.dump());
#endif
return this->send_message_(msg, T::MESSAGE_TYPE);
return this->send_message_(msg, message_type);
}
virtual void on_hello_request(const HelloRequest &value){};
@@ -69,7 +69,9 @@ class APIServerConnectionBase : public ProtoService {
virtual void on_get_time_request(const GetTimeRequest &value){};
virtual void on_get_time_response(const GetTimeResponse &value){};
#ifdef USE_API_SERVICES
virtual void on_execute_service_request(const ExecuteServiceRequest &value){};
#endif
#ifdef USE_CAMERA
virtual void on_camera_image_request(const CameraImageRequest &value){};
@@ -216,7 +218,9 @@ class APIServerConnection : public APIServerConnectionBase {
virtual void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) = 0;
virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0;
virtual GetTimeResponse get_time(const GetTimeRequest &msg) = 0;
#ifdef USE_API_SERVICES
virtual void execute_service(const ExecuteServiceRequest &msg) = 0;
#endif
#ifdef USE_API_NOISE
virtual NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) = 0;
#endif
@@ -333,7 +337,9 @@ class APIServerConnection : public APIServerConnectionBase {
void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &msg) override;
void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override;
void on_get_time_request(const GetTimeRequest &msg) override;
#ifdef USE_API_SERVICES
void on_execute_service_request(const ExecuteServiceRequest &msg) override;
#endif
#ifdef USE_API_NOISE
void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) override;
#endif

View File

@@ -1,359 +0,0 @@
#pragma once
#include "proto.h"
#include <cstdint>
#include <string>
namespace esphome {
namespace api {
class ProtoSize {
public:
/**
* @brief ProtoSize class for Protocol Buffer serialization size calculation
*
* This class provides static methods to calculate the exact byte counts needed
* for encoding various Protocol Buffer field types. All methods are designed to be
* efficient for the common case where many fields have default values.
*
* Implements Protocol Buffer encoding size calculation according to:
* https://protobuf.dev/programming-guides/encoding/
*
* Key features:
* - Early-return optimization for zero/default values
* - Direct total_size updates to avoid unnecessary additions
* - Specialized handling for different field types according to protobuf spec
* - Templated helpers for repeated fields and messages
*/
/**
* @brief Calculates the size in bytes needed to encode a uint32_t value as a varint
*
* @param value The uint32_t value to calculate size for
* @return The number of bytes needed to encode the value
*/
static inline uint32_t varint(uint32_t value) {
// Optimized varint size calculation using leading zeros
// Each 7 bits requires one byte in the varint encoding
if (value < 128)
return 1; // 7 bits, common case for small values
// For larger values, count bytes needed based on the position of the highest bit set
if (value < 16384) {
return 2; // 14 bits
} else if (value < 2097152) {
return 3; // 21 bits
} else if (value < 268435456) {
return 4; // 28 bits
} else {
return 5; // 32 bits (maximum for uint32_t)
}
}
/**
* @brief Calculates the size in bytes needed to encode a uint64_t value as a varint
*
* @param value The uint64_t value to calculate size for
* @return The number of bytes needed to encode the value
*/
static inline uint32_t varint(uint64_t value) {
// Handle common case of values fitting in uint32_t (vast majority of use cases)
if (value <= UINT32_MAX) {
return varint(static_cast<uint32_t>(value));
}
// For larger values, determine size based on highest bit position
if (value < (1ULL << 35)) {
return 5; // 35 bits
} else if (value < (1ULL << 42)) {
return 6; // 42 bits
} else if (value < (1ULL << 49)) {
return 7; // 49 bits
} else if (value < (1ULL << 56)) {
return 8; // 56 bits
} else if (value < (1ULL << 63)) {
return 9; // 63 bits
} else {
return 10; // 64 bits (maximum for uint64_t)
}
}
/**
* @brief Calculates the size in bytes needed to encode an int32_t value as a varint
*
* Special handling is needed for negative values, which are sign-extended to 64 bits
* in Protocol Buffers, resulting in a 10-byte varint.
*
* @param value The int32_t value to calculate size for
* @return The number of bytes needed to encode the value
*/
static inline uint32_t varint(int32_t value) {
// Negative values are sign-extended to 64 bits in protocol buffers,
// which always results in a 10-byte varint for negative int32
if (value < 0) {
return 10; // Negative int32 is always 10 bytes long
}
// For non-negative values, use the uint32_t implementation
return varint(static_cast<uint32_t>(value));
}
/**
* @brief Calculates the size in bytes needed to encode an int64_t value as a varint
*
* @param value The int64_t value to calculate size for
* @return The number of bytes needed to encode the value
*/
static inline uint32_t varint(int64_t value) {
// For int64_t, we convert to uint64_t and calculate the size
// This works because the bit pattern determines the encoding size,
// and we've handled negative int32 values as a special case above
return varint(static_cast<uint64_t>(value));
}
/**
* @brief Calculates the size in bytes needed to encode a field ID and wire type
*
* @param field_id The field identifier
* @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
*/
static inline uint32_t field(uint32_t field_id, uint32_t type) {
uint32_t tag = (field_id << 3) | (type & 0b111);
return varint(tag);
}
/**
* @brief Common parameters for all add_*_field methods
*
* All add_*_field methods follow these common patterns:
*
* @param total_size Reference to the total message size to update
* @param field_id_size Pre-calculated size of the field ID in bytes
* @param value The value to calculate size for (type varies)
* @param force Whether to calculate size even if the value is default/zero/empty
*
* Each method follows this implementation pattern:
* 1. Skip calculation if value is default (0, false, empty) and not forced
* 2. Calculate the size based on the field's encoding rules
* 3. Add the field_id_size + calculated value size to total_size
*/
/**
* @brief Calculates and adds the size of an int32 field to the total message size
*/
static inline void add_int32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value, bool force = false) {
// Skip calculation if value is zero and not forced
if (value == 0 && !force) {
return; // No need to update total_size
}
// Calculate and directly add to total_size
if (value < 0) {
// Negative values are encoded as 10-byte varints in protobuf
total_size += field_id_size + 10;
} else {
// For non-negative values, use the standard varint size
total_size += field_id_size + varint(static_cast<uint32_t>(value));
}
}
/**
* @brief Calculates and adds the size of a uint32 field to the total message size
*/
static inline void add_uint32_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value,
bool force = false) {
// Skip calculation if value is zero and not forced
if (value == 0 && !force) {
return; // No need to update total_size
}
// Calculate and directly add to total_size
total_size += field_id_size + varint(value);
}
/**
* @brief Calculates and adds the size of a boolean field to the total message size
*/
static inline void add_bool_field(uint32_t &total_size, uint32_t field_id_size, bool value, bool force = false) {
// Skip calculation if value is false and not forced
if (!value && !force) {
return; // No need to update total_size
}
// Boolean fields always use 1 byte when true
total_size += field_id_size + 1;
}
/**
* @brief Calculates and adds the size of a fixed field to the total message size
*
* Fixed fields always take exactly N bytes (4 for fixed32/float, 8 for fixed64/double).
*
* @tparam NumBytes The number of bytes for this fixed field (4 or 8)
* @param is_nonzero Whether the value is non-zero
*/
template<uint32_t NumBytes>
static inline void add_fixed_field(uint32_t &total_size, uint32_t field_id_size, bool is_nonzero,
bool force = false) {
// Skip calculation if value is zero and not forced
if (!is_nonzero && !force) {
return; // No need to update total_size
}
// Fixed fields always take exactly NumBytes
total_size += field_id_size + NumBytes;
}
/**
* @brief Calculates and adds the size of an enum field to the total message size
*
* Enum fields are encoded as uint32 varints.
*/
static inline void add_enum_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value, bool force = false) {
// Skip calculation if value is zero and not forced
if (value == 0 && !force) {
return; // No need to update total_size
}
// Enums are encoded as uint32
total_size += field_id_size + varint(value);
}
/**
* @brief Calculates and adds the size of a sint32 field to the total message size
*
* Sint32 fields use ZigZag encoding, which is more efficient for negative values.
*/
static inline void add_sint32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value, bool force = false) {
// Skip calculation if value is zero and not forced
if (value == 0 && !force) {
return; // No need to update total_size
}
// ZigZag encoding for sint32: (n << 1) ^ (n >> 31)
uint32_t zigzag = (static_cast<uint32_t>(value) << 1) ^ (static_cast<uint32_t>(value >> 31));
total_size += field_id_size + varint(zigzag);
}
/**
* @brief Calculates and adds the size of an int64 field to the total message size
*/
static inline void add_int64_field(uint32_t &total_size, uint32_t field_id_size, int64_t value, bool force = false) {
// Skip calculation if value is zero and not forced
if (value == 0 && !force) {
return; // No need to update total_size
}
// Calculate and directly add to total_size
total_size += field_id_size + varint(value);
}
/**
* @brief Calculates and adds the size of a uint64 field to the total message size
*/
static inline void add_uint64_field(uint32_t &total_size, uint32_t field_id_size, uint64_t value,
bool force = false) {
// Skip calculation if value is zero and not forced
if (value == 0 && !force) {
return; // No need to update total_size
}
// Calculate and directly add to total_size
total_size += field_id_size + varint(value);
}
/**
* @brief Calculates and adds the size of a sint64 field to the total message size
*
* Sint64 fields use ZigZag encoding, which is more efficient for negative values.
*/
static inline void add_sint64_field(uint32_t &total_size, uint32_t field_id_size, int64_t value, bool force = false) {
// Skip calculation if value is zero and not forced
if (value == 0 && !force) {
return; // No need to update total_size
}
// ZigZag encoding for sint64: (n << 1) ^ (n >> 63)
uint64_t zigzag = (static_cast<uint64_t>(value) << 1) ^ (static_cast<uint64_t>(value >> 63));
total_size += field_id_size + varint(zigzag);
}
/**
* @brief Calculates and adds the size of a string/bytes field to the total message size
*/
static inline void add_string_field(uint32_t &total_size, uint32_t field_id_size, const std::string &str,
bool force = false) {
// Skip calculation if string is empty and not forced
if (str.empty() && !force) {
return; // No need to update total_size
}
// Calculate and directly add to total_size
const uint32_t str_size = static_cast<uint32_t>(str.size());
total_size += field_id_size + varint(str_size) + str_size;
}
/**
* @brief Calculates and adds the size of a nested message field to the total message size
*
* This helper function directly updates the total_size reference if the nested size
* is greater than zero or force is true.
*
* @param nested_size The pre-calculated size of the nested message
*/
static inline void add_message_field(uint32_t &total_size, uint32_t field_id_size, uint32_t nested_size,
bool force = false) {
// Skip calculation if nested message is empty and not forced
if (nested_size == 0 && !force) {
return; // No need to update total_size
}
// Calculate and directly add to total_size
// Field ID + length varint + nested message content
total_size += field_id_size + varint(nested_size) + nested_size;
}
/**
* @brief Calculates and adds the size of a nested message field to the total message size
*
* This version takes a ProtoMessage object, calculates its size internally,
* and updates the total_size reference. This eliminates the need for a temporary variable
* at the call site.
*
* @param message The nested message object
*/
static inline void add_message_object(uint32_t &total_size, uint32_t field_id_size, const ProtoMessage &message,
bool force = false) {
uint32_t nested_size = 0;
message.calculate_size(nested_size);
// Use the base implementation with the calculated nested_size
add_message_field(total_size, field_id_size, nested_size, force);
}
/**
* @brief Calculates and adds the sizes of all messages in a repeated field to the total message size
*
* This helper processes a vector of message objects, calculating the size for each message
* and adding it to the total size.
*
* @tparam MessageType The type of the nested messages in the vector
* @param messages Vector of message objects
*/
template<typename MessageType>
static inline void add_repeated_message(uint32_t &total_size, uint32_t field_id_size,
const std::vector<MessageType> &messages) {
// Skip if the vector is empty
if (messages.empty()) {
return;
}
// For repeated fields, always use force=true
for (const auto &message : messages) {
add_message_object(total_size, field_id_size, message, true);
}
}
};
} // namespace api
} // namespace esphome

View File

@@ -24,14 +24,6 @@ static const char *const TAG = "api";
// APIServer
APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
#ifndef USE_API_YAML_SERVICES
// Global empty vector to avoid guard variables (saves 8 bytes)
// This is initialized at program startup before any threads
static const std::vector<UserServiceDescriptor *> empty_user_services{};
const std::vector<UserServiceDescriptor *> &get_empty_user_services_instance() { return empty_user_services; }
#endif
APIServer::APIServer() {
global_api_server = this;
// Pre-allocate shared write buffer
@@ -39,7 +31,6 @@ APIServer::APIServer() {
}
void APIServer::setup() {
ESP_LOGCONFIG(TAG, "Running setup");
this->setup_controller();
#ifdef USE_API_NOISE
@@ -113,7 +104,7 @@ void APIServer::setup() {
return;
}
for (auto &c : this->clients_) {
if (!c->flags_.remove)
if (!c->flags_.remove && c->get_log_subscription_level() >= level)
c->try_send_log_message(level, tag, message, message_len);
}
});
@@ -213,22 +204,20 @@ void APIServer::loop() {
void APIServer::dump_config() {
ESP_LOGCONFIG(TAG,
"API Server:\n"
"Server:\n"
" Address: %s:%u",
network::get_use_address().c_str(), this->port_);
#ifdef USE_API_NOISE
ESP_LOGCONFIG(TAG, " Using noise encryption: %s", YESNO(this->noise_ctx_->has_psk()));
ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_->has_psk()));
if (!this->noise_ctx_->has_psk()) {
ESP_LOGCONFIG(TAG, " Supports noise encryption: YES");
ESP_LOGCONFIG(TAG, " Supports encryption: YES");
}
#else
ESP_LOGCONFIG(TAG, " Using noise encryption: NO");
ESP_LOGCONFIG(TAG, " Noise encryption: NO");
#endif
}
#ifdef USE_API_PASSWORD
bool APIServer::uses_password() const { return !this->password_.empty(); }
bool APIServer::check_password(const std::string &password) const {
// depend only on input password length
const char *a = this->password_.c_str();
@@ -436,10 +425,11 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
ESP_LOGD(TAG, "Noise PSK saved");
if (make_active) {
this->set_timeout(100, [this, psk]() {
ESP_LOGW(TAG, "Disconnecting all clients to reset connections");
ESP_LOGW(TAG, "Disconnecting all clients to reset PSK");
this->set_noise_psk(psk);
for (auto &c : this->clients_) {
c->send_message(DisconnectRequest());
DisconnectRequest req;
c->send_message(req, DisconnectRequest::MESSAGE_TYPE);
}
});
}
@@ -472,10 +462,12 @@ void APIServer::on_shutdown() {
// Send disconnect requests to all connected clients
for (auto &c : this->clients_) {
if (!c->send_message(DisconnectRequest())) {
DisconnectRequest req;
if (!c->send_message(req, DisconnectRequest::MESSAGE_TYPE)) {
// If we can't send the disconnect request directly (tx_buffer full),
// schedule it at the front of the batch so it will be sent with priority
c->schedule_message_front_(nullptr, &APIConnection::try_send_disconnect_request, DisconnectRequest::MESSAGE_TYPE);
c->schedule_message_front_(nullptr, &APIConnection::try_send_disconnect_request, DisconnectRequest::MESSAGE_TYPE,
DisconnectRequest::ESTIMATED_SIZE);
}
}
}

View File

@@ -12,7 +12,9 @@
#include "esphome/core/log.h"
#include "list_entities.h"
#include "subscribe_state.h"
#ifdef USE_API_SERVICES
#include "user_services.h"
#endif
#include <vector>
@@ -25,11 +27,6 @@ struct SavedNoisePsk {
} PACKED; // NOLINT
#endif
#ifndef USE_API_YAML_SERVICES
// Forward declaration of helper function
const std::vector<UserServiceDescriptor *> &get_empty_user_services_instance();
#endif
class APIServer : public Component, public Controller {
public:
APIServer();
@@ -42,7 +39,6 @@ class APIServer : public Component, public Controller {
bool teardown() override;
#ifdef USE_API_PASSWORD
bool check_password(const std::string &password) const;
bool uses_password() const;
void set_password(const std::string &password);
#endif
void set_port(uint16_t port);
@@ -112,18 +108,9 @@ class APIServer : public Component, public Controller {
void on_media_player_update(media_player::MediaPlayer *obj) override;
#endif
void send_homeassistant_service_call(const HomeassistantServiceResponse &call);
void register_user_service(UserServiceDescriptor *descriptor) {
#ifdef USE_API_YAML_SERVICES
// Vector is pre-allocated when services are defined in YAML
this->user_services_.push_back(descriptor);
#else
// Lazy allocate vector on first use for CustomAPIDevice
if (!this->user_services_) {
this->user_services_ = std::make_unique<std::vector<UserServiceDescriptor *>>();
}
this->user_services_->push_back(descriptor);
#ifdef USE_API_SERVICES
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
#endif
}
#ifdef USE_HOMEASSISTANT_TIME
void request_time();
#endif
@@ -152,17 +139,9 @@ class APIServer : public Component, public Controller {
void get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(std::string)> f);
const std::vector<HomeAssistantStateSubscription> &get_state_subs() const;
const std::vector<UserServiceDescriptor *> &get_user_services() const {
#ifdef USE_API_YAML_SERVICES
return this->user_services_;
#else
if (this->user_services_) {
return *this->user_services_;
}
// Return reference to global empty instance (no guard needed)
return get_empty_user_services_instance();
#ifdef USE_API_SERVICES
const std::vector<UserServiceDescriptor *> &get_user_services() const { return this->user_services_; }
#endif
}
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
Trigger<std::string, std::string> *get_client_connected_trigger() const { return this->client_connected_trigger_; }
@@ -194,14 +173,8 @@ class APIServer : public Component, public Controller {
#endif
std::vector<uint8_t> shared_write_buffer_; // Shared proto write buffer for all connections
std::vector<HomeAssistantStateSubscription> state_subs_;
#ifdef USE_API_YAML_SERVICES
// When services are defined in YAML, we know at compile time that services will be registered
#ifdef USE_API_SERVICES
std::vector<UserServiceDescriptor *> user_services_;
#else
// Services can still be registered at runtime by CustomAPIDevice components even when not
// defined in YAML. Using unique_ptr allows lazy allocation, saving 12 bytes in the common
// case where no services (YAML or custom) are used.
std::unique_ptr<std::vector<UserServiceDescriptor *>> user_services_;
#endif
// Group smaller types together

View File

@@ -3,10 +3,13 @@
#include <map>
#include "api_server.h"
#ifdef USE_API
#ifdef USE_API_SERVICES
#include "user_services.h"
#endif
namespace esphome {
namespace api {
#ifdef USE_API_SERVICES
template<typename T, typename... Ts> class CustomAPIDeviceService : public UserServiceBase<Ts...> {
public:
CustomAPIDeviceService(const std::string &name, const std::array<std::string, sizeof...(Ts)> &arg_names, T *obj,
@@ -19,6 +22,7 @@ template<typename T, typename... Ts> class CustomAPIDeviceService : public UserS
T *obj_;
void (T::*callback_)(Ts...);
};
#endif // USE_API_SERVICES
class CustomAPIDevice {
public:
@@ -46,12 +50,14 @@ class CustomAPIDevice {
* @param name The name of the service to register.
* @param arg_names The name of the arguments for the service, must match the arguments of the function.
*/
#ifdef USE_API_SERVICES
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) {
auto *service = new CustomAPIDeviceService<T, Ts...>(name, arg_names, (T *) this, callback); // NOLINT
global_api_server->register_user_service(service);
}
#endif
/** Register a custom native API service that will show up in Home Assistant.
*
@@ -71,10 +77,12 @@ class CustomAPIDevice {
* @param callback The member function to call when the service is triggered.
* @param name The name of the arguments for the service, must match the arguments of the function.
*/
#ifdef USE_API_SERVICES
template<typename T> void register_service(void (T::*callback)(), const std::string &name) {
auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback); // NOLINT
global_api_server->register_user_service(service);
}
#endif
/** Subscribe to the state (or attribute state) of an entity from Home Assistant.
*

View File

@@ -11,6 +11,18 @@ namespace esphome {
namespace api {
template<typename... X> class TemplatableStringValue : public TemplatableValue<std::string, X...> {
private:
// Helper to convert value to string - handles the case where value is already a string
template<typename T> static std::string value_to_string(T &&val) { return to_string(std::forward<T>(val)); }
// Overloads for string types - needed because std::to_string doesn't support them
static std::string value_to_string(char *val) {
return val ? std::string(val) : std::string();
} // For lambdas returning char* (e.g., itoa)
static std::string value_to_string(const char *val) { return std::string(val); } // For lambdas returning .c_str()
static std::string value_to_string(const std::string &val) { return val; }
static std::string value_to_string(std::string &&val) { return std::move(val); }
public:
TemplatableStringValue() : TemplatableValue<std::string, X...>() {}
@@ -19,7 +31,7 @@ template<typename... X> class TemplatableStringValue : public TemplatableValue<s
template<typename F, enable_if_t<is_invocable<F, X...>::value, int> = 0>
TemplatableStringValue(F f)
: TemplatableValue<std::string, X...>([f](X... x) -> std::string { return to_string(f(x...)); }) {}
: TemplatableValue<std::string, X...>([f](X... x) -> std::string { return value_to_string(f(x...)); }) {}
};
template<typename... Ts> class TemplatableKeyValuePair {

View File

@@ -83,10 +83,12 @@ bool ListEntitiesIterator::on_end() { return this->client_->send_list_info_done(
ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(client) {}
#ifdef USE_API_SERVICES
bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) {
auto resp = service->encode_list_service_response();
return this->client_->send_message(resp);
return this->client_->send_message(resp, ListEntitiesServicesResponse::MESSAGE_TYPE);
}
#endif
} // namespace api
} // namespace esphome

View File

@@ -14,7 +14,7 @@ class APIConnection;
#define LIST_ENTITIES_HANDLER(entity_type, EntityClass, ResponseType) \
bool ListEntitiesIterator::on_##entity_type(EntityClass *entity) { /* NOLINT(bugprone-macro-parentheses) */ \
return this->client_->schedule_message_(entity, &APIConnection::try_send_##entity_type##_info, \
ResponseType::MESSAGE_TYPE); \
ResponseType::MESSAGE_TYPE, ResponseType::ESTIMATED_SIZE); \
}
class ListEntitiesIterator : public ComponentIterator {
@@ -44,7 +44,9 @@ class ListEntitiesIterator : public ComponentIterator {
#ifdef USE_TEXT_SENSOR
bool on_text_sensor(text_sensor::TextSensor *entity) override;
#endif
#ifdef USE_API_SERVICES
bool on_service(UserServiceDescriptor *service) override;
#endif
#ifdef USE_CAMERA
bool on_camera(camera::Camera *entity) override;
#endif

View File

@@ -8,7 +8,7 @@ namespace api {
static const char *const TAG = "api.proto";
void ProtoMessage::decode(const uint8_t *buffer, size_t length) {
void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
uint32_t i = 0;
bool error = false;
while (i < length) {

View File

@@ -4,6 +4,7 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <cassert>
#include <vector>
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
@@ -59,7 +60,6 @@ class ProtoVarInt {
uint32_t as_uint32() const { return this->value_; }
uint64_t as_uint64() const { return this->value_; }
bool as_bool() const { return this->value_; }
template<typename T> T as_enum() const { return static_cast<T>(this->as_uint32()); }
int32_t as_int32() const {
// Not ZigZag encoded
return static_cast<int32_t>(this->as_int64());
@@ -133,15 +133,25 @@ class ProtoVarInt {
uint64_t value_;
};
// Forward declaration for decode_to_message and encode_to_writer
class ProtoMessage;
class ProtoDecodableMessage;
class ProtoLengthDelimited {
public:
explicit ProtoLengthDelimited(const uint8_t *value, size_t length) : value_(value), length_(length) {}
std::string as_string() const { return std::string(reinterpret_cast<const char *>(this->value_), this->length_); }
template<class C> C as_message() const {
auto msg = C();
msg.decode(this->value_, this->length_);
return msg;
}
/**
* Decode the length-delimited data into an existing ProtoDecodableMessage instance.
*
* This method allows decoding without templates, enabling use in contexts
* where the message type is not known at compile time. The ProtoDecodableMessage's
* decode() method will be called with the raw data and length.
*
* @param msg The ProtoDecodableMessage instance to decode into
*/
void decode_to_message(ProtoDecodableMessage &msg) const;
protected:
const uint8_t *const value_;
@@ -166,23 +176,7 @@ class Proto32Bit {
const uint32_t value_;
};
class Proto64Bit {
public:
explicit Proto64Bit(uint64_t value) : value_(value) {}
uint64_t as_fixed64() const { return this->value_; }
int64_t as_sfixed64() const { return static_cast<int64_t>(this->value_); }
double as_double() const {
union {
uint64_t raw;
double value;
} s{};
s.raw = this->value_;
return s.value;
}
protected:
const uint64_t value_;
};
// NOTE: Proto64Bit class removed - wire type 1 (64-bit fixed) not supported
class ProtoWriteBuffer {
public:
@@ -196,9 +190,9 @@ class ProtoWriteBuffer {
* @param field_id Field number (tag) in the protobuf message
* @param type Wire type value:
* - 0: Varint (int32, int64, uint32, uint64, sint32, sint64, bool, enum)
* - 1: 64-bit (fixed64, sfixed64, double)
* - 2: Length-delimited (string, bytes, embedded messages, packed repeated fields)
* - 5: 32-bit (fixed32, sfixed32, float)
* - Note: Wire type 1 (64-bit fixed) is not supported
*
* Following https://protobuf.dev/programming-guides/encoding/#structure
*/
@@ -249,23 +243,10 @@ class ProtoWriteBuffer {
this->write((value >> 16) & 0xFF);
this->write((value >> 24) & 0xFF);
}
void encode_fixed64(uint32_t field_id, uint64_t value, bool force = false) {
if (value == 0 && !force)
return;
this->encode_field_raw(field_id, 1); // type 1: 64-bit fixed64
this->write((value >> 0) & 0xFF);
this->write((value >> 8) & 0xFF);
this->write((value >> 16) & 0xFF);
this->write((value >> 24) & 0xFF);
this->write((value >> 32) & 0xFF);
this->write((value >> 40) & 0xFF);
this->write((value >> 48) & 0xFF);
this->write((value >> 56) & 0xFF);
}
template<typename T> void encode_enum(uint32_t field_id, T value, bool force = false) {
this->encode_uint32(field_id, static_cast<uint32_t>(value), force);
}
// NOTE: Wire type 1 (64-bit fixed: double, fixed64, sfixed64) is intentionally
// not supported to reduce overhead on embedded systems. All ESPHome devices are
// 32-bit microcontrollers where 64-bit operations are expensive. If 64-bit support
// is needed in the future, the necessary encoding/decoding functions must be added.
void encode_float(uint32_t field_id, float value, bool force = false) {
if (value == 0.0f && !force)
return;
@@ -306,18 +287,7 @@ class ProtoWriteBuffer {
}
this->encode_uint64(field_id, uvalue, force);
}
template<class C> void encode_message(uint32_t field_id, const C &value, bool force = false) {
this->encode_field_raw(field_id, 2); // type 2: Length-delimited message
size_t begin = this->buffer_->size();
value.encode(*this);
const uint32_t nested_length = this->buffer_->size() - begin;
// add size varint
std::vector<uint8_t> var;
ProtoVarInt(nested_length).encode(var);
this->buffer_->insert(this->buffer_->begin() + begin, var.begin(), var.end());
}
void encode_message(uint32_t field_id, const ProtoMessage &value, bool force = false);
std::vector<uint8_t> *get_buffer() const { return buffer_; }
protected:
@@ -329,7 +299,6 @@ class ProtoMessage {
virtual ~ProtoMessage() = default;
// Default implementation for messages with no fields
virtual void encode(ProtoWriteBuffer buffer) const {}
void decode(const uint8_t *buffer, size_t length);
// Default implementation for messages with no fields
virtual void calculate_size(uint32_t &total_size) const {}
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -337,14 +306,519 @@ class ProtoMessage {
virtual void dump_to(std::string &out) const = 0;
virtual const char *message_name() const { return "unknown"; }
#endif
};
// Base class for messages that support decoding
class ProtoDecodableMessage : public ProtoMessage {
public:
void decode(const uint8_t *buffer, size_t length);
protected:
virtual bool decode_varint(uint32_t field_id, ProtoVarInt value) { return false; }
virtual bool decode_length(uint32_t field_id, ProtoLengthDelimited value) { return false; }
virtual bool decode_32bit(uint32_t field_id, Proto32Bit value) { return false; }
virtual bool decode_64bit(uint32_t field_id, Proto64Bit value) { return false; }
// NOTE: decode_64bit removed - wire type 1 not supported
};
class ProtoSize {
public:
/**
* @brief ProtoSize class for Protocol Buffer serialization size calculation
*
* This class provides static methods to calculate the exact byte counts needed
* for encoding various Protocol Buffer field types. All methods are designed to be
* efficient for the common case where many fields have default values.
*
* Implements Protocol Buffer encoding size calculation according to:
* https://protobuf.dev/programming-guides/encoding/
*
* Key features:
* - Early-return optimization for zero/default values
* - Direct total_size updates to avoid unnecessary additions
* - Specialized handling for different field types according to protobuf spec
* - Templated helpers for repeated fields and messages
*/
/**
* @brief Calculates the size in bytes needed to encode a uint32_t value as a varint
*
* @param value The uint32_t value to calculate size for
* @return The number of bytes needed to encode the value
*/
static inline uint32_t varint(uint32_t value) {
// Optimized varint size calculation using leading zeros
// Each 7 bits requires one byte in the varint encoding
if (value < 128)
return 1; // 7 bits, common case for small values
// For larger values, count bytes needed based on the position of the highest bit set
if (value < 16384) {
return 2; // 14 bits
} else if (value < 2097152) {
return 3; // 21 bits
} else if (value < 268435456) {
return 4; // 28 bits
} else {
return 5; // 32 bits (maximum for uint32_t)
}
}
/**
* @brief Calculates the size in bytes needed to encode a uint64_t value as a varint
*
* @param value The uint64_t value to calculate size for
* @return The number of bytes needed to encode the value
*/
static inline uint32_t varint(uint64_t value) {
// Handle common case of values fitting in uint32_t (vast majority of use cases)
if (value <= UINT32_MAX) {
return varint(static_cast<uint32_t>(value));
}
// For larger values, determine size based on highest bit position
if (value < (1ULL << 35)) {
return 5; // 35 bits
} else if (value < (1ULL << 42)) {
return 6; // 42 bits
} else if (value < (1ULL << 49)) {
return 7; // 49 bits
} else if (value < (1ULL << 56)) {
return 8; // 56 bits
} else if (value < (1ULL << 63)) {
return 9; // 63 bits
} else {
return 10; // 64 bits (maximum for uint64_t)
}
}
/**
* @brief Calculates the size in bytes needed to encode an int32_t value as a varint
*
* Special handling is needed for negative values, which are sign-extended to 64 bits
* in Protocol Buffers, resulting in a 10-byte varint.
*
* @param value The int32_t value to calculate size for
* @return The number of bytes needed to encode the value
*/
static inline uint32_t varint(int32_t value) {
// Negative values are sign-extended to 64 bits in protocol buffers,
// which always results in a 10-byte varint for negative int32
if (value < 0) {
return 10; // Negative int32 is always 10 bytes long
}
// For non-negative values, use the uint32_t implementation
return varint(static_cast<uint32_t>(value));
}
/**
* @brief Calculates the size in bytes needed to encode an int64_t value as a varint
*
* @param value The int64_t value to calculate size for
* @return The number of bytes needed to encode the value
*/
static inline uint32_t varint(int64_t value) {
// For int64_t, we convert to uint64_t and calculate the size
// This works because the bit pattern determines the encoding size,
// and we've handled negative int32 values as a special case above
return varint(static_cast<uint64_t>(value));
}
/**
* @brief Calculates the size in bytes needed to encode a field ID and wire type
*
* @param field_id The field identifier
* @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
*/
static inline uint32_t field(uint32_t field_id, uint32_t type) {
uint32_t tag = (field_id << 3) | (type & 0b111);
return varint(tag);
}
/**
* @brief Common parameters for all add_*_field methods
*
* All add_*_field methods follow these common patterns:
*
* @param total_size Reference to the total message size to update
* @param field_id_size Pre-calculated size of the field ID in bytes
* @param value The value to calculate size for (type varies)
* @param force Whether to calculate size even if the value is default/zero/empty
*
* Each method follows this implementation pattern:
* 1. Skip calculation if value is default (0, false, empty) and not forced
* 2. Calculate the size based on the field's encoding rules
* 3. Add the field_id_size + calculated value size to total_size
*/
/**
* @brief Calculates and adds the size of an int32 field to the total message size
*/
static inline void add_int32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value) {
// Skip calculation if value is zero
if (value == 0) {
return; // No need to update total_size
}
// Calculate and directly add to total_size
if (value < 0) {
// Negative values are encoded as 10-byte varints in protobuf
total_size += field_id_size + 10;
} else {
// For non-negative values, use the standard varint size
total_size += field_id_size + varint(static_cast<uint32_t>(value));
}
}
/**
* @brief Calculates and adds the size of an int32 field to the total message size (repeated field version)
*/
static inline void add_int32_field_repeated(uint32_t &total_size, uint32_t field_id_size, int32_t value) {
// Always calculate size for repeated fields
if (value < 0) {
// Negative values are encoded as 10-byte varints in protobuf
total_size += field_id_size + 10;
} else {
// For non-negative values, use the standard varint size
total_size += field_id_size + varint(static_cast<uint32_t>(value));
}
}
/**
* @brief Calculates and adds the size of a uint32 field to the total message size
*/
static inline void add_uint32_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value) {
// Skip calculation if value is zero
if (value == 0) {
return; // No need to update total_size
}
// Calculate and directly add to total_size
total_size += field_id_size + varint(value);
}
/**
* @brief Calculates and adds the size of a uint32 field to the total message size (repeated field version)
*/
static inline void add_uint32_field_repeated(uint32_t &total_size, uint32_t field_id_size, uint32_t value) {
// Always calculate size for repeated fields
total_size += field_id_size + varint(value);
}
/**
* @brief Calculates and adds the size of a boolean field to the total message size
*/
static inline void add_bool_field(uint32_t &total_size, uint32_t field_id_size, bool value) {
// Skip calculation if value is false
if (!value) {
return; // No need to update total_size
}
// Boolean fields always use 1 byte when true
total_size += field_id_size + 1;
}
/**
* @brief Calculates and adds the size of a boolean field to the total message size (repeated field version)
*/
static inline void add_bool_field_repeated(uint32_t &total_size, uint32_t field_id_size, bool value) {
// Always calculate size for repeated fields
// Boolean fields always use 1 byte
total_size += field_id_size + 1;
}
/**
* @brief Calculates and adds the size of a fixed field to the total message size
*
* Fixed fields always take exactly N bytes (4 for fixed32/float, 8 for fixed64/double).
*
* @tparam NumBytes The number of bytes for this fixed field (4 or 8)
* @param is_nonzero Whether the value is non-zero
*/
template<uint32_t NumBytes>
static inline void add_fixed_field(uint32_t &total_size, uint32_t field_id_size, bool is_nonzero) {
// Skip calculation if value is zero
if (!is_nonzero) {
return; // No need to update total_size
}
// Fixed fields always take exactly NumBytes
total_size += field_id_size + NumBytes;
}
/**
* @brief Calculates and adds the size of a float field to the total message size
*/
static inline void add_float_field(uint32_t &total_size, uint32_t field_id_size, float value) {
if (value != 0.0f) {
total_size += field_id_size + 4;
}
}
// NOTE: add_double_field removed - wire type 1 (64-bit: double) not supported
// to reduce overhead on embedded systems
/**
* @brief Calculates and adds the size of a fixed32 field to the total message size
*/
static inline void add_fixed32_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value) {
if (value != 0) {
total_size += field_id_size + 4;
}
}
// NOTE: add_fixed64_field removed - wire type 1 (64-bit: fixed64) not supported
// to reduce overhead on embedded systems
/**
* @brief Calculates and adds the size of a sfixed32 field to the total message size
*/
static inline void add_sfixed32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value) {
if (value != 0) {
total_size += field_id_size + 4;
}
}
// NOTE: add_sfixed64_field removed - wire type 1 (64-bit: sfixed64) not supported
// to reduce overhead on embedded systems
/**
* @brief Calculates and adds the size of an enum field to the total message size
*
* Enum fields are encoded as uint32 varints.
*/
static inline void add_enum_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value) {
// Skip calculation if value is zero
if (value == 0) {
return; // No need to update total_size
}
// Enums are encoded as uint32
total_size += field_id_size + varint(value);
}
/**
* @brief Calculates and adds the size of an enum field to the total message size (repeated field version)
*
* Enum fields are encoded as uint32 varints.
*/
static inline void add_enum_field_repeated(uint32_t &total_size, uint32_t field_id_size, uint32_t value) {
// Always calculate size for repeated fields
// Enums are encoded as uint32
total_size += field_id_size + varint(value);
}
/**
* @brief Calculates and adds the size of a sint32 field to the total message size
*
* Sint32 fields use ZigZag encoding, which is more efficient for negative values.
*/
static inline void add_sint32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value) {
// Skip calculation if value is zero
if (value == 0) {
return; // No need to update total_size
}
// ZigZag encoding for sint32: (n << 1) ^ (n >> 31)
uint32_t zigzag = (static_cast<uint32_t>(value) << 1) ^ (static_cast<uint32_t>(value >> 31));
total_size += field_id_size + varint(zigzag);
}
/**
* @brief Calculates and adds the size of a sint32 field to the total message size (repeated field version)
*
* Sint32 fields use ZigZag encoding, which is more efficient for negative values.
*/
static inline void add_sint32_field_repeated(uint32_t &total_size, uint32_t field_id_size, int32_t value) {
// Always calculate size for repeated fields
// ZigZag encoding for sint32: (n << 1) ^ (n >> 31)
uint32_t zigzag = (static_cast<uint32_t>(value) << 1) ^ (static_cast<uint32_t>(value >> 31));
total_size += field_id_size + varint(zigzag);
}
/**
* @brief Calculates and adds the size of an int64 field to the total message size
*/
static inline void add_int64_field(uint32_t &total_size, uint32_t field_id_size, int64_t value) {
// Skip calculation if value is zero
if (value == 0) {
return; // No need to update total_size
}
// Calculate and directly add to total_size
total_size += field_id_size + varint(value);
}
/**
* @brief Calculates and adds the size of an int64 field to the total message size (repeated field version)
*/
static inline void add_int64_field_repeated(uint32_t &total_size, uint32_t field_id_size, int64_t value) {
// Always calculate size for repeated fields
total_size += field_id_size + varint(value);
}
/**
* @brief Calculates and adds the size of a uint64 field to the total message size
*/
static inline void add_uint64_field(uint32_t &total_size, uint32_t field_id_size, uint64_t value) {
// Skip calculation if value is zero
if (value == 0) {
return; // No need to update total_size
}
// Calculate and directly add to total_size
total_size += field_id_size + varint(value);
}
/**
* @brief Calculates and adds the size of a uint64 field to the total message size (repeated field version)
*/
static inline void add_uint64_field_repeated(uint32_t &total_size, uint32_t field_id_size, uint64_t value) {
// Always calculate size for repeated fields
total_size += field_id_size + varint(value);
}
// NOTE: sint64 support functions (add_sint64_field, add_sint64_field_repeated) removed
// sint64 type is not supported by ESPHome API to reduce overhead on embedded systems
/**
* @brief Calculates and adds the size of a string/bytes field to the total message size
*/
static inline void add_string_field(uint32_t &total_size, uint32_t field_id_size, const std::string &str) {
// Skip calculation if string is empty
if (str.empty()) {
return; // No need to update total_size
}
// Calculate and directly add to total_size
const uint32_t str_size = static_cast<uint32_t>(str.size());
total_size += field_id_size + varint(str_size) + str_size;
}
/**
* @brief Calculates and adds the size of a string/bytes field to the total message size (repeated field version)
*/
static inline void add_string_field_repeated(uint32_t &total_size, uint32_t field_id_size, const std::string &str) {
// Always calculate size for repeated fields
const uint32_t str_size = static_cast<uint32_t>(str.size());
total_size += field_id_size + varint(str_size) + str_size;
}
/**
* @brief Calculates and adds the size of a nested message field to the total message size
*
* This helper function directly updates the total_size reference if the nested size
* is greater than zero.
*
* @param nested_size The pre-calculated size of the nested message
*/
static inline void add_message_field(uint32_t &total_size, uint32_t field_id_size, uint32_t nested_size) {
// Skip calculation if nested message is empty
if (nested_size == 0) {
return; // No need to update total_size
}
// Calculate and directly add to total_size
// Field ID + length varint + nested message content
total_size += field_id_size + varint(nested_size) + nested_size;
}
/**
* @brief Calculates and adds the size of a nested message field to the total message size (repeated field version)
*
* @param nested_size The pre-calculated size of the nested message
*/
static inline void add_message_field_repeated(uint32_t &total_size, uint32_t field_id_size, uint32_t nested_size) {
// Always calculate size for repeated fields
// Field ID + length varint + nested message content
total_size += field_id_size + varint(nested_size) + nested_size;
}
/**
* @brief Calculates and adds the size of a nested message field to the total message size
*
* This version takes a ProtoMessage object, calculates its size internally,
* and updates the total_size reference. This eliminates the need for a temporary variable
* at the call site.
*
* @param message The nested message object
*/
static inline void add_message_object(uint32_t &total_size, uint32_t field_id_size, const ProtoMessage &message) {
uint32_t nested_size = 0;
message.calculate_size(nested_size);
// Use the base implementation with the calculated nested_size
add_message_field(total_size, field_id_size, nested_size);
}
/**
* @brief Calculates and adds the size of a nested message field to the total message size (repeated field version)
*
* @param message The nested message object
*/
static inline void add_message_object_repeated(uint32_t &total_size, uint32_t field_id_size,
const ProtoMessage &message) {
uint32_t nested_size = 0;
message.calculate_size(nested_size);
// Use the base implementation with the calculated nested_size
add_message_field_repeated(total_size, field_id_size, nested_size);
}
/**
* @brief Calculates and adds the sizes of all messages in a repeated field to the total message size
*
* This helper processes a vector of message objects, calculating the size for each message
* and adding it to the total size.
*
* @tparam MessageType The type of the nested messages in the vector
* @param messages Vector of message objects
*/
template<typename MessageType>
static inline void add_repeated_message(uint32_t &total_size, uint32_t field_id_size,
const std::vector<MessageType> &messages) {
// Skip if the vector is empty
if (messages.empty()) {
return;
}
// Use the repeated field version for all messages
for (const auto &message : messages) {
add_message_object_repeated(total_size, field_id_size, message);
}
}
};
// Implementation of encode_message - must be after ProtoMessage is defined
inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value, bool force) {
this->encode_field_raw(field_id, 2); // type 2: Length-delimited message
// Calculate the message size first
uint32_t msg_length_bytes = 0;
value.calculate_size(msg_length_bytes);
// Calculate how many bytes the length varint needs
uint32_t varint_length_bytes = ProtoSize::varint(msg_length_bytes);
// Reserve exact space for the length varint
size_t begin = this->buffer_->size();
this->buffer_->resize(this->buffer_->size() + varint_length_bytes);
// Write the length varint directly
ProtoVarInt(msg_length_bytes).encode_to_buffer_unchecked(this->buffer_->data() + begin, varint_length_bytes);
// Now encode the message content - it will append to the buffer
value.encode(*this);
// Verify that the encoded size matches what we calculated
assert(this->buffer_->size() == begin + varint_length_bytes + msg_length_bytes);
}
// Implementation of decode_to_message - must be after ProtoDecodableMessage is defined
inline void ProtoLengthDelimited::decode_to_message(ProtoDecodableMessage &msg) const {
msg.decode(this->value_, this->length_);
}
template<typename T> const char *proto_enum_to_string(T value);
class ProtoService {
@@ -363,11 +837,11 @@ class ProtoService {
* @return A ProtoWriteBuffer object with the reserved size.
*/
virtual ProtoWriteBuffer create_buffer(uint32_t reserve_size) = 0;
virtual bool send_buffer(ProtoWriteBuffer buffer, uint16_t message_type) = 0;
virtual bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) = 0;
virtual void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) = 0;
// Optimized method that pre-allocates buffer based on message size
bool send_message_(const ProtoMessage &msg, uint16_t message_type) {
bool send_message_(const ProtoMessage &msg, uint8_t message_type) {
uint32_t msg_size = 0;
msg.calculate_size(msg_size);

View File

@@ -7,6 +7,7 @@
#include "esphome/core/automation.h"
#include "api_pb2.h"
#ifdef USE_API_SERVICES
namespace esphome {
namespace api {
@@ -15,6 +16,8 @@ class UserServiceDescriptor {
virtual ListEntitiesServicesResponse encode_list_service_response() = 0;
virtual bool execute_service(const ExecuteServiceRequest &req) = 0;
bool is_internal() { return false; }
};
template<typename T> T get_execute_arg_value(const ExecuteServiceArgument &arg);
@@ -73,3 +76,4 @@ template<typename... Ts> class UserServiceTrigger : public UserServiceBase<Ts...
} // namespace api
} // namespace esphome
#endif // USE_API_SERVICES

View File

@@ -3,8 +3,6 @@
#include "esphome/core/component.h"
#include "esphome/components/as3935/as3935.h"
#include "esphome/components/spi/spi.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
namespace esphome {
namespace as3935_spi {

View File

@@ -31,7 +31,7 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
if CORE.is_esp32 or CORE.is_libretiny:
# https://github.com/ESP32Async/AsyncTCP
cg.add_library("ESP32Async/AsyncTCP", "3.4.4")
cg.add_library("ESP32Async/AsyncTCP", "3.4.5")
elif CORE.is_esp8266:
# https://github.com/ESP32Async/ESPAsyncTCP
cg.add_library("ESP32Async/ESPAsyncTCP", "2.0.0")

View File

@@ -85,13 +85,13 @@ async def to_code(config):
await cg.register_component(var, config)
cg.add(var.set_active(config[CONF_ACTIVE]))
await esp32_ble_tracker.register_ble_device(var, config)
await esp32_ble_tracker.register_raw_ble_device(var, config)
for connection_conf in config.get(CONF_CONNECTIONS, []):
connection_var = cg.new_Pvariable(connection_conf[CONF_ID])
await cg.register_component(connection_var, connection_conf)
cg.add(var.register_connection(connection_var))
await esp32_ble_tracker.register_client(connection_var, connection_conf)
await esp32_ble_tracker.register_raw_client(connection_var, connection_conf)
if config.get(CONF_CACHE_SERVICES):
add_idf_sdkconfig_option("CONFIG_BT_GATTC_CACHE_NVS_FLASH", True)

View File

@@ -75,7 +75,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
resp.data.reserve(param->read.value_len);
// Use bulk insert instead of individual push_backs
resp.data.insert(resp.data.end(), param->read.value, param->read.value + param->read.value_len);
this->proxy_->get_api_connection()->send_message(resp);
this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTReadResponse::MESSAGE_TYPE);
break;
}
case ESP_GATTC_WRITE_CHAR_EVT:
@@ -89,7 +89,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
api::BluetoothGATTWriteResponse resp;
resp.address = this->address_;
resp.handle = param->write.handle;
this->proxy_->get_api_connection()->send_message(resp);
this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTWriteResponse::MESSAGE_TYPE);
break;
}
case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: {
@@ -103,7 +103,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
api::BluetoothGATTNotifyResponse resp;
resp.address = this->address_;
resp.handle = param->unreg_for_notify.handle;
this->proxy_->get_api_connection()->send_message(resp);
this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTNotifyResponse::MESSAGE_TYPE);
break;
}
case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
@@ -116,7 +116,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
api::BluetoothGATTNotifyResponse resp;
resp.address = this->address_;
resp.handle = param->reg_for_notify.handle;
this->proxy_->get_api_connection()->send_message(resp);
this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTNotifyResponse::MESSAGE_TYPE);
break;
}
case ESP_GATTC_NOTIFY_EVT: {
@@ -128,7 +128,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
resp.data.reserve(param->notify.value_len);
// Use bulk insert instead of individual push_backs
resp.data.insert(resp.data.end(), param->notify.value, param->notify.value + param->notify.value_len);
this->proxy_->get_api_connection()->send_message(resp);
this->proxy_->get_api_connection()->send_message(resp, api::BluetoothGATTNotifyDataResponse::MESSAGE_TYPE);
break;
}
default:

View File

@@ -3,6 +3,7 @@
#include "esphome/core/log.h"
#include "esphome/core/macros.h"
#include "esphome/core/application.h"
#include <cstring>
#ifdef USE_ESP32
@@ -24,9 +25,30 @@ std::vector<uint64_t> get_128bit_uuid_vec(esp_bt_uuid_t uuid_source) {
((uint64_t) uuid.uuid.uuid128[1] << 8) | ((uint64_t) uuid.uuid.uuid128[0])};
}
// Batch size for BLE advertisements to maximize WiFi efficiency
// Each advertisement is up to 80 bytes when packaged (including protocol overhead)
// 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)
static_assert(sizeof(((api::BluetoothLERawAdvertisement *) nullptr)->data) == 62,
"BLE advertisement data array size mismatch");
BluetoothProxy::BluetoothProxy() { global_bluetooth_proxy = this; }
void BluetoothProxy::setup() {
// Pre-allocate response object
this->response_ = std::make_unique<api::BluetoothLERawAdvertisementsResponse>();
// Reserve capacity but start with size 0
// Reserve 50% since we'll grow naturally and flush at FLUSH_BATCH_SIZE
this->response_->advertisements.reserve(FLUSH_BATCH_SIZE / 2);
// Don't pre-allocate pool - let it grow only if needed in busy environments
// Many devices in quiet areas will never need the overflow pool
this->parent_->add_scanner_state_callback([this](esp32_ble_tracker::ScannerState state) {
if (this->api_connection_ != nullptr) {
this->send_bluetooth_scanner_state_(state);
@@ -39,83 +61,86 @@ void BluetoothProxy::send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerSta
resp.state = static_cast<api::enums::BluetoothScannerState>(state);
resp.mode = this->parent_->get_scan_active() ? api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE
: api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_PASSIVE;
this->api_connection_->send_message(resp);
this->api_connection_->send_message(resp, api::BluetoothScannerStateResponse::MESSAGE_TYPE);
}
#ifdef USE_ESP32_BLE_DEVICE
bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || this->raw_advertisements_)
return false;
ESP_LOGV(TAG, "Proxying packet from %s - %s. RSSI: %d dB", device.get_name().c_str(), device.address_str().c_str(),
device.get_rssi());
this->send_api_packet_(device);
return true;
// This method should never be called since bluetooth_proxy always uses raw advertisements
// but we need to provide an implementation to satisfy the virtual method requirement
return false;
}
// Batch size for BLE advertisements to maximize WiFi efficiency
// Each advertisement is up to 80 bytes when packaged (including protocol overhead)
// 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;
namespace {
// Batch buffer in anonymous namespace to avoid guard variable (saves 8 bytes)
// This is initialized at program startup before any threads
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
std::vector<api::BluetoothLERawAdvertisement> batch_buffer;
} // namespace
static std::vector<api::BluetoothLERawAdvertisement> &get_batch_buffer() { return batch_buffer; }
#endif
bool BluetoothProxy::parse_devices(const esp32_ble::BLEScanResult *scan_results, size_t count) {
if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || !this->raw_advertisements_)
if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr)
return false;
// Get the batch buffer reference
auto &batch_buffer = get_batch_buffer();
auto &advertisements = this->response_->advertisements;
// Reserve additional capacity if needed
size_t new_size = batch_buffer.size() + count;
if (batch_buffer.capacity() < new_size) {
batch_buffer.reserve(new_size);
}
// Add new advertisements to the batch buffer
for (size_t i = 0; i < count; i++) {
auto &result = scan_results[i];
uint8_t length = result.adv_data_len + result.scan_rsp_len;
batch_buffer.emplace_back();
auto &adv = batch_buffer.back();
// 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
auto &adv = advertisements[this->advertisement_count_];
adv.address = esp32_ble::ble_addr_to_uint64(result.bda);
adv.rssi = result.rssi;
adv.address_type = result.ble_addr_type;
adv.data.assign(&result.ble_adv[0], &result.ble_adv[length]);
adv.data_len = length;
std::memcpy(adv.data, result.ble_adv, length);
this->advertisement_count_++;
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);
}
// Only send if we've accumulated a good batch size to maximize batching efficiency
// https://github.com/esphome/backlog/issues/21
if (batch_buffer.size() >= FLUSH_BATCH_SIZE) {
this->flush_pending_advertisements();
// Flush if we have reached FLUSH_BATCH_SIZE
if (this->advertisement_count_ >= FLUSH_BATCH_SIZE) {
this->flush_pending_advertisements();
}
}
return true;
}
void BluetoothProxy::flush_pending_advertisements() {
auto &batch_buffer = get_batch_buffer();
if (batch_buffer.empty() || !api::global_api_server->is_connected() || this->api_connection_ == nullptr)
if (this->advertisement_count_ == 0 || !api::global_api_server->is_connected() || this->api_connection_ == nullptr)
return;
api::BluetoothLERawAdvertisementsResponse resp;
resp.advertisements.swap(batch_buffer);
this->api_connection_->send_message(resp);
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
this->api_connection_->send_message(*this->response_, api::BluetoothLERawAdvertisementsResponse::MESSAGE_TYPE);
// Reset count - existing items will be overwritten in next batch
this->advertisement_count_ = 0;
}
#ifdef USE_ESP32_BLE_DEVICE
void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device) {
api::BluetoothLEAdvertisementResponse resp;
resp.address = device.address_uint64();
@@ -151,16 +176,16 @@ void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &devi
manufacturer_data.data.assign(data.data.begin(), data.data.end());
}
this->api_connection_->send_message(resp);
this->api_connection_->send_message(resp, api::BluetoothLEAdvertisementResponse::MESSAGE_TYPE);
}
#endif // USE_ESP32_BLE_DEVICE
void BluetoothProxy::dump_config() {
ESP_LOGCONFIG(TAG, "Bluetooth Proxy:");
ESP_LOGCONFIG(TAG,
" Active: %s\n"
" Connections: %d\n"
" Raw advertisements: %s",
YESNO(this->active_), this->connections_.size(), YESNO(this->raw_advertisements_));
" Connections: %d",
YESNO(this->active_), this->connections_.size());
}
int BluetoothProxy::get_bluetooth_connections_free() {
@@ -188,15 +213,13 @@ void BluetoothProxy::loop() {
}
// Flush any pending BLE advertisements that have been accumulated but not yet sent
if (this->raw_advertisements_) {
static uint32_t last_flush_time = 0;
uint32_t now = App.get_loop_component_start_time();
static uint32_t last_flush_time = 0;
uint32_t now = App.get_loop_component_start_time();
// Flush accumulated advertisements every 100ms
if (now - last_flush_time >= 100) {
this->flush_pending_advertisements();
last_flush_time = now;
}
// Flush accumulated advertisements every 100ms
if (now - last_flush_time >= 100) {
this->flush_pending_advertisements();
last_flush_time = now;
}
for (auto *connection : this->connections_) {
if (connection->send_service_ == connection->service_count_) {
@@ -312,15 +335,13 @@ void BluetoothProxy::loop() {
service_resp.characteristics.push_back(std::move(characteristic_resp));
}
resp.services.push_back(std::move(service_resp));
this->api_connection_->send_message(resp);
this->api_connection_->send_message(resp, api::BluetoothGATTGetServicesResponse::MESSAGE_TYPE);
}
}
}
esp32_ble_tracker::AdvertisementParserType BluetoothProxy::get_advertisement_parser_type() {
if (this->raw_advertisements_)
return esp32_ble_tracker::AdvertisementParserType::RAW_ADVERTISEMENTS;
return esp32_ble_tracker::AdvertisementParserType::PARSED_ADVERTISEMENTS;
return esp32_ble_tracker::AdvertisementParserType::RAW_ADVERTISEMENTS;
}
BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool reserve) {
@@ -465,7 +486,7 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
call.success = ret == ESP_OK;
call.error = ret;
this->api_connection_->send_message(call);
this->api_connection_->send_message(call, api::BluetoothDeviceClearCacheResponse::MESSAGE_TYPE);
break;
}
@@ -565,7 +586,6 @@ void BluetoothProxy::subscribe_api_connection(api::APIConnection *api_connection
return;
}
this->api_connection_ = api_connection;
this->raw_advertisements_ = flags & BluetoothProxySubscriptionFlag::SUBSCRIPTION_RAW_ADVERTISEMENTS;
this->parent_->recalculate_advertisement_parser_types();
this->send_bluetooth_scanner_state_(this->parent_->get_scanner_state());
@@ -577,7 +597,6 @@ void BluetoothProxy::unsubscribe_api_connection(api::APIConnection *api_connecti
return;
}
this->api_connection_ = nullptr;
this->raw_advertisements_ = false;
this->parent_->recalculate_advertisement_parser_types();
}
@@ -589,7 +608,7 @@ void BluetoothProxy::send_device_connection(uint64_t address, bool connected, ui
call.connected = connected;
call.mtu = mtu;
call.error = error;
this->api_connection_->send_message(call);
this->api_connection_->send_message(call, api::BluetoothDeviceConnectionResponse::MESSAGE_TYPE);
}
void BluetoothProxy::send_connections_free() {
if (this->api_connection_ == nullptr)
@@ -602,7 +621,7 @@ void BluetoothProxy::send_connections_free() {
call.allocated.push_back(connection->address_);
}
}
this->api_connection_->send_message(call);
this->api_connection_->send_message(call, api::BluetoothConnectionsFreeResponse::MESSAGE_TYPE);
}
void BluetoothProxy::send_gatt_services_done(uint64_t address) {
@@ -610,7 +629,7 @@ void BluetoothProxy::send_gatt_services_done(uint64_t address) {
return;
api::BluetoothGATTGetServicesDoneResponse call;
call.address = address;
this->api_connection_->send_message(call);
this->api_connection_->send_message(call, api::BluetoothGATTGetServicesDoneResponse::MESSAGE_TYPE);
}
void BluetoothProxy::send_gatt_error(uint64_t address, uint16_t handle, esp_err_t error) {
@@ -620,7 +639,7 @@ void BluetoothProxy::send_gatt_error(uint64_t address, uint16_t handle, esp_err_
call.address = address;
call.handle = handle;
call.error = error;
this->api_connection_->send_message(call);
this->api_connection_->send_message(call, api::BluetoothGATTWriteResponse::MESSAGE_TYPE);
}
void BluetoothProxy::send_device_pairing(uint64_t address, bool paired, esp_err_t error) {
@@ -629,7 +648,7 @@ void BluetoothProxy::send_device_pairing(uint64_t address, bool paired, esp_err_
call.paired = paired;
call.error = error;
this->api_connection_->send_message(call);
this->api_connection_->send_message(call, api::BluetoothDevicePairingResponse::MESSAGE_TYPE);
}
void BluetoothProxy::send_device_unpairing(uint64_t address, bool success, esp_err_t error) {
@@ -638,7 +657,7 @@ void BluetoothProxy::send_device_unpairing(uint64_t address, bool success, esp_e
call.success = success;
call.error = error;
this->api_connection_->send_message(call);
this->api_connection_->send_message(call, api::BluetoothDeviceUnpairingResponse::MESSAGE_TYPE);
}
void BluetoothProxy::bluetooth_scanner_set_mode(bool active) {

View File

@@ -51,7 +51,9 @@ enum BluetoothProxySubscriptionFlag : uint32_t {
class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Component {
public:
BluetoothProxy();
#ifdef USE_ESP32_BLE_DEVICE
bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
#endif
bool parse_devices(const esp32_ble::BLEScanResult *scan_results, size_t count) override;
void dump_config() override;
void setup() override;
@@ -129,7 +131,9 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
}
protected:
#ifdef USE_ESP32_BLE_DEVICE
void send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device);
#endif
void send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerState state);
BluetoothConnection *get_connection_(uint64_t address, bool reserve);
@@ -141,9 +145,13 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
// Group 2: Container types (typically 12 bytes on 32-bit)
std::vector<BluetoothConnection *> connections_{};
// BLE advertisement batching
std::vector<api::BluetoothLERawAdvertisement> advertisement_pool_;
std::unique_ptr<api::BluetoothLERawAdvertisementsResponse> response_;
// Group 3: 1-byte types grouped together
bool active_;
bool raw_advertisements_{false};
uint8_t advertisement_count_{0};
// 2 bytes used, 2 bytes padding
};

View File

@@ -3,6 +3,7 @@
CODEOWNERS = ["@esphome/core"]
CONF_BYTE_ORDER = "byte_order"
CONF_COLOR_DEPTH = "color_depth"
CONF_DRAW_ROUNDING = "draw_rounding"
CONF_ON_STATE_CHANGE = "on_state_change"
CONF_REQUEST_HEADERS = "request_headers"

View File

@@ -53,6 +53,7 @@ void DebugComponent::on_shutdown() {
auto pref = global_preferences->make_preference(REBOOT_MAX_LEN, fnv1_hash(REBOOT_KEY + App.get_name()));
if (component != nullptr) {
strncpy(buffer, component->get_component_source(), REBOOT_MAX_LEN - 1);
buffer[REBOOT_MAX_LEN - 1] = '\0';
}
ESP_LOGD(TAG, "Storing reboot source: %s", buffer);
pref.save(&buffer);
@@ -68,6 +69,7 @@ std::string DebugComponent::get_reset_reason_() {
auto pref = global_preferences->make_preference(REBOOT_MAX_LEN, fnv1_hash(REBOOT_KEY + App.get_name()));
char buffer[REBOOT_MAX_LEN]{};
if (pref.load(&buffer)) {
buffer[REBOOT_MAX_LEN - 1] = '\0';
reset_reason = "Reboot request from " + std::string(buffer);
}
}

View File

@@ -17,6 +17,7 @@ from esphome.const import (
CONF_MODE,
CONF_NUMBER,
CONF_ON_VALUE,
CONF_SWITCH,
CONF_TEXT,
CONF_TRIGGER_ID,
CONF_TYPE,
@@ -33,7 +34,6 @@ CONF_LABEL = "label"
CONF_MENU = "menu"
CONF_BACK = "back"
CONF_SELECT = "select"
CONF_SWITCH = "switch"
CONF_ON_TEXT = "on_text"
CONF_OFF_TEXT = "off_text"
CONF_VALUE_LAMBDA = "value_lambda"

View File

@@ -4,6 +4,7 @@
#include "esphome/components/network/ip_address.h"
#include "esphome/core/log.h"
#include "esphome/core/util.h"
#include "esphome/core/helpers.h"
#include <lwip/igmp.h>
#include <lwip/init.h>
@@ -71,7 +72,11 @@ bool E131Component::join_igmp_groups_() {
ip4_addr_t multicast_addr =
network::IPAddress(239, 255, ((universe.first >> 8) & 0xff), ((universe.first >> 0) & 0xff));
auto err = igmp_joingroup(IP4_ADDR_ANY4, &multicast_addr);
err_t err;
{
LwIPLock lock;
err = igmp_joingroup(IP4_ADDR_ANY4, &multicast_addr);
}
if (err) {
ESP_LOGW(TAG, "IGMP join for %d universe of E1.31 failed. Multicast might not work.", universe.first);
@@ -104,6 +109,7 @@ void E131Component::leave_(int universe) {
if (listen_method_ == E131_MULTICAST) {
ip4_addr_t multicast_addr = network::IPAddress(239, 255, ((universe >> 8) & 0xff), ((universe >> 0) & 0xff));
LwIPLock lock;
igmp_leavegroup(IP4_ADDR_ANY4, &multicast_addr);
}

View File

@@ -39,7 +39,7 @@ import esphome.final_validate as fv
from esphome.helpers import copy_file_if_changed, mkdir_p, write_file_if_changed
from esphome.types import ConfigType
from .boards import BOARDS
from .boards import BOARDS, STANDARD_BOARDS
from .const import ( # noqa
KEY_BOARD,
KEY_COMPONENTS,
@@ -487,25 +487,32 @@ def _platform_is_platformio(value):
def _detect_variant(value):
board = value[CONF_BOARD]
if board in BOARDS:
variant = BOARDS[board][KEY_VARIANT]
if CONF_VARIANT in value and variant != value[CONF_VARIANT]:
board = value.get(CONF_BOARD)
variant = value.get(CONF_VARIANT)
if variant and board is None:
# If variant is set, we can derive the board from it
# variant has already been validated against the known set
value = value.copy()
value[CONF_BOARD] = STANDARD_BOARDS[variant]
elif board in BOARDS:
variant = variant or BOARDS[board][KEY_VARIANT]
if variant != BOARDS[board][KEY_VARIANT]:
raise cv.Invalid(
f"Option '{CONF_VARIANT}' does not match selected board.",
path=[CONF_VARIANT],
)
value = value.copy()
value[CONF_VARIANT] = variant
elif not variant:
raise cv.Invalid(
"This board is unknown, if you are sure you want to compile with this board selection, "
f"override with option '{CONF_VARIANT}'",
path=[CONF_BOARD],
)
else:
if CONF_VARIANT not in value:
raise cv.Invalid(
"This board is unknown, if you are sure you want to compile with this board selection, "
f"override with option '{CONF_VARIANT}'",
path=[CONF_BOARD],
)
_LOGGER.warning(
"This board is unknown. Make sure the chosen chip component is correct.",
"This board is unknown; the specified variant '%s' will be used but this may not work as expected.",
variant,
)
return value
@@ -676,7 +683,7 @@ CONF_PARTITIONS = "partitions"
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.Required(CONF_BOARD): cv.string_strict,
cv.Optional(CONF_BOARD): cv.string_strict,
cv.Optional(CONF_CPU_FREQUENCY): cv.one_of(
*FULL_CPU_FREQUENCIES, upper=True
),
@@ -691,6 +698,7 @@ CONFIG_SCHEMA = cv.All(
_detect_variant,
_set_default_framework,
set_core_data,
cv.has_at_least_one_key(CONF_BOARD, CONF_VARIANT),
)
@@ -707,6 +715,7 @@ async def to_code(config):
cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[config[CONF_VARIANT]])
cg.add_platformio_option("lib_ldf_mode", "off")
cg.add_platformio_option("lib_compat_mode", "strict")
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]

View File

@@ -2,13 +2,30 @@ from .const import (
VARIANT_ESP32,
VARIANT_ESP32C2,
VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6,
VARIANT_ESP32H2,
VARIANT_ESP32P4,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
VARIANTS,
)
STANDARD_BOARDS = {
VARIANT_ESP32: "esp32dev",
VARIANT_ESP32C2: "esp32-c2-devkitm-1",
VARIANT_ESP32C3: "esp32-c3-devkitm-1",
VARIANT_ESP32C5: "esp32-c5-devkitc-1",
VARIANT_ESP32C6: "esp32-c6-devkitm-1",
VARIANT_ESP32H2: "esp32-h2-devkitm-1",
VARIANT_ESP32P4: "esp32-p4-evboard",
VARIANT_ESP32S2: "esp32-s2-kaluga-1",
VARIANT_ESP32S3: "esp32-s3-devkitc-1",
}
# Make sure not missed here if a new variant added.
assert all(v in STANDARD_BOARDS for v in VARIANTS)
ESP32_BASE_PINS = {
"TX": 1,
"RX": 3,

View File

@@ -114,7 +114,6 @@ void ESP32InternalGPIOPin::setup() {
if (flags_ & gpio::FLAG_OUTPUT) {
gpio_set_drive_capability(pin_, drive_strength_);
}
ESP_LOGD(TAG, "rtc: %d", SOC_GPIO_SUPPORT_RTC_INDEPENDENT);
}
void ESP32InternalGPIOPin::pin_mode(gpio::Flags flags) {

View File

@@ -1,4 +1,5 @@
#include "esphome/core/helpers.h"
#include "esphome/core/defines.h"
#ifdef USE_ESP32
@@ -30,6 +31,45 @@ void Mutex::unlock() { xSemaphoreGive(this->handle_); }
IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); }
IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); }
#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING
#include "lwip/priv/tcpip_priv.h"
#endif
LwIPLock::LwIPLock() {
#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING
// When CONFIG_LWIP_TCPIP_CORE_LOCKING is enabled, lwIP uses a global mutex to protect
// its internal state. Any thread can take this lock to safely access lwIP APIs.
//
// sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER) returns true if the current thread
// already holds the lwIP core lock. This prevents recursive locking attempts and
// allows nested LwIPLock instances to work correctly.
//
// If we don't already hold the lock, acquire it. This will block until the lock
// is available if another thread currently holds it.
if (!sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER)) {
LOCK_TCPIP_CORE();
}
#endif
}
LwIPLock::~LwIPLock() {
#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING
// Only release the lwIP core lock if this thread currently holds it.
//
// sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER) queries lwIP's internal lock
// ownership tracking. It returns true only if the current thread is registered
// as the lock holder.
//
// This check is essential because:
// 1. We may not have acquired the lock in the constructor (if we already held it)
// 2. The lock might have been released by other means between constructor and destructor
// 3. Calling UNLOCK_TCPIP_CORE() without holding the lock causes undefined behavior
if (sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER)) {
UNLOCK_TCPIP_CORE();
}
#endif
}
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
#if defined(CONFIG_SOC_IEEE802154_SUPPORTED)
// When CONFIG_SOC_IEEE802154_SUPPORTED is defined, esp_efuse_mac_get_default

View File

@@ -105,6 +105,7 @@ void BLEClientBase::dump_config() {
}
}
#ifdef USE_ESP32_BLE_DEVICE
bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) {
if (!this->auto_connect_)
return false;
@@ -122,6 +123,7 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) {
this->remote_addr_type_ = device.get_address_type();
return true;
}
#endif
void BLEClientBase::connect() {
ESP_LOGI(TAG, "[%d] [%s] 0x%02x Attempting BLE connection", this->connection_index_, this->address_str_.c_str(),

View File

@@ -31,7 +31,9 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
void dump_config() override;
void run_later(std::function<void()> &&f); // NOLINT
#ifdef USE_ESP32_BLE_DEVICE
bool parse_device(const espbt::ESPBTDevice &device) override;
#endif
void on_scan_end() override {}
bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override;

View File

@@ -31,6 +31,8 @@ from esphome.const import (
CONF_TRIGGER_ID,
)
from esphome.core import CORE
from esphome.enum import StrEnum
from esphome.types import ConfigType
AUTO_LOAD = ["esp32_ble"]
DEPENDENCIES = ["esp32"]
@@ -50,6 +52,25 @@ IDF_MAX_CONNECTIONS = 9
_LOGGER = logging.getLogger(__name__)
# Enum for BLE features
class BLEFeatures(StrEnum):
ESP_BT_DEVICE = "ESP_BT_DEVICE"
# Set to track which features are needed by components
_required_features: set[BLEFeatures] = set()
def register_ble_features(features: set[BLEFeatures]) -> None:
"""Register BLE features that a component needs.
Args:
features: Set of BLEFeatures enum members
"""
_required_features.update(features)
esp32_ble_tracker_ns = cg.esphome_ns.namespace("esp32_ble_tracker")
ESP32BLETracker = esp32_ble_tracker_ns.class_(
"ESP32BLETracker",
@@ -277,6 +298,15 @@ async def to_code(config):
cg.add(var.set_scan_window(int(params[CONF_WINDOW].total_milliseconds / 0.625)))
cg.add(var.set_scan_active(params[CONF_ACTIVE]))
cg.add(var.set_scan_continuous(params[CONF_CONTINUOUS]))
# Register ESP_BT_DEVICE feature if any of the automation triggers are used
if (
config.get(CONF_ON_BLE_ADVERTISE)
or config.get(CONF_ON_BLE_SERVICE_DATA_ADVERTISE)
or config.get(CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE)
):
register_ble_features({BLEFeatures.ESP_BT_DEVICE})
for conf in config.get(CONF_ON_BLE_ADVERTISE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
if CONF_MAC_ADDRESS in conf:
@@ -334,6 +364,11 @@ async def to_code(config):
cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts
cg.add_define("USE_ESP32_BLE_CLIENT")
# Add feature-specific defines based on what's needed
if BLEFeatures.ESP_BT_DEVICE in _required_features:
cg.add_define("USE_ESP32_BLE_DEVICE")
if config.get(CONF_SOFTWARE_COEXISTENCE):
cg.add_define("USE_ESP32_BLE_SOFTWARE_COEXISTENCE")
@@ -382,13 +417,43 @@ async def esp32_ble_tracker_stop_scan_action_to_code(
return var
async def register_ble_device(var, config):
async def register_ble_device(
var: cg.SafeExpType, config: ConfigType
) -> cg.SafeExpType:
register_ble_features({BLEFeatures.ESP_BT_DEVICE})
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
cg.add(paren.register_listener(var))
return var
async def register_client(var, config):
async def register_client(var: cg.SafeExpType, config: ConfigType) -> cg.SafeExpType:
register_ble_features({BLEFeatures.ESP_BT_DEVICE})
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
cg.add(paren.register_client(var))
return var
async def register_raw_ble_device(
var: cg.SafeExpType, config: ConfigType
) -> cg.SafeExpType:
"""Register a BLE device listener that only needs raw advertisement data.
This does NOT register the ESP_BT_DEVICE feature, meaning ESPBTDevice
will not be compiled in if this is the only registration method used.
"""
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
cg.add(paren.register_listener(var))
return var
async def register_raw_client(
var: cg.SafeExpType, config: ConfigType
) -> cg.SafeExpType:
"""Register a BLE client that only needs raw advertisement data.
This does NOT register the ESP_BT_DEVICE feature, meaning ESPBTDevice
will not be compiled in if this is the only registration method used.
"""
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
cg.add(paren.register_client(var))
return var

View File

@@ -7,6 +7,7 @@
namespace esphome {
namespace esp32_ble_tracker {
#ifdef USE_ESP32_BLE_DEVICE
class ESPBTAdvertiseTrigger : public Trigger<const ESPBTDevice &>, public ESPBTDeviceListener {
public:
explicit ESPBTAdvertiseTrigger(ESP32BLETracker *parent) { parent->register_listener(this); }
@@ -87,6 +88,7 @@ class BLEEndOfScanTrigger : public Trigger<>, public ESPBTDeviceListener {
bool parse_device(const ESPBTDevice &device) override { return false; }
void on_scan_end() override { this->trigger(); }
};
#endif // USE_ESP32_BLE_DEVICE
template<typename... Ts> class ESP32BLEStartScanAction : public Action<Ts...> {
public:

View File

@@ -141,6 +141,7 @@ void ESP32BLETracker::loop() {
}
if (this->parse_advertisements_) {
#ifdef USE_ESP32_BLE_DEVICE
ESPBTDevice device;
device.parse_scan_rst(scan_result);
@@ -162,6 +163,7 @@ void ESP32BLETracker::loop() {
if (!found && !this->scan_continuous_) {
this->print_bt_device_info(device);
}
#endif // USE_ESP32_BLE_DEVICE
}
// Move to next entry in ring buffer
@@ -511,6 +513,7 @@ void ESP32BLETracker::set_scanner_state_(ScannerState state) {
this->scanner_state_callbacks_.call(state);
}
#ifdef USE_ESP32_BLE_DEVICE
ESPBLEiBeacon::ESPBLEiBeacon(const uint8_t *data) { memcpy(&this->beacon_data_, data, sizeof(beacon_data_)); }
optional<ESPBLEiBeacon> ESPBLEiBeacon::from_manufacturer_data(const ServiceData &data) {
if (!data.uuid.contains(0x4C, 0x00))
@@ -751,13 +754,16 @@ void ESPBTDevice::parse_adv_(const uint8_t *payload, uint8_t len) {
}
}
}
std::string ESPBTDevice::address_str() const {
char mac[24];
snprintf(mac, sizeof(mac), "%02X:%02X:%02X:%02X:%02X:%02X", this->address_[0], this->address_[1], this->address_[2],
this->address_[3], this->address_[4], this->address_[5]);
return mac;
}
uint64_t ESPBTDevice::address_uint64() const { return esp32_ble::ble_addr_to_uint64(this->address_); }
#endif // USE_ESP32_BLE_DEVICE
void ESP32BLETracker::dump_config() {
ESP_LOGCONFIG(TAG, "BLE Tracker:");
@@ -796,6 +802,7 @@ void ESP32BLETracker::dump_config() {
}
}
#ifdef USE_ESP32_BLE_DEVICE
void ESP32BLETracker::print_bt_device_info(const ESPBTDevice &device) {
const uint64_t address = device.address_uint64();
for (auto &disc : this->already_discovered_) {
@@ -866,8 +873,9 @@ bool ESPBTDevice::resolve_irk(const uint8_t *irk) const {
return ecb_ciphertext[15] == (addr64 & 0xff) && ecb_ciphertext[14] == ((addr64 >> 8) & 0xff) &&
ecb_ciphertext[13] == ((addr64 >> 16) & 0xff);
}
#endif // USE_ESP32_BLE_DEVICE
} // namespace esp32_ble_tracker
} // namespace esphome
#endif
#endif // USE_ESP32

View File

@@ -39,6 +39,7 @@ struct ServiceData {
adv_data_t data;
};
#ifdef USE_ESP32_BLE_DEVICE
class ESPBLEiBeacon {
public:
ESPBLEiBeacon() { memset(&this->beacon_data_, 0, sizeof(this->beacon_data_)); }
@@ -116,13 +117,16 @@ class ESPBTDevice {
std::vector<ServiceData> service_datas_{};
const BLEScanResult *scan_result_{nullptr};
};
#endif // USE_ESP32_BLE_DEVICE
class ESP32BLETracker;
class ESPBTDeviceListener {
public:
virtual void on_scan_end() {}
#ifdef USE_ESP32_BLE_DEVICE
virtual bool parse_device(const ESPBTDevice &device) = 0;
#endif
virtual bool parse_devices(const BLEScanResult *scan_results, size_t count) { return false; };
virtual AdvertisementParserType get_advertisement_parser_type() {
return AdvertisementParserType::PARSED_ADVERTISEMENTS;
@@ -237,7 +241,9 @@ class ESP32BLETracker : public Component,
void register_client(ESPBTClient *client);
void recalculate_advertisement_parser_types();
#ifdef USE_ESP32_BLE_DEVICE
void print_bt_device_info(const ESPBTDevice &device);
#endif
void start_scan();
void stop_scan();

View File

@@ -1,3 +1,5 @@
import logging
from esphome import automation, pins
import esphome.codegen as cg
from esphome.components import i2c
@@ -8,6 +10,7 @@ from esphome.const import (
CONF_CONTRAST,
CONF_DATA_PINS,
CONF_FREQUENCY,
CONF_I2C,
CONF_I2C_ID,
CONF_ID,
CONF_PIN,
@@ -20,6 +23,9 @@ from esphome.const import (
)
from esphome.core import CORE
from esphome.core.entity_helpers import setup_entity
import esphome.final_validate as fv
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ["esp32"]
@@ -113,6 +119,12 @@ ENUM_SPECIAL_EFFECT = {
"SEPIA": ESP32SpecialEffect.ESP32_SPECIAL_EFFECT_SEPIA,
}
camera_fb_location_t = cg.global_ns.enum("camera_fb_location_t")
ENUM_FB_LOCATION = {
"PSRAM": cg.global_ns.CAMERA_FB_IN_PSRAM,
"DRAM": cg.global_ns.CAMERA_FB_IN_DRAM,
}
# pin assignment
CONF_HREF_PIN = "href_pin"
CONF_PIXEL_CLOCK_PIN = "pixel_clock_pin"
@@ -143,6 +155,7 @@ CONF_MAX_FRAMERATE = "max_framerate"
CONF_IDLE_FRAMERATE = "idle_framerate"
# frame buffer
CONF_FRAME_BUFFER_COUNT = "frame_buffer_count"
CONF_FRAME_BUFFER_LOCATION = "frame_buffer_location"
# stream trigger
CONF_ON_STREAM_START = "on_stream_start"
@@ -224,6 +237,9 @@ CONFIG_SCHEMA = cv.All(
cv.framerate, cv.Range(min=0, max=1)
),
cv.Optional(CONF_FRAME_BUFFER_COUNT, default=1): cv.int_range(min=1, max=2),
cv.Optional(CONF_FRAME_BUFFER_LOCATION, default="PSRAM"): cv.enum(
ENUM_FB_LOCATION, upper=True
),
cv.Optional(CONF_ON_STREAM_START): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
@@ -250,6 +266,22 @@ CONFIG_SCHEMA = cv.All(
cv.has_exactly_one_key(CONF_I2C_PINS, CONF_I2C_ID),
)
def _final_validate(config):
if CONF_I2C_PINS not in config:
return
fconf = fv.full_config.get()
if fconf.get(CONF_I2C):
raise cv.Invalid(
"The `i2c_pins:` config option is incompatible with an dedicated `i2c:` block, use `i2c_id` instead"
)
_LOGGER.warning(
"The `i2c_pins:` config option is deprecated. Use `i2c_id:` with a dedicated `i2c:` definition instead."
)
FINAL_VALIDATE_SCHEMA = _final_validate
SETTERS = {
# pin assignment
CONF_DATA_PINS: "set_data_pins",
@@ -279,6 +311,7 @@ SETTERS = {
CONF_WB_MODE: "set_wb_mode",
# test pattern
CONF_TEST_PATTERN: "set_test_pattern",
CONF_FRAME_BUFFER_LOCATION: "set_frame_buffer_location",
}
@@ -306,9 +339,10 @@ async def to_code(config):
else:
cg.add(var.set_idle_update_interval(1000 / config[CONF_IDLE_FRAMERATE]))
cg.add(var.set_frame_buffer_count(config[CONF_FRAME_BUFFER_COUNT]))
cg.add(var.set_frame_buffer_location(config[CONF_FRAME_BUFFER_LOCATION]))
cg.add(var.set_frame_size(config[CONF_RESOLUTION]))
cg.add_define("USE_ESP32_CAMERA")
cg.add_define("USE_CAMERA")
if CORE.using_esp_idf:
add_idf_component(name="espressif/esp32-camera", ref="2.0.15")

View File

@@ -133,6 +133,7 @@ void ESP32Camera::dump_config() {
ESP_LOGCONFIG(TAG,
" JPEG Quality: %u\n"
" Framebuffer Count: %u\n"
" Framebuffer Location: %s\n"
" Contrast: %d\n"
" Brightness: %d\n"
" Saturation: %d\n"
@@ -140,8 +141,9 @@ void ESP32Camera::dump_config() {
" Horizontal Mirror: %s\n"
" Special Effect: %u\n"
" White Balance Mode: %u",
st.quality, conf.fb_count, st.contrast, st.brightness, st.saturation, ONOFF(st.vflip),
ONOFF(st.hmirror), st.special_effect, st.wb_mode);
st.quality, conf.fb_count, this->config_.fb_location == CAMERA_FB_IN_PSRAM ? "PSRAM" : "DRAM",
st.contrast, st.brightness, st.saturation, ONOFF(st.vflip), ONOFF(st.hmirror), st.special_effect,
st.wb_mode);
// ESP_LOGCONFIG(TAG, " Auto White Balance: %u", st.awb);
// ESP_LOGCONFIG(TAG, " Auto White Balance Gain: %u", st.awb_gain);
ESP_LOGCONFIG(TAG,
@@ -350,6 +352,9 @@ void ESP32Camera::set_frame_buffer_count(uint8_t fb_count) {
this->config_.fb_count = fb_count;
this->set_frame_buffer_mode(fb_count > 1 ? CAMERA_GRAB_LATEST : CAMERA_GRAB_WHEN_EMPTY);
}
void ESP32Camera::set_frame_buffer_location(camera_fb_location_t fb_location) {
this->config_.fb_location = fb_location;
}
/* ---------------- public API (specific) ---------------- */
void ESP32Camera::add_image_callback(std::function<void(std::shared_ptr<camera::CameraImage>)> &&callback) {

View File

@@ -152,6 +152,7 @@ class ESP32Camera : public camera::Camera {
/* -- frame buffer */
void set_frame_buffer_mode(camera_grab_mode_t mode);
void set_frame_buffer_count(uint8_t fb_count);
void set_frame_buffer_location(camera_fb_location_t fb_location);
/* public API (derivated) */
void setup() override;

View File

@@ -109,6 +109,7 @@ void ESP32TouchComponent::loop() {
// Only publish if state changed - this filters out repeated events
if (new_state != child->last_state_) {
child->initial_state_published_ = true;
child->last_state_ = new_state;
child->publish_state(new_state);
// Original ESP32: ISR only fires when touched, release is detected by timeout
@@ -175,6 +176,9 @@ void ESP32TouchComponent::on_shutdown() {
void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) {
ESP32TouchComponent *component = static_cast<ESP32TouchComponent *>(arg);
uint32_t mask = 0;
touch_ll_read_trigger_status_mask(&mask);
touch_ll_clear_trigger_status_mask();
touch_pad_clear_status();
// INTERRUPT BEHAVIOR: On ESP32 v1 hardware, the interrupt fires when ANY configured
@@ -184,6 +188,11 @@ void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) {
// as any pad remains touched. This allows us to detect both new touches and
// continued touches, but releases must be detected by timeout in the main loop.
// IMPORTANT: ESP32 v1 touch detection logic - INVERTED compared to v2!
// ESP32 v1: Touch is detected when capacitance INCREASES, causing the measured value to DECREASE
// Therefore: touched = (value < threshold)
// This is opposite to ESP32-S2/S3 v2 where touched = (value > threshold)
// Process all configured pads to check their current state
// Note: ESP32 v1 doesn't tell us which specific pad triggered the interrupt,
// so we must scan all configured pads to find which ones were touched
@@ -201,19 +210,12 @@ void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) {
value = touch_ll_read_raw_data(pad);
}
// Skip pads with 0 value - they haven't been measured in this cycle
// This is important: not all pads are measured every interrupt cycle,
// only those that the hardware has updated
if (value == 0) {
// Skip pads that arent in the trigger mask
bool is_touched = (mask >> pad) & 1;
if (!is_touched) {
continue;
}
// IMPORTANT: ESP32 v1 touch detection logic - INVERTED compared to v2!
// ESP32 v1: Touch is detected when capacitance INCREASES, causing the measured value to DECREASE
// Therefore: touched = (value < threshold)
// This is opposite to ESP32-S2/S3 v2 where touched = (value > threshold)
bool is_touched = value < child->get_threshold();
// Always send the current state - the main loop will filter for changes
// We send both touched and untouched states because the ISR doesn't
// track previous state (to keep ISR fast and simple)

View File

@@ -180,6 +180,7 @@ async def to_code(config):
cg.add(esp8266_ns.setup_preferences())
cg.add_platformio_option("lib_ldf_mode", "off")
cg.add_platformio_option("lib_compat_mode", "strict")
cg.add_platformio_option("board", config[CONF_BOARD])
cg.add_build_flag("-DUSE_ESP8266")

View File

@@ -22,6 +22,10 @@ void Mutex::unlock() {}
IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); }
IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(state_); }
// ESP8266 doesn't support lwIP core locking, so this is a no-op
LwIPLock::LwIPLock() {}
LwIPLock::~LwIPLock() {}
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
wifi_get_macaddr(STATION_IF, mac);
}

View File

@@ -20,14 +20,16 @@ adjusted_ids = set()
CONFIG_SCHEMA = cv.All(
cv.ensure_list(
{
cv.GenerateID(): cv.declare_id(EspLdo),
cv.Required(CONF_VOLTAGE): cv.All(
cv.voltage, cv.float_range(min=0.5, max=2.7)
),
cv.Required(CONF_CHANNEL): cv.one_of(*CHANNELS, int=True),
cv.Optional(CONF_ADJUSTABLE, default=False): cv.boolean,
}
cv.COMPONENT_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(EspLdo),
cv.Required(CONF_VOLTAGE): cv.All(
cv.voltage, cv.float_range(min=0.5, max=2.7)
),
cv.Required(CONF_CHANNEL): cv.one_of(*CHANNELS, int=True),
cv.Optional(CONF_ADJUSTABLE, default=False): cv.boolean,
}
)
),
cv.only_with_esp_idf,
only_on_variant(supported=[VARIANT_ESP32P4]),

View File

@@ -17,6 +17,9 @@ class EspLdo : public Component {
void set_adjustable(bool adjustable) { this->adjustable_ = adjustable; }
void set_voltage(float voltage) { this->voltage_ = voltage; }
void adjust_voltage(float voltage);
float get_setup_priority() const override {
return setup_priority::BUS; // LDO setup should be done early
}
protected:
int channel_;

View File

@@ -342,5 +342,11 @@ async def to_code(config):
cg.add_define("USE_ETHERNET")
# Disable WiFi when using Ethernet to save memory
if CORE.using_esp_idf:
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_ENABLED", False)
# Also disable WiFi/BT coexistence since WiFi is disabled
add_idf_sdkconfig_option("CONFIG_SW_COEXIST_ENABLE", False)
if CORE.using_arduino:
cg.add_library("WiFi", None)

View File

@@ -420,6 +420,7 @@ network::IPAddresses EthernetComponent::get_ip_addresses() {
}
network::IPAddress EthernetComponent::get_dns_address(uint8_t num) {
LwIPLock lock;
const ip_addr_t *dns_ip = dns_getserver(num);
return dns_ip;
}
@@ -527,6 +528,7 @@ void EthernetComponent::start_connect_() {
ESPHL_ERROR_CHECK(err, "DHCPC set IP info error");
if (this->manual_ip_.has_value()) {
LwIPLock lock;
if (this->manual_ip_->dns1.is_set()) {
ip_addr_t d;
d = this->manual_ip_->dns1;
@@ -559,8 +561,13 @@ bool EthernetComponent::is_connected() { return this->state_ == EthernetComponen
void EthernetComponent::dump_connect_params_() {
esp_netif_ip_info_t ip;
esp_netif_get_ip_info(this->eth_netif_, &ip);
const ip_addr_t *dns_ip1 = dns_getserver(0);
const ip_addr_t *dns_ip2 = dns_getserver(1);
const ip_addr_t *dns_ip1;
const ip_addr_t *dns_ip2;
{
LwIPLock lock;
dns_ip1 = dns_getserver(0);
dns_ip2 = dns_getserver(1);
}
ESP_LOGCONFIG(TAG,
" IP Address: %s\n"

View File

@@ -29,7 +29,6 @@ class IPAddressEthernetInfo : public PollingComponent, public text_sensor::TextS
}
float get_setup_priority() const override { return setup_priority::ETHERNET; }
std::string unique_id() override { return get_mac_address() + "-ethernetinfo"; }
void dump_config() override;
void add_ip_sensors(uint8_t index, text_sensor::TextSensor *s) { this->ip_sensors_[index] = s; }
@@ -52,7 +51,6 @@ class DNSAddressEthernetInfo : public PollingComponent, public text_sensor::Text
}
}
float get_setup_priority() const override { return setup_priority::ETHERNET; }
std::string unique_id() override { return get_mac_address() + "-ethernetinfo-dns"; }
void dump_config() override;
protected:
@@ -63,7 +61,6 @@ class MACAddressEthernetInfo : public Component, public text_sensor::TextSensor
public:
void setup() override { this->publish_state(ethernet::global_eth_component->get_eth_mac_address_pretty()); }
float get_setup_priority() const override { return setup_priority::ETHERNET; }
std::string unique_id() override { return get_mac_address() + "-ethernetinfo-mac"; }
void dump_config() override;
};

View File

@@ -177,6 +177,10 @@ optional<FanRestoreState> Fan::restore_state_() {
return {};
}
void Fan::save_state_() {
if (this->restore_mode_ == FanRestoreMode::NO_RESTORE) {
return;
}
FanRestoreState state{};
state.state = this->state;
state.oscillating = this->oscillating;

View File

@@ -1,6 +1,6 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c, sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
DEVICE_CLASS_DISTANCE,

View File

@@ -1,11 +1,16 @@
import logging
from esphome import pins
import esphome.codegen as cg
from esphome.components import binary_sensor
import esphome.config_validation as cv
from esphome.const import CONF_PIN
from esphome.const import CONF_ID, CONF_NAME, CONF_NUMBER, CONF_PIN
from esphome.core import CORE
from .. import gpio_ns
_LOGGER = logging.getLogger(__name__)
GPIOBinarySensor = gpio_ns.class_(
"GPIOBinarySensor", binary_sensor.BinarySensor, cg.Component
)
@@ -24,7 +29,21 @@ CONFIG_SCHEMA = (
.extend(
{
cv.Required(CONF_PIN): pins.gpio_input_pin_schema,
cv.Optional(CONF_USE_INTERRUPT, default=True): cv.boolean,
# Interrupts are disabled by default for bk72xx, ln882x, and rtl87xx platforms
# due to hardware limitations or lack of reliable interrupt support. This ensures
# stable operation on these platforms. Future maintainers should verify platform
# capabilities before changing this default behavior.
cv.SplitDefault(
CONF_USE_INTERRUPT,
bk72xx=False,
esp32=True,
esp8266=True,
host=True,
ln882x=False,
nrf52=True,
rp2040=True,
rtl87xx=False,
): cv.boolean,
cv.Optional(CONF_INTERRUPT_TYPE, default="ANY"): cv.enum(
INTERRUPT_TYPES, upper=True
),
@@ -41,6 +60,22 @@ async def to_code(config):
pin = await cg.gpio_pin_expression(config[CONF_PIN])
cg.add(var.set_pin(pin))
cg.add(var.set_use_interrupt(config[CONF_USE_INTERRUPT]))
if config[CONF_USE_INTERRUPT]:
# Check for ESP8266 GPIO16 interrupt limitation
# GPIO16 on ESP8266 is a special pin that doesn't support interrupts through
# the Arduino attachInterrupt() function. This is the only known GPIO pin
# across all supported platforms that has this limitation, so we handle it
# here instead of in the platform-specific code.
use_interrupt = config[CONF_USE_INTERRUPT]
if use_interrupt and CORE.is_esp8266 and config[CONF_PIN][CONF_NUMBER] == 16:
_LOGGER.warning(
"GPIO binary_sensor '%s': GPIO16 on ESP8266 doesn't support interrupts. "
"Falling back to polling mode (same as in ESPHome <2025.7). "
"The sensor will work exactly as before, but other pins have better "
"performance with interrupts.",
config.get(CONF_NAME, config[CONF_ID]),
)
use_interrupt = False
cg.add(var.set_use_interrupt(use_interrupt))
if use_interrupt:
cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE]))

View File

@@ -45,3 +45,4 @@ async def to_code(config):
cg.add_define("ESPHOME_BOARD", "host")
cg.add_platformio_option("platform", "platformio/native")
cg.add_platformio_option("lib_ldf_mode", "off")
cg.add_platformio_option("lib_compat_mode", "strict")

View File

@@ -83,7 +83,7 @@ void HttpRequestUpdate::update_task(void *params) {
container.reset(); // Release ownership of the container's shared_ptr
valid = json::parse_json(response, [this_update](JsonObject root) -> bool {
if (!root.containsKey("name") || !root.containsKey("version") || !root.containsKey("builds")) {
if (!root["name"].is<const char *>() || !root["version"].is<const char *>() || !root["builds"].is<JsonArray>()) {
ESP_LOGE(TAG, "Manifest does not contain required fields");
return false;
}
@@ -91,26 +91,26 @@ void HttpRequestUpdate::update_task(void *params) {
this_update->update_info_.latest_version = root["version"].as<std::string>();
for (auto build : root["builds"].as<JsonArray>()) {
if (!build.containsKey("chipFamily")) {
if (!build["chipFamily"].is<const char *>()) {
ESP_LOGE(TAG, "Manifest does not contain required fields");
return false;
}
if (build["chipFamily"] == ESPHOME_VARIANT) {
if (!build.containsKey("ota")) {
if (!build["ota"].is<JsonObject>()) {
ESP_LOGE(TAG, "Manifest does not contain required fields");
return false;
}
auto ota = build["ota"];
if (!ota.containsKey("path") || !ota.containsKey("md5")) {
JsonObject ota = build["ota"].as<JsonObject>();
if (!ota["path"].is<const char *>() || !ota["md5"].is<const char *>()) {
ESP_LOGE(TAG, "Manifest does not contain required fields");
return false;
}
this_update->update_info_.firmware_url = ota["path"].as<std::string>();
this_update->update_info_.md5 = ota["md5"].as<std::string>();
if (ota.containsKey("summary"))
if (ota["summary"].is<const char *>())
this_update->update_info_.summary = ota["summary"].as<std::string>();
if (ota.containsKey("release_url"))
if (ota["release_url"].is<const char *>())
this_update->update_info_.release_url = ota["release_url"].as<std::string>();
return true;

View File

@@ -7,6 +7,7 @@
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <driver/gpio.h>
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 3, 0)
#define SOC_HP_I2C_NUM SOC_I2C_NUM
@@ -20,21 +21,72 @@ static const char *const TAG = "i2c.idf";
void IDFI2CBus::setup() {
ESP_LOGCONFIG(TAG, "Running setup");
static i2c_port_t next_port = I2C_NUM_0;
port_ = next_port;
this->port_ = next_port;
if (this->port_ == I2C_NUM_MAX) {
ESP_LOGE(TAG, "No more than %u buses supported", I2C_NUM_MAX);
this->mark_failed();
return;
}
if (this->timeout_ > 13000) {
ESP_LOGW(TAG, "Using max allowed timeout: 13 ms");
this->timeout_ = 13000;
}
this->recover_();
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2)
next_port = (i2c_port_t) (next_port + 1);
i2c_master_bus_config_t bus_conf{};
memset(&bus_conf, 0, sizeof(bus_conf));
bus_conf.sda_io_num = gpio_num_t(sda_pin_);
bus_conf.scl_io_num = gpio_num_t(scl_pin_);
bus_conf.i2c_port = this->port_;
bus_conf.glitch_ignore_cnt = 7;
#if SOC_LP_I2C_SUPPORTED
if (this->port_ < SOC_HP_I2C_NUM) {
bus_conf.clk_source = I2C_CLK_SRC_DEFAULT;
} else {
bus_conf.lp_source_clk = LP_I2C_SCLK_DEFAULT;
}
#else
bus_conf.clk_source = I2C_CLK_SRC_DEFAULT;
#endif
bus_conf.flags.enable_internal_pullup = sda_pullup_enabled_ || scl_pullup_enabled_;
esp_err_t err = i2c_new_master_bus(&bus_conf, &this->bus_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "i2c_new_master_bus failed: %s", esp_err_to_name(err));
this->mark_failed();
return;
}
i2c_device_config_t dev_conf{};
memset(&dev_conf, 0, sizeof(dev_conf));
dev_conf.dev_addr_length = I2C_ADDR_BIT_LEN_7;
dev_conf.device_address = I2C_DEVICE_ADDRESS_NOT_USED;
dev_conf.scl_speed_hz = this->frequency_;
dev_conf.scl_wait_us = this->timeout_;
err = i2c_master_bus_add_device(this->bus_, &dev_conf, &this->dev_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "i2c_master_bus_add_device failed: %s", esp_err_to_name(err));
this->mark_failed();
return;
}
this->initialized_ = true;
if (this->scan_) {
ESP_LOGV(TAG, "Scanning for devices");
this->i2c_scan_();
}
#else
#if SOC_HP_I2C_NUM > 1
next_port = (next_port == I2C_NUM_0) ? I2C_NUM_1 : I2C_NUM_MAX;
#else
next_port = I2C_NUM_MAX;
#endif
if (port_ == I2C_NUM_MAX) {
ESP_LOGE(TAG, "No more than %u buses supported", SOC_HP_I2C_NUM);
this->mark_failed();
return;
}
recover_();
i2c_config_t conf{};
memset(&conf, 0, sizeof(conf));
conf.mode = I2C_MODE_MASTER;
@@ -53,11 +105,7 @@ void IDFI2CBus::setup() {
this->mark_failed();
return;
}
if (timeout_ > 0) { // if timeout specified in yaml:
if (timeout_ > 13000) {
ESP_LOGW(TAG, "i2c timeout of %" PRIu32 "us greater than max of 13ms on esp-idf, setting to max", timeout_);
timeout_ = 13000;
}
if (timeout_ > 0) {
err = i2c_set_timeout(port_, timeout_ * 80); // unit: APB 80MHz clock cycle
if (err != ESP_OK) {
ESP_LOGW(TAG, "i2c_set_timeout failed: %s", esp_err_to_name(err));
@@ -73,12 +121,15 @@ void IDFI2CBus::setup() {
this->mark_failed();
return;
}
initialized_ = true;
if (this->scan_) {
ESP_LOGV(TAG, "Scanning bus for active devices");
this->i2c_scan_();
}
#endif
}
void IDFI2CBus::dump_config() {
ESP_LOGCONFIG(TAG, "I2C Bus:");
ESP_LOGCONFIG(TAG,
@@ -123,6 +174,74 @@ ErrorCode IDFI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) {
ESP_LOGVV(TAG, "i2c bus not initialized!");
return ERROR_NOT_INITIALIZED;
}
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2)
i2c_operation_job_t jobs[cnt + 4];
uint8_t read = (address << 1) | I2C_MASTER_READ;
size_t last = 0, num = 0;
jobs[num].command = I2C_MASTER_CMD_START;
num++;
jobs[num].command = I2C_MASTER_CMD_WRITE;
jobs[num].write.ack_check = true;
jobs[num].write.data = &read;
jobs[num].write.total_bytes = 1;
num++;
// find the last valid index
for (size_t i = 0; i < cnt; i++) {
const auto &buf = buffers[i];
if (buf.len == 0) {
continue;
}
last = i;
}
for (size_t i = 0; i < cnt; i++) {
const auto &buf = buffers[i];
if (buf.len == 0) {
continue;
}
if (i == last) {
// the last byte read before stop should always be a nack,
// split the last read if len is larger than 1
if (buf.len > 1) {
jobs[num].command = I2C_MASTER_CMD_READ;
jobs[num].read.ack_value = I2C_ACK_VAL;
jobs[num].read.data = (uint8_t *) buf.data;
jobs[num].read.total_bytes = buf.len - 1;
num++;
}
jobs[num].command = I2C_MASTER_CMD_READ;
jobs[num].read.ack_value = I2C_NACK_VAL;
jobs[num].read.data = (uint8_t *) buf.data + buf.len - 1;
jobs[num].read.total_bytes = 1;
num++;
} else {
jobs[num].command = I2C_MASTER_CMD_READ;
jobs[num].read.ack_value = I2C_ACK_VAL;
jobs[num].read.data = (uint8_t *) buf.data;
jobs[num].read.total_bytes = buf.len;
num++;
}
}
jobs[num].command = I2C_MASTER_CMD_STOP;
num++;
esp_err_t err = i2c_master_execute_defined_operations(this->dev_, jobs, num, 20);
if (err == ESP_ERR_INVALID_STATE) {
ESP_LOGVV(TAG, "RX from %02X failed: not acked", address);
return ERROR_NOT_ACKNOWLEDGED;
} else if (err == ESP_ERR_TIMEOUT) {
ESP_LOGVV(TAG, "RX from %02X failed: timeout", address);
return ERROR_TIMEOUT;
} else if (err != ESP_OK) {
ESP_LOGVV(TAG, "RX from %02X failed: %s", address, esp_err_to_name(err));
return ERROR_UNKNOWN;
}
#else
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
esp_err_t err = i2c_master_start(cmd);
if (err != ESP_OK) {
@@ -168,6 +287,7 @@ ErrorCode IDFI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) {
ESP_LOGVV(TAG, "RX from %02X failed: %s", address, esp_err_to_name(err));
return ERROR_UNKNOWN;
}
#endif
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
char debug_buf[4];
@@ -185,6 +305,7 @@ ErrorCode IDFI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) {
return ERROR_OK;
}
ErrorCode IDFI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) {
// logging is only enabled with vv level, if warnings are shown the caller
// should log them
@@ -207,6 +328,49 @@ ErrorCode IDFI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt, b
ESP_LOGVV(TAG, "0x%02X TX %s", address, debug_hex.c_str());
#endif
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2)
i2c_operation_job_t jobs[cnt + 3];
uint8_t write = (address << 1) | I2C_MASTER_WRITE;
size_t num = 0;
jobs[num].command = I2C_MASTER_CMD_START;
num++;
jobs[num].command = I2C_MASTER_CMD_WRITE;
jobs[num].write.ack_check = true;
jobs[num].write.data = &write;
jobs[num].write.total_bytes = 1;
num++;
for (size_t i = 0; i < cnt; i++) {
const auto &buf = buffers[i];
if (buf.len == 0) {
continue;
}
jobs[num].command = I2C_MASTER_CMD_WRITE;
jobs[num].write.ack_check = true;
jobs[num].write.data = (uint8_t *) buf.data;
jobs[num].write.total_bytes = buf.len;
num++;
}
if (stop) {
jobs[num].command = I2C_MASTER_CMD_STOP;
num++;
}
esp_err_t err = i2c_master_execute_defined_operations(this->dev_, jobs, num, 20);
if (err == ESP_ERR_INVALID_STATE) {
ESP_LOGVV(TAG, "TX to %02X failed: not acked", address);
return ERROR_NOT_ACKNOWLEDGED;
} else if (err == ESP_ERR_TIMEOUT) {
ESP_LOGVV(TAG, "TX to %02X failed: timeout", address);
return ERROR_TIMEOUT;
} else if (err != ESP_OK) {
ESP_LOGVV(TAG, "TX to %02X failed: %s", address, esp_err_to_name(err));
return ERROR_UNKNOWN;
}
#else
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
esp_err_t err = i2c_master_start(cmd);
if (err != ESP_OK) {
@@ -252,6 +416,7 @@ ErrorCode IDFI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt, b
ESP_LOGVV(TAG, "TX to %02X failed: %s", address, esp_err_to_name(err));
return ERROR_UNKNOWN;
}
#endif
return ERROR_OK;
}

View File

@@ -2,9 +2,14 @@
#ifdef USE_ESP_IDF
#include <driver/i2c.h>
#include "esphome/core/component.h"
#include "i2c_bus.h"
#include "esp_idf_version.h"
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2)
#include <driver/i2c_master.h>
#else
#include <driver/i2c.h>
#endif
namespace esphome {
namespace i2c {
@@ -38,6 +43,10 @@ class IDFI2CBus : public InternalI2CBus, public Component {
RecoveryCode recovery_result_;
protected:
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2)
i2c_master_dev_handle_t dev_;
i2c_master_bus_handle_t bus_;
#endif
i2c_port_t port_;
uint8_t sda_pin_;
bool sda_pullup_enabled_;

View File

@@ -36,8 +36,8 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub
#ifdef USE_I2S_LEGACY
#if SOC_I2S_SUPPORTS_ADC
void set_adc_channel(adc1_channel_t channel) {
this->adc_channel_ = channel;
void set_adc_channel(adc_channel_t channel) {
this->adc_channel_ = (adc1_channel_t) channel;
this->adc_ = true;
}
#endif

View File

@@ -180,7 +180,7 @@ async def to_code(config):
await speaker.register_speaker(var, config)
if config[CONF_DAC_TYPE] == "internal":
cg.add(var.set_internal_dac_mode(config[CONF_CHANNEL]))
cg.add(var.set_internal_dac_mode(config[CONF_MODE]))
else:
cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN]))
if use_legacy():

View File

@@ -12,6 +12,6 @@ CONFIG_SCHEMA = cv.All(
@coroutine_with_priority(1.0)
async def to_code(config):
cg.add_library("bblanchon/ArduinoJson", "6.18.5")
cg.add_library("bblanchon/ArduinoJson", "7.4.2")
cg.add_define("USE_JSON")
cg.add_global(json_ns.using)

View File

@@ -1,83 +1,76 @@
#include "json_util.h"
#include "esphome/core/log.h"
// ArduinoJson::Allocator is included via ArduinoJson.h in json_util.h
namespace esphome {
namespace json {
static const char *const TAG = "json";
static std::vector<char> global_json_build_buffer; // NOLINT
static const auto ALLOCATOR = RAMAllocator<uint8_t>(RAMAllocator<uint8_t>::ALLOC_INTERNAL);
// Build an allocator for the JSON Library using the RAMAllocator class
struct SpiRamAllocator : ArduinoJson::Allocator {
void *allocate(size_t size) override { return this->allocator_.allocate(size); }
void deallocate(void *pointer) override {
// ArduinoJson's Allocator interface doesn't provide the size parameter in deallocate.
// RAMAllocator::deallocate() requires the size, which we don't have access to here.
// RAMAllocator::deallocate implementation just calls free() regardless of whether
// the memory was allocated with heap_caps_malloc or malloc.
// This is safe because ESP-IDF's heap implementation internally tracks the memory region
// and routes free() to the appropriate heap.
free(pointer); // NOLINT(cppcoreguidelines-owning-memory,cppcoreguidelines-no-malloc)
}
void *reallocate(void *ptr, size_t new_size) override {
return this->allocator_.reallocate(static_cast<uint8_t *>(ptr), new_size);
}
protected:
RAMAllocator<uint8_t> allocator_{RAMAllocator<uint8_t>(RAMAllocator<uint8_t>::NONE)};
};
std::string build_json(const json_build_t &f) {
// Here we are allocating up to 5kb of memory,
// with the heap size minus 2kb to be safe if less than 5kb
// as we can not have a true dynamic sized document.
// The excess memory is freed below with `shrinkToFit()`
auto free_heap = ALLOCATOR.get_max_free_block_size();
size_t request_size = std::min(free_heap, (size_t) 512);
while (true) {
ESP_LOGV(TAG, "Attempting to allocate %zu bytes for JSON serialization", request_size);
DynamicJsonDocument json_document(request_size);
if (json_document.capacity() == 0) {
ESP_LOGE(TAG, "Could not allocate memory for document! Requested %zu bytes, largest free heap block: %zu bytes",
request_size, free_heap);
return "{}";
}
JsonObject root = json_document.to<JsonObject>();
f(root);
if (json_document.overflowed()) {
if (request_size == free_heap) {
ESP_LOGE(TAG, "Could not allocate memory for document! Overflowed largest free heap block: %zu bytes",
free_heap);
return "{}";
}
request_size = std::min(request_size * 2, free_heap);
continue;
}
json_document.shrinkToFit();
ESP_LOGV(TAG, "Size after shrink %zu bytes", json_document.capacity());
std::string output;
serializeJson(json_document, output);
return output;
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
auto doc_allocator = SpiRamAllocator();
JsonDocument json_document(&doc_allocator);
if (json_document.overflowed()) {
ESP_LOGE(TAG, "Could not allocate memory for JSON document!");
return "{}";
}
JsonObject root = json_document.to<JsonObject>();
f(root);
if (json_document.overflowed()) {
ESP_LOGE(TAG, "Could not allocate memory for JSON document!");
return "{}";
}
std::string output;
serializeJson(json_document, output);
return output;
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
}
bool parse_json(const std::string &data, const json_parse_t &f) {
// Here we are allocating 1.5 times the data size,
// with the heap size minus 2kb to be safe if less than that
// as we can not have a true dynamic sized document.
// The excess memory is freed below with `shrinkToFit()`
auto free_heap = ALLOCATOR.get_max_free_block_size();
size_t request_size = std::min(free_heap, (size_t) (data.size() * 1.5));
while (true) {
DynamicJsonDocument json_document(request_size);
if (json_document.capacity() == 0) {
ESP_LOGE(TAG, "Could not allocate memory for document! Requested %zu bytes, free heap: %zu", request_size,
free_heap);
return false;
}
DeserializationError err = deserializeJson(json_document, data);
json_document.shrinkToFit();
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
auto doc_allocator = SpiRamAllocator();
JsonDocument json_document(&doc_allocator);
if (json_document.overflowed()) {
ESP_LOGE(TAG, "Could not allocate memory for JSON document!");
return false;
}
DeserializationError err = deserializeJson(json_document, data);
JsonObject root = json_document.as<JsonObject>();
JsonObject root = json_document.as<JsonObject>();
if (err == DeserializationError::Ok) {
return f(root);
} else if (err == DeserializationError::NoMemory) {
if (request_size * 2 >= free_heap) {
ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source string smaller");
return false;
}
ESP_LOGV(TAG, "Increasing memory allocation.");
request_size *= 2;
continue;
} else {
ESP_LOGE(TAG, "Parse error: %s", err.c_str());
return false;
}
};
if (err == DeserializationError::Ok) {
return f(root);
} else if (err == DeserializationError::NoMemory) {
ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source string smaller");
return false;
}
ESP_LOGE(TAG, "Parse error: %s", err.c_str());
return false;
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
}
} // namespace json

View File

@@ -178,13 +178,8 @@ static constexpr uint8_t NO_MAC[] = {0x08, 0x05, 0x04, 0x03, 0x02, 0x01};
static inline int two_byte_to_int(char firstbyte, char secondbyte) { return (int16_t) (secondbyte << 8) + firstbyte; }
static bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) {
for (uint8_t i = 0; i < HEADER_FOOTER_SIZE; i++) {
if (header_footer[i] != buffer[i]) {
return false; // Mismatch in header/footer
}
}
return true; // Valid header/footer
static inline bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) {
return std::memcmp(header_footer, buffer, HEADER_FOOTER_SIZE) == 0;
}
void LD2410Component::dump_config() {
@@ -300,14 +295,12 @@ void LD2410Component::send_command_(uint8_t command, const uint8_t *command_valu
if (command_value != nullptr) {
len += command_value_len;
}
uint8_t len_cmd[] = {lowbyte(len), highbyte(len), command, 0x00};
// 2 length bytes (low, high) + 2 command bytes (low, high)
uint8_t len_cmd[] = {len, 0x00, command, 0x00};
this->write_array(len_cmd, sizeof(len_cmd));
// command value bytes
if (command_value != nullptr) {
for (uint8_t i = 0; i < command_value_len; i++) {
this->write_byte(command_value[i]);
}
this->write_array(command_value, command_value_len);
}
// frame footer bytes
this->write_array(CMD_FRAME_FOOTER, sizeof(CMD_FRAME_FOOTER));
@@ -401,7 +394,7 @@ void LD2410Component::handle_periodic_data_() {
/*
Moving distance range: 18th byte
Still distance range: 19th byte
Moving enery: 20~28th bytes
Moving energy: 20~28th bytes
*/
for (std::vector<sensor::Sensor *>::size_type i = 0; i != this->gate_move_sensors_.size(); i++) {
sensor::Sensor *s = this->gate_move_sensors_[i];
@@ -480,7 +473,7 @@ bool LD2410Component::handle_ack_data_() {
ESP_LOGE(TAG, "Invalid status");
return true;
}
if (ld2410::two_byte_to_int(this->buffer_data_[8], this->buffer_data_[9]) != 0x00) {
if (this->buffer_data_[8] || this->buffer_data_[9]) {
ESP_LOGW(TAG, "Invalid command: %02X, %02X", this->buffer_data_[8], this->buffer_data_[9]);
return true;
}
@@ -534,8 +527,8 @@ bool LD2410Component::handle_ack_data_() {
const auto *light_function_str = find_str(LIGHT_FUNCTIONS_BY_UINT, this->light_function_);
const auto *out_pin_level_str = find_str(OUT_PIN_LEVELS_BY_UINT, this->out_pin_level_);
ESP_LOGV(TAG,
"Light function is: %s\n"
"Light threshold is: %u\n"
"Light function: %s\n"
"Light threshold: %u\n"
"Out pin level: %s",
light_function_str, this->light_threshold_, out_pin_level_str);
#ifdef USE_SELECT
@@ -600,7 +593,7 @@ bool LD2410Component::handle_ack_data_() {
break;
case CMD_QUERY: { // Query parameters response
if (this->buffer_data_[10] != 0xAA)
if (this->buffer_data_[10] != HEADER)
return true; // value head=0xAA
#ifdef USE_NUMBER
/*
@@ -656,17 +649,11 @@ void LD2410Component::readline_(int readch) {
if (this->buffer_pos_ < 4) {
return; // Not enough data to process yet
}
if (this->buffer_data_[this->buffer_pos_ - 4] == DATA_FRAME_FOOTER[0] &&
this->buffer_data_[this->buffer_pos_ - 3] == DATA_FRAME_FOOTER[1] &&
this->buffer_data_[this->buffer_pos_ - 2] == DATA_FRAME_FOOTER[2] &&
this->buffer_data_[this->buffer_pos_ - 1] == DATA_FRAME_FOOTER[3]) {
if (ld2410::validate_header_footer(DATA_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) {
ESP_LOGV(TAG, "Handling Periodic Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str());
this->handle_periodic_data_();
this->buffer_pos_ = 0; // Reset position index for next message
} else if (this->buffer_data_[this->buffer_pos_ - 4] == CMD_FRAME_FOOTER[0] &&
this->buffer_data_[this->buffer_pos_ - 3] == CMD_FRAME_FOOTER[1] &&
this->buffer_data_[this->buffer_pos_ - 2] == CMD_FRAME_FOOTER[2] &&
this->buffer_data_[this->buffer_pos_ - 1] == CMD_FRAME_FOOTER[3]) {
} else if (ld2410::validate_header_footer(CMD_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) {
ESP_LOGV(TAG, "Handling Ack Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str());
if (this->handle_ack_data_()) {
this->buffer_pos_ = 0; // Reset position index for next message
@@ -772,7 +759,6 @@ void LD2410Component::set_max_distances_timeout() {
0x00};
this->set_config_mode_(true);
this->send_command_(CMD_MAXDIST_DURATION, value, sizeof(value));
delay(50); // NOLINT
this->query_parameters_();
this->set_timeout(200, [this]() { this->restart_and_read_all_info(); });
this->set_config_mode_(false);
@@ -802,7 +788,6 @@ void LD2410Component::set_gate_threshold(uint8_t gate) {
0x01, 0x00, lowbyte(motion), highbyte(motion), 0x00, 0x00,
0x02, 0x00, lowbyte(still), highbyte(still), 0x00, 0x00};
this->send_command_(CMD_GATE_SENS, value, sizeof(value));
delay(50); // NOLINT
this->query_parameters_();
this->set_config_mode_(false);
}
@@ -833,7 +818,6 @@ void LD2410Component::set_light_out_control() {
this->set_config_mode_(true);
uint8_t value[4] = {this->light_function_, this->light_threshold_, this->out_pin_level_, 0x00};
this->send_command_(CMD_SET_LIGHT_CONTROL, value, sizeof(value));
delay(50); // NOLINT
this->query_light_control_();
this->set_timeout(200, [this]() { this->restart_and_read_all_info(); });
this->set_config_mode_(false);

View File

@@ -5,10 +5,10 @@
namespace esphome {
namespace ld2420 {
static const char *const TAG = "LD2420.binary_sensor";
static const char *const TAG = "ld2420.binary_sensor";
void LD2420BinarySensor::dump_config() {
ESP_LOGCONFIG(TAG, "LD2420 BinarySensor:");
ESP_LOGCONFIG(TAG, "Binary Sensor:");
LOG_BINARY_SENSOR(" ", "Presence", this->presence_bsensor_);
}

View File

@@ -2,7 +2,7 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
static const char *const TAG = "LD2420.button";
static const char *const TAG = "ld2420.button";
namespace esphome {
namespace ld2420 {

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