Compare commits

...

770 Commits

Author SHA1 Message Date
J. Nick Koston
4f58e1c8b9
[core] Convert entity vectors to static allocation for reduced memory usage (#10018) 2025-08-01 20:26:22 -10:00
J. Nick Koston
00d9baed11
[bluetooth_proxy] Eliminate heap allocations in connection state reporting (#10010) 2025-08-01 20:26:00 -10:00
dependabot[bot]
f1877ca084
Bump aioesphomeapi from 37.2.2 to 37.2.3 (#10012)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-01 20:16:28 +00:00
dependabot[bot]
1f7c59f88d
Bump esptool from 4.9.0 to 5.0.2 (#9983)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-01 10:02:29 -10:00
dependabot[bot]
b5f42bc493
Bump aioesphomeapi from 37.2.1 to 37.2.2 (#10009)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-01 09:53:56 -10:00
Jesse Hills
d8a46c7482
[CI] Allow multiple grep options for clang-tidy (#10004) 2025-08-01 21:40:53 +12:00
J. Nick Koston
20ad1ab4eb
[wifi] Fix crash during WiFi reconnection on ESP32 with poor signal quality (#9989) 2025-08-01 02:46:52 -05:00
Clyde Stubbs
940a8b43fa
[esp32] Add config option to execute from PSRAM (#9907)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-08-01 16:07:11 +10:00
tomaszduda23
f761404bf6
[nrf52, gpio] check different port notation (#9737) 2025-08-01 16:54:20 +12:00
tomaszduda23
e4dc62ea74
[nrf52, debug] debug component for nrf52 (#8315) 2025-08-01 16:53:40 +12:00
NP v/d Spek
c42c5dd946
[espnow] Basic communication between ESP32 devices (#9582)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-08-01 16:51:01 +12:00
Keith Burzinski
291215909a
[sensor] A little bit of filter clean-up (#9986) 2025-08-01 02:55:59 +00:00
Jonathan Swoboda
0954a6185c
[sensor] Fix bug in percentage based delta filter (#8157)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-08-01 02:15:56 +00:00
J. Nick Koston
f13e742bd5
[ruff] Enable RET and fix all violations (#9929) 2025-08-01 02:10:56 +00:00
tomaszduda23
7a4738ec4e
[nrf52] add adc (#9321)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-08-01 13:49:39 +12:00
Clyde Stubbs
549b0d12b6
[image] Improve schemas (#9791) 2025-08-01 13:19:32 +12:00
Djordje Mandic
412f4ac341
[midea] Use c++17 constexpr and inline static in IrFollowMeData (#10002) 2025-07-31 14:28:22 -10:00
J. Nick Koston
d4ff1bcf5c
[bluetooth_proxy] Implement dynamic service batching based on MTU constraints (#10001)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-31 14:15:12 -10:00
Clyde Stubbs
161f51e1f4
[esp32] Fix strapping pin validation for P4 and H2 (#9980) 2025-08-01 11:48:25 +12:00
Jonathan Swoboda
da0c47629a
[esp32] Bump ESP32 platform to 54.03.21-2 (#10000) 2025-07-31 21:58:57 +00:00
J. Nick Koston
28b277c1c4
[bluetooth_proxy] Optimize UUID transmission with efficient short_uuid field (#9995) 2025-07-31 16:20:53 -05:00
dependabot[bot]
936a090aaa
Bump aioesphomeapi from 37.2.0 to 37.2.1 (#9998)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-31 04:41:19 -10:00
dependabot[bot]
1be6d27012
Bump aioesphomeapi from 37.1.6 to 37.2.0 (#9996)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-30 23:20:43 -10:00
J. Nick Koston
71557c9f58
[bluetooth_proxy] Batch BLE service discovery messages for 67% reduction in API traffic (#9992) 2025-07-30 23:11:11 -05:00
J. Nick Koston
88cfcc1967
[esp32_ble_client] Fix BLE connection stability for WiFi-based proxies (#9993)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-07-30 22:36:33 -05:00
GilDev
fb379bbb88
[wifi] Allow fast_connect with multiple networks (#9947) 2025-07-31 15:34:49 +12:00
mrtoy-me
88d8cfe6a2
[tm1651] Remove dependency on Arduino Library (#9645)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
2025-07-30 20:20:55 -05:00
J. Nick Koston
f25abc3248
[esp32_ble] Fix spurious BLE 5.0 event warnings on ESP32-S3 (#9969) 2025-07-30 20:18:50 -05:00
J. Nick Koston
5b6e152d6c
[esp32_touch] Work around ESP-IDF v5.4 regression in touch_pad_read_filtered (#9957)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-30 20:17:35 -05:00
J. Nick Koston
1d0a38446f
[api] Reduce flash usage through targeted optimizations (#9979) 2025-07-30 20:10:23 -05:00
rwrozelle
853dca6c5c
[api] Bump APIVersion to 1.11 (#9990) 2025-07-30 15:02:09 -10:00
Jesse Hills
97560fd9ef
[CI] Add labels for checkboxes (#9991) 2025-07-31 12:17:20 +12:00
Clyde Stubbs
4b7f3355ea
[core] Fix regex for lambda id() replacement (#9975) 2025-07-30 12:56:43 -10:00
dependabot[bot]
110eac4f09
Bump aioesphomeapi from 37.1.5 to 37.1.6 (#9988)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-30 12:46:01 -10:00
rwrozelle
79533cb0d7
media_player add off on capability (#9294)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2025-07-30 12:02:53 -10:00
dependabot[bot]
f4f69e827b
Bump ruff from 0.12.5 to 0.12.7 (#9976)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-07-30 09:17:47 +00:00
dependabot[bot]
48a4dde824
Bump aioesphomeapi from 37.1.4 to 37.1.5 (#9977)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-30 09:16:00 +00:00
J. Nick Koston
9b4fe54f45
[esp32_ble_client] Fix connection failures with short discovery timeout devices and speed up BLE connections (#9971) 2025-07-29 19:19:12 -10:00
Keith Burzinski
913c58cd2c
[template] Add tests for more sensor filters (#9973) 2025-07-30 14:20:25 +12:00
Keith Burzinski
374858efeb
[sensor] Add new filter: `throttle_with_priority` (#9937) 2025-07-30 12:53:14 +12:00
Samuel Sieb
14dd48f9c3
[wifi] add more disconnect reason descriptions (#9955)
Co-authored-by: Samuel Sieb <samuel@sieb.net>
2025-07-30 12:41:31 +12:00
J. Nick Koston
76d33308d9
[api] Eliminate heap allocations when populating repeated fields from containers (#9948) 2025-07-30 10:41:37 +12:00
Dayowe
daccaf36a7
Fix WiFi to prefer strongest AP when multiple APs have same SSID (#9963)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-07-30 09:10:53 +12:00
Clyde Stubbs
56c88807ee
[mipi_dsi] Add dependencies (#9952) 2025-07-30 08:16:32 +12:00
dependabot[bot]
9c6dbbd8ea
Bump aioesphomeapi from 37.1.3 to 37.1.4 (#9964) 2025-07-29 17:43:35 +00:00
rwrozelle
a7dd849a8e
Media player API enumeration alignment and feature flags (#9949)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-07-29 07:00:47 -10:00
Clyde Stubbs
1f0c606be4
[component] Revert setup messages to LOG_CONFIG level (#9956) 2025-07-29 07:32:45 +00:00
Jesse Hills
ace375944c
[esp32] Fix post build (#9951)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-29 06:44:45 +00:00
Clyde Stubbs
5f7c2f771f
[adc] Enable ADC on ESP32-P4 (#9954) 2025-07-29 18:20:37 +12:00
Jonathan Swoboda
3d5b602288
[esp32] Bump platform to 54.03.21-1 and add support for tagged releases (#9926)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-07-29 05:52:34 +00:00
Djordje Mandic
6d30269565
[output] Add set_min_power & set_max_power actions for FloatOutput (#8934)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-29 17:22:44 +12:00
Keith Burzinski
4ff3137c0d
[gps] Fix slow parsing (#9953) 2025-07-29 17:21:52 +12:00
rwrozelle
9d43ddd6f1
Openthread add Teardown (#9275)
Co-authored-by: mc <mc@debian>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-29 16:25:17 +12:00
Jonathan Swoboda
f733c43dec
[heatpumpir] Fix issue with IRremoteESP8266 being included on ESP32 (#9950) 2025-07-29 15:59:58 +12:00
Keith Burzinski
f5f0a01a85
[text_sensor] Add support for default filters (#9936) 2025-07-29 11:35:40 +12:00
Keith Burzinski
908891a096
[binary_sensor] Add support for default filters (#9935) 2025-07-29 11:35:11 +12:00
Keith Burzinski
7657316a92
[sensor] Add support for default filters (#9934) 2025-07-29 11:34:52 +12:00
J. Nick Koston
4f425c700a
[esp32] Enable LWIP core locking on ESP-IDF to reduce socket operation overhead (#9857) 2025-07-29 11:33:54 +12:00
J. Nick Koston
2c9987869e
[api] Align ProtoSize API design with ProtoWriteBuffer pattern (#9920) 2025-07-29 10:28:32 +12:00
J. Nick Koston
68f388f78e
[api] Optimize protobuf empty message handling to reduce flash and runtime overhead (#9908) 2025-07-29 10:25:07 +12:00
Jesse Hills
189d20a822
[heatpumpir] Bump library to 1.0.37 (#9944) 2025-07-28 16:21:53 -05:00
dependabot[bot]
08defd7360
Bump aioesphomeapi from 37.1.2 to 37.1.3 (#9943)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-28 11:02:53 -10:00
J. Nick Koston
59d466a6c8
[api] Remove unnecessary string copies from optional access (#9897) 2025-07-29 08:55:41 +12:00
J. Nick Koston
85435e6b5f
[scheduler] Eliminate more runtime string allocations from retry (#9930) 2025-07-29 08:54:16 +12:00
Clyde Stubbs
f9453f9642
[lvgl] Bugfix for tileview (#9938) 2025-07-29 08:43:22 +12:00
Jesse Hills
f6cdbe37f9
Merge branch 'release' into dev 2025-07-28 19:34:23 +12:00
Jesse Hills
d6b222c370
Merge pull request #9933 from esphome/bump-2025.7.4
2025.7.4
2025-07-28 19:33:19 +12:00
Clyde Stubbs
eecdaa5163
[config_validation] extend should combine extra validations (#9939) 2025-07-28 19:23:35 +12:00
J. Nick Koston
4933ef780b
[bluetooth_proxy] Fix service discovery cache pollution and descriptor count parameter bug (#9902) 2025-07-27 23:50:17 -05:00
J. Nick Koston
1702356fc8
[api] Fix string lifetime issue in Home Assistant service calls with templated values (#9909) 2025-07-28 16:39:25 +12:00
J. Nick Koston
05f6d01cbe
[api] Add conditional compilation for Home Assistant service subscriptions (#9900) 2025-07-27 18:35:35 -10:00
Jesse Hills
573dad1736
Bump version to 2025.7.4 2025-07-28 15:55:07 +12:00
Jimmy Hedman
3a6cc0ea3d
Fail with old lerp (#9914)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-28 15:55:07 +12:00
cryptk
2f9475a927
Add seed flag when running setup with uv present (#9932) 2025-07-28 15:55:07 +12:00
Jesse Hills
8dce7b0905
[logger] Don't allow `logger.log actions without configuring the logger` (#9821) 2025-07-28 15:55:07 +12:00
Eric Hoffmann
8b0ad3072f
fix: non-optional x/y target calculation for ld2450 (#9849) 2025-07-28 15:55:07 +12:00
Clyde Stubbs
93028a4d90
[gt911] i2c fixes (#9822) 2025-07-28 15:55:07 +12:00
Jonathan Swoboda
c9793f3741
[remote_receiver] Fix idle validation (#9819) 2025-07-28 15:55:07 +12:00
tomaszduda23
5029e248eb
[packages] add example from documentation to component tests (#9891) 2025-07-28 15:28:27 +12:00
Cornelius Mosch
087970bca8
replace os.getlogin() with getpass.getuser() (#9928) 2025-07-28 15:25:32 +12:00
J. Nick Koston
7f0c66f835
[api] Reduce code duplication in send_noise_encryption_set_key_response (#9918) 2025-07-28 15:24:15 +12:00
J. Nick Koston
84ed1bcf34
[light] Reduce flash usage by 832 bytes through code optimization (#9924) 2025-07-28 15:22:56 +12:00
J. Nick Koston
6ed9214465
[core] Use nullptr defaults in status_set_error/warning to reduce flash usage (#9931) 2025-07-28 15:20:30 +12:00
Jimmy Hedman
a3690422bf
Fail with old lerp (#9914)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-28 15:19:17 +12:00
cryptk
20b61d4bdb
Add seed flag when running setup with uv present (#9932) 2025-07-28 14:20:51 +12:00
Clyde Stubbs
a2ed209542
[wifi] Disallow psram config with arduino (#9922) 2025-07-27 02:57:37 -05:00
Keith Burzinski
14862904ac
[power_supply] Optimize logging, reduce flash footprint (#9923) 2025-07-26 19:54:10 -10:00
J. Nick Koston
bcc56648c0
[light] Reduce flash memory usage by optimizing validation and color mode logic (#9921) 2025-07-26 23:56:35 -05:00
Clyde Stubbs
e00839a608
[ci-custom] Report actual changes needed for absolute import (#9919) 2025-07-27 11:51:57 +10:00
Clyde Stubbs
cf73f72119
[wifi] Allow config to use PSRAM (#9866) 2025-07-27 07:45:20 +10:00
J. Nick Koston
981b906579
[logger] Use C++17 nested namespace syntax (#9916) 2025-07-26 11:06:01 -10:00
Clyde Stubbs
0e2520e4c0
[core] Fix format error in log printf (#9911) 2025-07-26 08:02:02 -10:00
dependabot[bot]
84ffa4274c
Bump aioesphomeapi from 37.1.0 to 37.1.2 (#9910)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-25 23:27:35 -10:00
J. Nick Koston
d64e4d3c49
[ruff] Enable FURB rules for code modernization (#9896) 2025-07-26 20:54:03 +12:00
J. Nick Koston
d54db471bd
[i2c] Fix logging level for bus scan results in dump_config (#9904)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-26 01:29:57 -05:00
J. Nick Koston
a9d6ece752
[api] Add conditional compilation for Home Assistant state subscriptions (#9898) 2025-07-26 01:28:44 -05:00
J. Nick Koston
da491f7090
[api] Add missing USE_API_PASSWORD guards to reduce flash usage (#9899) 2025-07-26 01:21:09 -05:00
dependabot[bot]
11f970edec
Bump aioesphomeapi from 37.0.4 to 37.1.0 (#9905)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-26 04:36:42 +00:00
Jesse Hills
6d37b916dc
[logger] Don't allow `logger.log actions without configuring the logger` (#9821) 2025-07-26 16:23:36 +12:00
Clyde Stubbs
b6e0188c42
[mipi_dsi] New display driver for P4 DSI (#9403)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Co-authored-by: Samuel Sieb <samuel-github@sieb.net>
Co-authored-by: Adam Liddell <git@aliddell.com>
Co-authored-by: DT-art1 <81360462+DT-art1@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-07-26 07:36:52 +12:00
J. Nick Koston
b7ce8c116b
[core] Centralize component setup logging to reduce flash usage (#9885) 2025-07-25 19:27:03 +00:00
Clyde Stubbs
2b87589562
[scheduler] Fix null pointer crash (#9893) 2025-07-25 09:12:33 -10:00
J. Nick Koston
f808c38f10
[ruff] Enable PERF rules and fix all violations (#9874) 2025-07-25 08:15:54 -10:00
J. Nick Koston
88ccde4ba1
[scheduler] Fix retry race condition on cancellation (#9788) 2025-07-25 08:14:15 -10:00
GilDev
9ac10d7276
[mqtt] Don’t log state topic subscription for buttons (#9887) 2025-07-25 23:33:29 +12:00
Jesse Hills
457689fa1d
[CI] Fix auto-label workflow - codeowners & listFiles (#9890) 2025-07-25 21:40:42 +12:00
Jesse Hills
773a8b8fb7
[CI] Better mega-pr label handling (#9888) 2025-07-25 21:14:28 +12:00
Jesse Hills
c5c0237a4b
Remove redundant platformio environments (#9886) 2025-07-25 03:16:23 -05:00
Keith Burzinski
e79589efee
[platformio.ini] Add GPS to nrf52-zephyr lib_deps (#9884) 2025-07-25 06:31:32 +00:00
J. Nick Koston
ffebd30033
[ruff] Enable SIM rules and fix code simplification violations (#9872) 2025-07-25 18:26:08 +12:00
Keith Burzinski
cb87f156d0
[platformio.ini] Move GPS to common lib_deps (#9883) 2025-07-24 18:10:03 -10:00
dependabot[bot]
06eba96fdc
Bump ruff from 0.12.4 to 0.12.5 (#9871)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-07-24 10:12:22 -10:00
@RubenKelevra
27119ef7ad
rc522: fix buffer overflow in UID/buffer formatting helpers (#9375) 2025-07-25 00:43:44 +12:00
tomaszduda23
73f58dfe80
[sound_level] fix spelling mistake (#9843) 2025-07-24 23:26:21 +12:00
Keith Burzinski
729f20d765
[gps] Patches to build on IDF, other optimizations (#9728) 2025-07-24 23:23:42 +12:00
Clyde Stubbs
ba72298a63
[factory_reset] Allow factory reset by rapid power cycle (#9749) 2025-07-24 23:21:59 +12:00
Jesse Hills
ba1de5feff
[CI] Refactor auto-label workflow: modular architecture, CODEOWNERS automation, and performance improvements (#9860) 2025-07-24 23:18:29 +12:00
J. Nick Koston
1344103086
[core] Revert #9851 and rename ESPHOME_CORES to ESPHOME_THREAD (#9862) 2025-07-24 11:04:00 +00:00
Keith Burzinski
5bff9bc8d9
[ld2450] Use `Deduplicator` for sensors (#9863) 2025-07-24 04:02:03 -05:00
Clyde Stubbs
568e774116
[mipi] Keep models from different drivers separate (#9865) 2025-07-24 20:31:37 +12:00
J. Nick Koston
c74f12be98
[api] Use C++17 nested namespace syntax (#9856) 2025-07-24 07:15:42 +00:00
Keith Burzinski
705ea4ebaa
[ld2410] Use `Deduplicator` for sensors (#9584)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-24 16:50:50 +12:00
J. Nick Koston
ec2e0c50f1
[bluetooth_proxy] [esp32_ble_tracker] [esp32_ble] Use C++17 nested namespace syntax (#9825) 2025-07-24 15:23:45 +12:00
J. Nick Koston
544cf9b9c0
[core] Fix component state documentation and add state helper method (#9824) 2025-07-24 15:22:42 +12:00
J. Nick Koston
99850255f0
[api] Use emplace_back for TemplatableKeyValuePair construction in HomeAssistant services (#9804) 2025-07-24 15:21:35 +12:00
J. Nick Koston
4a27b34685
[api] Reduce code duplication in protobuf dump methods with helper functions (#9809) 2025-07-24 15:19:58 +12:00
J. Nick Koston
f863189f96
[api] Simplify generated authentication check code (#9806) 2025-07-24 15:18:01 +12:00
J. Nick Koston
04d9698681
[api] Replace magic numbers with MESSAGE_TYPE constants in protobuf switch cases (#9776) 2025-07-24 15:16:54 +12:00
J. Nick Koston
15ba2326ad
[esp32] Fix threading model for single-core variants (S2, C3, C6, H2) (#9851) 2025-07-24 15:15:32 +12:00
Kevin Ahrendt
6398bb2fdf
[i2s_audio] Speaker improvements: CPU core agnostic and more accurate timestamps (#9800)
Co-authored-by: NP v/d Spek <github_mail@lumensoft.nl>
2025-07-24 15:14:00 +12:00
TJ Horner
108e447072
[logger] remove unnecessary call to setTxTimeoutMs (#9854) 2025-07-24 14:51:47 +12:00
Brandon Harvey
cc187ef276
[ld2450] Set `accuracy_decimals=0` as default for "target" entities (#9842) 2025-07-24 14:29:39 +12:00
Keith Burzinski
a3e626757e
[helpers] Add "unknown" value handling to `Deduplicator` (#9855) 2025-07-23 21:22:54 -05:00
Mayur Panchal
5cd7f156b9
Update post_build.py.script to Fix #7137 (#9578)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-24 01:34:39 +00:00
Clyde Stubbs
3960e2bae7
[mipi] Refactor constants and functions (#9853) 2025-07-24 13:27:05 +12:00
Clyde Stubbs
f9534fbd5d
[interval] Fix startup behaviour (#9793) 2025-07-24 08:03:36 +10:00
Eric Hoffmann
0744abe098
fix: non-optional x/y target calculation for ld2450 (#9849) 2025-07-23 11:55:31 -10:00
Clyde Stubbs
49df68beb6
[gt911] i2c fixes (#9822) 2025-07-24 09:52:07 +12:00
Olivier ARCHER
e94cb03272
[modem] network component change (#9801)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-24 09:36:20 +12:00
J. Nick Koston
6ac1073469
[ci] Support C++17 nested namespace syntax in linter (#9826) 2025-07-23 23:32:35 +12:00
J. Nick Koston
378b687a82
[core] Restore COMPONENT_STATE_LOOP_DONE check in calculate_looping_components (#9832) 2025-07-23 23:31:30 +12:00
Jesse Hills
babaa1db3f
[i2c] Use `i2c_master_probe` to scan i2c bus (#9831) 2025-07-23 23:31:13 +12:00
Jonathan Swoboda
bb6f8aeb94
[remote_receiver] Fix idle validation (#9819) 2025-07-22 21:57:42 -05:00
J. Nick Koston
b636b844fc
[core] Initialize looping_components_ before setup blocking phase (#9820) 2025-07-22 16:43:22 -10:00
Jesse Hills
d7a5db3dda
[CI] Paginate codeowner comments to make sure we find it (#9818) 2025-07-23 13:23:06 +12:00
Jesse Hills
ac7f125eb5
[CI] Paginate codeowner comments to make sure we find it (#9817) 2025-07-23 13:22:54 +12:00
Jesse Hills
7bfb08e602
[core] Match LockFreeQueue initialization order (#9813) 2025-07-22 23:46:14 +00:00
Clyde Stubbs
a994ad3642
Workflow - check all comments to find previous bot comment (#9815) 2025-07-23 11:28:15 +12:00
Jonathan Swoboda
116c91e9c5
Bump ESP32 IDF version to 5.4.2 and Arduino version to 3.2.1 (#9770) 2025-07-22 13:15:31 -10:00
Jesse Hills
5a4e2a3eaf
[udp] Move `on_receive` to const (#9811) 2025-07-22 17:56:00 -05:00
Stas
1a7757e7ca
[http_request] set correct duration_ms for failed requests (#9789) 2025-07-22 11:39:03 -10:00
Jonathan Swoboda
e2976162b5
[sgp4x] Fix build (#9794) 2025-07-23 08:54:03 +12:00
Thomas Rupprecht
cf40306297
[audio] fix typo gneneral and divison (#9808) 2025-07-22 20:24:40 +00:00
Jesse Hills
fef2369e66
Merge branch 'release' into dev 2025-07-23 08:10:21 +12:00
Jesse Hills
2b5cceda58
Merge pull request #9796 from esphome/bump-2025.7.3
2025.7.3
2025-07-23 08:09:40 +12:00
Guillermo Ruffino
3bb5a9e2f7
[schema-gen] fix referenced schemas when schema in component platform (#9755) 2025-07-23 06:52:56 +12:00
J. Nick Koston
a614a68f1a
[api] Implement zero-copy string optimization for outgoing protobuf messages (#9790)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-22 07:33:03 -10:00
Jesse Hills
dc26ed9c46
Bump version to 2025.7.3 2025-07-23 00:34:13 +12:00
Keith Burzinski
8674012406
[bme680_bsec] Add suggested alternate when using IDF (#9785) 2025-07-23 00:34:12 +12:00
Keith Burzinski
ae12deff87
[neopixelbus] Add suggested alternate when using IDF (#9783) 2025-07-23 00:34:12 +12:00
Keith Burzinski
cb6acfe24b
[fastled_clockless, fastled_spi] Add suggested alternate when using IDF (#9784) 2025-07-23 00:34:12 +12:00
J. Nick Koston
fc8c5a7438
[core] Process pending loop enables during setup blocking phase (#9787) 2025-07-23 00:34:06 +12:00
Keith Burzinski
f8777d3b66
[config_validation] Add support for suggesting alternate component/platform (#9757)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-23 00:30:36 +12:00
Jesse Hills
76e75f4cdc
[tuya] Update use of fan_schema (#9762) 2025-07-23 00:29:40 +12:00
Jonathan Swoboda
896d7f8f76
[esp32_touch] Fix setup mode in v1 driver (#9725) 2025-07-23 00:29:40 +12:00
JonasB2497
d92ee563f2
[sdl][mipi_spi] Respect clipping when drawing (#9722)
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2025-07-23 00:29:34 +12:00
tmpeh
d6ff790823
Fix format string error in ota_web_server.cpp (#9711) 2025-07-23 00:25:51 +12:00
J. Nick Koston
7ac60c15dc
[gpio] Auto-disable interrupts for shared GPIO pins in binary sensors (#9701) 2025-07-23 00:25:51 +12:00
Keith Burzinski
71cb429a86
[bme680_bsec] Add suggested alternate when using IDF (#9785) 2025-07-22 23:54:09 +12:00
Keith Burzinski
89924ae468
[neopixelbus] Add suggested alternate when using IDF (#9783) 2025-07-22 23:53:45 +12:00
Keith Burzinski
7efe1b8698
[fastled_clockless, fastled_spi] Add suggested alternate when using IDF (#9784) 2025-07-22 23:53:33 +12:00
J. Nick Koston
ac08fb314f
[api] Optimize protobuf memory usage with fixed-size arrays for Bluetooth UUIDs (#9782) 2025-07-22 21:50:49 +12:00
J. Nick Koston
0f0038df24
[core] Process pending loop enables during setup blocking phase (#9787) 2025-07-22 15:47:43 +12:00
Jesse Hills
b17e2019c7
[esp32_ble_tracker] Write require feature defines after all clients are registered (#9780) 2025-07-22 00:49:48 +00:00
J. Nick Koston
e56b681506
[nrf52] Add missing CoreModel define for scheduler (#9777) 2025-07-22 12:32:50 +12:00
Keith Burzinski
238c72b66f
[config_validation] Add support for suggesting alternate component/platform (#9757)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-22 11:29:05 +12:00
J. Nick Koston
118b74b7cd
[api] Optimize noise handshake with memcpy for faster connection setup (#9779) 2025-07-21 17:56:32 -05:00
J. Nick Koston
5343a6d16a
[api] Optimize string encoding with memcpy for 10x performance improvement (#9778) 2025-07-22 09:39:28 +12:00
J. Nick Koston
db62a94712
[api] Implement zero-copy for all protobuf bytes fields (#9761) 2025-07-22 09:38:39 +12:00
Jesse Hills
74ce3d2c0b
[tuya] Update use of fan_schema (#9762) 2025-07-21 15:20:25 -05:00
Jonathan Swoboda
a04c2c8471
[esp32_touch] Fix setup mode in v1 driver (#9725) 2025-07-22 07:25:08 +12:00
Katherine Whitlock
16a426c182
Factor PlatformIO buildgen out of writer.py (#9378)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-21 20:28:11 +12:00
J. Nick Koston
e485895d97
[bluetooth_proxy] Optimize service discovery with in-place construction (#9765) 2025-07-21 20:26:20 +12:00
dependabot[bot]
5fed708761
Bump aioesphomeapi from 37.0.3 to 37.0.4 (#9764)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-20 21:41:18 -10:00
J. Nick Koston
fe1050a583
[tests] Fix flaky scheduler retry test timing (#9760) 2025-07-21 17:21:51 +12:00
J. Nick Koston
305667b06d
[api] Sync uses_password field_ifdef optimization from aioesphomeapi (#9756) 2025-07-21 16:59:48 +12:00
dependabot[bot]
fc286c8bf4
Bump aioesphomeapi from 37.0.2 to 37.0.3 (#9754)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-20 16:20:54 -10:00
Jesse Hills
c60fe4c372
[CI] Dont create new review if existing and dont count tests (#9753) 2025-07-21 13:59:25 +12:00
Jesse Hills
a8d53b7c68
[CI] Use comment marker in too-big reviews (#9751) 2025-07-21 13:33:20 +12:00
Jesse Hills
9508871474
[CI] Fix codeowner workflow requesting the same multiple times (#9750) 2025-07-21 13:20:02 +12:00
J. Nick Koston
a45a45c688
[api] Split frame helper implementation into protocol-specific files (#9746) 2025-07-21 13:10:08 +12:00
Jesse Hills
46da075226
[CI] Add url and dismiss reviews once conditions are met (#9748) 2025-07-21 12:49:00 +12:00
Jesse Hills
efd83dedda
[CI] Fetch platform components and target platforms from hosted json file (#9747) 2025-07-21 12:48:00 +12:00
Jesse Hills
06bd1472de
[CI] Keep original labels when PR has too many lines (#9745) 2025-07-21 12:10:47 +12:00
Jesse Hills
bb9011d65d
[CI] Label PR too-big if it has more than 1000 lines changed (#9744) 2025-07-21 12:01:16 +12:00
J. Nick Koston
5b5982cfdd
[api] Reduce memory usage by eliminating duplicate client info strings (#9740)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-21 11:34:59 +12:00
J. Nick Koston
ecd310dae1
[core] Refactor scheduler to eliminate hidden side effects in empty_ (#9743) 2025-07-20 23:11:30 +00:00
J. Nick Koston
acca629c5c
[api] Fix missing ifdef guards for AreaInfo and DeviceInfo messages (#9730) 2025-07-20 23:05:53 +00:00
J. Nick Koston
0aabdaa0c7
[api] Consolidate error handling and remove unused code (#9726) 2025-07-20 22:52:46 +00:00
Jesse Hills
e5aed29231
[CI] Only mention codeowners once (#9727)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-21 10:39:30 +12:00
J. Nick Koston
2540e7edb2
[api] Remove deprecated protobuf fields to reduce flash usage (#9679) 2025-07-21 10:35:53 +12:00
J. Nick Koston
5511d61dba
[api] Eliminate heap allocation in process_batch_ using stack-allocated PacketInfo array (#9703)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-21 10:24:57 +12:00
J. Nick Koston
e474a33abd
[api] Memory optimizations for API frame helper buffering (#9724) 2025-07-21 10:20:35 +12:00
J. Nick Koston
534a1cf2e7
[esp32_ble_tracker] Batch BLE advertisement processing to reduce overhead (#9699) 2025-07-21 10:17:38 +12:00
J. Nick Koston
335110d71f
[bluetooth_proxy] Fix service discovery on disconnect and refactor connection handling (#9697) 2025-07-21 10:15:34 +12:00
@RubenKelevra
6e31fb181e
core/scheduler: Make millis_64_ rollover monotonic on SMP (#9716)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-07-20 21:57:52 +00:00
DT-art1
7d30d1e987
[const] Move CONF_FLIP_X and CONF_FLIP_Y to `const.py` (#9741) 2025-07-20 20:07:56 +00:00
dependabot[bot]
1e35c07327
Bump aioesphomeapi from 37.0.1 to 37.0.2 (#9738)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-20 07:37:11 -10:00
J. Nick Koston
5b3d61b4a6
[api] Fix missing ifdef guards for field_ifdef fields in protobuf base classes (#9693) 2025-07-20 15:41:00 +12:00
JonasB2497
727e8ca376
[sdl][mipi_spi] Respect clipping when drawing (#9722)
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2025-07-19 22:29:02 +00:00
tmpeh
5ed77c10ae
Fix format string error in ota_web_server.cpp (#9711) 2025-07-19 11:24:26 -10:00
J. Nick Koston
89b9bddf1b
[CI] Fix clang-tidy not running when platformio.ini changes (#9678) 2025-07-19 20:55:21 +12:00
J. Nick Koston
65cbb0d741
[gpio] Auto-disable interrupts for shared GPIO pins in binary sensors (#9701) 2025-07-19 05:31:53 +00:00
Jesse Hills
9533d52d86
Merge branch 'release' into dev 2025-07-19 12:05:32 +12:00
Jesse Hills
6fe4ffa0cf
Merge pull request #9691 from esphome/bump-2025.7.2
2025.7.2
2025-07-19 12:04:51 +12:00
Jesse Hills
19a68dc650
Add core team as codeowner of .github folder (#9663) 2025-07-19 10:55:22 +12:00
Jesse Hills
576ce7ee35
Bump version to 2025.7.2 2025-07-19 09:56:08 +12:00
J. Nick Koston
8a45e877bb
[gpio] Disable interrupt mode by default for LibreTiny platforms (#9687)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-19 09:56:08 +12:00
Kevin Ahrendt
84607c1255
[voice_assistant] Use media player callbacks to track TTS response status (#9670) 2025-07-19 09:56:01 +12:00
Kevin Ahrendt
8664ec0a3b
[speaker] Media player's pipeline properly returns playing state near end of file (#9668) 2025-07-19 09:54:15 +12:00
J. Nick Koston
32d8c60a0b
Fix AsyncTCP version mismatch between platformio.ini and async_tcp component (#9676) 2025-07-19 09:54:00 +12:00
Jesse Hills
976a1e27b4
[lvgl] Prevent keyerror on min/max value widgets with no default (#9660) 2025-07-19 09:53:47 +12:00
J. Nick Koston
cc2c1b1d89
[libretiny] Remove unsupported lock-free queue and event pool implementations (#9653) 2025-07-19 09:53:47 +12:00
Clyde Stubbs
85495d38b7
[lvgl] Fix meter rotation (#9605)
Co-authored-by: clydeps <U5yx99dok9>
2025-07-19 09:53:47 +12:00
J. Nick Koston
84a77ee427
[scheduler] Fix DelayAction cancellation in restart mode scripts (#9646) 2025-07-19 09:53:47 +12:00
@RubenKelevra
11a4115e30
esp32_camera: deprecate i2c_pins; throw error if combined with i2c: block (#9615) 2025-07-19 09:53:47 +12:00
Samuel Sieb
121ed687f3
[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-19 09:53:47 +12:00
J. Nick Koston
c602f3082e
[scheduler] Fix cancellation of timers with empty string names (#9641) 2025-07-19 09:53:39 +12:00
J. Nick Koston
4a43f922c6
[wireguard] Fix boot loop when CONFIG_LWIP_TCPIP_CORE_LOCKING is enabled (#9637) 2025-07-19 09:50:36 +12:00
J. Nick Koston
21e66b76e4
[api] Fix compilation error with char* lambdas in HomeAssistant services (#9638)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-19 09:50:36 +12:00
Flo
cdeed7afa7
Fix template event web_server crash (#9618) 2025-07-19 09:50:36 +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
Thomas Rupprecht
0ffc446315
[web_server] fix Arudino typo (#9404) 2025-07-09 04:15:01 -10:00
Jesse Hills
a692bd98ef
Merge branch 'beta' into dev 2025-07-09 19:34:26 +12:00
Jesse Hills
6178ab7513
Merge pull request #9394 from esphome/bump-2025.7.0b1
2025.7.0b1
2025-07-09 19:33:49 +12:00
Jesse Hills
d24e237967
Bump version to 2025.8.0-dev 2025-07-09 12:10:51 +12:00
Jesse Hills
267574f24c
Bump version to 2025.7.0b1 2025-07-09 12:06:52 +12:00
dependabot[bot]
5235c80781
Bump aioesphomeapi from 34.1.0 to 34.2.0 (#9391)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-08 23:54:33 +00:00
Merikei
0ccc5e340e
[apds9960] Add 0x9E ID (#9392) 2025-07-08 23:52:30 +00:00
Craig Andrews
86c6e4da2a
ESP_EXT1_WAKEUP_ANY_LOW is for s2/s3/c6/h2; ESP_EXT1_WAKEUP_ALL_LOW otherwise (#9387)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-09 11:30:06 +12:00
Jesse Hills
5c8b330eaa
[esp32] Improve flexibility of `only_on_variant` (#9390) 2025-07-09 10:51:17 +12:00
Petr Kejval
4158a5c2a3
Add support for GL-R01 I2C - Time of Flight sensor (#8329)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-09 10:50:45 +12:00
Jesse Hills
05c5364490
[helpers] Fix `format_hex_pretty` resize without separator (#9389)
Co-authored-by: RubenKelevra <cyrond@gmail.com>
2025-07-08 22:13:21 +00:00
Jesse Hills
78eb236a4a
[nfc] Update code to use `format_hex_pretty` (#9384) 2025-07-08 16:47:42 -05:00
Simonas Kazlauskas
691cc5f7dc
lps22: add a component (#7540)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
2025-07-09 09:13:58 +12:00
J. Nick Koston
b3d7f001af
Fix race condition in scheduler string lifetime integration test (#9382) 2025-07-08 06:54:47 -05:00
tmpeh
3f8b691c32
Fix format string error in waveshare_epaper.cpp (#9322)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-08 04:39:07 +00:00
J. Nick Koston
a30f01d668
Fix integration test race condition by isolating PlatformIO directories (#9383) 2025-07-08 04:34:39 +00:00
Clyde Stubbs
4648804db6
[image] Add byte order option and unit tests (#9326) 2025-07-08 02:28:00 +00:00
functionpointer
51377b2625
hydreon_rgxx: remove precipitation_intensity from RG9 (#9367) 2025-07-08 14:27:33 +12:00
Jesse Hills
256f9f9943
[helpers] Improve `format_hex_pretty` (#9380) 2025-07-08 01:30:23 +00:00
J. Nick Koston
a72905191a
Fix flaky test_api_conditional_memory and improve integration test patterns (#9379) 2025-07-08 11:08:21 +12:00
J. Nick Koston
7150f2806f
Run integration tests only on Python 3.13 to reduce CI resource usage (#9377) 2025-07-07 22:14:34 +00:00
J. Nick Koston
ee8ee4e646
Optimize logger callback API by including message length parameter (#9368)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-07 22:00:03 +00:00
Steffen Arntz
fb357b8965
Fix brightness setting not working on SSD1305 128x32 OLEDs (#9376) 2025-07-08 09:25:11 +12:00
Edward Firmo
c4fac1a2ae
[nextion] Optimize component memory usage with bitfield state management (#9373) 2025-07-08 09:21:14 +12:00
J. Nick Koston
42a1f6922f
Eliminate bluetooth_proxy guard variable to save 8 bytes RAM (#9343) 2025-07-08 09:16:48 +12:00
J. Nick Koston
206659ddb8
Refactor voice assistant API methods to reduce code duplication (#9374) 2025-07-08 09:15:49 +12:00
J. Nick Koston
440de12e3f
Don't compile unnecessary platform files (e.g. ESP8266 files on ESP32) (#9354) 2025-07-08 09:04:41 +12:00
J. Nick Koston
b122112d58
Refactor API entity update dispatch to reduce code duplication (#9372) 2025-07-08 08:51:17 +12:00
J. Nick Koston
fe258e1007
Refactor entity lookup methods with macros in preparation for device_id support (#9371) 2025-07-08 08:49:23 +12:00
J. Nick Koston
3976fd02ea
Refactor duplicate socket read error handling in API frame helper (#9370) 2025-07-08 08:39:13 +12:00
J. Nick Koston
e58c793da2
Replace deprecated sprintf with snprintf in API protobuf code generation (#9365) 2025-07-08 08:38:41 +12:00
J. Nick Koston
90fb3680d4
Optimize logger performance by eliminating redundant strlen calls (#9369) 2025-07-08 08:36:36 +12:00
J. Nick Koston
832a787271
Fix format specifier warnings in QuantileFilter logging (#9364) 2025-07-08 08:35:27 +12:00
J. Nick Koston
29747fc730
Fix flaky test_api_conditional_memory by disabling API batch delay (#9360) 2025-07-08 08:35:11 +12:00
J. Nick Koston
e2de6ee29d
Reduce core RAM usage by 40 bytes with static initialization optimizations (#9340) 2025-07-08 08:28:14 +12:00
J. Nick Koston
053feb5e3b
Optimize entity icon memory usage with USE_ENTITY_ICON flag (#9337) 2025-07-08 08:22:40 +12:00
J. Nick Koston
31f36df4ba
Reduce LightCall memory usage by 50 bytes per call (#9333) 2025-07-08 08:20:40 +12:00
J. Nick Koston
3ef392d433
Fix scheduler race conditions and add comprehensive test suite (#9348) 2025-07-08 07:57:55 +12:00
J. Nick Koston
138ff749f3
Optimize Bluetooth proxy batching and increase scan buffer capacity (#9328) 2025-07-08 07:34:12 +12:00
Edward Firmo
e88b8d10ec
[nextion] Add optional device info storage configuration (#9366) 2025-07-07 12:04:01 -05:00
Jesse Hills
8147d117a0
[core] Move platform helper implementations into their own file (#9361) 2025-07-07 15:55:02 +00:00
Edward Firmo
c6f7e84256
[nextion] Review touch_sleep_timeout (#9345) 2025-07-07 07:30:34 -05:00
Keith Burzinski
db877e688a
[ld2450] Clean-up for consistency, reduce CPU usage when idle (#9363) 2025-07-07 07:22:49 -05:00
Edward Firmo
4e25b6da7b
[nextion] Optimize settings memory usage with compile-time defines (#9350)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-07 09:15:13 +00:00
Jonathan Swoboda
83512b88c4
[sx126x] Add sx126x component (#8516)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2025-07-07 05:41:23 +00:00
Jesse Hills
fde5f88192
[inkplate6] Require 240mhz cpu frequency (#9356) 2025-07-06 23:36:34 -05:00
Edward Firmo
2510b5ffb5
[nextion] Replace boolean flags with bitfields to optimize memory usage (#9359) 2025-07-07 04:07:03 +00:00
Keith Burzinski
364b6ca8d0
[scd4x] Memory optimization (#9358) 2025-07-07 03:54:19 +00:00
DT-art1
e49b89a051
Introduce base Camera class to support alternative camera implementations (#9285)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2025-07-07 15:45:00 +12:00
Jonathan Swoboda
bdd52dbaa4
[sx127x] Fix shaping print in dump_config and preallocate packet (#9357) 2025-07-06 22:41:47 -05:00
J. Nick Koston
765793505d
Use std::span to eliminate heap allocation for single-packet API transmissions (#9313) 2025-07-07 14:53:23 +12:00
J. Nick Koston
a303f93236
Fix bluetooth proxy busy loop when disconnecting pending BLE connections (#9332) 2025-07-07 14:50:36 +12:00
J. Nick Koston
492580edc3
Split LockFreeQueue into base and notifying variants to reduce memory usage (#9330) 2025-07-07 14:50:14 +12:00
Jan-Henrik Bruhn
1368139f4d
[update, http_request_update] Implement update available trigger (#9174) 2025-07-07 12:36:09 +12:00
J. Nick Koston
b6fade7339
Fix defer() thread safety issues on multi-core platforms (#9317)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-07 10:01:51 +12:00
Jonathan Swoboda
8da322fe9e
[sx127x] Improve error handling (#9351) 2025-07-06 18:04:43 +00:00
Keith Burzinski
e5a699a004
[ld2410] Reduce RAM usage, general clean-up (#9346) 2025-07-06 09:16:30 -05:00
Keith Burzinski
e061b6dc55
[scd4x] Optimize logging + minor code clean-up (#9347) 2025-07-06 08:37:50 -05:00
J. Nick Koston
4673a5b48c
Eliminate web_server_idf guard variable to save 8 bytes RAM (#9344) 2025-07-06 05:06:32 -05:00
J. Nick Koston
0bc18a8281
Eliminate API component guard variable to save 8 bytes RAM (#9341) 2025-07-05 23:34:55 -05:00
J. Nick Koston
20ba035e3b
Reduce RAM usage by optimizing Color constant storage (#9339) 2025-07-05 22:30:18 -05:00
Edward Firmo
f7019a4ed7
[nextion] Memory optimization (#9338) 2025-07-05 21:56:53 -05:00
J. Nick Koston
a1291c2730
[ld2450] Reduce CPU usage, eliminate redundant sensor updates (#9334) 2025-07-05 21:48:58 -05:00
Adrian Freund
b0f8922056
Mark ESPTime comparison operators as const (#9335) 2025-07-05 22:00:39 +00:00
Thomas Rupprecht
4e9e48e2e7
[rtttl] trim extraneous whitespace in "ac_dimmer" in "PWM_BAD" list (#9318) 2025-07-05 01:23:24 -05:00
J. Nick Koston
86e7013f40
Add const char overload for Component::defer() (#9324) 2025-07-04 21:52:12 -05:00
dependabot[bot]
58b4e7dab2
Bump puremagic from 1.29 to 1.30 (#9320) 2025-07-04 20:54:46 +00:00
J. Nick Koston
d686257cff
Fix web_server busy loop with ungracefully disconnected clients (#9312)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-04 02:07:25 +00:00
Big Mike
adb7ccdbc7
Fix compiler warning in tsl2591 component (#9310) 2025-07-04 13:00:50 +12:00
J. Nick Koston
d00e20ccdf
Reduce web_server loop overhead on ESP32 by avoiding unnecessary semaphore operations (#9308) 2025-07-04 12:53:14 +12:00
J. Nick Koston
25457da97c
Fix web_server URL parsing lifetime issue (#9309) 2025-07-04 12:33:19 +12:00
J. Nick Koston
14d7c4bdbd
Add device_id to entity state messages for sub-device support (#9304) 2025-07-04 12:31:03 +12:00
dependabot[bot]
eef71a79da
Bump ruff from 0.12.1 to 0.12.2 (#9311)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-07-03 21:49:51 +00:00
Kevin Ahrendt
547c7d6dc8
[microphone] simplify mute handling to avoid unnecessary copies (#9303) 2025-07-03 11:17:01 -05:00
Jonathan Swoboda
1ef7b2d64f
[sx127x] Add sx127x component (#7490)
Co-authored-by: Jonathan Swoboda <jonathan.swoboda>
2025-07-03 10:37:18 -05:00
dependabot[bot]
107304b274
Bump aioesphomeapi from 34.0.0 to 34.1.0 (#9301)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-03 14:08:04 +00:00
Sergey Dudanov
b2b6f41ef3
Packages: optional base path for remote git packages (#9279)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-03 19:11:40 +12:00
J. Nick Koston
34db02661c
Allow disabling API batch delay for real-time state updates (#9298) 2025-07-02 21:50:53 -05:00
DanielV
798eef41b9
[Packet transport] Ping timeout sensor (#8694) 2025-07-03 11:25:46 +10:00
Jesse Hills
658e4bac47
Merge branch 'release' into dev 2025-07-03 13:07:58 +12:00
Jesse Hills
f5aab154a6
Merge pull request #9299 from esphome/bump-2025.6.3
2025.6.3
2025-07-03 13:07:17 +12:00
J. Nick Koston
5b55e205ef
Save flash and RAM by conditionally compiling unused API password code (#9297) 2025-07-03 09:42:08 +12:00
J. Nick Koston
4ef5c941c9
Fix missing ifdef guards in API protobuf generator (#9296) 2025-07-03 09:39:20 +12:00
Mariusz Kryński
b9391f2cd4
[ds2484] New component (#9147) 2025-07-03 09:15:37 +12:00
Jesse Hills
66e090ff5b
Bump version to 2025.6.3 2025-07-03 08:27:46 +12:00
Craig Andrews
d41298897f
[http_request] allow retrieval of more than just the first header (#9242) 2025-07-03 08:27:46 +12:00
J. Nick Koston
ba42de536c
Fix crash when event last_event_type is null in web_server (#9266) 2025-07-03 08:27:46 +12:00
Jesse Hills
bdc9f5f3b2
Fix api log client crashing when api encryption is dynamic (#9245) 2025-07-03 08:27:46 +12:00
Rezoran
90f9ab0d3e
[uart] fix: missing uart_config_t struct initialisation (#9235) 2025-07-03 08:27:46 +12:00
Clyde Stubbs
00eb56d8db
[esp32_touch] Fix threshold (#9291)
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-02 09:08:10 -05:00
tomaszduda23
60eac6ea07
[time] fix clang-tidy (#9292) 2025-07-02 14:02:56 +00:00
Jesse Hills
9b3ece4caf
[time] Add `USE_TIME_TIMEZONE` define (#9290) 2025-07-02 08:51:25 -05:00
Colm
289aedcfe2
Don't compile state_to_string() unless debugging. (#7473) 2025-07-03 00:23:37 +12:00
rwrozelle
4cdc804c17
OpenThread - add Device Type (#9272)
Co-authored-by: mc <mc@debian>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-03 00:16:28 +12:00
mrtntome
56a963dfe6
[heatpumpir] Add Support for PHS32 HeatPump (#7378)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-03 00:05:54 +12:00
Aleksey Zinchenko
f6f0e52d5e
[core] Deleting CMakeCache.txt for fast recompilation with ESP-IDF (#8750) 2025-07-02 17:37:31 +10:00
J. Nick Koston
eba2c82fec
Use encode_bytes() for protobuf bytes fields (#9289) 2025-07-02 04:36:09 +00:00
Edward Firmo
fae96e279c
[nextion] memory optimization (#9164)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-02 03:25:06 +00:00
JonasB2497
2fb23becec
made qr_code elements optional (#8896) 2025-07-02 14:56:48 +12:00
Jeremy Brown
095acce3e2
Mmc5603 fix for devices that don't retrieve chip_id (#8959)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-02 02:48:42 +00:00
Craig Andrews
5fa9d22c5d
[http_request] allow retrieval of more than just the first header (#9242) 2025-07-02 14:17:34 +12:00
George
785b14ac84
pulse_meter total (#9282) 2025-07-02 14:14:16 +12:00
J. Nick Koston
84ab758b22
Replace custom OTA implementation in web_server_base (#9274)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-02 13:50:45 +12:00
J. Nick Koston
03566c34ed
Reduce Component memory usage by 40% (8 bytes per component) (#9278) 2025-07-02 13:43:40 +12:00
Jesse Hills
6a096c1d5a
[api] Dump bytes fields as hex instead of unreadable string (#9288) 2025-07-02 13:36:15 +12:00
Jonathan Swoboda
04a46de237
[esp32_rmt_led_strip] Reduce memory usage by 32x with IDF 5.3 (#8388) 2025-07-02 11:40:39 +12:00
J. Nick Koston
0083abe3b5
Fix regression: BK7231N devices not returning entities via API (#9283) 2025-07-02 11:30:03 +12:00
Jonathan Swoboda
3470305d9d
[esp32] Remove IDF 4 support and clean up code (#9145) 2025-07-01 16:22:41 +00:00
Javier Peletier
35de36d690
[modbus] Modbus server role: write holding registers (#9156) 2025-07-01 15:39:06 +12:00
J. Nick Koston
16ef5a9377
Add OTA support to ESP-IDF webserver (#9264) 2025-07-01 15:21:11 +12:00
J. Nick Koston
e3ccb9b46c
Use interrupt based approach for esp32_touch (#9059)
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
2025-07-01 15:04:50 +12:00
Javier Peletier
8c34b72b62
Jinja expressions in configs (Take #3) (#8955) 2025-07-01 14:57:00 +12:00
Jesse Hills
27c745d5a1
[host] Disable platformio ldf (#9277) 2025-07-01 14:38:39 +12:00
J. Nick Koston
9a0ba1657e
Fix entity hash collisions by enforcing unique names across devices per platform (#9276) 2025-07-01 14:38:19 +12:00
Mathieu Rene
db7a420e54
Fix - Pass thread TLVs down to openthread if they are defined (#9182)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-01 10:07:30 +12:00
Jonathan Swoboda
e58baab563
[ethernet] P4 changes and 5.3.0 deprecated warnings (#8457)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-07-01 10:06:59 +12:00
piechade
08c88ba0f2
[smt100] Rename `dielectric_constant to permittivity` (#9175)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-06-30 20:54:23 +00:00
Jesse Hills
78c8cd4c4e
[http_request.update] Fix `size_t` printing (#9144) 2025-06-30 15:50:19 -05:00
Jesse Hills
98e106e0ae
[pins] Update `internal_gpio_pin_number to work directly like internal_gpio_output_pin_number` (#9270) 2025-07-01 08:09:11 +12:00
J. Nick Koston
0cbb5e6c1c
Fix flaky test_api_conditional_memory by waiting for all required states (#9271) 2025-07-01 08:02:43 +12:00
David Woodhouse
8014cbc71e
Fixes for async MQTT (#9273) 2025-06-30 13:25:54 -05:00
J. Nick Koston
aaa7117ec9
Update libsodium to 1.0.20 (#9240)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-06-30 20:47:04 +12:00
Keith Burzinski
3930609d8b
[ld2420] Move consts to cpp file, optimize memory use (#9216) 2025-06-30 01:05:59 -05:00
Gábor Poczkodi
3e553f517b
[remote_base] Fix dumper base class and enable schema extension (#9218) 2025-06-30 17:12:44 +12:00
Keith Burzinski
af0bb634c6
[light] Fix transitions with `lerp` (#9269) 2025-06-30 05:05:52 +00:00
Bjørn Mork
8a9769d4e9
Support DM9051 SPI ethernet device (#6861)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-06-30 16:49:38 +12:00
lamauny
d86f319d66
Add support for LN882X Family (with LibreTiny) (#8954)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-06-30 16:20:36 +12:00
J. Nick Koston
9890659f61
Optimize web_server UrlMatch to avoid heap allocations (#9263) 2025-06-30 04:12:03 +00:00
J. Nick Koston
140ca070a2
Optimize scheduler string storage to eliminate heap allocations (#9251)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-06-30 15:40:36 +12:00
J. Nick Koston
6a354d7c94
Reduce API component memory usage with conditional compilation (#9262) 2025-06-30 15:33:35 +12:00
J. Nick Koston
7f8dd4b254
Fix thread-safe cleanup of event source connections in ESP-IDF web server (#9268) 2025-06-29 19:19:18 -05:00
J. Nick Koston
0b1b8f05e1
Reduce loop enable/disable log spam by using very verbose level (#9267) 2025-06-30 11:49:31 +12:00
Jesse Hills
53e9ffe656
[pi4ioe5v6408] Add new IO Expander (#8888)
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
2025-06-30 11:48:19 +12:00
J. Nick Koston
2289073a1e
Add interrupt support to GPIO binary sensors (#9115)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-06-30 11:47:50 +12:00
J. Nick Koston
687cb1cd2b
Reduce web_server RAM usage by 96 bytes with conditional sorting compilation (#9227) 2025-06-30 11:47:20 +12:00
J. Nick Koston
e907050a17
Remove unused return value from read_message and fix ifdef placement in generated API code (#9256) 2025-06-30 11:45:03 +12:00
J. Nick Koston
a4b57c7e44
Reduce flash usage by making add_message_object non-template (#9258) 2025-06-30 11:43:47 +12:00
J. Nick Koston
24bbfcdce7
Reduce API memory footprint through bitfield consolidation and type sizing (#9252) 2025-06-30 11:42:57 +12:00
J. Nick Koston
d78b720350
Remove single-use send_*_info wrappers in API connection (#9255) 2025-06-30 11:38:11 +12:00
J. Nick Koston
d592208c74
Fix crash when event last_event_type is null in web_server (#9266) 2025-06-29 22:45:41 +00:00
David Woodhouse
971bbd088c
Fix MQTT blocking main loop for multiple seconds at a time (#8325)
Co-authored-by: patagona <patagonahn@gmail.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2025-06-30 08:34:59 +12:00
Jesse Hills
b743577ebe
Fix api log client crashing when api encryption is dynamic (#9245) 2025-06-30 08:07:29 +12:00
dependabot[bot]
a4cc6166a0
Bump aioesphomeapi from 33.1.1 to 34.0.0 (#9265)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-29 14:20:52 -05:00
J. Nick Koston
ed9850c4a4
Remove redundant get_setup_priority() overrides returning default value (#9253) 2025-06-29 13:46:28 -05:00
J. Nick Koston
ddbcf8549c
Reduce web_server code duplication by extracting detail parameter parsing (#9257) 2025-06-29 13:29:18 -05:00
Rezoran
921d0888cd
[uart] fix: missing uart_config_t struct initialisation (#9235) 2025-06-29 15:05:23 +00:00
Keith Burzinski
21e1f3d103
[light] Memory optimizations (#9260) 2025-06-29 11:28:51 +00:00
Keith Burzinski
53ab016098
[adc] Memory optimizations (#9247) 2025-06-29 06:17:53 -05:00
Keith Burzinski
0c249a7006
[thermostat] Memory optimizations (#9259) 2025-06-29 06:16:34 -05:00
J. Nick Koston
86c0fb48a3
Replace ping retry timer with batch queue fallback (#9207) 2025-06-29 09:08:30 +12:00
J. Nick Koston
3f1f99cf37
Extract lock-free queue and event pool to core helpers (#9238) 2025-06-29 08:08:33 +12:00
J. Nick Koston
13d4823db6
Fix buffer corruption in API message encoding with very verbose logging (#9249) 2025-06-29 08:04:42 +12:00
Jimmy Hedman
30f61b26ff
Remove backports of std (#9246) 2025-06-29 07:56:12 +12:00
dependabot[bot]
58b7d0b412
Bump ruff from 0.12.0 to 0.12.1 (#9241)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-06-28 10:21:53 +00:00
Jonathan Swoboda
d37f5b87bd
[esp32] Allow 5.4.2 (#9243) 2025-06-28 01:30:59 -05:00
J. Nick Koston
3f65cee17c
Silence protobuf compatibility warnings when importing aioesphomeapi (#9236) 2025-06-28 16:59:52 +12:00
J. Nick Koston
094bf19ec4
Disable dynamic log level control for ESP32 ESP-IDF builds (#9233) 2025-06-28 16:58:53 +12:00
J. Nick Koston
f8d59b5aeb
Reduce libretiny logconfig messages (#9239) 2025-06-28 15:53:40 +12:00
Jesse Hills
e9870c2922
Merge branch 'release' into dev 2025-06-28 15:48:11 +12:00
Jonathan Swoboda
52ca8deb10
[i2c] Disable i2c scan on certain idf versions (#9237) 2025-06-28 13:32:18 +12:00
Samuel Sieb
156a9160ba
[mcp23xxx_base] fix pin interrupts (#9244)
Co-authored-by: Samuel Sieb <samuel@sieb.net>
2025-06-28 13:31:23 +12:00
Jimmy Hedman
68d66c873e
Upgrade to use C++20 (#9135)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-06-27 17:31:50 +00:00
scaiper
c0b1f32889
[esp32] Change `enable_lwip_mdns_queries default to True` (#9188) 2025-06-27 22:43:18 +12:00
J. Nick Koston
837dd46adf
Reduce component_iterator memory usage (#9205) 2025-06-27 01:56:54 -05:00
J. Nick Koston
13512440ac
[gpio] Reduce ESP32 memory usage by optimizing struct padding (#9230) 2025-06-27 01:53:40 -05:00
J. Nick Koston
7931423e8c
Reduce ethernet component memory usage by 8 bytes (#9231) 2025-06-27 01:52:12 -05:00
J. Nick Koston
62f28902c5
[wifi] Reduce memory usage (#9232) 2025-06-27 01:50:26 -05:00
Jonathan Swoboda
1f94e4cc14
[esp32] Update IDF components to use the registry (#9223) 2025-06-27 03:37:30 +00:00
Thomas Rupprecht
61dfd5541f
use c++17 [[fallthrough]]; (#9149) 2025-06-27 02:40:42 +00:00
Jonathan Swoboda
87321ce10b
[esp32_hosted] Add support for remote wifi (#8833)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-06-27 11:51:13 +12:00
J. Nick Koston
4f5aacdb3a
Optimize SafeModeComponent memory layout to reduce padding (#9228) 2025-06-27 01:25:26 +02:00
Kevin Ahrendt
b182f2d544
[voice_assistant] Support streaming TTS responses and fixes crash for long responses (#9224) 2025-06-27 07:18:51 +12:00
Kevin Ahrendt
4fac8e9cd5
[speaker] bugfix: continue to block tasks if stop flag is set (#9222) 2025-06-27 07:12:58 +12:00
Kevin Ahrendt
d94896c0fb
[audio] Bugfix: improve timeout handling (#9221) 2025-06-27 07:11:50 +12:00
Jesse Hills
15c5dd222f
[tests] Remove extra newline (#9213) 2025-06-26 11:21:19 +00:00
Keith Burzinski
2930c8e9a8
[ld2450] Move consts to cpp file, optimize memory use (#9215) 2025-06-26 04:37:27 -05:00
Keith Burzinski
b12b9b97f4
[ld2410] More optimizations (#9209)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-06-26 04:04:38 -05:00
Jesse Hills
09e5aa6011
[script] Add exec bit to run-in-env (#9212) 2025-06-26 00:59:16 -05:00
Jesse Hills
9549304007
[ci] Lint lock.yml (#9214) 2025-06-26 17:44:02 +12:00
Keith Burzinski
f7ac32ceda
[ld2450] More optimizing, fix copypasta (#9210) 2025-06-26 00:35:30 -05:00
Jonathan Swoboda
92365f133d
[esp32] Improve and simplify IDF component support (#9163) 2025-06-26 17:29:42 +12:00
Jesse Hills
9daa9a6de8
Use shared workflow for locking (#9211) 2025-06-26 16:21:51 +12:00
J. Nick Koston
23b1e428de
Optimize Application class memory layout and reduce loop_interval size (#9208) 2025-06-26 15:35:01 +12:00
J. Nick Koston
f029f4f20e
Fix missing protobuf message dump for batched messages with very verbose logging (#9206) 2025-06-26 13:57:41 +12:00
J. Nick Koston
79e3d2b2d7
Optimize API connection memory with tagged pointers (#9203) 2025-06-26 13:55:12 +12:00
J. Nick Koston
c74e5e0f04
Optimize TemplatableValue memory (#9202) 2025-06-26 13:51:51 +12:00
J. Nick Koston
15ef93ccc9
Optimize API connection loop performance (#9184) 2025-06-26 13:47:41 +12:00
J. Nick Koston
e017250445
Reduce logger CPU usage by disabling loop when buffer is empty (#9160)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-06-26 13:44:07 +12:00
J. Nick Koston
17497eec43
Reduce memory required for sensor entities (#9201)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-06-25 18:15:59 -05:00
Clyde Stubbs
6d0c6329ad
[lvgl] Allow linear positioning of grid cells (#9196) 2025-06-26 10:45:14 +12:00
Clyde Stubbs
f35be6b5cc
[binary_sensor] Add timeout filter (#9198) 2025-06-25 14:09:43 +02:00
DanielV
b18ff48b4a
[API] Sub devices and areas (#8544)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2025-06-25 12:03:41 +00:00
Artem Draft
7c28134214
Rename kVARh/VARh to kvarh/varh (#9191) 2025-06-25 22:36:24 +12:00
Rodrigo Martín
16860e8a30
fix(MQTT): Call disconnect callback on DNS error (#9016)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-06-25 22:20:29 +12:00
Jonathan Swoboda
5362d1a89f
[esp32_hall] Add dummy component (#9125)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-06-25 21:49:31 +12:00
Keith Burzinski
5531296ee0
[ld2410] Use `App.get_loop_component_start_time()`, shorten log messages (#9194)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2025-06-25 21:48:32 +12:00
Keith Burzinski
47db5e26f3
[ld2420] Shorten log messages + other clean-up (#9200) 2025-06-25 03:16:05 -05:00
Keith Burzinski
cf5197b68a
[ld2450] Use `App.get_loop_component_start_time()`, shorten log messages (#9192) 2025-06-25 03:15:50 -05:00
Keith Burzinski
9f831e91b3
[helpers] Add `format_mac_address_pretty` function, migrate components (#9193) 2025-06-25 12:36:33 +12:00
Javier Peletier
2df0ebd895
[modbus_controller] Fix modbus read_lambda precision for non-floats or large integers (#9159) 2025-06-25 11:31:23 +12:00
Jesse Hills
7ad6dab383
[mqtt] Don't wait for connection unless configured to (#8933) 2025-06-24 13:31:38 +12:00
Clyde Stubbs
612c8d5841
[lvgl] Fix dangling pointer issue with qrcode (#9190) 2025-06-24 09:43:40 +10:00
Cody Cutrer
a35e476be5
[opt3001] New component (#6625)
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
2025-06-23 14:31:20 -05:00
Jesse Hills
87a7157fc4
Merge branch 'release' into dev 2025-06-24 07:28:40 +12:00
dependabot[bot]
ac942e0670
Bump aioesphomeapi from 33.1.0 to 33.1.1 (#9187) 2025-06-23 19:58:32 +02:00
Gustavo Ambrozio
2ad266582f
[online_image] Allow suppressing update on url change (#8885) 2025-06-23 20:40:07 +10:00
JonasB2497
1a47164876
Feature fontmetrics (#8978) 2025-06-23 14:47:47 +10:00
myhomeiot
cd22723623
Restore access to BLEScanResult as get_scan_result (#9148) 2025-06-23 15:42:20 +12:00
rwrozelle
aecaffa2f5
Fixes for setup of OpenThread either using TLV or entering Credentials directly (#9157)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-06-23 15:41:29 +12:00
Jesse Hills
87df3596a2
[config validation] Add more ip address / network validators (#9181) 2025-06-23 15:41:06 +12:00
Clyde Stubbs
41c7852128
[lvgl] Use styles instead of object properties for themes (#9116) 2025-06-23 14:25:26 +12:00
Clyde Stubbs
78ec9856fb
[lvgl] Add start_value to bar; make values templatable and updateable (#9056) 2025-06-23 14:23:41 +12:00
J. Nick Koston
2a45467bf6
Pre-reserve looping components vector to reduce memory allocations (#9177) 2025-06-23 14:10:09 +12:00
J. Nick Koston
7fc5bfd787
Reduce RAM usage for scheduled tasks (#9180) 2025-06-23 14:09:34 +12:00
J. Nick Koston
04f592ba6d
Fix slow noise handshake by reading multiple messages per loop (#9130) 2025-06-23 14:07:53 +12:00
J. Nick Koston
59889a6286
Reduce Logger memory usage by optimizing variable sizes (#9161) 2025-06-23 14:06:02 +12:00
Jesse Hills
dc5cbd4df8
[const] Move `CONF_DEVICES to const.py` (#9179) 2025-06-23 09:54:49 +12:00
Edward Firmo
7ab9083d77
[nextion] Revert to millis() on recv_ret_string_ (#9168) 2025-06-22 20:56:50 +00:00
dependabot[bot]
788803d588
Bump flake8 from 7.2.0 to 7.3.0 (#9172)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-06-22 19:05:54 +00:00
dependabot[bot]
cbfd904b9f
Bump aioesphomeapi from 32.2.4 to 33.1.0 (#9173)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-22 11:00:42 +00:00
Jimmy Hedman
c81dbf9d59
Improve on C++17 (#9170) 2025-06-22 12:09:38 +02:00
dependabot[bot]
ac9c608542
Bump esptool from 4.8.1 to 4.9.0 (#9158) 2025-06-21 18:13:07 +02:00
Edward Firmo
a6c20853ca
[nextion] Extract common upload_end_ function to shared file (#9155) 2025-06-21 11:26:14 +02:00
Jesse Hills
4ef0264ed3
Clean up RAMAllocators in light related code (#9142) 2025-06-21 17:32:24 +10:00
Clyde Stubbs
169db9cc0a
[spi] Enable >6 devices with ESP-IDF (#9128) 2025-06-21 07:55:08 +10:00
RoganDawes
b693b8ccb1
[usb-host] Add support for USB Hubs (#9154) 2025-06-20 22:03:15 +10:00
Keith Burzinski
3e98cceb00
[bh1750] Remove redundant platform name from logging (#9153) 2025-06-20 12:33:46 +02:00
Keith Burzinski
46d962dcf1
[wifi, wifi_info] Tidy up/shorten more log messages (#9151) 2025-06-20 22:02:36 +12:00
Edward Firmo
7dbad42470
[nextion] Cached timing optimization (#9150)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
2025-06-20 07:46:12 +00:00
Edward Firmo
eb97781f68
[nextion] Add command queuing to prevent command loss when spacing is active (#9139)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-06-20 01:38:40 -05:00
Jesse Hills
4d0f8528d2
[esp32_camera] Allow sharing i2c bus (#9137)
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
2025-06-19 01:31:19 -05:00
Jesse Hills
2c17b2bacc
[i2c] Make `get_port()` public (#9146) 2025-06-19 05:44:33 +00:00
Jesse Hills
30bea20f7a
Clean up RAMAllocators in display related code (#9141) 2025-06-19 05:17:08 +00:00
Jesse Hills
d4cb4ef994
Clean up RAMAllocators in http_request code (#9143) 2025-06-19 03:11:18 +00:00
J. Nick Koston
9c90ca297a
Fix missing BLE GAP events causing RSSI sensor and beacon failures (#9138) 2025-06-19 03:03:09 +00:00
Jesse Hills
a9e1a4cef3
Clean up RAMAllocators in audio related code (#9140) 2025-06-19 02:53:54 +00:00
J. Nick Koston
0ce3621ac0
Disable Ethernet loop polling when connected and stable (#9102) 2025-06-19 14:49:31 +12:00
Jesse Hills
d527398dae
[i2c] Expose internal i2c bus port number (#9136) 2025-06-18 20:50:47 -05:00
Edward Firmo
2e9ac8945d
[nextion] Fix command spacing double timing and response blocking issues (#9134) 2025-06-19 13:41:20 +12:00
J. Nick Koston
40a5638005
Optimize OTA loop to avoid unnecessary stack allocations (#9129) 2025-06-19 13:33:00 +12:00
J. Nick Koston
8ba22183b9
Add enable_loop_soon_any_context() for thread and ISR-safe loop enabling (#9127) 2025-06-19 13:30:41 +12:00
J. Nick Koston
2e11e66db4
Optimize bluetooth_proxy memory usage on ESP32 (#9114)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-06-19 13:11:13 +12:00
J. Nick Koston
eeb0710ad4
Optimize API component memory usage by reordering class members to reduce padding (#9111) 2025-06-19 13:08:25 +12:00
J. Nick Koston
43c677ef37
Optimize API server performance by using cached loop time (#9104) 2025-06-19 12:12:14 +12:00
J. Nick Koston
95544e489d
Use smaller atomic types for ESP32 BLE Tracker ring buffer indices (#9106) 2025-06-19 12:10:50 +12:00
J. Nick Koston
a08d021f77
Reduce code duplication in auto-generated API protocol code (#9097) 2025-06-19 12:10:01 +12:00
J. Nick Koston
b7b1d17ecb
Remove empty generated protobuf methods (#9098) 2025-06-19 12:06:39 +12:00
Jonathan Swoboda
aa180b9581
Bump ESP32 Arduino version to 3.1.3 (#8604)
Co-authored-by: Kuba Szczodrzyński <kuba@szczodrzynski.pl>
2025-06-19 08:16:25 +12:00
dependabot[bot]
57388254c4
Bump pytest from 8.4.0 to 8.4.1 (#9131)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-18 19:56:26 +00:00
dependabot[bot]
f16f4e2c4c
Bump aioesphomeapi from 32.2.3 to 32.2.4 (#9132)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-18 19:55:59 +00:00
dependabot[bot]
89b70e4352
Bump docker/setup-buildx-action from 3.11.0 to 3.11.1 in the docker-actions group (#9133)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-18 21:37:21 +02:00
J. Nick Koston
6667336bd8
Eliminate memory fragmentation with BLE event pool (#9101) 2025-06-18 21:57:49 +12:00
Kuba Szczodrzyński
669ef7a0b1
[web_server] Upgrade ESPAsync libraries (#8867) 2025-06-18 21:51:00 +12:00
Severin von Wnuck-Lipinski
c612985930
Add support for Xiaomi XMWSDJ04MMC (#8591) 2025-06-18 21:49:39 +12:00
J. Nick Koston
2e534ce41e
Reduce CPU overhead by allowing components to disable their loop() (#9089) 2025-06-18 21:49:25 +12:00
Jesse Hills
fedb54bb38
Merge branch 'release' into dev 2025-06-18 21:41:59 +12:00
Jonathan Swoboda
fd3c22945b
[i2s_audio] Bump esphome/ESP32-audioI2S to 2.3.0 (#9124) 2025-06-18 04:18:23 +00:00
Jonathan Swoboda
53496a1ecd
[heatpumpir] Bump HeatpumpIR to 1.0.35 (#9123) 2025-06-18 04:15:26 +00:00
Jesse Hills
808f964841
Merge branch 'beta' into dev 2025-06-18 12:37:57 +12:00
J. Nick Koston
3bc5db4fd7
Bump ruff in pre-commit to 0.12.0 (#9121) 2025-06-18 10:54:45 +12:00
dependabot[bot]
0bf613bd34
Bump ruff from 0.11.13 to 0.12.0 (#9120)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-18 00:08:22 +02:00
Jonathan Swoboda
43ab63455b
Pin libretiny to 1.9.1 (#9118) 2025-06-17 22:42:36 +02:00
J. Nick Koston
47e7988c8e
Reduce Switch component memory usage by 8 bytes per instance (#9112) 2025-06-17 13:14:03 -05:00
J. Nick Koston
7ed095e635
Optimize LightState memory layout (#9113) 2025-06-17 13:07:45 -05:00
Michael Hansen
cb8b0ec62e
Add intent progress event to voice assistant enum (#9103) 2025-06-17 13:05:06 -05:00
J. Nick Koston
bf161f1eaa
Resolve esphome::optional vs std::optional ambiguity in code generation (#9119) 2025-06-17 13:04:45 -05:00
Jonathan Swoboda
78c8447d1e
[esp32_hall] Remove esp32_hall (#9117) 2025-06-17 15:47:42 +00:00
dependabot[bot]
5ffe50381a
Bump docker/setup-buildx-action from 3.10.0 to 3.11.0 in the docker-actions group (#9105)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-17 10:41:54 +02:00
Jonathan Swoboda
b08bd0c24a
Bump LibreTiny recommended version to 1.9.1 (#9110) 2025-06-17 04:41:18 +02:00
Clyde Stubbs
738ad8e9d3
[spi] Cater for non-word-aligned buffers on esp8266 (#9108) 2025-06-17 02:30:09 +00:00
Kevin Ahrendt
fa7c42511a
[i2s_audio] Bugfix: crashes when unlocking i2s bus multiple times (#9100) 2025-06-17 12:59:07 +12:00
Keith Burzinski
68ef9cb3dc
[i2s_audio] Add `dump_config` methods, shorten log messages (#9099) 2025-06-16 07:36:49 +00:00
Jesse Hills
8e176b9c61
Merge branch 'beta' into dev 2025-06-16 17:07:31 +12:00
Jesse Hills
c4f7c2d259
[ruff] Apply various ruff suggestions (#8947) 2025-06-15 22:13:14 -05:00
Jesse Hills
882bfc79c7
Remove `std::` prefix as not all platforms have access yet. (#9095) 2025-06-16 12:55:23 +12:00
J. Nick Koston
c17a3b6fcc
Reduce Component memory usage by 20 bytes per component (#9080)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-06-16 09:34:37 +12:00
J. Nick Koston
28d11553e0
Reduce Component blocking threshold memory usage by 2 bytes per component (#9081) 2025-06-16 09:33:38 +12:00
J. Nick Koston
1dbebe90ba
Add common base classes for entity protobuf messages to reduce duplicate code (#9090) 2025-06-16 09:29:25 +12:00
J. Nick Koston
06810e8e6a
Ensure we can send batches where the first message exceeds MAX_PACKET_SIZE (#9068) 2025-06-16 09:22:14 +12:00
Kevin Ahrendt
bd85ba9b6a
[i2s_audio] Check for a nullptr before disabling and deleting channel (#9062) 2025-06-16 09:19:50 +12:00
J. Nick Koston
be58cdda3b
Fix protobuf encoding size mismatch by passing force parameter in encode_string (#9074) 2025-06-16 09:19:04 +12:00
J. Nick Koston
fcce4a8be6
Make BLE queue lock free (#9088) 2025-06-16 09:16:46 +12:00
J. Nick Koston
61a558a062
Implement a lock free ring buffer for BLEScanResult to avoid drops (#9087) 2025-06-16 08:53:45 +12:00
dhewg
59f69ac5ca
[fan] fix initial FanCall to properly set speed (#8277) 2025-06-15 13:16:33 -05:00
dependabot[bot]
f82ac34784
Bump aioesphomeapi from 32.2.1 to 32.2.3 (#9091)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-15 10:42:01 -05:00
J. Nick Koston
07cf6e723b
Fix unbound BLE event queue growth and reduce memory usage (#9052) 2025-06-15 04:45:41 +00:00
J. Nick Koston
78e3c6333f
Optimize Application area_ from std::string to const char* (#9085) 2025-06-14 22:46:40 -05:00
J. Nick Koston
98e2684107
Fix API message encoding to return actual size instead of calculated size (#9073) 2025-06-15 15:46:02 +12:00
J. Nick Koston
cb019fff9a
Optimize memory usage by lazy-allocating raw callbacks in sensors (#9077) 2025-06-15 15:28:15 +12:00
J. Nick Koston
4305c44440
Reduce entity memory usage by eliminating field shadowing and bit-packing (#9076) 2025-06-15 15:21:55 +12:00
J. Nick Koston
a1e4143600
Small optimizations to api buffer helper (#9071) 2025-06-15 14:55:03 +12:00
J. Nick Koston
374c33e8dc
Optimize Component and Application state storage from uint32_t to uint8_t (#9082) 2025-06-15 14:48:53 +12:00
J. Nick Koston
dcfe7af9d3
Make ParseOnOffState enum uint8_t (#9083) 2025-06-15 14:44:45 +12:00
Keith Burzinski
049c7e00ca
Move some consts to `const.py` (#9084) 2025-06-14 23:23:52 +00:00
Jimmy Hedman
ee37d2f9c8
Build with C++17 (#8603)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-06-14 08:21:39 -05:00
J. Nick Koston
92ea697119
Fix captive_portal loading entire web_server (#9066) 2025-06-14 08:19:41 -05:00
dependabot[bot]
1c488d375f
Bump pytest-asyncio from 0.26.0 to 1.0.0 (#9067)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-13 18:40:18 -05:00
Jesse Hills
1a03b4949f
[esp32] Dynamically set default framework based on variant (#9060) 2025-06-14 11:17:06 +12:00
Jesse Hills
731b7808cd
[prometheus] Remove `cv.only_with_arduino` (#9061) 2025-06-14 11:08:07 +12:00
J. Nick Koston
d9da4cf24d
Fix misleading comment in API (#9069) 2025-06-14 09:10:33 +12:00
Nate Clark
666a3ee5e9
Fix BYPASS_AUTO feature to work with or without an arming delay (#9051) 2025-06-13 13:31:00 -05:00
Nico B
02469c2d4c
ina219: powerdown the sensor on shutdown (#9053) 2025-06-13 18:17:38 +00:00
Edward Firmo
2a629cae93
[nextion] Remove upload flags reset from success path to prevent TFT corruption (#9064) 2025-06-13 13:39:32 +12:00
dependabot[bot]
1f14c316a3
Bump pytest-cov from 6.1.1 to 6.2.1 (#9063)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-12 18:16:37 -05:00
J. Nick Koston
dac738a916
Always perform select() when loop duration exceeds interval (#9058) 2025-06-12 03:27:10 +00:00
Clyde Stubbs
261b561bb2
[binary_sensor] Add action to invalidate state and pass to HA (#8961)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-06-12 09:16:20 +10:00
J. Nick Koston
0228379a2e
Fix dashboard logging being escaped before parser (#9054) 2025-06-11 16:17:47 -05:00
Jesse Hills
da79215bc3
Merge branch 'beta' into dev 2025-06-12 07:56:24 +12:00
Thomas Rupprecht
a59e1c7011
[core/pins] improve pins types (#8848)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-06-11 18:06:41 +00:00
Jesse Hills
f467c79a20
Bump version to 2025.7.0-dev 2025-06-11 23:16:56 +12:00
1580 changed files with 54833 additions and 26809 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 @@
6af8b429b94191fe8e239fcb3b73f7982d0266cb5b05ffbc81edaeac1bc8c273

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 blank_issues_enabled: false
contact_links: contact_links:
- name: Issue Tracker - name: Report an issue with the ESPHome documentation
url: https://github.com/esphome/issues url: https://github.com/esphome/esphome-docs/issues/new/choose
about: Please create bug reports in the dedicated issue tracker. about: Report an issue with the ESPHome documentation.
- name: Feature Request Tracker - name: Report an issue with the ESPHome web server
url: https://github.com/esphome/feature-requests url: https://github.com/esphome/esphome-webserver/issues/new/choose
about: | about: Report an issue with the ESPHome web server.
Please create feature requests in the dedicated feature request tracker. - 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 - name: Frequently Asked Question
url: https://esphome.io/guides/faq.html url: https://esphome.io/guides/faq.html
about: | about: Please view the FAQ for common questions and what to include in a bug report.
Please view the FAQ for common questions and what
to include in a bug report.

View File

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

View File

@ -41,7 +41,7 @@ runs:
shell: bash shell: bash
run: | run: |
python -m venv venv python -m venv venv
./venv/Scripts/activate source ./venv/Scripts/activate
python --version python --version
pip install -r requirements.txt -r requirements_test.txt pip install -r requirements.txt -r requirements_test.txt
pip install -e . 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 # Hypotehsis is only used for testing and is updated quite often
- dependency-name: hypothesis - dependency-name: hypothesis
- package-ecosystem: github-actions - package-ecosystem: github-actions
labels:
- "dependencies"
- "github-actions"
directory: "/" directory: "/"
schedule: schedule:
interval: daily interval: daily
@ -20,11 +23,17 @@ updates:
- "docker/login-action" - "docker/login-action"
- "docker/setup-buildx-action" - "docker/setup-buildx-action"
- package-ecosystem: github-actions - package-ecosystem: github-actions
labels:
- "dependencies"
- "github-actions"
directory: "/.github/actions/build-image" directory: "/.github/actions/build-image"
schedule: schedule:
interval: daily interval: daily
open-pull-requests-limit: 10 open-pull-requests-limit: 10
- package-ecosystem: github-actions - package-ecosystem: github-actions
labels:
- "dependencies"
- "github-actions"
directory: "/.github/actions/restore-python" directory: "/.github/actions/restore-python"
schedule: schedule:
interval: daily interval: daily

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

@ -0,0 +1,652 @@
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:
SMALL_PR_THRESHOLD: 30
MAX_LABELS: 15
TOO_BIG_THRESHOLD: 1000
COMPONENT_LABEL_THRESHOLD: 10
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: 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');
// Constants
const SMALL_PR_THRESHOLD = parseInt('${{ env.SMALL_PR_THRESHOLD }}');
const MAX_LABELS = parseInt('${{ env.MAX_LABELS }}');
const TOO_BIG_THRESHOLD = parseInt('${{ env.TOO_BIG_THRESHOLD }}');
const COMPONENT_LABEL_THRESHOLD = parseInt('${{ env.COMPONENT_LABEL_THRESHOLD }}');
const BOT_COMMENT_MARKER = '<!-- auto-label-pr-bot -->';
const CODEOWNERS_MARKER = '<!-- codeowners-request -->';
const TOO_BIG_MARKER = '<!-- too-big-request -->';
const MANAGED_LABELS = [
'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',
'needs-codeowners',
'too-big',
'labeller-recheck',
'bugfix',
'new-feature',
'breaking-change',
'code-quality'
];
const DOCS_PR_PATTERNS = [
/https:\/\/github\.com\/esphome\/esphome-docs\/pull\/\d+/,
/esphome\/esphome-docs#\d+/
];
// Global state
const { owner, repo } = context.repo;
const pr_number = context.issue.number;
// Get current labels and PR data
const { data: currentLabelsData } = await github.rest.issues.listLabelsOnIssue({
owner,
repo,
issue_number: pr_number
});
const currentLabels = currentLabelsData.map(label => label.name);
const managedLabels = currentLabels.filter(label =>
label.startsWith('component: ') || MANAGED_LABELS.includes(label)
);
// Check for mega-PR early - if present, skip most automatic labeling
const isMegaPR = currentLabels.includes('mega-pr');
// Get all PR files with automatic pagination
const prFiles = await github.paginate(
github.rest.pulls.listFiles,
{
owner,
repo,
pull_number: pr_number
}
);
// Calculate data from PR files
const changedFiles = prFiles.map(file => file.filename);
const totalChanges = prFiles.reduce((sum, file) => sum + (file.additions || 0) + (file.deletions || 0), 0);
console.log('Current labels:', currentLabels.join(', '));
console.log('Changed files:', changedFiles.length);
console.log('Total changes:', totalChanges);
if (isMegaPR) {
console.log('Mega-PR detected - applying limited labeling logic');
}
// Fetch API data
async function fetchApiData() {
try {
const response = await fetch('https://data.esphome.io/components.json');
const componentsData = await response.json();
return {
targetPlatforms: componentsData.target_platforms || [],
platformComponents: componentsData.platform_components || []
};
} catch (error) {
console.log('Failed to fetch components data from API:', error.message);
return { targetPlatforms: [], platformComponents: [] };
}
}
// Strategy: Merge branch detection
async function detectMergeBranch() {
const labels = new Set();
const baseRef = context.payload.pull_request.base.ref;
if (baseRef === 'release') {
labels.add('merging-to-release');
} else if (baseRef === 'beta') {
labels.add('merging-to-beta');
}
return labels;
}
// Strategy: Component and platform labeling
async function detectComponentPlatforms(apiData) {
const labels = new Set();
const componentRegex = /^esphome\/components\/([^\/]+)\//;
const targetPlatformRegex = new RegExp(`^esphome\/components\/(${apiData.targetPlatforms.join('|')})/`);
for (const file of changedFiles) {
const componentMatch = file.match(componentRegex);
if (componentMatch) {
labels.add(`component: ${componentMatch[1]}`);
}
const platformMatch = file.match(targetPlatformRegex);
if (platformMatch) {
labels.add(`platform: ${platformMatch[1]}`);
}
}
return labels;
}
// Strategy: New component detection
async function detectNewComponents() {
const labels = new Set();
const addedFiles = prFiles.filter(file => file.status === 'added').map(file => file.filename);
for (const file of addedFiles) {
const componentMatch = file.match(/^esphome\/components\/([^\/]+)\/__init__\.py$/);
if (componentMatch) {
try {
const content = fs.readFileSync(file, 'utf8');
if (content.includes('IS_TARGET_PLATFORM = True')) {
labels.add('new-target-platform');
}
} catch (error) {
console.log(`Failed to read content of ${file}:`, error.message);
}
labels.add('new-component');
}
}
return labels;
}
// Strategy: New platform detection
async function detectNewPlatforms(apiData) {
const labels = new Set();
const addedFiles = prFiles.filter(file => file.status === 'added').map(file => file.filename);
for (const file of addedFiles) {
const platformFileMatch = file.match(/^esphome\/components\/([^\/]+)\/([^\/]+)\.py$/);
if (platformFileMatch) {
const [, component, platform] = platformFileMatch;
if (apiData.platformComponents.includes(platform)) {
labels.add('new-platform');
}
}
const platformDirMatch = file.match(/^esphome\/components\/([^\/]+)\/([^\/]+)\/__init__\.py$/);
if (platformDirMatch) {
const [, component, platform] = platformDirMatch;
if (apiData.platformComponents.includes(platform)) {
labels.add('new-platform');
}
}
}
return labels;
}
// Strategy: Core files detection
async function detectCoreChanges() {
const labels = new Set();
const coreFiles = changedFiles.filter(file =>
file.startsWith('esphome/core/') ||
(file.startsWith('esphome/') && file.split('/').length === 2)
);
if (coreFiles.length > 0) {
labels.add('core');
}
return labels;
}
// Strategy: PR size detection
async function detectPRSize() {
const labels = new Set();
const testChanges = prFiles
.filter(file => file.filename.startsWith('tests/'))
.reduce((sum, file) => sum + (file.additions || 0) + (file.deletions || 0), 0);
const nonTestChanges = totalChanges - testChanges;
if (totalChanges <= SMALL_PR_THRESHOLD) {
labels.add('small-pr');
}
// Don't add too-big if mega-pr label is already present
if (nonTestChanges > TOO_BIG_THRESHOLD && !isMegaPR) {
labels.add('too-big');
}
return labels;
}
// Strategy: Dashboard changes
async function detectDashboardChanges() {
const labels = new Set();
const dashboardFiles = changedFiles.filter(file =>
file.startsWith('esphome/dashboard/') ||
file.startsWith('esphome/components/dashboard_import/')
);
if (dashboardFiles.length > 0) {
labels.add('dashboard');
}
return labels;
}
// Strategy: GitHub Actions changes
async function detectGitHubActionsChanges() {
const labels = new Set();
const githubActionsFiles = changedFiles.filter(file =>
file.startsWith('.github/workflows/')
);
if (githubActionsFiles.length > 0) {
labels.add('github-actions');
}
return labels;
}
// Strategy: Code owner detection
async function detectCodeOwner() {
const labels = new Set();
try {
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;
const codeownersLines = codeownersContent.split('\n')
.map(line => line.trim())
.filter(line => line && !line.startsWith('#'));
const codeownersRegexes = codeownersLines.map(line => {
const parts = line.split(/\s+/);
const pattern = parts[0];
const owners = parts.slice(1);
let regex;
if (pattern.endsWith('*')) {
const dir = pattern.slice(0, -1);
regex = new RegExp(`^${dir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`);
} else if (pattern.includes('*')) {
// First escape all regex special chars except *, then replace * with .*
const regexPattern = pattern
.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
.replace(/\*/g, '.*');
regex = new RegExp(`^${regexPattern}$`);
} else {
regex = new RegExp(`^${pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`);
}
return { regex, owners };
});
for (const file of changedFiles) {
for (const { regex, owners } of codeownersRegexes) {
if (regex.test(file) && owners.some(owner => owner === `@${prAuthor}`)) {
labels.add('by-code-owner');
return labels;
}
}
}
} catch (error) {
console.log('Failed to read or parse CODEOWNERS file:', error.message);
}
return labels;
}
// Strategy: Test detection
async function detectTests() {
const labels = new Set();
const testFiles = changedFiles.filter(file => file.startsWith('tests/'));
if (testFiles.length > 0) {
labels.add('has-tests');
}
return labels;
}
// Strategy: PR Template Checkbox detection
async function detectPRTemplateCheckboxes() {
const labels = new Set();
const prBody = context.payload.pull_request.body || '';
console.log('Checking PR template checkboxes...');
// Check for checked checkboxes in the "Types of changes" section
const checkboxPatterns = [
{ pattern: /- \[x\] Bugfix \(non-breaking change which fixes an issue\)/i, label: 'bugfix' },
{ pattern: /- \[x\] New feature \(non-breaking change which adds functionality\)/i, label: 'new-feature' },
{ pattern: /- \[x\] Breaking change \(fix or feature that would cause existing functionality to not work as expected\)/i, label: 'breaking-change' },
{ pattern: /- \[x\] Code quality improvements to existing code or addition of tests/i, label: 'code-quality' }
];
for (const { pattern, label } of checkboxPatterns) {
if (pattern.test(prBody)) {
console.log(`Found checked checkbox for: ${label}`);
labels.add(label);
}
}
return labels;
}
// Strategy: Requirements detection
async function detectRequirements(allLabels) {
const labels = new Set();
// Check for missing tests
if ((allLabels.has('new-component') || allLabels.has('new-platform')) && !allLabels.has('has-tests')) {
labels.add('needs-tests');
}
// Check for missing docs
if (allLabels.has('new-component') || allLabels.has('new-platform') || allLabels.has('new-feature')) {
const prBody = context.payload.pull_request.body || '';
const hasDocsLink = DOCS_PR_PATTERNS.some(pattern => pattern.test(prBody));
if (!hasDocsLink) {
labels.add('needs-docs');
}
}
// Check for missing CODEOWNERS
if (allLabels.has('new-component')) {
const codeownersModified = prFiles.some(file =>
file.filename === 'CODEOWNERS' &&
(file.status === 'modified' || file.status === 'added') &&
(file.additions || 0) > 0
);
if (!codeownersModified) {
labels.add('needs-codeowners');
}
}
return labels;
}
// Generate review messages
function generateReviewMessages(finalLabels) {
const messages = [];
const prAuthor = context.payload.pull_request.user.login;
// Too big message
if (finalLabels.includes('too-big')) {
const testChanges = prFiles
.filter(file => file.filename.startsWith('tests/'))
.reduce((sum, file) => sum + (file.additions || 0) + (file.deletions || 0), 0);
const nonTestChanges = totalChanges - testChanges;
const tooManyLabels = finalLabels.length > MAX_LABELS;
const tooManyChanges = nonTestChanges > TOO_BIG_THRESHOLD;
let message = `${TOO_BIG_MARKER}\n### 📦 Pull Request Size\n\n`;
if (tooManyLabels && tooManyChanges) {
message += `This PR is too large with ${nonTestChanges} line changes (excluding tests) and affects ${finalLabels.length} different components/areas.`;
} else if (tooManyLabels) {
message += `This PR affects ${finalLabels.length} different components/areas.`;
} else {
message += `This PR is too large with ${nonTestChanges} line changes (excluding tests).`;
}
message += ` Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.\n\n`;
message += `For guidance on breaking down large PRs, see: https://developers.esphome.io/contributing/submitting-your-work/#how-to-approach-large-submissions`;
messages.push(message);
}
// CODEOWNERS message
if (finalLabels.includes('needs-codeowners')) {
const message = `${CODEOWNERS_MARKER}\n### 👥 Code Ownership\n\n` +
`Hey there @${prAuthor},\n` +
`Thanks for submitting this pull request! Can you add yourself as a codeowner for this integration? ` +
`This way we can notify you if a bug report for this integration is reported.\n\n` +
`In \`__init__.py\` of the integration, please add:\n\n` +
`\`\`\`python\nCODEOWNERS = ["@${prAuthor}"]\n\`\`\`\n\n` +
`And run \`script/build_codeowners.py\``;
messages.push(message);
}
return messages;
}
// Handle reviews
async function handleReviews(finalLabels) {
const reviewMessages = generateReviewMessages(finalLabels);
const hasReviewableLabels = finalLabels.some(label =>
['too-big', 'needs-codeowners'].includes(label)
);
const { data: reviews } = await github.rest.pulls.listReviews({
owner,
repo,
pull_number: pr_number
});
const botReviews = reviews.filter(review =>
review.user.type === 'Bot' &&
review.state === 'CHANGES_REQUESTED' &&
review.body && review.body.includes(BOT_COMMENT_MARKER)
);
if (hasReviewableLabels) {
const reviewBody = `${BOT_COMMENT_MARKER}\n\n${reviewMessages.join('\n\n---\n\n')}`;
if (botReviews.length > 0) {
// Update existing review
await github.rest.pulls.updateReview({
owner,
repo,
pull_number: pr_number,
review_id: botReviews[0].id,
body: reviewBody
});
console.log('Updated existing bot review');
} else {
// Create new review
await github.rest.pulls.createReview({
owner,
repo,
pull_number: pr_number,
body: reviewBody,
event: 'REQUEST_CHANGES'
});
console.log('Created new bot review');
}
} else if (botReviews.length > 0) {
// Dismiss existing reviews
for (const review of botReviews) {
try {
await github.rest.pulls.dismissReview({
owner,
repo,
pull_number: pr_number,
review_id: review.id,
message: 'Review dismissed: All requirements have been met'
});
console.log(`Dismissed bot review ${review.id}`);
} catch (error) {
console.log(`Failed to dismiss review ${review.id}:`, error.message);
}
}
}
}
// Main execution
const apiData = await fetchApiData();
const baseRef = context.payload.pull_request.base.ref;
// Early exit for non-dev branches
if (baseRef !== 'dev') {
const branchLabels = await detectMergeBranch();
const finalLabels = Array.from(branchLabels);
console.log('Computed labels (merge branch only):', finalLabels.join(', '));
// Apply labels
if (finalLabels.length > 0) {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: pr_number,
labels: finalLabels
});
}
// Remove old managed labels
const labelsToRemove = managedLabels.filter(label => !finalLabels.includes(label));
for (const label of labelsToRemove) {
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;
}
// Run all strategies
const [
branchLabels,
componentLabels,
newComponentLabels,
newPlatformLabels,
coreLabels,
sizeLabels,
dashboardLabels,
actionsLabels,
codeOwnerLabels,
testLabels,
checkboxLabels
] = await Promise.all([
detectMergeBranch(),
detectComponentPlatforms(apiData),
detectNewComponents(),
detectNewPlatforms(apiData),
detectCoreChanges(),
detectPRSize(),
detectDashboardChanges(),
detectGitHubActionsChanges(),
detectCodeOwner(),
detectTests(),
detectPRTemplateCheckboxes()
]);
// Combine all labels
const allLabels = new Set([
...branchLabels,
...componentLabels,
...newComponentLabels,
...newPlatformLabels,
...coreLabels,
...sizeLabels,
...dashboardLabels,
...actionsLabels,
...codeOwnerLabels,
...testLabels,
...checkboxLabels
]);
// Detect requirements based on all other labels
const requirementLabels = await detectRequirements(allLabels);
for (const label of requirementLabels) {
allLabels.add(label);
}
let finalLabels = Array.from(allLabels);
// For mega-PRs, exclude component labels if there are too many
if (isMegaPR) {
const componentLabels = finalLabels.filter(label => label.startsWith('component: '));
if (componentLabels.length > COMPONENT_LABEL_THRESHOLD) {
finalLabels = finalLabels.filter(label => !label.startsWith('component: '));
console.log(`Mega-PR detected - excluding ${componentLabels.length} component labels (threshold: ${COMPONENT_LABEL_THRESHOLD})`);
}
}
// Handle too many labels (only for non-mega PRs)
const tooManyLabels = finalLabels.length > MAX_LABELS;
if (tooManyLabels && !isMegaPR && !finalLabels.includes('too-big')) {
finalLabels = ['too-big'];
}
console.log('Computed labels:', finalLabels.join(', '));
// Handle reviews
await handleReviews(finalLabels);
// Apply 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
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,9 +47,9 @@ jobs:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5.6.0 uses: actions/setup-python@v5.6.0
with: with:
python-version: "3.10" python-version: "3.11"
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.10.0 uses: docker/setup-buildx-action@v3.11.1
- name: Set TAG - name: Set TAG
run: | run: |

View File

@ -20,8 +20,8 @@ permissions:
contents: read contents: read
env: env:
DEFAULT_PYTHON: "3.10" DEFAULT_PYTHON: "3.11"
PYUPGRADE_TARGET: "--py310-plus" PYUPGRADE_TARGET: "--py311-plus"
concurrency: concurrency:
# yamllint disable-line rule:line-length # yamllint disable-line rule:line-length
@ -39,7 +39,7 @@ jobs:
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Generate cache-key - name: Generate cache-key
id: 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 }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@v5.6.0 uses: actions/setup-python@v5.6.0
@ -58,56 +58,16 @@ jobs:
python -m venv venv python -m venv venv
. venv/bin/activate . venv/bin/activate
python --version 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 . 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: pylint:
name: Check pylint name: Check pylint
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: needs:
- common - common
- determine-jobs
if: needs.determine-jobs.outputs.python-linters == 'true'
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
@ -124,27 +84,6 @@ jobs:
run: script/ci-suggest-changes run: script/ci-suggest-changes
if: always() 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: ci-custom:
name: Run script/ci-custom name: Run script/ci-custom
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
@ -173,7 +112,6 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: python-version:
- "3.10"
- "3.11" - "3.11"
- "3.12" - "3.12"
- "3.13" - "3.13"
@ -189,14 +127,10 @@ jobs:
os: windows-latest os: windows-latest
- python-version: "3.12" - python-version: "3.12"
os: windows-latest os: windows-latest
- python-version: "3.10"
os: windows-latest
- python-version: "3.13" - python-version: "3.13"
os: macOS-latest os: macOS-latest
- python-version: "3.12" - python-version: "3.12"
os: macOS-latest os: macOS-latest
- python-version: "3.10"
os: macOS-latest
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
needs: needs:
- common - common
@ -204,6 +138,7 @@ jobs:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Restore Python - name: Restore Python
id: restore-python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
@ -213,56 +148,108 @@ jobs:
- name: Run pytest - name: Run pytest
if: matrix.os == 'windows-latest' if: matrix.os == 'windows-latest'
run: | run: |
./venv/Scripts/activate . ./venv/Scripts/activate.ps1
pytest -vv --cov-report=xml --tb=native -n auto tests pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/
- name: Run pytest - name: Run pytest
if: matrix.os == 'ubuntu-latest' || matrix.os == 'macOS-latest' if: matrix.os == 'ubuntu-latest' || matrix.os == 'macOS-latest'
run: | run: |
. venv/bin/activate . venv/bin/activate
pytest -vv --cov-report=xml --tb=native -n auto tests pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@v5.4.3 uses: codecov/codecov-action@v5.4.3
with: with:
token: ${{ secrets.CODECOV_TOKEN }} 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 }}
clang-format: determine-jobs:
name: Check clang-format name: Determine which jobs to run
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: needs:
- common - 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: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
with:
# Fetch enough history to find the merge base
fetch-depth: 2
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }} cache-key: ${{ needs.common.outputs.cache-key }}
- name: Install clang-format - name: Determine which tests to run
id: determine
env:
GH_TOKEN: ${{ github.token }}
run: | run: |
. venv/bin/activate . venv/bin/activate
pip install clang-format -c requirements_dev.txt output=$(python script/determine-jobs.py)
- name: Run clang-format 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
- name: Set up Python 3.13
id: python
uses: actions/setup-python@v5.6.0
with:
python-version: "3.13"
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@v4.2.3
with:
path: venv
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
- name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true'
run: |
python -m venv venv
. venv/bin/activate
python --version
pip install -r requirements.txt -r requirements_test.txt
pip install -e .
- name: Register matcher
run: echo "::add-matcher::.github/workflows/matchers/pytest.json"
- name: Run integration tests
run: | run: |
. venv/bin/activate . venv/bin/activate
script/clang-format -i pytest -vv --no-cov --tb=native -n auto tests/integration/
git diff-index --quiet HEAD --
- name: Suggested changes
run: script/ci-suggest-changes
if: always()
clang-tidy: clang-tidy:
name: ${{ matrix.name }} name: ${{ matrix.name }}
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: needs:
- common - common
- ruff - determine-jobs
- ci-custom if: needs.determine-jobs.outputs.clang-tidy == 'true'
- clang-format env:
- flake8 GH_TOKEN: ${{ github.token }}
- pylint
- pytest
- pyupgrade
strategy: strategy:
fail-fast: false fail-fast: false
max-parallel: 2 max-parallel: 2
@ -294,13 +281,17 @@ jobs:
pio_cache_key: tidyesp32-idf pio_cache_key: tidyesp32-idf
- id: clang-tidy - id: clang-tidy
name: Run script/clang-tidy for ZEPHYR name: Run script/clang-tidy for ZEPHYR
options: --environment nrf52-tidy --grep USE_ZEPHYR options: --environment nrf52-tidy --grep USE_ZEPHYR --grep USE_NRF52
pio_cache_key: tidy-zephyr pio_cache_key: tidy-zephyr
ignore_errors: false ignore_errors: false
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
@ -312,14 +303,14 @@ jobs:
uses: actions/cache@v4.2.3 uses: actions/cache@v4.2.3
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }} key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
- name: Cache platformio - name: Cache platformio
if: github.ref != 'refs/heads/dev' if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@v4.2.3 uses: actions/cache/restore@v4.2.3
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }} key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
- name: Register problem matchers - name: Register problem matchers
run: | run: |
@ -333,10 +324,28 @@ jobs:
mkdir -p .temp mkdir -p .temp
pio run --list-targets -e esp32-idf-tidy 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 - name: Run clang-tidy
run: | run: |
. venv/bin/activate . 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: env:
# Also cache libdeps, store them in a ~/.platformio subfolder # Also cache libdeps, store them in a ~/.platformio subfolder
PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
@ -346,59 +355,18 @@ jobs:
# yamllint disable-line rule:line-length # yamllint disable-line rule:line-length
if: always() 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: test-build-components:
name: Component test ${{ matrix.file }} name: Component test ${{ matrix.file }}
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: needs:
- common - common
- list-components - determine-jobs
if: github.event_name == 'pull_request' && fromJSON(needs.list-components.outputs.count) > 0 && fromJSON(needs.list-components.outputs.count) < 100 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: strategy:
fail-fast: false fail-fast: false
max-parallel: 2 max-parallel: 2
matrix: matrix:
file: ${{ fromJson(needs.list-components.outputs.components) }} file: ${{ fromJson(needs.determine-jobs.outputs.changed-components) }}
steps: steps:
- name: Install dependencies - name: Install dependencies
run: | run: |
@ -426,8 +394,8 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: needs:
- common - common
- list-components - determine-jobs
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
outputs: outputs:
matrix: ${{ steps.split.outputs.components }} matrix: ${{ steps.split.outputs.components }}
steps: steps:
@ -436,7 +404,7 @@ jobs:
- name: Split components into 20 groups - name: Split components into 20 groups
id: split id: split
run: | 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 echo "components=$components" >> $GITHUB_OUTPUT
test-build-components-split: test-build-components-split:
@ -444,9 +412,9 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: needs:
- common - common
- list-components - determine-jobs
- test-build-components-splitter - 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: strategy:
fail-fast: false fail-fast: false
max-parallel: 4 max-parallel: 4
@ -483,23 +451,41 @@ jobs:
./script/test_build_components -e compile -c $component ./script/test_build_components -e compile -c $component
done 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: ci-status:
name: CI Status name: CI Status
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: needs:
- common - common
- ruff
- ci-custom - ci-custom
- clang-format
- flake8
- pylint - pylint
- pytest - pytest
- pyupgrade - integration-tests
- clang-tidy - clang-tidy
- list-components - determine-jobs
- test-build-components - test-build-components
- test-build-components-splitter - test-build-components-splitter
- test-build-components-split - test-build-components-split
- pre-commit-ci-lite
if: always() if: always()
steps: steps:
- name: Success - name: Success

View File

@ -0,0 +1,324 @@
# 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`);
// Hidden marker to identify bot comments from this workflow
const BOT_COMMENT_MARKER = '<!-- codeowner-review-request-bot -->';
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 `${BOT_COMMENT_MARKER}\n👋 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 `${BOT_COMMENT_MARKER}\n👋 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);
});
// Check for previous comments from this workflow to avoid duplicate pings
const comments = await github.paginate(
github.rest.issues.listComments,
{
owner,
repo,
issue_number: pr_number
}
);
const previouslyPingedUsers = new Set();
const previouslyPingedTeams = new Set();
// Look for comments from github-actions bot that contain our bot marker
const workflowComments = comments.filter(comment =>
comment.user.type === 'Bot' &&
comment.body.includes(BOT_COMMENT_MARKER)
);
// Extract previously mentioned users and teams from workflow comments
for (const comment of workflowComments) {
// Match @username patterns (not team mentions)
const userMentions = comment.body.match(/@([a-zA-Z0-9_.-]+)(?![/])/g) || [];
userMentions.forEach(mention => {
const username = mention.slice(1); // remove @
previouslyPingedUsers.add(username);
});
// Match @org/team patterns
const teamMentions = comment.body.match(/@[a-zA-Z0-9_.-]+\/([a-zA-Z0-9_.-]+)/g) || [];
teamMentions.forEach(mention => {
const teamName = mention.split('/')[1];
previouslyPingedTeams.add(teamName);
});
}
console.log(`Found ${previouslyPingedUsers.size} previously pinged users and ${previouslyPingedTeams.size} previously pinged teams`);
// Remove users who have already been pinged in previous workflow comments
previouslyPingedUsers.forEach(user => {
matchedOwners.delete(user);
});
previouslyPingedTeams.forEach(team => {
matchedTeams.delete(team);
});
// 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, reviewed, or previously pinged)');
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');
}
// Only add a comment if there are new codeowners to mention (not previously pinged)
if (reviewersList.length > 0 || teamsList.length > 0) {
const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, true);
await github.rest.issues.createComment({
owner,
repo,
issue_number: pr_number,
body: commentBody
});
console.log(`Added comment mentioning ${reviewersList.length} users and ${teamsList.length} teams`);
} else {
console.log('No new codeowners to mention in comment (all previously pinged)');
}
} catch (error) {
if (error.status === 422) {
console.log('Some reviewers may already be requested or unavailable:', error.message);
// Only try to add a comment if there are new codeowners to mention
if (reviewersList.length > 0 || teamsList.length > 0) {
const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, false);
try {
await github.rest.issues.createComment({
owner,
repo,
issue_number: pr_number,
body: commentBody
});
console.log(`Added fallback comment mentioning ${reviewersList.length} users and ${teamsList.length} teams`);
} catch (commentError) {
console.log('Failed to add comment:', commentError.message);
}
} else {
console.log('No new codeowners to mention in fallback comment');
}
} else {
throw error;
}
}
} catch (error) {
console.log('Failed to process codeowner review requests:', error.message);
console.error(error);
}

View File

@ -0,0 +1,157 @@
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 the external-component-bot workflow. -->";
const legacyCommentMarker = "<!-- 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.paginate(
github.rest.issues.listComments,
{
owner: owner,
repo: repo,
issue_number: prNumber,
per_page: 100,
}
);
const sorted = comments.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at));
const botComment = sorted.find(comment =>
(
comment.body.includes(commentMarker) ||
comment.body.includes(legacyCommentMarker)
) && comment.user.type === "Bot"
);
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,163 @@
# 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}`);
// Hidden marker to identify bot comments from this workflow
const BOT_COMMENT_MARKER = '<!-- issue-codeowner-notify-bot -->';
// 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}`
);
// Check for previous comments from this workflow to avoid duplicate pings
const comments = await github.paginate(
github.rest.issues.listComments,
{
owner,
repo,
issue_number: issue_number
}
);
const previouslyPingedUsers = new Set();
const previouslyPingedTeams = new Set();
// Look for comments from github-actions bot that contain codeowner pings for this component
const workflowComments = comments.filter(comment =>
comment.user.type === 'Bot' &&
comment.body.includes(BOT_COMMENT_MARKER) &&
comment.body.includes(`component: ${componentName}`)
);
// Extract previously mentioned users and teams from workflow comments
for (const comment of workflowComments) {
// Match @username patterns (not team mentions)
const userMentions = comment.body.match(/@([a-zA-Z0-9_.-]+)(?![/])/g) || [];
userMentions.forEach(mention => {
previouslyPingedUsers.add(mention); // Keep @ prefix for easy comparison
});
// Match @org/team patterns
const teamMentions = comment.body.match(/@[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+/g) || [];
teamMentions.forEach(mention => {
previouslyPingedTeams.add(mention);
});
}
console.log(`Found ${previouslyPingedUsers.size} previously pinged users and ${previouslyPingedTeams.size} previously pinged teams for component ${componentName}`);
// Remove previously pinged users and teams
const newUserOwners = filteredUserOwners.filter(mention => !previouslyPingedUsers.has(mention));
const newTeamOwners = teamOwners.filter(mention => !previouslyPingedTeams.has(mention));
const allMentions = [...newUserOwners, ...newTeamOwners];
if (allMentions.length === 0) {
console.log('No new codeowners to notify (all previously pinged or issue author is the only codeowner)');
return;
}
// Create comment body
const mentionString = allMentions.join(', ');
const commentBody = `${BOT_COMMENT_MARKER}\n👋 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 new codeowners: ${mentionString}`);
} catch (error) {
console.log('Failed to process codeowner notifications:', error.message);
console.error(error);
}

View File

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

View File

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

View File

@ -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 for more information
# See https://pre-commit.com/hooks.html for more hooks # 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: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: v0.11.10 rev: v0.12.7
hooks: hooks:
# Run the linter. # Run the linter.
- id: ruff - id: ruff
@ -12,7 +19,7 @@ repos:
# Run the formatter. # Run the formatter.
- id: ruff-format - id: ruff-format
- repo: https://github.com/PyCQA/flake8 - repo: https://github.com/PyCQA/flake8
rev: 7.2.0 rev: 7.3.0
hooks: hooks:
- id: flake8 - id: flake8
additional_dependencies: additional_dependencies:
@ -20,22 +27,25 @@ repos:
- pydocstyle==5.1.1 - pydocstyle==5.1.1
files: ^(esphome|tests)/.+\.py$ files: ^(esphome|tests)/.+\.py$
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.4.0 rev: v5.0.0
hooks: hooks:
- id: no-commit-to-branch - id: no-commit-to-branch
args: args:
- --branch=dev - --branch=dev
- --branch=release - --branch=release
- --branch=beta - --branch=beta
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v3.20.0 rev: v3.20.0
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py310-plus] args: [--py311-plus]
- repo: https://github.com/adrienverge/yamllint.git - repo: https://github.com/adrienverge/yamllint.git
rev: v1.37.1 rev: v1.37.1
hooks: hooks:
- id: yamllint - id: yamllint
exclude: ^(\.clang-format|\.clang-tidy)$
- repo: https://github.com/pre-commit/mirrors-clang-format - repo: https://github.com/pre-commit/mirrors-clang-format
rev: v13.0.1 rev: v13.0.1
hooks: hooks:
@ -48,3 +58,10 @@ repos:
entry: python3 script/run-in-env.py pylint entry: python3 script/run-in-env.py pylint
language: system language: system
types: [python] 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 pyproject.toml @esphome/core
esphome/*.py @esphome/core esphome/*.py @esphome/core
esphome/core/* @esphome/core esphome/core/* @esphome/core
.github/** @esphome/core
# Integrations # Integrations
esphome/components/a01nyub/* @MrSuicideParrot esphome/components/a01nyub/* @MrSuicideParrot
@ -28,7 +29,7 @@ esphome/components/aic3204/* @kbx81
esphome/components/airthings_ble/* @jeromelaban esphome/components/airthings_ble/* @jeromelaban
esphome/components/airthings_wave_base/* @jeromelaban @kpfleming @ncareau esphome/components/airthings_wave_base/* @jeromelaban @kpfleming @ncareau
esphome/components/airthings_wave_mini/* @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/alarm_control_panel/* @grahambrown11 @hwstar
esphome/components/alpha3/* @jan-hofmeier esphome/components/alpha3/* @jan-hofmeier
esphome/components/am2315c/* @swoboda1337 esphome/components/am2315c/* @swoboda1337
@ -87,6 +88,7 @@ esphome/components/bp1658cj/* @Cossid
esphome/components/bp5758d/* @Cossid esphome/components/bp5758d/* @Cossid
esphome/components/button/* @esphome/core esphome/components/button/* @esphome/core
esphome/components/bytebuffer/* @clydebarrow esphome/components/bytebuffer/* @clydebarrow
esphome/components/camera/* @DT-art1 @bdraco
esphome/components/canbus/* @danielschramm @mvturnho esphome/components/canbus/* @danielschramm @mvturnho
esphome/components/cap1188/* @mreditor97 esphome/components/cap1188/* @mreditor97
esphome/components/captive_portal/* @OttoWinter esphome/components/captive_portal/* @OttoWinter
@ -124,6 +126,7 @@ esphome/components/dht/* @OttoWinter
esphome/components/display_menu_base/* @numo68 esphome/components/display_menu_base/* @numo68
esphome/components/dps310/* @kbx81 esphome/components/dps310/* @kbx81
esphome/components/ds1307/* @badbadc0ffee esphome/components/ds1307/* @badbadc0ffee
esphome/components/ds2484/* @mrk-its
esphome/components/dsmr/* @glmnet @zuidwijk esphome/components/dsmr/* @glmnet @zuidwijk
esphome/components/duty_time/* @dudanov esphome/components/duty_time/* @dudanov
esphome/components/ee895/* @Stock-M esphome/components/ee895/* @Stock-M
@ -146,11 +149,13 @@ esphome/components/esp32_ble_client/* @jesserockz
esphome/components/esp32_ble_server/* @Rapsssito @clydebarrow @jesserockz esphome/components/esp32_ble_server/* @Rapsssito @clydebarrow @jesserockz
esphome/components/esp32_camera_web_server/* @ayufan esphome/components/esp32_camera_web_server/* @ayufan
esphome/components/esp32_can/* @Sympatron esphome/components/esp32_can/* @Sympatron
esphome/components/esp32_hosted/* @swoboda1337
esphome/components/esp32_improv/* @jesserockz esphome/components/esp32_improv/* @jesserockz
esphome/components/esp32_rmt/* @jesserockz esphome/components/esp32_rmt/* @jesserockz
esphome/components/esp32_rmt_led_strip/* @jesserockz esphome/components/esp32_rmt_led_strip/* @jesserockz
esphome/components/esp8266/* @esphome/core esphome/components/esp8266/* @esphome/core
esphome/components/esp_ldo/* @clydebarrow esphome/components/esp_ldo/* @clydebarrow
esphome/components/espnow/* @jesserockz
esphome/components/ethernet_info/* @gtjadsonsantos esphome/components/ethernet_info/* @gtjadsonsantos
esphome/components/event/* @nohat esphome/components/event/* @nohat
esphome/components/event_emitter/* @Rapsssito esphome/components/event_emitter/* @Rapsssito
@ -167,6 +172,7 @@ esphome/components/ft5x06/* @clydebarrow
esphome/components/ft63x6/* @gpambrozio esphome/components/ft63x6/* @gpambrozio
esphome/components/gcja5/* @gcormier esphome/components/gcja5/* @gcormier
esphome/components/gdk101/* @Szewcson esphome/components/gdk101/* @Szewcson
esphome/components/gl_r01_i2c/* @pkejval
esphome/components/globals/* @esphome/core esphome/components/globals/* @esphome/core
esphome/components/gp2y1010au0f/* @zry98 esphome/components/gp2y1010au0f/* @zry98
esphome/components/gp8403/* @jesserockz esphome/components/gp8403/* @jesserockz
@ -241,15 +247,18 @@ esphome/components/lcd_menu/* @numo68
esphome/components/ld2410/* @regevbr @sebcaps esphome/components/ld2410/* @regevbr @sebcaps
esphome/components/ld2420/* @descipher esphome/components/ld2420/* @descipher
esphome/components/ld2450/* @hareeshmu esphome/components/ld2450/* @hareeshmu
esphome/components/ld24xx/* @kbx81
esphome/components/ledc/* @OttoWinter esphome/components/ledc/* @OttoWinter
esphome/components/libretiny/* @kuba2k2 esphome/components/libretiny/* @kuba2k2
esphome/components/libretiny_pwm/* @kuba2k2 esphome/components/libretiny_pwm/* @kuba2k2
esphome/components/light/* @esphome/core esphome/components/light/* @esphome/core
esphome/components/lightwaverf/* @max246 esphome/components/lightwaverf/* @max246
esphome/components/lilygo_t5_47/touchscreen/* @jesserockz esphome/components/lilygo_t5_47/touchscreen/* @jesserockz
esphome/components/ln882x/* @lamauny
esphome/components/lock/* @esphome/core esphome/components/lock/* @esphome/core
esphome/components/logger/* @esphome/core esphome/components/logger/* @esphome/core
esphome/components/logger/select/* @clydebarrow esphome/components/logger/select/* @clydebarrow
esphome/components/lps22/* @nagisa
esphome/components/ltr390/* @latonita @sjtrny esphome/components/ltr390/* @latonita @sjtrny
esphome/components/ltr501/* @latonita esphome/components/ltr501/* @latonita
esphome/components/ltr_als_ps/* @latonita esphome/components/ltr_als_ps/* @latonita
@ -286,6 +295,7 @@ esphome/components/microphone/* @jesserockz @kahrendt
esphome/components/mics_4514/* @jesserockz esphome/components/mics_4514/* @jesserockz
esphome/components/midea/* @dudanov esphome/components/midea/* @dudanov
esphome/components/midea_ir/* @dudanov esphome/components/midea_ir/* @dudanov
esphome/components/mipi_dsi/* @clydebarrow
esphome/components/mipi_spi/* @clydebarrow esphome/components/mipi_spi/* @clydebarrow
esphome/components/mitsubishi/* @RubyBailey esphome/components/mitsubishi/* @RubyBailey
esphome/components/mixer/speaker/* @kahrendt esphome/components/mixer/speaker/* @kahrendt
@ -318,11 +328,13 @@ esphome/components/nextion/text_sensor/* @senexcrenshaw
esphome/components/nfc/* @jesserockz @kbx81 esphome/components/nfc/* @jesserockz @kbx81
esphome/components/noblex/* @AGalfra esphome/components/noblex/* @AGalfra
esphome/components/npi19/* @bakerkj esphome/components/npi19/* @bakerkj
esphome/components/nrf52/* @tomaszduda23
esphome/components/number/* @esphome/core esphome/components/number/* @esphome/core
esphome/components/one_wire/* @ssieb esphome/components/one_wire/* @ssieb
esphome/components/online_image/* @clydebarrow @guillempages esphome/components/online_image/* @clydebarrow @guillempages
esphome/components/opentherm/* @olegtarasov esphome/components/opentherm/* @olegtarasov
esphome/components/openthread/* @mrene esphome/components/openthread/* @mrene
esphome/components/opt3001/* @ccutrer
esphome/components/ota/* @esphome/core esphome/components/ota/* @esphome/core
esphome/components/output/* @esphome/core esphome/components/output/* @esphome/core
esphome/components/packet_transport/* @clydebarrow esphome/components/packet_transport/* @clydebarrow
@ -330,6 +342,7 @@ esphome/components/pca6416a/* @Mat931
esphome/components/pca9554/* @clydebarrow @hwstar esphome/components/pca9554/* @clydebarrow @hwstar
esphome/components/pcf85063/* @brogon esphome/components/pcf85063/* @brogon
esphome/components/pcf8563/* @KoenBreeman esphome/components/pcf8563/* @KoenBreeman
esphome/components/pi4ioe5v6408/* @jesserockz
esphome/components/pid/* @OttoWinter esphome/components/pid/* @OttoWinter
esphome/components/pipsolar/* @andreashergert1984 esphome/components/pipsolar/* @andreashergert1984
esphome/components/pm1006/* @habbie esphome/components/pm1006/* @habbie
@ -370,6 +383,7 @@ esphome/components/rp2040_pwm/* @jesserockz
esphome/components/rpi_dpi_rgb/* @clydebarrow esphome/components/rpi_dpi_rgb/* @clydebarrow
esphome/components/rtl87xx/* @kuba2k2 esphome/components/rtl87xx/* @kuba2k2
esphome/components/rtttl/* @glmnet esphome/components/rtttl/* @glmnet
esphome/components/runtime_stats/* @bdraco
esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti
esphome/components/scd4x/* @martgras @sjtrny esphome/components/scd4x/* @martgras @sjtrny
esphome/components/script/* @esphome/core esphome/components/script/* @esphome/core
@ -436,6 +450,8 @@ esphome/components/sun/* @OttoWinter
esphome/components/sun_gtil2/* @Mat931 esphome/components/sun_gtil2/* @Mat931
esphome/components/switch/* @esphome/core esphome/components/switch/* @esphome/core
esphome/components/switch/binary_sensor/* @ssieb esphome/components/switch/binary_sensor/* @ssieb
esphome/components/sx126x/* @swoboda1337
esphome/components/sx127x/* @swoboda1337
esphome/components/syslog/* @clydebarrow esphome/components/syslog/* @clydebarrow
esphome/components/t6615/* @tylermenezes esphome/components/t6615/* @tylermenezes
esphome/components/tc74/* @sethgirvan esphome/components/tc74/* @sethgirvan
@ -457,7 +473,7 @@ esphome/components/tlc5971/* @IJIJI
esphome/components/tm1621/* @Philippe12 esphome/components/tm1621/* @Philippe12
esphome/components/tm1637/* @glmnet esphome/components/tm1637/* @glmnet
esphome/components/tm1638/* @skykingjwc esphome/components/tm1638/* @skykingjwc
esphome/components/tm1651/* @freekode esphome/components/tm1651/* @mrtoy-me
esphome/components/tmp102/* @timsavage esphome/components/tmp102/* @timsavage
esphome/components/tmp1075/* @sybrenstuvel esphome/components/tmp1075/* @sybrenstuvel
esphome/components/tmp117/* @Azimath esphome/components/tmp117/* @Azimath
@ -494,6 +510,7 @@ esphome/components/voice_assistant/* @jesserockz @kahrendt
esphome/components/wake_on_lan/* @clydebarrow @willwill2will54 esphome/components/wake_on_lan/* @clydebarrow @willwill2will54
esphome/components/watchdog/* @oarcher esphome/components/watchdog/* @oarcher
esphome/components/waveshare_epaper/* @clydebarrow esphome/components/waveshare_epaper/* @clydebarrow
esphome/components/web_server/ota/* @esphome/core
esphome/components/web_server_base/* @OttoWinter esphome/components/web_server_base/* @OttoWinter
esphome/components/web_server_idf/* @dentra esphome/components/web_server_idf/* @dentra
esphome/components/weikai/* @DrCoolZic esphome/components/weikai/* @DrCoolZic
@ -520,8 +537,10 @@ esphome/components/xiaomi_lywsd03mmc/* @ahpohl
esphome/components/xiaomi_mhoc303/* @drug123 esphome/components/xiaomi_mhoc303/* @drug123
esphome/components/xiaomi_mhoc401/* @vevsvevs esphome/components/xiaomi_mhoc401/* @vevsvevs
esphome/components/xiaomi_rtcgq02lm/* @jesserockz esphome/components/xiaomi_rtcgq02lm/* @jesserockz
esphome/components/xiaomi_xmwsdj04mmc/* @medusalix
esphome/components/xl9535/* @mreditor97 esphome/components/xl9535/* @mreditor97
esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68 esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68
esphome/components/xxtea/* @clydebarrow esphome/components/xxtea/* @clydebarrow
esphome/components/zephyr/* @tomaszduda23
esphome/components/zhlt01/* @cfeenstra1024 esphome/components/zhlt01/* @cfeenstra1024
esphome/components/zio_ultrasonic/* @kahrendt 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:** **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)
--- ---

View File

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

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

@ -2,6 +2,7 @@
import argparse import argparse
from datetime import datetime from datetime import datetime
import functools import functools
import getpass
import importlib import importlib
import logging import logging
import os import os
@ -34,11 +35,10 @@ from esphome.const import (
CONF_PORT, CONF_PORT,
CONF_SUBSTITUTIONS, CONF_SUBSTITUTIONS,
CONF_TOPIC, CONF_TOPIC,
PLATFORM_BK72XX, ENV_NOGITIGNORE,
PLATFORM_ESP32, PLATFORM_ESP32,
PLATFORM_ESP8266, PLATFORM_ESP8266,
PLATFORM_RP2040, PLATFORM_RP2040,
PLATFORM_RTL87XX,
SECRETS_FILES, SECRETS_FILES,
) )
from esphome.core import CORE, EsphomeError, coroutine from esphome.core import CORE, EsphomeError, coroutine
@ -90,9 +90,9 @@ def choose_prompt(options, purpose: str = None):
def choose_upload_log_host( def choose_upload_log_host(
default, check_default, show_ota, show_mqtt, show_api, purpose: str = None default, check_default, show_ota, show_mqtt, show_api, purpose: str = None
): ):
options = [] options = [
for port in get_serial_ports(): (f"{port.path} ({port.description})", port.path) for port in get_serial_ports()
options.append((f"{port.path} ({port.description})", port.path)) ]
if default == "SERIAL": if default == "SERIAL":
return choose_prompt(options, purpose=purpose) return choose_prompt(options, purpose=purpose)
if (show_ota and "ota" in CORE.config) or (show_api and "api" in CORE.config): if (show_ota and "ota" in CORE.config) or (show_api and "api" in CORE.config):
@ -120,9 +120,7 @@ def mqtt_logging_enabled(mqtt_config):
return False return False
if CONF_TOPIC not in log_topic: if CONF_TOPIC not in log_topic:
return False return False
if log_topic.get(CONF_LEVEL, None) == "NONE": return log_topic.get(CONF_LEVEL, None) != "NONE"
return False
return True
def get_port_type(port): def get_port_type(port):
@ -211,6 +209,9 @@ def wrap_to_code(name, comp):
def write_cpp(config): def write_cpp(config):
if not get_bool_env(ENV_NOGITIGNORE):
writer.write_gitignore()
generate_cpp_contents(config) generate_cpp_contents(config)
return write_cpp_file() return write_cpp_file()
@ -227,10 +228,13 @@ def generate_cpp_contents(config):
def write_cpp_file(): def write_cpp_file():
writer.write_platformio_project()
code_s = indent(CORE.cpp_main_section) code_s = indent(CORE.cpp_main_section)
writer.write_cpp(code_s) writer.write_cpp(code_s)
from esphome.build_gen import platformio
platformio.write_project()
return 0 return 0
@ -332,7 +336,7 @@ def check_permissions(port):
raise EsphomeError( raise EsphomeError(
"You do not have read or write permission on the selected serial port. " "You do not have read or write permission on the selected serial port. "
"To resolve this issue, you can add your user to the dialout group " "To resolve this issue, you can add your user to the dialout group "
f"by running the following command: sudo usermod -a -G dialout {os.getlogin()}. " f"by running the following command: sudo usermod -a -G dialout {getpass.getuser()}. "
"You will need to log out & back in or reboot to activate the new group access." "You will need to log out & back in or reboot to activate the new group access."
) )
@ -354,7 +358,7 @@ def upload_program(config, args, host):
if CORE.target_platform in (PLATFORM_RP2040): if CORE.target_platform in (PLATFORM_RP2040):
return upload_using_platformio(config, args.device) return upload_using_platformio(config, args.device)
if CORE.target_platform in (PLATFORM_BK72XX, PLATFORM_RTL87XX): if CORE.is_libretiny:
return upload_using_platformio(config, host) return upload_using_platformio(config, host)
return 1 # Unknown target platform return 1 # Unknown target platform

View File

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

View File

View File

@ -0,0 +1,102 @@
import os
from esphome.const import __version__
from esphome.core import CORE
from esphome.helpers import mkdir_p, read_file, write_file_if_changed
from esphome.writer import find_begin_end, update_storage_json
INI_AUTO_GENERATE_BEGIN = "; ========== AUTO GENERATED CODE BEGIN ==========="
INI_AUTO_GENERATE_END = "; =========== AUTO GENERATED CODE END ============"
INI_BASE_FORMAT = (
"""; Auto generated code by esphome
[common]
lib_deps =
build_flags =
upload_flags =
""",
"""
""",
)
def format_ini(data: dict[str, str | list[str]]) -> str:
content = ""
for key, value in sorted(data.items()):
if isinstance(value, list):
content += f"{key} =\n"
for x in value:
content += f" {x}\n"
else:
content += f"{key} = {value}\n"
return content
def get_ini_content():
CORE.add_platformio_option(
"lib_deps",
[x.as_lib_dep for x in CORE.platformio_libraries.values()]
+ ["${common.lib_deps}"],
)
# Sort to avoid changing build flags order
CORE.add_platformio_option("build_flags", sorted(CORE.build_flags))
# Sort to avoid changing build unflags order
CORE.add_platformio_option("build_unflags", sorted(CORE.build_unflags))
# Add extra script for C++ flags
CORE.add_platformio_option("extra_scripts", [f"pre:{CXX_FLAGS_FILE_NAME}"])
content = "[platformio]\n"
content += f"description = ESPHome {__version__}\n"
content += f"[env:{CORE.name}]\n"
content += format_ini(CORE.platformio_options)
return content
def write_ini(content):
update_storage_json()
path = CORE.relative_build_path("platformio.ini")
if os.path.isfile(path):
text = read_file(path)
content_format = find_begin_end(
text, INI_AUTO_GENERATE_BEGIN, INI_AUTO_GENERATE_END
)
else:
content_format = INI_BASE_FORMAT
full_file = f"{content_format[0] + INI_AUTO_GENERATE_BEGIN}\n{content}"
full_file += INI_AUTO_GENERATE_END + content_format[1]
write_file_if_changed(path, full_file)
def write_project():
mkdir_p(CORE.build_path)
content = get_ini_content()
write_ini(content)
# Write extra script for C++ specific flags
write_cxx_flags_script()
CXX_FLAGS_FILE_NAME = "cxx_flags.py"
CXX_FLAGS_FILE_CONTENTS = """# Auto-generated ESPHome script for C++ specific compiler flags
Import("env")
# Add C++ specific flags
"""
def write_cxx_flags_script() -> None:
path = CORE.relative_build_path(CXX_FLAGS_FILE_NAME)
contents = CXX_FLAGS_FILE_CONTENTS
if not CORE.is_host:
contents += 'env.Append(CXXFLAGS=["-Wno-volatile"])'
contents += "\n"
write_file_if_changed(path, contents)

View File

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

View File

@ -7,7 +7,6 @@ namespace a4988 {
static const char *const TAG = "a4988.stepper"; static const char *const TAG = "a4988.stepper";
void A4988::setup() { void A4988::setup() {
ESP_LOGCONFIG(TAG, "Running setup");
if (this->sleep_pin_ != nullptr) { if (this->sleep_pin_ != nullptr) {
this->sleep_pin_->setup(); this->sleep_pin_->setup();
this->sleep_pin_->digital_write(false); this->sleep_pin_->digital_write(false);

View File

@ -7,8 +7,6 @@ namespace absolute_humidity {
static const char *const TAG = "absolute_humidity.sensor"; static const char *const TAG = "absolute_humidity.sensor";
void AbsoluteHumidityComponent::setup() { void AbsoluteHumidityComponent::setup() {
ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->get_name().c_str());
ESP_LOGD(TAG, " Added callback for temperature '%s'", this->temperature_sensor_->get_name().c_str()); ESP_LOGD(TAG, " Added callback for temperature '%s'", this->temperature_sensor_->get_name().c_str());
this->temperature_sensor_->add_on_state_callback([this](float state) { this->temperature_callback_(state); }); this->temperature_sensor_->add_on_state_callback([this](float state) { this->temperature_callback_(state); });
if (this->temperature_sensor_->has_state()) { if (this->temperature_sensor_->has_state()) {

View File

@ -4,6 +4,7 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <cmath> #include <cmath>
#include <numbers>
#ifdef USE_ESP8266 #ifdef USE_ESP8266
#include <core_esp8266_waveform.h> #include <core_esp8266_waveform.h>
@ -193,18 +194,17 @@ void AcDimmer::setup() {
setTimer1Callback(&timer_interrupt); setTimer1Callback(&timer_interrupt);
#endif #endif
#ifdef USE_ESP32 #ifdef USE_ESP32
// 80 Divider -> 1 count=1µs // timer frequency of 1mhz
dimmer_timer = timerBegin(0, 80, true); dimmer_timer = timerBegin(1000000);
timerAttachInterrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr, true); timerAttachInterrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr);
// For ESP32, we can't use dynamic interval calculation because the timerX functions // For ESP32, we can't use dynamic interval calculation because the timerX functions
// are not callable from ISR (placed in flash storage). // are not callable from ISR (placed in flash storage).
// Here we just use an interrupt firing every 50 µs. // Here we just use an interrupt firing every 50 µs.
timerAlarmWrite(dimmer_timer, 50, true); timerAlarm(dimmer_timer, 50, true, 0);
timerAlarmEnable(dimmer_timer);
#endif #endif
} }
void AcDimmer::write_state(float state) { void AcDimmer::write_state(float state) {
state = std::acos(1 - (2 * state)) / 3.14159; // RMS power compensation state = std::acos(1 - (2 * state)) / std::numbers::pi; // RMS power compensation
auto new_value = static_cast<uint16_t>(roundf(state * 65535)); auto new_value = static_cast<uint16_t>(roundf(state * 65535));
if (new_value != 0 && this->store_.value == 0) if (new_value != 0 && this->store_.value == 0)
this->store_.init_cycle = this->init_with_half_cycle_; this->store_.init_cycle = this->init_with_half_cycle_;

View File

@ -1,17 +1,25 @@
from esphome import pins from esphome import pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components.esp32 import get_esp32_variant from esphome.components.esp32 import VARIANT_ESP32P4, get_esp32_variant
from esphome.components.esp32.const import ( from esphome.components.esp32.const import (
VARIANT_ESP32, VARIANT_ESP32,
VARIANT_ESP32C2, VARIANT_ESP32C2,
VARIANT_ESP32C3, VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6, VARIANT_ESP32C6,
VARIANT_ESP32H2, VARIANT_ESP32H2,
VARIANT_ESP32S2, VARIANT_ESP32S2,
VARIANT_ESP32S3, VARIANT_ESP32S3,
) )
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER, PLATFORM_ESP8266 from esphome.const import (
CONF_ANALOG,
CONF_INPUT,
CONF_NUMBER,
PLATFORM_ESP8266,
PlatformFramework,
)
from esphome.core import CORE from esphome.core import CORE
CODEOWNERS = ["@esphome/core"] CODEOWNERS = ["@esphome/core"]
@ -44,82 +52,103 @@ SAMPLING_MODES = {
"max": sampling_mode.MAX, "max": sampling_mode.MAX,
} }
adc1_channel_t = cg.global_ns.enum("adc1_channel_t") adc_unit_t = cg.global_ns.enum("adc_unit_t", is_class=True)
adc2_channel_t = cg.global_ns.enum("adc2_channel_t")
adc_channel_t = cg.global_ns.enum("adc_channel_t", is_class=True)
# pin to adc1 channel mapping # pin to adc1 channel mapping
# https://github.com/espressif/esp-idf/blob/v4.4.8/components/driver/include/driver/adc.h # https://github.com/espressif/esp-idf/blob/v4.4.8/components/driver/include/driver/adc.h
ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = { ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/adc_channel.h # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/adc_channel.h
VARIANT_ESP32: { VARIANT_ESP32: {
36: adc1_channel_t.ADC1_CHANNEL_0, 36: adc_channel_t.ADC_CHANNEL_0,
37: adc1_channel_t.ADC1_CHANNEL_1, 37: adc_channel_t.ADC_CHANNEL_1,
38: adc1_channel_t.ADC1_CHANNEL_2, 38: adc_channel_t.ADC_CHANNEL_2,
39: adc1_channel_t.ADC1_CHANNEL_3, 39: adc_channel_t.ADC_CHANNEL_3,
32: adc1_channel_t.ADC1_CHANNEL_4, 32: adc_channel_t.ADC_CHANNEL_4,
33: adc1_channel_t.ADC1_CHANNEL_5, 33: adc_channel_t.ADC_CHANNEL_5,
34: adc1_channel_t.ADC1_CHANNEL_6, 34: adc_channel_t.ADC_CHANNEL_6,
35: adc1_channel_t.ADC1_CHANNEL_7, 35: adc_channel_t.ADC_CHANNEL_7,
}, },
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h
VARIANT_ESP32C2: { VARIANT_ESP32C2: {
0: adc1_channel_t.ADC1_CHANNEL_0, 0: adc_channel_t.ADC_CHANNEL_0,
1: adc1_channel_t.ADC1_CHANNEL_1, 1: adc_channel_t.ADC_CHANNEL_1,
2: adc1_channel_t.ADC1_CHANNEL_2, 2: adc_channel_t.ADC_CHANNEL_2,
3: adc1_channel_t.ADC1_CHANNEL_3, 3: adc_channel_t.ADC_CHANNEL_3,
4: adc1_channel_t.ADC1_CHANNEL_4, 4: adc_channel_t.ADC_CHANNEL_4,
}, },
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c3/include/soc/adc_channel.h # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c3/include/soc/adc_channel.h
VARIANT_ESP32C3: { VARIANT_ESP32C3: {
0: adc1_channel_t.ADC1_CHANNEL_0, 0: adc_channel_t.ADC_CHANNEL_0,
1: adc1_channel_t.ADC1_CHANNEL_1, 1: adc_channel_t.ADC_CHANNEL_1,
2: adc1_channel_t.ADC1_CHANNEL_2, 2: adc_channel_t.ADC_CHANNEL_2,
3: adc1_channel_t.ADC1_CHANNEL_3, 3: adc_channel_t.ADC_CHANNEL_3,
4: adc1_channel_t.ADC1_CHANNEL_4, 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 # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h
VARIANT_ESP32C6: { VARIANT_ESP32C6: {
0: adc1_channel_t.ADC1_CHANNEL_0, 0: adc_channel_t.ADC_CHANNEL_0,
1: adc1_channel_t.ADC1_CHANNEL_1, 1: adc_channel_t.ADC_CHANNEL_1,
2: adc1_channel_t.ADC1_CHANNEL_2, 2: adc_channel_t.ADC_CHANNEL_2,
3: adc1_channel_t.ADC1_CHANNEL_3, 3: adc_channel_t.ADC_CHANNEL_3,
4: adc1_channel_t.ADC1_CHANNEL_4, 4: adc_channel_t.ADC_CHANNEL_4,
5: adc1_channel_t.ADC1_CHANNEL_5, 5: adc_channel_t.ADC_CHANNEL_5,
6: adc1_channel_t.ADC1_CHANNEL_6, 6: adc_channel_t.ADC_CHANNEL_6,
}, },
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h
VARIANT_ESP32H2: { VARIANT_ESP32H2: {
1: adc1_channel_t.ADC1_CHANNEL_0, 1: adc_channel_t.ADC_CHANNEL_0,
2: adc1_channel_t.ADC1_CHANNEL_1, 2: adc_channel_t.ADC_CHANNEL_1,
3: adc1_channel_t.ADC1_CHANNEL_2, 3: adc_channel_t.ADC_CHANNEL_2,
4: adc1_channel_t.ADC1_CHANNEL_3, 4: adc_channel_t.ADC_CHANNEL_3,
5: adc1_channel_t.ADC1_CHANNEL_4, 5: adc_channel_t.ADC_CHANNEL_4,
}, },
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h
VARIANT_ESP32S2: { VARIANT_ESP32S2: {
1: adc1_channel_t.ADC1_CHANNEL_0, 1: adc_channel_t.ADC_CHANNEL_0,
2: adc1_channel_t.ADC1_CHANNEL_1, 2: adc_channel_t.ADC_CHANNEL_1,
3: adc1_channel_t.ADC1_CHANNEL_2, 3: adc_channel_t.ADC_CHANNEL_2,
4: adc1_channel_t.ADC1_CHANNEL_3, 4: adc_channel_t.ADC_CHANNEL_3,
5: adc1_channel_t.ADC1_CHANNEL_4, 5: adc_channel_t.ADC_CHANNEL_4,
6: adc1_channel_t.ADC1_CHANNEL_5, 6: adc_channel_t.ADC_CHANNEL_5,
7: adc1_channel_t.ADC1_CHANNEL_6, 7: adc_channel_t.ADC_CHANNEL_6,
8: adc1_channel_t.ADC1_CHANNEL_7, 8: adc_channel_t.ADC_CHANNEL_7,
9: adc1_channel_t.ADC1_CHANNEL_8, 9: adc_channel_t.ADC_CHANNEL_8,
10: adc1_channel_t.ADC1_CHANNEL_9, 10: adc_channel_t.ADC_CHANNEL_9,
}, },
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h
VARIANT_ESP32S3: { VARIANT_ESP32S3: {
1: adc1_channel_t.ADC1_CHANNEL_0, 1: adc_channel_t.ADC_CHANNEL_0,
2: adc1_channel_t.ADC1_CHANNEL_1, 2: adc_channel_t.ADC_CHANNEL_1,
3: adc1_channel_t.ADC1_CHANNEL_2, 3: adc_channel_t.ADC_CHANNEL_2,
4: adc1_channel_t.ADC1_CHANNEL_3, 4: adc_channel_t.ADC_CHANNEL_3,
5: adc1_channel_t.ADC1_CHANNEL_4, 5: adc_channel_t.ADC_CHANNEL_4,
6: adc1_channel_t.ADC1_CHANNEL_5, 6: adc_channel_t.ADC_CHANNEL_5,
7: adc1_channel_t.ADC1_CHANNEL_6, 7: adc_channel_t.ADC_CHANNEL_6,
8: adc1_channel_t.ADC1_CHANNEL_7, 8: adc_channel_t.ADC_CHANNEL_7,
9: adc1_channel_t.ADC1_CHANNEL_8, 9: adc_channel_t.ADC_CHANNEL_8,
10: adc1_channel_t.ADC1_CHANNEL_9, 10: adc_channel_t.ADC_CHANNEL_9,
},
VARIANT_ESP32P4: {
16: adc_channel_t.ADC_CHANNEL_0,
17: adc_channel_t.ADC_CHANNEL_1,
18: adc_channel_t.ADC_CHANNEL_2,
19: adc_channel_t.ADC_CHANNEL_3,
20: adc_channel_t.ADC_CHANNEL_4,
21: adc_channel_t.ADC_CHANNEL_5,
22: adc_channel_t.ADC_CHANNEL_6,
23: adc_channel_t.ADC_CHANNEL_7,
}, },
} }
@ -128,54 +157,64 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
ESP32_VARIANT_ADC2_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 # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/adc_channel.h
VARIANT_ESP32: { VARIANT_ESP32: {
4: adc2_channel_t.ADC2_CHANNEL_0, 4: adc_channel_t.ADC_CHANNEL_0,
0: adc2_channel_t.ADC2_CHANNEL_1, 0: adc_channel_t.ADC_CHANNEL_1,
2: adc2_channel_t.ADC2_CHANNEL_2, 2: adc_channel_t.ADC_CHANNEL_2,
15: adc2_channel_t.ADC2_CHANNEL_3, 15: adc_channel_t.ADC_CHANNEL_3,
13: adc2_channel_t.ADC2_CHANNEL_4, 13: adc_channel_t.ADC_CHANNEL_4,
12: adc2_channel_t.ADC2_CHANNEL_5, 12: adc_channel_t.ADC_CHANNEL_5,
14: adc2_channel_t.ADC2_CHANNEL_6, 14: adc_channel_t.ADC_CHANNEL_6,
27: adc2_channel_t.ADC2_CHANNEL_7, 27: adc_channel_t.ADC_CHANNEL_7,
25: adc2_channel_t.ADC2_CHANNEL_8, 25: adc_channel_t.ADC_CHANNEL_8,
26: adc2_channel_t.ADC2_CHANNEL_9, 26: adc_channel_t.ADC_CHANNEL_9,
}, },
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h
VARIANT_ESP32C2: { 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 # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c3/include/soc/adc_channel.h
VARIANT_ESP32C3: { 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 # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h
VARIANT_ESP32C6: {}, # no ADC2 VARIANT_ESP32C6: {}, # no ADC2
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h
VARIANT_ESP32H2: {}, # no ADC2 VARIANT_ESP32H2: {}, # no ADC2
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h
VARIANT_ESP32S2: { VARIANT_ESP32S2: {
11: adc2_channel_t.ADC2_CHANNEL_0, 11: adc_channel_t.ADC_CHANNEL_0,
12: adc2_channel_t.ADC2_CHANNEL_1, 12: adc_channel_t.ADC_CHANNEL_1,
13: adc2_channel_t.ADC2_CHANNEL_2, 13: adc_channel_t.ADC_CHANNEL_2,
14: adc2_channel_t.ADC2_CHANNEL_3, 14: adc_channel_t.ADC_CHANNEL_3,
15: adc2_channel_t.ADC2_CHANNEL_4, 15: adc_channel_t.ADC_CHANNEL_4,
16: adc2_channel_t.ADC2_CHANNEL_5, 16: adc_channel_t.ADC_CHANNEL_5,
17: adc2_channel_t.ADC2_CHANNEL_6, 17: adc_channel_t.ADC_CHANNEL_6,
18: adc2_channel_t.ADC2_CHANNEL_7, 18: adc_channel_t.ADC_CHANNEL_7,
19: adc2_channel_t.ADC2_CHANNEL_8, 19: adc_channel_t.ADC_CHANNEL_8,
20: adc2_channel_t.ADC2_CHANNEL_9, 20: adc_channel_t.ADC_CHANNEL_9,
}, },
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h
VARIANT_ESP32S3: { VARIANT_ESP32S3: {
11: adc2_channel_t.ADC2_CHANNEL_0, 11: adc_channel_t.ADC_CHANNEL_0,
12: adc2_channel_t.ADC2_CHANNEL_1, 12: adc_channel_t.ADC_CHANNEL_1,
13: adc2_channel_t.ADC2_CHANNEL_2, 13: adc_channel_t.ADC_CHANNEL_2,
14: adc2_channel_t.ADC2_CHANNEL_3, 14: adc_channel_t.ADC_CHANNEL_3,
15: adc2_channel_t.ADC2_CHANNEL_4, 15: adc_channel_t.ADC_CHANNEL_4,
16: adc2_channel_t.ADC2_CHANNEL_5, 16: adc_channel_t.ADC_CHANNEL_5,
17: adc2_channel_t.ADC2_CHANNEL_6, 17: adc_channel_t.ADC_CHANNEL_6,
18: adc2_channel_t.ADC2_CHANNEL_7, 18: adc_channel_t.ADC_CHANNEL_7,
19: adc2_channel_t.ADC2_CHANNEL_8, 19: adc_channel_t.ADC_CHANNEL_8,
20: adc2_channel_t.ADC2_CHANNEL_9, 20: adc_channel_t.ADC_CHANNEL_9,
},
VARIANT_ESP32P4: {
49: adc_channel_t.ADC_CHANNEL_0,
50: adc_channel_t.ADC_CHANNEL_1,
51: adc_channel_t.ADC_CHANNEL_2,
52: adc_channel_t.ADC_CHANNEL_3,
53: adc_channel_t.ADC_CHANNEL_4,
54: adc_channel_t.ADC_CHANNEL_5,
}, },
} }
@ -228,4 +267,27 @@ def validate_adc_pin(value):
{CONF_ANALOG: True, CONF_INPUT: True}, internal=True {CONF_ANALOG: True, CONF_INPUT: True}, internal=True
)(value) )(value)
if CORE.is_nrf52:
return pins.gpio_pin_schema(
{CONF_ANALOG: True, CONF_INPUT: True}, internal=True
)(value)
raise NotImplementedError raise NotImplementedError
FILTER_SOURCE_FILES = filter_source_files_from_platform(
{
"adc_sensor_esp32.cpp": {
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF,
},
"adc_sensor_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
"adc_sensor_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO},
"adc_sensor_libretiny.cpp": {
PlatformFramework.BK72XX_ARDUINO,
PlatformFramework.RTL87XX_ARDUINO,
PlatformFramework.LN882X_ARDUINO,
},
"adc_sensor_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR},
}
)

View File

@ -3,20 +3,26 @@
#include "esphome/components/sensor/sensor.h" #include "esphome/components/sensor/sensor.h"
#include "esphome/components/voltage_sampler/voltage_sampler.h" #include "esphome/components/voltage_sampler/voltage_sampler.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#ifdef USE_ESP32 #ifdef USE_ESP32
#include <esp_adc_cal.h> #include "esp_adc/adc_cali.h"
#include "driver/adc.h" #include "esp_adc/adc_cali_scheme.h"
#endif // USE_ESP32 #include "esp_adc/adc_oneshot.h"
#include "hal/adc_types.h" // This defines ADC_CHANNEL_MAX
#endif // USE_ESP32
#ifdef USE_ZEPHYR
#include <zephyr/drivers/adc.h>
#endif
namespace esphome { namespace esphome {
namespace adc { namespace adc {
#ifdef USE_ESP32 #ifdef USE_ESP32
// clang-format off // clang-format off
#if (ESP_IDF_VERSION_MAJOR == 4 && ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 7)) || \ #if (ESP_IDF_VERSION_MAJOR == 5 && \
(ESP_IDF_VERSION_MAJOR == 5 && \
((ESP_IDF_VERSION_MINOR == 0 && ESP_IDF_VERSION_PATCH >= 5) || \ ((ESP_IDF_VERSION_MINOR == 0 && ESP_IDF_VERSION_PATCH >= 5) || \
(ESP_IDF_VERSION_MINOR == 1 && ESP_IDF_VERSION_PATCH >= 3) || \ (ESP_IDF_VERSION_MINOR == 1 && ESP_IDF_VERSION_PATCH >= 3) || \
(ESP_IDF_VERSION_MINOR >= 2)) \ (ESP_IDF_VERSION_MINOR >= 2)) \
@ -28,79 +34,136 @@ static const adc_atten_t ADC_ATTEN_DB_12_COMPAT = ADC_ATTEN_DB_11;
#endif #endif
#endif // USE_ESP32 #endif // USE_ESP32
enum class SamplingMode : uint8_t { AVG = 0, MIN = 1, MAX = 2 }; enum class SamplingMode : uint8_t {
AVG = 0,
MIN = 1,
MAX = 2,
};
const LogString *sampling_mode_to_str(SamplingMode mode); const LogString *sampling_mode_to_str(SamplingMode mode);
class Aggregator { template<typename T> class Aggregator {
public: public:
void add_sample(uint32_t value);
uint32_t aggregate();
Aggregator(SamplingMode mode); Aggregator(SamplingMode mode);
void add_sample(T value);
T aggregate();
protected: protected:
T aggr_{0};
uint8_t samples_{0};
SamplingMode mode_{SamplingMode::AVG}; SamplingMode mode_{SamplingMode::AVG};
uint32_t aggr_{0};
uint32_t samples_{0};
}; };
class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler { class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler {
public: public:
#ifdef USE_ESP32 /// Update the sensor's state by reading the current ADC value.
/// Set the attenuation for this pin. Only available on the ESP32. /// This method is called periodically based on the update interval.
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
void update() override; 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; 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; 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; float get_setup_priority() const override;
#ifdef USE_ZEPHYR
/// Set the ADC channel to be used by the ADC sensor.
/// @param channel Pointer to an adc_dt_spec structure representing the ADC channel.
void set_adc_channel(const adc_dt_spec *channel) { this->channel_ = channel; }
#endif
/// Set the GPIO pin to be used by the ADC sensor.
/// @param pin Pointer to an InternalGPIOPin representing the ADC input pin.
void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; } void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; }
/// 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; } 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); 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); 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; float sample() override;
#ifdef USE_ESP8266 #ifdef USE_ESP32
std::string unique_id() override; /// Set the ADC attenuation level to adjust the input voltage range.
#endif // USE_ESP8266 /// 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 #ifdef USE_RP2040
void set_is_temperature() { this->is_temperature_ = true; } void set_is_temperature() { this->is_temperature_ = true; }
#endif // USE_RP2040 #endif // USE_RP2040
protected: protected:
InternalGPIOPin *pin_;
bool output_raw_{false};
uint8_t sample_count_{1}; uint8_t sample_count_{1};
bool output_raw_{false};
InternalGPIOPin *pin_;
SamplingMode sampling_mode_{SamplingMode::AVG}; 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 #ifdef USE_RP2040
bool is_temperature_{false}; bool is_temperature_{false};
#endif // USE_RP2040 #endif // USE_RP2040
#ifdef USE_ESP32 #ifdef USE_ZEPHYR
adc_atten_t attenuation_{ADC_ATTEN_DB_0}; const struct adc_dt_spec *channel_ = nullptr;
adc1_channel_t channel1_{ADC1_CHANNEL_MAX}; #endif
adc2_channel_t channel2_{ADC2_CHANNEL_MAX};
bool autorange_{false};
#if ESP_IDF_VERSION_MAJOR >= 5
esp_adc_cal_characteristics_t cal_characteristics_[SOC_ADC_ATTEN_NUM] = {};
#else
esp_adc_cal_characteristics_t cal_characteristics_[ADC_ATTEN_MAX] = {};
#endif // ESP_IDF_VERSION_MAJOR
#endif // USE_ESP32
}; };
} // namespace adc } // namespace adc

View File

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

View File

@ -8,137 +8,313 @@ namespace adc {
static const char *const TAG = "adc.esp32"; 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 const LogString *attenuation_to_str(adc_atten_t attenuation) {
#if USE_ESP32_VARIANT_ESP32S2 switch (attenuation) {
static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 13; case ADC_ATTEN_DB_0:
#else return LOG_STR("0 dB");
static const int32_t SOC_ADC_RTC_MAX_BITWIDTH = 12; case ADC_ATTEN_DB_2_5:
#endif // USE_ESP32_VARIANT_ESP32S2 return LOG_STR("2.5 dB");
#endif // SOC_ADC_RTC_MAX_BITWIDTH 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");
}
}
static const int ADC_MAX = (1 << SOC_ADC_RTC_MAX_BITWIDTH) - 1; const LogString *adc_unit_to_str(adc_unit_t unit) {
static const int ADC_HALF = (1 << SOC_ADC_RTC_MAX_BITWIDTH) >> 1; 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");
}
}
void ADCSensor::setup() { 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) {
if (this->channel1_ != ADC1_CHANNEL_MAX) { adc_oneshot_unit_init_cfg_t init_config = {}; // Zero initialize
adc1_config_width(ADC_WIDTH_MAX_SOC_BITS); init_config.unit_id = this->adc_unit_;
if (!this->autorange_) { init_config.ulp_mode = ADC_ULP_MODE_DISABLE;
adc1_config_channel_atten(this->channel1_, this->attenuation_); #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;
} else if (this->channel2_ != ADC2_CHANNEL_MAX) { #endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 ||
if (!this->autorange_) { // USE_ESP32_VARIANT_ESP32H2
adc2_config_channel_atten(this->channel2_, this->attenuation_); 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_];
for (int32_t i = 0; i <= ADC_ATTEN_DB_12_COMPAT; i++) { this->setup_flags_.handle_init_complete = true;
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, adc_oneshot_chan_cfg_t config = {
1100, // default vref .atten = this->attenuation_,
&this->cal_characteristics_[i]); .bitwidth = ADC_BITWIDTH_DEFAULT,
switch (cal_value) { };
case ESP_ADC_CAL_VAL_EFUSE_VREF: esp_err_t err = adc_oneshot_config_channel(this->adc_handle_, this->channel_, &config);
ESP_LOGV(TAG, "Using eFuse Vref for calibration"); if (err != ESP_OK) {
break; ESP_LOGE(TAG, "Error configuring channel: %d", err);
case ESP_ADC_CAL_VAL_EFUSE_TP: this->mark_failed();
ESP_LOGV(TAG, "Using two-point eFuse Vref for calibration"); return;
break;
case ESP_ADC_CAL_VAL_DEFAULT_VREF:
default:
break;
}
} }
this->setup_flags_.config_complete = true;
// Initialize ADC calibration
if (this->calibration_handle_ == nullptr) {
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 || USE_ESP32_VARIANT_ESP32P4
// 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() { void ADCSensor::dump_config() {
LOG_SENSOR("", "ADC Sensor", this); LOG_SENSOR("", "ADC Sensor", this);
LOG_PIN(" Pin: ", this->pin_); LOG_PIN(" Pin: ", this->pin_);
if (this->autorange_) {
ESP_LOGCONFIG(TAG, " Attenuation: auto");
} else {
switch (this->attenuation_) {
case ADC_ATTEN_DB_0:
ESP_LOGCONFIG(TAG, " Attenuation: 0db");
break;
case ADC_ATTEN_DB_2_5:
ESP_LOGCONFIG(TAG, " Attenuation: 2.5db");
break;
case ADC_ATTEN_DB_6:
ESP_LOGCONFIG(TAG, " Attenuation: 6db");
break;
case ADC_ATTEN_DB_12_COMPAT:
ESP_LOGCONFIG(TAG, " Attenuation: 12db");
break;
default: // This is to satisfy the unused ADC_ATTEN_MAX
break;
}
}
ESP_LOGCONFIG(TAG, ESP_LOGCONFIG(TAG,
" Samples: %i\n" " Channel: %d\n"
" Unit: %s\n"
" Attenuation: %s\n"
" Samples: %i\n"
" Sampling mode: %s", " Sampling mode: %s",
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); LOG_UPDATE_INTERVAL(this);
} }
float ADCSensor::sample() { float ADCSensor::sample() {
if (!this->autorange_) { if (this->autorange_) {
auto aggr = Aggregator(this->sampling_mode_); return this->sample_autorange_();
} else {
return this->sample_fixed_attenuation_();
}
}
for (uint8_t sample = 0; sample < this->sample_count_; sample++) { float ADCSensor::sample_fixed_attenuation_() {
int raw = -1; auto aggr = Aggregator<uint32_t>(this->sampling_mode_);
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;
}
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_]); aggr.add_sample(raw);
return mv / 1000.0f;
} }
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) { if (this->output_raw_) {
adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_12_COMPAT); return final_value;
raw12 = adc1_get_raw(this->channel1_); }
if (raw12 < ADC_MAX) {
adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_6); if (this->calibration_handle_ != nullptr) {
raw6 = adc1_get_raw(this->channel1_); int voltage_mv;
if (raw6 < ADC_MAX) { esp_err_t err = adc_cali_raw_to_voltage(this->calibration_handle_, final_value, &voltage_mv);
adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_2_5); if (err == ESP_OK) {
raw2 = adc1_get_raw(this->channel1_); return voltage_mv / 1000.0f;
if (raw2 < ADC_MAX) { } else {
adc1_config_channel_atten(this->channel1_, ADC_ATTEN_DB_0); ESP_LOGW(TAG, "ADC calibration conversion failed with error %d, disabling calibration", err);
raw0 = adc1_get_raw(this->channel1_); 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 || USE_ESP32_VARIANT_ESP32P4
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); return final_value * 3.3f / 4095.0f;
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); float ADCSensor::sample_autorange_() {
if (raw6 < ADC_MAX) { // Auto-range mode
adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_2_5); auto read_atten = [this](adc_atten_t atten) -> std::pair<int, float> {
adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw2); // First reconfigure the attenuation for this reading
if (raw2 < ADC_MAX) { adc_oneshot_chan_cfg_t config = {
adc2_config_channel_atten(this->channel2_, ADC_ATTEN_DB_0); .atten = atten,
adc2_get_raw(this->channel2_, ADC_WIDTH_MAX_SOC_BITS, &raw0); .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 || USE_ESP32_VARIANT_ESP32P4
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 || USE_ESP32_VARIANT_ESP32P4
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 || USE_ESP32_VARIANT_ESP32P4
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 || USE_ESP32_VARIANT_ESP32P4
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;
} }
} }
} }
@ -147,19 +323,19 @@ float ADCSensor::sample() {
return NAN; return NAN;
} }
uint32_t mv12 = esp_adc_cal_raw_to_voltage(raw12, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_12_COMPAT]); const int adc_half = 2048;
uint32_t mv6 = esp_adc_cal_raw_to_voltage(raw6, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_6]); uint32_t c12 = std::min(raw12, adc_half);
uint32_t mv2 = esp_adc_cal_raw_to_voltage(raw2, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_2_5]); uint32_t c6 = adc_half - std::abs(raw6 - adc_half);
uint32_t mv0 = esp_adc_cal_raw_to_voltage(raw0, &this->cal_characteristics_[(int32_t) ADC_ATTEN_DB_0]); uint32_t c2 = adc_half - std::abs(raw2 - adc_half);
uint32_t c0 = std::min(4095 - raw0, adc_half);
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);
uint32_t csum = c12 + c6 + c2 + c0; uint32_t csum = c12 + c6 + c2 + c0;
uint32_t mv_scaled = (mv12 * c12) + (mv6 * c6) + (mv2 * c2) + (mv0 * c0); if (csum == 0) {
return mv_scaled / (float) (csum * 1000U); ESP_LOGE(TAG, "Invalid weight sum in autorange calculation");
return NAN;
}
return (mv12 * c12 + mv6 * c6 + mv2 * c2 + mv0 * c0) / csum;
} }
} // namespace adc } // namespace adc

View File

@ -17,7 +17,6 @@ namespace adc {
static const char *const TAG = "adc.esp8266"; static const char *const TAG = "adc.esp8266";
void ADCSensor::setup() { void ADCSensor::setup() {
ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->get_name().c_str());
#ifndef USE_ADC_SENSOR_VCC #ifndef USE_ADC_SENSOR_VCC
this->pin_->setup(); this->pin_->setup();
#endif #endif
@ -38,7 +37,7 @@ void ADCSensor::dump_config() {
} }
float ADCSensor::sample() { float ADCSensor::sample() {
auto aggr = Aggregator(this->sampling_mode_); auto aggr = Aggregator<uint32_t>(this->sampling_mode_);
for (uint8_t sample = 0; sample < this->sample_count_; sample++) { for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
uint32_t raw = 0; uint32_t raw = 0;
@ -56,8 +55,6 @@ float ADCSensor::sample() {
return aggr.aggregate() / 1024.0f; return aggr.aggregate() / 1024.0f;
} }
std::string ADCSensor::unique_id() { return get_mac_address() + "-adc"; }
} // namespace adc } // namespace adc
} // namespace esphome } // namespace esphome

View File

@ -9,7 +9,6 @@ namespace adc {
static const char *const TAG = "adc.libretiny"; static const char *const TAG = "adc.libretiny";
void ADCSensor::setup() { void ADCSensor::setup() {
ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->get_name().c_str());
#ifndef USE_ADC_SENSOR_VCC #ifndef USE_ADC_SENSOR_VCC
this->pin_->setup(); this->pin_->setup();
#endif // !USE_ADC_SENSOR_VCC #endif // !USE_ADC_SENSOR_VCC
@ -31,7 +30,7 @@ void ADCSensor::dump_config() {
float ADCSensor::sample() { float ADCSensor::sample() {
uint32_t raw = 0; uint32_t raw = 0;
auto aggr = Aggregator(this->sampling_mode_); auto aggr = Aggregator<uint32_t>(this->sampling_mode_);
if (this->output_raw_) { if (this->output_raw_) {
for (uint8_t sample = 0; sample < this->sample_count_; sample++) { for (uint8_t sample = 0; sample < this->sample_count_; sample++) {

View File

@ -14,7 +14,6 @@ namespace adc {
static const char *const TAG = "adc.rp2040"; static const char *const TAG = "adc.rp2040";
void ADCSensor::setup() { void ADCSensor::setup() {
ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->get_name().c_str());
static bool initialized = false; static bool initialized = false;
if (!initialized) { if (!initialized) {
adc_init(); adc_init();
@ -42,7 +41,7 @@ void ADCSensor::dump_config() {
float ADCSensor::sample() { float ADCSensor::sample() {
uint32_t raw = 0; uint32_t raw = 0;
auto aggr = Aggregator(this->sampling_mode_); auto aggr = Aggregator<uint32_t>(this->sampling_mode_);
if (this->is_temperature_) { if (this->is_temperature_) {
adc_set_temp_sensor_enabled(true); adc_set_temp_sensor_enabled(true);

View File

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

View File

@ -3,6 +3,12 @@ import logging
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import sensor, voltage_sampler from esphome.components import sensor, voltage_sampler
from esphome.components.esp32 import get_esp32_variant from esphome.components.esp32 import get_esp32_variant
from esphome.components.nrf52.const import AIN_TO_GPIO, EXTRA_ADC
from esphome.components.zephyr import (
zephyr_add_overlay,
zephyr_add_prj_conf,
zephyr_add_user,
)
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_ATTENUATION, CONF_ATTENUATION,
@ -10,13 +16,12 @@ from esphome.const import (
CONF_NUMBER, CONF_NUMBER,
CONF_PIN, CONF_PIN,
CONF_RAW, CONF_RAW,
CONF_WIFI,
DEVICE_CLASS_VOLTAGE, DEVICE_CLASS_VOLTAGE,
PLATFORM_NRF52,
STATE_CLASS_MEASUREMENT, STATE_CLASS_MEASUREMENT,
UNIT_VOLT, UNIT_VOLT,
) )
from esphome.core import CORE from esphome.core import CORE
import esphome.final_validate as fv
from . import ( from . import (
ATTENUATION_MODES, ATTENUATION_MODES,
@ -24,6 +29,7 @@ from . import (
ESP32_VARIANT_ADC2_PIN_TO_CHANNEL, ESP32_VARIANT_ADC2_PIN_TO_CHANNEL,
SAMPLING_MODES, SAMPLING_MODES,
adc_ns, adc_ns,
adc_unit_t,
validate_adc_pin, validate_adc_pin,
) )
@ -57,25 +63,14 @@ def validate_config(config):
return 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 = adc_ns.class_(
"ADCSensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler "ADCSensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler
) )
CONF_NRF_SAADC = "nrf_saadc"
adc_dt_spec = cg.global_ns.class_("adc_dt_spec")
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
sensor.sensor_schema( sensor.sensor_schema(
ADCSensor, ADCSensor,
@ -91,6 +86,7 @@ CONFIG_SCHEMA = cv.All(
cv.SplitDefault(CONF_ATTENUATION, esp32="0db"): cv.All( cv.SplitDefault(CONF_ATTENUATION, esp32="0db"): cv.All(
cv.only_on_esp32, _attenuation cv.only_on_esp32, _attenuation
), ),
cv.OnlyWith(CONF_NRF_SAADC, PLATFORM_NRF52): cv.declare_id(adc_dt_spec),
cv.Optional(CONF_SAMPLES, default=1): cv.int_range(min=1, max=255), cv.Optional(CONF_SAMPLES, default=1): cv.int_range(min=1, max=255),
cv.Optional(CONF_SAMPLING_MODE, default="avg"): _sampling_mode, cv.Optional(CONF_SAMPLING_MODE, default="avg"): _sampling_mode,
} }
@ -99,7 +95,7 @@ CONFIG_SCHEMA = cv.All(
validate_config, validate_config,
) )
FINAL_VALIDATE_SCHEMA = final_validate_config CONF_ADC_CHANNEL_ID = "adc_channel_id"
async def to_code(config): async def to_code(config):
@ -111,7 +107,7 @@ async def to_code(config):
cg.add_define("USE_ADC_SENSOR_VCC") cg.add_define("USE_ADC_SENSOR_VCC")
elif config[CONF_PIN] == "TEMPERATURE": elif config[CONF_PIN] == "TEMPERATURE":
cg.add(var.set_is_temperature()) cg.add(var.set_is_temperature())
else: elif not CORE.is_nrf52 or config[CONF_PIN][CONF_NUMBER] not in EXTRA_ADC:
pin = await cg.gpio_pin_expression(config[CONF_PIN]) pin = await cg.gpio_pin_expression(config[CONF_PIN])
cg.add(var.set_pin(pin)) cg.add(var.set_pin(pin))
@ -119,13 +115,13 @@ async def to_code(config):
cg.add(var.set_sample_count(config[CONF_SAMPLES])) cg.add(var.set_sample_count(config[CONF_SAMPLES]))
cg.add(var.set_sampling_mode(config[CONF_SAMPLING_MODE])) 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 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() variant = get_esp32_variant()
pin_num = config[CONF_PIN][CONF_NUMBER] pin_num = config[CONF_PIN][CONF_NUMBER]
if ( if (
@ -133,10 +129,48 @@ async def to_code(config):
and pin_num in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant] and pin_num in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant]
): ):
chan = ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant][pin_num] 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 ( elif (
variant in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL variant in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL
and pin_num in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant] and pin_num in ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant]
): ):
chan = ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant][pin_num] 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))
elif CORE.is_nrf52:
CORE.data.setdefault(CONF_ADC_CHANNEL_ID, 0)
channel_id = CORE.data[CONF_ADC_CHANNEL_ID]
CORE.data[CONF_ADC_CHANNEL_ID] = channel_id + 1
zephyr_add_prj_conf("ADC", True)
nrf_saadc = config[CONF_NRF_SAADC]
rhs = cg.RawExpression(
f"ADC_DT_SPEC_GET_BY_IDX(DT_PATH(zephyr_user), {channel_id})"
)
adc = cg.new_Pvariable(nrf_saadc, rhs)
cg.add(var.set_adc_channel(adc))
gain = "ADC_GAIN_1_6"
pin_number = config[CONF_PIN][CONF_NUMBER]
if pin_number == "VDDHDIV5":
gain = "ADC_GAIN_1_2"
if isinstance(pin_number, int):
GPIO_TO_AIN = {v: k for k, v in AIN_TO_GPIO.items()}
pin_number = GPIO_TO_AIN[pin_number]
zephyr_add_user("io-channels", f"<&adc {channel_id}>")
zephyr_add_overlay(
f"""
&adc {{
#address-cells = <1>;
#size-cells = <0>;
channel@{channel_id} {{
reg = <{channel_id}>;
zephyr,gain = "{gain}";
zephyr,reference = "ADC_REF_INTERNAL";
zephyr,acquisition-time = <ADC_ACQ_TIME_DEFAULT>;
zephyr,input-positive = <NRF_SAADC_{pin_number}>;
zephyr,resolution = <14>;
zephyr,oversampling = <8>;
}};
}};
"""
)

View File

@ -8,10 +8,7 @@ static const char *const TAG = "adc128s102";
float ADC128S102::get_setup_priority() const { return setup_priority::HARDWARE; } float ADC128S102::get_setup_priority() const { return setup_priority::HARDWARE; }
void ADC128S102::setup() { void ADC128S102::setup() { this->spi_setup(); }
ESP_LOGCONFIG(TAG, "Running setup");
this->spi_setup();
}
void ADC128S102::dump_config() { void ADC128S102::dump_config() {
ESP_LOGCONFIG(TAG, "ADC128S102:"); ESP_LOGCONFIG(TAG, "ADC128S102:");

View File

@ -85,8 +85,6 @@ class ADE7880 : public i2c::I2CDevice, public PollingComponent {
void dump_config() override; void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
protected: protected:
ADE7880Store store_{}; ADE7880Store store_{};
InternalGPIOPin *irq0_pin_{nullptr}; InternalGPIOPin *irq0_pin_{nullptr};

View File

@ -10,7 +10,6 @@ static const uint8_t ADS1115_REGISTER_CONVERSION = 0x00;
static const uint8_t ADS1115_REGISTER_CONFIG = 0x01; static const uint8_t ADS1115_REGISTER_CONFIG = 0x01;
void ADS1115Component::setup() { void ADS1115Component::setup() {
ESP_LOGCONFIG(TAG, "Running setup");
uint16_t value; uint16_t value;
if (!this->read_byte_16(ADS1115_REGISTER_CONVERSION, &value)) { if (!this->read_byte_16(ADS1115_REGISTER_CONVERSION, &value)) {
this->mark_failed(); this->mark_failed();

View File

@ -49,7 +49,6 @@ class ADS1115Component : public Component, public i2c::I2CDevice {
void setup() override; void setup() override;
void dump_config() override; void dump_config() override;
/// HARDWARE_LATE setup priority /// HARDWARE_LATE setup priority
float get_setup_priority() const override { return setup_priority::DATA; }
void set_continuous_mode(bool continuous_mode) { continuous_mode_ = continuous_mode; } void set_continuous_mode(bool continuous_mode) { continuous_mode_ = continuous_mode; }
/// Helper method to request a measurement from a sensor. /// Helper method to request a measurement from a sensor.

View File

@ -9,7 +9,6 @@ static const char *const TAG = "ads1118";
static const uint8_t ADS1118_DATA_RATE_860_SPS = 0b111; static const uint8_t ADS1118_DATA_RATE_860_SPS = 0b111;
void ADS1118::setup() { void ADS1118::setup() {
ESP_LOGCONFIG(TAG, "Running setup");
this->spi_setup(); this->spi_setup();
this->config_ = 0; this->config_ = 0;

View File

@ -34,7 +34,6 @@ class ADS1118 : public Component,
ADS1118() = default; ADS1118() = default;
void setup() override; void setup() override;
void dump_config() override; void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
/// Helper method to request a measurement from a sensor. /// Helper method to request a measurement from a sensor.
float request_measurement(ADS1118Multiplexer multiplexer, ADS1118Gain gain, bool temperature_mode); float request_measurement(ADS1118Multiplexer multiplexer, ADS1118Gain gain, bool temperature_mode);

View File

@ -24,8 +24,6 @@ static const uint16_t ZP_CURRENT = 0x0000;
static const uint16_t ZP_DEFAULT = 0xFFFF; static const uint16_t ZP_DEFAULT = 0xFFFF;
void AGS10Component::setup() { void AGS10Component::setup() {
ESP_LOGCONFIG(TAG, "Running setup");
auto version = this->read_version_(); auto version = this->read_version_();
if (version) { if (version) {
ESP_LOGD(TAG, "AGS10 Sensor Version: 0x%02X", *version); ESP_LOGD(TAG, "AGS10 Sensor Version: 0x%02X", *version);
@ -45,8 +43,6 @@ void AGS10Component::setup() {
} else { } else {
ESP_LOGE(TAG, "AGS10 Sensor Resistance: unknown"); ESP_LOGE(TAG, "AGS10 Sensor Resistance: unknown");
} }
ESP_LOGD(TAG, "Sensor initialized");
} }
void AGS10Component::update() { void AGS10Component::update() {

View File

@ -31,8 +31,6 @@ class AGS10Component : public PollingComponent, public i2c::I2CDevice {
void dump_config() override; void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
/** /**
* Modifies target address of AGS10. * Modifies target address of AGS10.
* *

View File

@ -38,8 +38,6 @@ static const uint8_t AHT10_STATUS_BUSY = 0x80;
static const float AHT10_DIVISOR = 1048576.0f; // 2^20, used for temperature and humidity calculations static const float AHT10_DIVISOR = 1048576.0f; // 2^20, used for temperature and humidity calculations
void AHT10Component::setup() { void AHT10Component::setup() {
ESP_LOGCONFIG(TAG, "Running setup");
if (this->write(AHT10_SOFTRESET_CMD, sizeof(AHT10_SOFTRESET_CMD)) != i2c::ERROR_OK) { if (this->write(AHT10_SOFTRESET_CMD, sizeof(AHT10_SOFTRESET_CMD)) != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Reset failed"); ESP_LOGE(TAG, "Reset failed");
} }
@ -80,8 +78,6 @@ void AHT10Component::setup() {
this->mark_failed(); this->mark_failed();
return; return;
} }
ESP_LOGV(TAG, "Initialization complete");
} }
void AHT10Component::restart_read_() { void AHT10Component::restart_read_() {

View File

@ -17,8 +17,6 @@ static const char *const TAG = "aic3204";
} }
void AIC3204::setup() { void AIC3204::setup() {
ESP_LOGCONFIG(TAG, "Running setup");
// Set register page to 0 // Set register page to 0
ERROR_CHECK(this->write_byte(AIC3204_PAGE_CTRL, 0x00), "Set page 0 failed"); ERROR_CHECK(this->write_byte(AIC3204_PAGE_CTRL, 0x00), "Set page 0 failed");
// Initiate SW reset (PLL is powered off as part of reset) // Initiate SW reset (PLL is powered off as part of reset)

View File

@ -66,7 +66,6 @@ class AIC3204 : public audio_dac::AudioDac, public Component, public i2c::I2CDev
public: public:
void setup() override; void setup() override;
void dump_config() override; void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
bool set_mute_off() override; bool set_mute_off() override;
bool set_mute_on() override; bool set_mute_on() override;

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_); LOG_SENSOR(" ", "Illuminance", this->illuminance_sensor_);
} }
AirthingsWavePlus::AirthingsWavePlus() { void AirthingsWavePlus::setup() {
this->service_uuid_ = espbt::ESPBTUUID::from_raw(SERVICE_UUID); const char *service_uuid;
this->sensors_data_characteristic_uuid_ = espbt::ESPBTUUID::from_raw(CHARACTERISTIC_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_ = 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 } // namespace airthings_wave_plus

View File

@ -9,13 +9,20 @@ namespace airthings_wave_plus {
namespace espbt = esphome::esp32_ble_tracker; 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 SERVICE_UUID = "b42e1c08-ade7-11e4-89d3-123b93f75cba";
static const char *const CHARACTERISTIC_UUID = "b42e2a68-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 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 { class AirthingsWavePlus : public airthings_wave_base::AirthingsWaveBase {
public: public:
AirthingsWavePlus(); void setup() override;
void dump_config() 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_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_co2(sensor::Sensor *co2) { co2_sensor_ = co2; }
void set_illuminance(sensor::Sensor *illuminance) { illuminance_sensor_ = illuminance; } void set_illuminance(sensor::Sensor *illuminance) { illuminance_sensor_ = illuminance; }
void set_device_type(WaveDeviceType wave_device_type) { wave_device_type_ = wave_device_type; }
protected: protected:
bool is_valid_radon_value_(uint16_t radon); bool is_valid_radon_value_(uint16_t radon);
bool is_valid_co2_value_(uint16_t co2); bool is_valid_co2_value_(uint16_t co2);
void read_sensors(uint8_t *raw_value, uint16_t value_len) override; 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_sensor_{nullptr};
sensor::Sensor *radon_long_term_sensor_{nullptr}; sensor::Sensor *radon_long_term_sensor_{nullptr};

View File

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

View File

@ -14,8 +14,8 @@ from esphome.const import (
CONF_WEB_SERVER, CONF_WEB_SERVER,
) )
from esphome.core import CORE, coroutine_with_priority from esphome.core import CORE, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass from esphome.cpp_generator import MockObjClass
from esphome.cpp_helpers import setup_entity
CODEOWNERS = ["@grahambrown11", "@hwstar"] CODEOWNERS = ["@grahambrown11", "@hwstar"]
IS_PLATFORM_COMPONENT = True IS_PLATFORM_COMPONENT = True
@ -149,6 +149,9 @@ _ALARM_CONTROL_PANEL_SCHEMA = (
) )
_ALARM_CONTROL_PANEL_SCHEMA.add_extra(entity_duplicate_validator("alarm_control_panel"))
def alarm_control_panel_schema( def alarm_control_panel_schema(
class_: MockObjClass, class_: MockObjClass,
*, *,
@ -190,7 +193,7 @@ ALARM_CONTROL_PANEL_CONDITION_SCHEMA = maybe_simple_id(
async def setup_alarm_control_panel_core_(var, config): async def setup_alarm_control_panel_core_(var, config):
await setup_entity(var, config) await setup_entity(var, config, "alarm_control_panel")
for conf in config.get(CONF_ON_STATE, []): for conf in config.get(CONF_ON_STATE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf) await automation.build_automation(trigger, [], conf)
@ -298,8 +301,7 @@ async def alarm_action_disarm_to_code(config, action_id, template_arg, args):
) )
async def alarm_action_pending_to_code(config, action_id, template_arg, args): async def alarm_action_pending_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID]) paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren) return cg.new_Pvariable(action_id, template_arg, paren)
return var
@automation.register_action( @automation.register_action(
@ -307,8 +309,7 @@ async def alarm_action_pending_to_code(config, action_id, template_arg, args):
) )
async def alarm_action_trigger_to_code(config, action_id, template_arg, args): async def alarm_action_trigger_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID]) paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren) return cg.new_Pvariable(action_id, template_arg, paren)
return var
@automation.register_action( @automation.register_action(
@ -316,8 +317,7 @@ async def alarm_action_trigger_to_code(config, action_id, template_arg, args):
) )
async def alarm_action_chime_to_code(config, action_id, template_arg, args): async def alarm_action_chime_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID]) paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren) return cg.new_Pvariable(action_id, template_arg, paren)
return var
@automation.register_action( @automation.register_action(
@ -330,8 +330,7 @@ async def alarm_action_chime_to_code(config, action_id, template_arg, args):
) )
async def alarm_action_ready_to_code(config, action_id, template_arg, args): async def alarm_action_ready_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID]) paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren) return cg.new_Pvariable(action_id, template_arg, paren)
return var
@automation.register_condition( @automation.register_condition(

View File

@ -41,7 +41,6 @@ class Alpha3 : public esphome::ble_client::BLEClientNode, public PollingComponen
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override; esp_ble_gattc_cb_param_t *param) override;
void dump_config() override; void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
void set_flow_sensor(sensor::Sensor *sensor) { this->flow_sensor_ = sensor; } void set_flow_sensor(sensor::Sensor *sensor) { this->flow_sensor_ = sensor; }
void set_head_sensor(sensor::Sensor *sensor) { this->head_sensor_ = sensor; } void set_head_sensor(sensor::Sensor *sensor) { this->head_sensor_ = sensor; }
void set_power_sensor(sensor::Sensor *sensor) { this->power_sensor_ = sensor; } void set_power_sensor(sensor::Sensor *sensor) { this->power_sensor_ = sensor; }

View File

@ -90,8 +90,6 @@ bool AM2315C::convert_(uint8_t *data, float &humidity, float &temperature) {
} }
void AM2315C::setup() { void AM2315C::setup() {
ESP_LOGCONFIG(TAG, "Running setup");
// get status // get status
uint8_t status = 0; uint8_t status = 0;
if (this->read(&status, 1) != i2c::ERROR_OK) { if (this->read(&status, 1) != i2c::ERROR_OK) {

View File

@ -34,7 +34,6 @@ void AM2320Component::update() {
this->status_clear_warning(); this->status_clear_warning();
} }
void AM2320Component::setup() { void AM2320Component::setup() {
ESP_LOGCONFIG(TAG, "Running setup");
uint8_t data[8]; uint8_t data[8];
data[0] = 0; data[0] = 0;
data[1] = 4; data[1] = 4;

View File

@ -22,7 +22,6 @@ class Am43Component : public cover::Cover, public esphome::ble_client::BLEClient
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override; esp_ble_gattc_cb_param_t *param) override;
void dump_config() override; void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
cover::CoverTraits get_traits() override; cover::CoverTraits get_traits() override;
void set_pin(uint16_t pin) { this->pin_ = pin; } void set_pin(uint16_t pin) { this->pin_ = pin; }
void set_invert_position(bool invert_position) { this->invert_position_ = invert_position; } void set_invert_position(bool invert_position) { this->invert_position_ = invert_position; }

View File

@ -22,7 +22,6 @@ class Am43 : public esphome::ble_client::BLEClientNode, public PollingComponent
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override; esp_ble_gattc_cb_param_t *param) override;
void dump_config() override; void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
void set_battery(sensor::Sensor *battery) { battery_ = battery; } void set_battery(sensor::Sensor *battery) { battery_ = battery; }
void set_illuminance(sensor::Sensor *illuminance) { illuminance_ = illuminance; } void set_illuminance(sensor::Sensor *illuminance) { illuminance_ = illuminance; }

View File

@ -12,8 +12,6 @@ class AnalogThresholdBinarySensor : public Component, public binary_sensor::Bina
void dump_config() override; void dump_config() override;
void setup() override; void setup() override;
float get_setup_priority() const override { return setup_priority::DATA; }
void set_sensor(sensor::Sensor *analog_sensor); void set_sensor(sensor::Sensor *analog_sensor);
template<typename T> void set_upper_threshold(T upper_threshold) { this->upper_threshold_ = upper_threshold; } template<typename T> void set_upper_threshold(T upper_threshold) { this->upper_threshold_ = upper_threshold; }
template<typename T> void set_lower_threshold(T lower_threshold) { this->lower_threshold_ = lower_threshold; } template<typename T> void set_lower_threshold(T lower_threshold) { this->lower_threshold_ = lower_threshold; }

View File

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

View File

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

View File

@ -26,7 +26,6 @@ class Anova : public climate::Climate, public esphome::ble_client::BLEClientNode
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override; esp_ble_gattc_cb_param_t *param) override;
void dump_config() override; void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
climate::ClimateTraits traits() override { climate::ClimateTraits traits() override {
auto traits = climate::ClimateTraits(); auto traits = climate::ClimateTraits();
traits.set_supports_current_temperature(true); traits.set_supports_current_temperature(true);

View File

@ -54,8 +54,6 @@ enum { // APDS9306 registers
} }
void APDS9306::setup() { void APDS9306::setup() {
ESP_LOGCONFIG(TAG, "Running setup");
uint8_t id; uint8_t id;
if (!this->read_byte(APDS9306_PART_ID, &id)) { // Part ID register if (!this->read_byte(APDS9306_PART_ID, &id)) { // Part ID register
this->error_code_ = COMMUNICATION_FAILED; this->error_code_ = COMMUNICATION_FAILED;
@ -86,8 +84,6 @@ void APDS9306::setup() {
// Set to active mode // Set to active mode
APDS9306_WRITE_BYTE(APDS9306_MAIN_CTRL, 0x02); APDS9306_WRITE_BYTE(APDS9306_MAIN_CTRL, 0x02);
ESP_LOGCONFIG(TAG, "APDS9306 setup complete");
} }
void APDS9306::dump_config() { void APDS9306::dump_config() {

View File

@ -15,7 +15,6 @@ static const char *const TAG = "apds9960";
#define APDS9960_WRITE_BYTE(reg, value) APDS9960_ERROR_CHECK(this->write_byte(reg, value)); #define APDS9960_WRITE_BYTE(reg, value) APDS9960_ERROR_CHECK(this->write_byte(reg, value));
void APDS9960::setup() { void APDS9960::setup() {
ESP_LOGCONFIG(TAG, "Running setup");
uint8_t id; uint8_t id;
if (!this->read_byte(0x92, &id)) { // ID register if (!this->read_byte(0x92, &id)) { // ID register
this->error_code_ = COMMUNICATION_FAILED; this->error_code_ = COMMUNICATION_FAILED;
@ -23,7 +22,7 @@ void APDS9960::setup() {
return; return;
} }
if (id != 0xAB && id != 0x9C && id != 0xA8) { // APDS9960 all should have one of these IDs if (id != 0xAB && id != 0x9C && id != 0xA8 && id != 0x9E) { // APDS9960 all should have one of these IDs
this->error_code_ = WRONG_ID; this->error_code_ = WRONG_ID;
this->mark_failed(); this->mark_failed();
return; return;

View File

@ -3,6 +3,7 @@ import base64
from esphome import automation from esphome import automation
from esphome.automation import Condition from esphome.automation import Condition
import esphome.codegen as cg import esphome.codegen as cg
from esphome.config_helpers import get_logger_level
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_ACTION, CONF_ACTION,
@ -23,8 +24,9 @@ from esphome.const import (
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
CONF_VARIABLES, CONF_VARIABLES,
) )
from esphome.core import coroutine_with_priority from esphome.core import CORE, coroutine_with_priority
DOMAIN = "api"
DEPENDENCIES = ["network"] DEPENDENCIES = ["network"]
AUTO_LOAD = ["socket"] AUTO_LOAD = ["socket"]
CODEOWNERS = ["@OttoWinter"] CODEOWNERS = ["@OttoWinter"]
@ -50,6 +52,9 @@ SERVICE_ARG_NATIVE_TYPES = {
} }
CONF_ENCRYPTION = "encryption" CONF_ENCRYPTION = "encryption"
CONF_BATCH_DELAY = "batch_delay" CONF_BATCH_DELAY = "batch_delay"
CONF_CUSTOM_SERVICES = "custom_services"
CONF_HOMEASSISTANT_SERVICES = "homeassistant_services"
CONF_HOMEASSISTANT_STATES = "homeassistant_states"
def validate_encryption_key(value): def validate_encryption_key(value):
@ -110,9 +115,13 @@ CONFIG_SCHEMA = cv.All(
): ACTIONS_SCHEMA, ): ACTIONS_SCHEMA,
cv.Exclusive(CONF_ACTIONS, group_of_exclusion=CONF_ACTIONS): ACTIONS_SCHEMA, cv.Exclusive(CONF_ACTIONS, group_of_exclusion=CONF_ACTIONS): ACTIONS_SCHEMA,
cv.Optional(CONF_ENCRYPTION): _encryption_schema, cv.Optional(CONF_ENCRYPTION): _encryption_schema,
cv.Optional( cv.Optional(CONF_BATCH_DELAY, default="100ms"): cv.All(
CONF_BATCH_DELAY, default="100ms" cv.positive_time_period_milliseconds,
): cv.positive_time_period_milliseconds, cv.Range(max=cv.TimePeriod(milliseconds=65535)),
),
cv.Optional(CONF_CUSTOM_SERVICES, default=False): cv.boolean,
cv.Optional(CONF_HOMEASSISTANT_SERVICES, default=False): cv.boolean,
cv.Optional(CONF_HOMEASSISTANT_STATES, default=False): cv.boolean,
cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation( cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation(
single=True single=True
), ),
@ -131,27 +140,41 @@ async def to_code(config):
await cg.register_component(var, config) await cg.register_component(var, config)
cg.add(var.set_port(config[CONF_PORT])) cg.add(var.set_port(config[CONF_PORT]))
cg.add(var.set_password(config[CONF_PASSWORD])) if config[CONF_PASSWORD]:
cg.add_define("USE_API_PASSWORD")
cg.add(var.set_password(config[CONF_PASSWORD]))
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY])) cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY]))
for conf in config.get(CONF_ACTIONS, []): # Set USE_API_SERVICES if any services are enabled
template_args = [] if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]:
func_args = [] cg.add_define("USE_API_SERVICES")
service_arg_names = []
for name, var_ in conf[CONF_VARIABLES].items(): if config[CONF_HOMEASSISTANT_SERVICES]:
native = SERVICE_ARG_NATIVE_TYPES[var_] cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
template_args.append(native)
func_args.append((native, name)) if config[CONF_HOMEASSISTANT_STATES]:
service_arg_names.append(name) cg.add_define("USE_API_HOMEASSISTANT_STATES")
templ = cg.TemplateArguments(*template_args)
trigger = cg.new_Pvariable( if actions := config.get(CONF_ACTIONS, []):
conf[CONF_TRIGGER_ID], templ, conf[CONF_ACTION], service_arg_names for conf in actions:
) template_args = []
cg.add(var.register_user_service(trigger)) func_args = []
await automation.build_automation(trigger, func_args, conf) service_arg_names = []
for name, var_ in conf[CONF_VARIABLES].items():
native = SERVICE_ARG_NATIVE_TYPES[var_]
template_args.append(native)
func_args.append((native, name))
service_arg_names.append(name)
templ = cg.TemplateArguments(*template_args)
trigger = cg.new_Pvariable(
conf[CONF_TRIGGER_ID], templ, conf[CONF_ACTION], service_arg_names
)
cg.add(var.register_user_service(trigger))
await automation.build_automation(trigger, func_args, conf)
if CONF_ON_CLIENT_CONNECTED in config: if CONF_ON_CLIENT_CONNECTED in config:
cg.add_define("USE_API_CLIENT_CONNECTED_TRIGGER")
await automation.build_automation( await automation.build_automation(
var.get_client_connected_trigger(), var.get_client_connected_trigger(),
[(cg.std_string, "client_info"), (cg.std_string, "client_address")], [(cg.std_string, "client_info"), (cg.std_string, "client_address")],
@ -159,6 +182,7 @@ async def to_code(config):
) )
if CONF_ON_CLIENT_DISCONNECTED in config: if CONF_ON_CLIENT_DISCONNECTED in config:
cg.add_define("USE_API_CLIENT_DISCONNECTED_TRIGGER")
await automation.build_automation( await automation.build_automation(
var.get_client_disconnected_trigger(), var.get_client_disconnected_trigger(),
[(cg.std_string, "client_info"), (cg.std_string, "client_address")], [(cg.std_string, "client_info"), (cg.std_string, "client_address")],
@ -177,7 +201,7 @@ async def to_code(config):
# and plaintext disabled. Only a factory reset can remove it. # and plaintext disabled. Only a factory reset can remove it.
cg.add_define("USE_API_PLAINTEXT") cg.add_define("USE_API_PLAINTEXT")
cg.add_define("USE_API_NOISE") cg.add_define("USE_API_NOISE")
cg.add_library("esphome/noise-c", "0.1.6") cg.add_library("esphome/noise-c", "0.1.10")
else: else:
cg.add_define("USE_API_PLAINTEXT") cg.add_define("USE_API_PLAINTEXT")
@ -221,6 +245,7 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
HOMEASSISTANT_ACTION_ACTION_SCHEMA, HOMEASSISTANT_ACTION_ACTION_SCHEMA,
) )
async def homeassistant_service_to_code(config, action_id, template_arg, args): async def homeassistant_service_to_code(config, action_id, template_arg, args):
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
serv = await cg.get_variable(config[CONF_ID]) serv = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, serv, False) var = cg.new_Pvariable(action_id, template_arg, serv, False)
templ = await cg.templatable(config[CONF_ACTION], args, None) templ = await cg.templatable(config[CONF_ACTION], args, None)
@ -264,6 +289,7 @@ HOMEASSISTANT_EVENT_ACTION_SCHEMA = cv.Schema(
HOMEASSISTANT_EVENT_ACTION_SCHEMA, HOMEASSISTANT_EVENT_ACTION_SCHEMA,
) )
async def homeassistant_event_to_code(config, action_id, template_arg, args): async def homeassistant_event_to_code(config, action_id, template_arg, args):
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
serv = await cg.get_variable(config[CONF_ID]) serv = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, serv, True) var = cg.new_Pvariable(action_id, template_arg, serv, True)
templ = await cg.templatable(config[CONF_EVENT], args, None) templ = await cg.templatable(config[CONF_EVENT], args, None)
@ -306,3 +332,38 @@ async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, arg
@automation.register_condition("api.connected", APIConnectedCondition, {}) @automation.register_condition("api.connected", APIConnectedCondition, {})
async def api_connected_to_code(config, condition_id, template_arg, args): async def api_connected_to_code(config, condition_id, template_arg, args):
return cg.new_Pvariable(condition_id, template_arg) return cg.new_Pvariable(condition_id, template_arg)
def FILTER_SOURCE_FILES() -> list[str]:
"""Filter out api_pb2_dump.cpp when proto message dumping is not enabled,
user_services.cpp when no services are defined, and protocol-specific
implementations based on encryption configuration."""
files_to_filter: list[str] = []
# 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
#
# 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":
files_to_filter.append("api_pb2_dump.cpp")
# 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")
# Filter protocol-specific implementations based on encryption configuration
encryption_config = config.get(CONF_ENCRYPTION) if config else None
# If encryption is not configured at all, we only need plaintext
if encryption_config is None:
files_to_filter.append("api_frame_helper_noise.cpp")
# If encryption is configured with a key, we only need noise
elif encryption_config.get(CONF_KEY):
files_to_filter.append("api_frame_helper_plaintext.cpp")
# If encryption is configured but no key is provided, we need both
# (this allows a plaintext client to provide a noise key)
return files_to_filter

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -13,15 +13,41 @@
#include <vector> #include <vector>
#include <functional> #include <functional>
namespace esphome { namespace esphome::api {
namespace api {
// Client information structure
struct ClientInfo {
std::string name; // Client name from Hello message
std::string peername; // IP:port from socket
std::string get_combined_info() const {
if (name == peername) {
// Before Hello message, both are the same
return name;
}
return name + " (" + peername + ")";
}
};
// Keepalive timeout in milliseconds // Keepalive timeout in milliseconds
static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000; static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000;
// Maximum number of entities to process in a single batch during initial state/info sending
// This was increased from 20 to 24 after removing the unique_id field from entity info messages,
// which reduced message sizes allowing more entities per batch without exceeding packet limits
static constexpr size_t MAX_INITIAL_PER_BATCH = 24;
// Maximum number of packets to process in a single batch (platform-dependent)
// This limit exists to prevent stack overflow from the PacketInfo array in process_batch_
// Each PacketInfo is 8 bytes, so 64 * 8 = 512 bytes, 32 * 8 = 256 bytes
#if defined(USE_ESP32) || defined(USE_HOST)
static constexpr size_t MAX_PACKETS_PER_BATCH = 64; // ESP32 has 8KB+ stack, HOST has plenty
#else
static constexpr size_t MAX_PACKETS_PER_BATCH = 32; // ESP8266/RP2040/etc have smaller stacks
#endif
class APIConnection : public APIServerConnection { class APIConnection : public APIServerConnection {
public: public:
friend class APIServer; friend class APIServer;
friend class ListEntitiesIterator;
APIConnection(std::unique_ptr<socket::Socket> socket, APIServer *parent); APIConnection(std::unique_ptr<socket::Socket> socket, APIServer *parent);
virtual ~APIConnection(); virtual ~APIConnection();
@ -30,109 +56,91 @@ class APIConnection : public APIServerConnection {
bool send_list_info_done() { bool send_list_info_done() {
return this->schedule_message_(nullptr, &APIConnection::try_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 #ifdef USE_BINARY_SENSOR
bool send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor); bool send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor);
void send_binary_sensor_info(binary_sensor::BinarySensor *binary_sensor);
#endif #endif
#ifdef USE_COVER #ifdef USE_COVER
bool send_cover_state(cover::Cover *cover); bool send_cover_state(cover::Cover *cover);
void send_cover_info(cover::Cover *cover);
void cover_command(const CoverCommandRequest &msg) override; void cover_command(const CoverCommandRequest &msg) override;
#endif #endif
#ifdef USE_FAN #ifdef USE_FAN
bool send_fan_state(fan::Fan *fan); bool send_fan_state(fan::Fan *fan);
void send_fan_info(fan::Fan *fan);
void fan_command(const FanCommandRequest &msg) override; void fan_command(const FanCommandRequest &msg) override;
#endif #endif
#ifdef USE_LIGHT #ifdef USE_LIGHT
bool send_light_state(light::LightState *light); bool send_light_state(light::LightState *light);
void send_light_info(light::LightState *light);
void light_command(const LightCommandRequest &msg) override; void light_command(const LightCommandRequest &msg) override;
#endif #endif
#ifdef USE_SENSOR #ifdef USE_SENSOR
bool send_sensor_state(sensor::Sensor *sensor); bool send_sensor_state(sensor::Sensor *sensor);
void send_sensor_info(sensor::Sensor *sensor);
#endif #endif
#ifdef USE_SWITCH #ifdef USE_SWITCH
bool send_switch_state(switch_::Switch *a_switch); bool send_switch_state(switch_::Switch *a_switch);
void send_switch_info(switch_::Switch *a_switch);
void switch_command(const SwitchCommandRequest &msg) override; void switch_command(const SwitchCommandRequest &msg) override;
#endif #endif
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
bool send_text_sensor_state(text_sensor::TextSensor *text_sensor); bool send_text_sensor_state(text_sensor::TextSensor *text_sensor);
void send_text_sensor_info(text_sensor::TextSensor *text_sensor);
#endif #endif
#ifdef USE_ESP32_CAMERA #ifdef USE_CAMERA
void set_camera_state(std::shared_ptr<esp32_camera::CameraImage> image); void set_camera_state(std::shared_ptr<camera::CameraImage> image);
void send_camera_info(esp32_camera::ESP32Camera *camera);
void camera_image(const CameraImageRequest &msg) override; void camera_image(const CameraImageRequest &msg) override;
#endif #endif
#ifdef USE_CLIMATE #ifdef USE_CLIMATE
bool send_climate_state(climate::Climate *climate); bool send_climate_state(climate::Climate *climate);
void send_climate_info(climate::Climate *climate);
void climate_command(const ClimateCommandRequest &msg) override; void climate_command(const ClimateCommandRequest &msg) override;
#endif #endif
#ifdef USE_NUMBER #ifdef USE_NUMBER
bool send_number_state(number::Number *number); bool send_number_state(number::Number *number);
void send_number_info(number::Number *number);
void number_command(const NumberCommandRequest &msg) override; void number_command(const NumberCommandRequest &msg) override;
#endif #endif
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
bool send_date_state(datetime::DateEntity *date); bool send_date_state(datetime::DateEntity *date);
void send_date_info(datetime::DateEntity *date);
void date_command(const DateCommandRequest &msg) override; void date_command(const DateCommandRequest &msg) override;
#endif #endif
#ifdef USE_DATETIME_TIME #ifdef USE_DATETIME_TIME
bool send_time_state(datetime::TimeEntity *time); bool send_time_state(datetime::TimeEntity *time);
void send_time_info(datetime::TimeEntity *time);
void time_command(const TimeCommandRequest &msg) override; void time_command(const TimeCommandRequest &msg) override;
#endif #endif
#ifdef USE_DATETIME_DATETIME #ifdef USE_DATETIME_DATETIME
bool send_datetime_state(datetime::DateTimeEntity *datetime); bool send_datetime_state(datetime::DateTimeEntity *datetime);
void send_datetime_info(datetime::DateTimeEntity *datetime);
void datetime_command(const DateTimeCommandRequest &msg) override; void datetime_command(const DateTimeCommandRequest &msg) override;
#endif #endif
#ifdef USE_TEXT #ifdef USE_TEXT
bool send_text_state(text::Text *text); bool send_text_state(text::Text *text);
void send_text_info(text::Text *text);
void text_command(const TextCommandRequest &msg) override; void text_command(const TextCommandRequest &msg) override;
#endif #endif
#ifdef USE_SELECT #ifdef USE_SELECT
bool send_select_state(select::Select *select); bool send_select_state(select::Select *select);
void send_select_info(select::Select *select);
void select_command(const SelectCommandRequest &msg) override; void select_command(const SelectCommandRequest &msg) override;
#endif #endif
#ifdef USE_BUTTON #ifdef USE_BUTTON
void send_button_info(button::Button *button);
void button_command(const ButtonCommandRequest &msg) override; void button_command(const ButtonCommandRequest &msg) override;
#endif #endif
#ifdef USE_LOCK #ifdef USE_LOCK
bool send_lock_state(lock::Lock *a_lock); bool send_lock_state(lock::Lock *a_lock);
void send_lock_info(lock::Lock *a_lock);
void lock_command(const LockCommandRequest &msg) override; void lock_command(const LockCommandRequest &msg) override;
#endif #endif
#ifdef USE_VALVE #ifdef USE_VALVE
bool send_valve_state(valve::Valve *valve); bool send_valve_state(valve::Valve *valve);
void send_valve_info(valve::Valve *valve);
void valve_command(const ValveCommandRequest &msg) override; void valve_command(const ValveCommandRequest &msg) override;
#endif #endif
#ifdef USE_MEDIA_PLAYER #ifdef USE_MEDIA_PLAYER
bool send_media_player_state(media_player::MediaPlayer *media_player); bool send_media_player_state(media_player::MediaPlayer *media_player);
void send_media_player_info(media_player::MediaPlayer *media_player);
void media_player_command(const MediaPlayerCommandRequest &msg) override; void media_player_command(const MediaPlayerCommandRequest &msg) override;
#endif #endif
bool try_send_log_message(int level, const char *tag, const char *line); bool try_send_log_message(int level, const char *tag, const char *line, size_t message_len);
#ifdef USE_API_HOMEASSISTANT_SERVICES
void send_homeassistant_service_call(const HomeassistantServiceResponse &call) { void send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
if (!this->service_call_subscription_) if (!this->flags_.service_call_subscription)
return; return;
this->send_message(call); this->send_message(call, HomeassistantServiceResponse::MESSAGE_TYPE);
} }
#endif
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override; void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override;
bool send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &msg);
void bluetooth_device_request(const BluetoothDeviceRequest &msg) override; void bluetooth_device_request(const BluetoothDeviceRequest &msg) override;
void bluetooth_gatt_read(const BluetoothGATTReadRequest &msg) override; void bluetooth_gatt_read(const BluetoothGATTReadRequest &msg) override;
@ -141,15 +149,14 @@ class APIConnection : public APIServerConnection {
void bluetooth_gatt_write_descriptor(const BluetoothGATTWriteDescriptorRequest &msg) override; void bluetooth_gatt_write_descriptor(const BluetoothGATTWriteDescriptorRequest &msg) override;
void bluetooth_gatt_get_services(const BluetoothGATTGetServicesRequest &msg) override; void bluetooth_gatt_get_services(const BluetoothGATTGetServicesRequest &msg) override;
void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) override; void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) override;
BluetoothConnectionsFreeResponse subscribe_bluetooth_connections_free( bool send_subscribe_bluetooth_connections_free_response(const SubscribeBluetoothConnectionsFreeRequest &msg) override;
const SubscribeBluetoothConnectionsFreeRequest &msg) override;
void bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) override; void bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) override;
#endif #endif
#ifdef USE_HOMEASSISTANT_TIME #ifdef USE_HOMEASSISTANT_TIME
void send_time_request() { void send_time_request() {
GetTimeRequest req; GetTimeRequest req;
this->send_message(req); this->send_message(req, GetTimeRequest::MESSAGE_TYPE);
} }
#endif #endif
@ -160,72 +167,85 @@ class APIConnection : public APIServerConnection {
void on_voice_assistant_audio(const VoiceAssistantAudio &msg) override; void on_voice_assistant_audio(const VoiceAssistantAudio &msg) override;
void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg) override; void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg) override;
void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg) override; void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg) override;
VoiceAssistantConfigurationResponse voice_assistant_get_configuration( bool send_voice_assistant_get_configuration_response(const VoiceAssistantConfigurationRequest &msg) override;
const VoiceAssistantConfigurationRequest &msg) override;
void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override; void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override;
#endif #endif
#ifdef USE_ALARM_CONTROL_PANEL #ifdef USE_ALARM_CONTROL_PANEL
bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel); bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
void send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override; void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override;
#endif #endif
#ifdef USE_EVENT #ifdef USE_EVENT
void send_event(event::Event *event, const std::string &event_type); void send_event(event::Event *event, const std::string &event_type);
void send_event_info(event::Event *event);
#endif #endif
#ifdef USE_UPDATE #ifdef USE_UPDATE
bool send_update_state(update::UpdateEntity *update); bool send_update_state(update::UpdateEntity *update);
void send_update_info(update::UpdateEntity *update);
void update_command(const UpdateCommandRequest &msg) override; void update_command(const UpdateCommandRequest &msg) override;
#endif #endif
void on_disconnect_response(const DisconnectResponse &value) override; void on_disconnect_response(const DisconnectResponse &value) override;
void on_ping_response(const PingResponse &value) override { void on_ping_response(const PingResponse &value) override {
// we initiated ping // we initiated ping
this->ping_retries_ = 0; this->flags_.sent_ping = false;
this->sent_ping_ = false;
} }
#ifdef USE_API_HOMEASSISTANT_STATES
void on_home_assistant_state_response(const HomeAssistantStateResponse &msg) override; void on_home_assistant_state_response(const HomeAssistantStateResponse &msg) override;
#endif
#ifdef USE_HOMEASSISTANT_TIME #ifdef USE_HOMEASSISTANT_TIME
void on_get_time_response(const GetTimeResponse &value) override; void on_get_time_response(const GetTimeResponse &value) override;
#endif #endif
HelloResponse hello(const HelloRequest &msg) override; bool send_hello_response(const HelloRequest &msg) override;
ConnectResponse connect(const ConnectRequest &msg) override; bool send_connect_response(const ConnectRequest &msg) override;
DisconnectResponse disconnect(const DisconnectRequest &msg) override; bool send_disconnect_response(const DisconnectRequest &msg) override;
PingResponse ping(const PingRequest &msg) override { return {}; } bool send_ping_response(const PingRequest &msg) override;
DeviceInfoResponse device_info(const DeviceInfoRequest &msg) override; bool send_device_info_response(const DeviceInfoRequest &msg) override;
void list_entities(const ListEntitiesRequest &msg) override { this->list_entities_iterator_.begin(); } void list_entities(const ListEntitiesRequest &msg) override { this->list_entities_iterator_.begin(); }
void subscribe_states(const SubscribeStatesRequest &msg) override { void subscribe_states(const SubscribeStatesRequest &msg) override {
this->state_subscription_ = true; this->flags_.state_subscription = true;
this->initial_state_iterator_.begin(); this->initial_state_iterator_.begin();
} }
void subscribe_logs(const SubscribeLogsRequest &msg) override { void subscribe_logs(const SubscribeLogsRequest &msg) override {
this->log_subscription_ = msg.level; this->flags_.log_subscription = msg.level;
if (msg.dump_config) if (msg.dump_config)
App.schedule_dump_config(); App.schedule_dump_config();
} }
#ifdef USE_API_HOMEASSISTANT_SERVICES
void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) override { void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) override {
this->service_call_subscription_ = true; this->flags_.service_call_subscription = true;
} }
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override; void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override;
GetTimeResponse get_time(const GetTimeRequest &msg) override { #endif
// TODO bool send_get_time_response(const GetTimeRequest &msg) override;
return {}; #ifdef USE_API_SERVICES
}
void execute_service(const ExecuteServiceRequest &msg) override; void execute_service(const ExecuteServiceRequest &msg) override;
#endif
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) override; bool send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) override;
#endif #endif
bool is_authenticated() override { return this->connection_state_ == ConnectionState::AUTHENTICATED; } bool is_authenticated() override {
bool is_connection_setup() override { return static_cast<ConnectionState>(this->flags_.connection_state) == ConnectionState::AUTHENTICATED;
return this->connection_state_ == ConnectionState ::CONNECTED || this->is_authenticated();
} }
bool is_connection_setup() override {
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; }
// Get client API version for feature detection
bool client_supports_api_version(uint16_t major, uint16_t minor) const {
return this->client_api_version_major_ > major ||
(this->client_api_version_major_ == major && this->client_api_version_minor_ >= minor);
}
void on_fatal_error() override; void on_fatal_error() override;
#ifdef USE_API_PASSWORD
void on_unauthenticated_access() override; void on_unauthenticated_access() override;
#endif
void on_no_setup_connection() override; void on_no_setup_connection() override;
ProtoWriteBuffer create_buffer(uint32_t reserve_size) override { ProtoWriteBuffer create_buffer(uint32_t reserve_size) override {
// FIXME: ensure no recursive writes can happen // FIXME: ensure no recursive writes can happen
@ -273,39 +293,80 @@ class APIConnection : public APIServerConnection {
} }
bool try_to_clear_buffer(bool log_out_of_space); bool try_to_clear_buffer(bool log_out_of_space);
bool send_buffer(ProtoWriteBuffer buffer, uint16_t message_type) override; bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override;
std::string get_client_combined_info() const { return this->client_combined_info_; } std::string get_client_combined_info() const { return this->client_info_.get_combined_info(); }
// Buffer allocator methods for batch processing // Buffer allocator methods for batch processing
ProtoWriteBuffer allocate_single_message_buffer(uint16_t size); ProtoWriteBuffer allocate_single_message_buffer(uint16_t size);
ProtoWriteBuffer allocate_batch_message_buffer(uint16_t size); ProtoWriteBuffer allocate_batch_message_buffer(uint16_t size);
protected: protected:
// Helper function to fill common entity info fields // Helper function to handle authentication completion
static void fill_entity_info_base(esphome::EntityBase *entity, InfoResponseProtoMessage &response) { void complete_authentication_();
// 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()) #ifdef USE_API_HOMEASSISTANT_STATES
response.name = entity->get_name(); void process_state_subscriptions_();
#endif
// 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());
}
// Helper function to fill common entity state fields
static void fill_entity_state_base(esphome::EntityBase *entity, StateResponseProtoMessage &response) {
response.key = entity->get_object_id_hash();
}
// Non-template helper to encode any ProtoMessage // Non-template helper to encode any ProtoMessage
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); 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();
// IMPORTANT: get_object_id() may return a temporary std::string
std::string object_id = entity->get_object_id();
msg.set_object_id(StringRef(object_id));
if (entity->has_own_name()) {
msg.set_name(entity->get_name());
}
// Set common EntityBase properties
#ifdef USE_ENTITY_ICON
msg.set_icon(entity->get_icon_ref());
#endif
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;
#endif
// Helper method to process multiple entities from an iterator in a batch
template<typename Iterator> void process_iterator_batch_(Iterator &iterator) {
size_t initial_size = this->deferred_batch_.size();
while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < MAX_INITIAL_PER_BATCH) {
iterator.advance();
}
// If the batch is full, process it immediately
// Note: iterator.advance() already calls schedule_batch_() via schedule_message_()
if (this->deferred_batch_.size() >= MAX_INITIAL_PER_BATCH) {
this->process_batch_();
}
}
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
static uint16_t try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, static uint16_t try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single); bool is_single);
@ -416,7 +477,7 @@ class APIConnection : public APIServerConnection {
static uint16_t try_send_update_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, static uint16_t try_send_update_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single); bool is_single);
#endif #endif
#ifdef USE_ESP32_CAMERA #ifdef USE_CAMERA
static uint16_t try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, static uint16_t try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single); bool is_single);
#endif #endif
@ -429,127 +490,83 @@ class APIConnection : public APIServerConnection {
static uint16_t try_send_disconnect_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, static uint16_t try_send_disconnect_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single); bool is_single);
// Helper function to get estimated message size for buffer pre-allocation // Batch message method for ping requests
static uint16_t get_estimated_message_size(uint16_t message_type); static uint16_t try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single);
enum class ConnectionState { // === Optimal member ordering for 32-bit systems ===
WAITING_FOR_HELLO,
CONNECTED,
AUTHENTICATED,
} connection_state_{ConnectionState::WAITING_FOR_HELLO};
bool remove_{false};
// Group 1: Pointers (4 bytes each on 32-bit)
std::unique_ptr<APIFrameHelper> helper_; std::unique_ptr<APIFrameHelper> helper_;
std::string client_info_;
std::string client_peername_;
std::string client_combined_info_;
uint32_t client_api_version_major_{0};
uint32_t client_api_version_minor_{0};
#ifdef USE_ESP32_CAMERA
esp32_camera::CameraImageReader image_reader_;
#endif
bool state_subscription_{false};
int log_subscription_{ESPHOME_LOG_LEVEL_NONE};
uint32_t last_traffic_;
uint32_t next_ping_retry_{0};
uint8_t ping_retries_{0};
bool sent_ping_{false};
bool service_call_subscription_{false};
bool next_close_ = false;
APIServer *parent_; APIServer *parent_;
// Group 2: Larger objects (must be 4-byte aligned)
// These contain vectors/pointers internally, so putting them early ensures good alignment
InitialStateIterator initial_state_iterator_; InitialStateIterator initial_state_iterator_;
ListEntitiesIterator list_entities_iterator_; ListEntitiesIterator list_entities_iterator_;
#ifdef USE_CAMERA
std::unique_ptr<camera::CameraImageReader> image_reader_;
#endif
// Group 3: Client info struct (24 bytes on 32-bit: 2 strings × 12 bytes each)
ClientInfo client_info_;
// Group 4: 4-byte types
uint32_t last_traffic_;
#ifdef USE_API_HOMEASSISTANT_STATES
int state_subs_at_ = -1; int state_subs_at_ = -1;
#endif
// Function pointer type for message encoding // Function pointer type for message encoding
using MessageCreatorPtr = uint16_t (*)(EntityBase *, APIConnection *, uint32_t remaining_size, bool is_single); using MessageCreatorPtr = uint16_t (*)(EntityBase *, APIConnection *, uint32_t remaining_size, bool is_single);
// Optimized MessageCreator class using union dispatch
class MessageCreator { class MessageCreator {
public: public:
// Constructor for function pointer (message_type = 0) // Constructor for function pointer
MessageCreator(MessageCreatorPtr ptr) : message_type_(0) { data_.ptr = ptr; } MessageCreator(MessageCreatorPtr ptr) { data_.function_ptr = ptr; }
// Constructor for string state capture // Constructor for string state capture
MessageCreator(const std::string &value, uint16_t msg_type) : message_type_(msg_type) { explicit MessageCreator(const std::string &str_value) { data_.string_ptr = new std::string(str_value); }
data_.string_ptr = new std::string(value);
}
// Destructor // No destructor - cleanup must be called explicitly with message_type
~MessageCreator() {
// Clean up string data for string-based message types
if (uses_string_data_()) {
delete data_.string_ptr;
}
}
// Copy constructor // Delete copy operations - MessageCreator should only be moved
MessageCreator(const MessageCreator &other) : message_type_(other.message_type_) { MessageCreator(const MessageCreator &other) = delete;
if (message_type_ == 0) { MessageCreator &operator=(const MessageCreator &other) = delete;
data_.ptr = other.data_.ptr;
} else if (uses_string_data_()) {
data_.string_ptr = new std::string(*other.data_.string_ptr);
} else {
data_ = other.data_; // For POD types
}
}
// Move constructor // Move constructor
MessageCreator(MessageCreator &&other) noexcept : data_(other.data_), message_type_(other.message_type_) { MessageCreator(MessageCreator &&other) noexcept : data_(other.data_) { other.data_.function_ptr = nullptr; }
other.message_type_ = 0; // Reset other to function pointer type
other.data_.ptr = nullptr;
}
// Assignment operators (needed for batch deduplication)
MessageCreator &operator=(const MessageCreator &other) {
if (this != &other) {
// Clean up current string data if needed
if (uses_string_data_()) {
delete data_.string_ptr;
}
// Copy new data
message_type_ = other.message_type_;
if (other.message_type_ == 0) {
data_.ptr = other.data_.ptr;
} else if (other.uses_string_data_()) {
data_.string_ptr = new std::string(*other.data_.string_ptr);
} else {
data_ = other.data_;
}
}
return *this;
}
// Move assignment
MessageCreator &operator=(MessageCreator &&other) noexcept { MessageCreator &operator=(MessageCreator &&other) noexcept {
if (this != &other) { if (this != &other) {
// Clean up current string data if needed // IMPORTANT: Caller must ensure cleanup() was called if this contains a string!
if (uses_string_data_()) { // In our usage, this happens in add_item() deduplication and vector::erase()
delete data_.string_ptr;
}
// Move data
message_type_ = other.message_type_;
data_ = other.data_; data_ = other.data_;
// Reset other to safe state other.data_.function_ptr = nullptr;
other.message_type_ = 0;
other.data_.ptr = nullptr;
} }
return *this; return *this;
} }
// Call operator // Call operator - uses message_type to determine union type
uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) const; uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single,
uint8_t message_type) const;
// Manual cleanup method - must be called before destruction for string types
void cleanup(uint8_t message_type) {
#ifdef USE_EVENT
if (message_type == EventResponse::MESSAGE_TYPE && data_.string_ptr != nullptr) {
delete data_.string_ptr;
data_.string_ptr = nullptr;
}
#endif
}
private: private:
// Helper to check if this message type uses heap-allocated strings union Data {
bool uses_string_data_() const { return message_type_ == EventResponse::MESSAGE_TYPE; } MessageCreatorPtr function_ptr;
union CreatorData { std::string *string_ptr;
MessageCreatorPtr ptr; // 8 bytes } data_; // 4 bytes on 32-bit, 8 bytes on 64-bit - same as before
std::string *string_ptr; // 8 bytes
} data_; // 8 bytes
uint16_t message_type_; // 2 bytes (0 = function ptr, >0 = state capture)
}; };
// Generic batching mechanism for both state updates and entity info // Generic batching mechanism for both state updates and entity info
@ -557,33 +574,96 @@ class APIConnection : public APIServerConnection {
struct BatchItem { struct BatchItem {
EntityBase *entity; // Entity pointer EntityBase *entity; // Entity pointer
MessageCreator creator; // Function that creates the message when needed 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 // Constructor for creating BatchItem
BatchItem(EntityBase *entity, MessageCreator creator, uint16_t 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) {} : entity(entity), creator(std::move(creator)), message_type(message_type), estimated_size(estimated_size) {}
}; };
std::vector<BatchItem> items; std::vector<BatchItem> items;
uint32_t batch_start_time{0}; uint32_t batch_start_time{0};
bool batch_scheduled{false};
private:
// Helper to cleanup items from the beginning
void cleanup_items_(size_t count) {
for (size_t i = 0; i < count; i++) {
items[i].creator.cleanup(items[i].message_type);
}
}
public:
DeferredBatch() { DeferredBatch() {
// Pre-allocate capacity for typical batch sizes to avoid reallocation // Pre-allocate capacity for typical batch sizes to avoid reallocation
items.reserve(8); items.reserve(8);
} }
~DeferredBatch() {
// Ensure cleanup of any remaining items
clear();
}
// Add item to the batch // 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, uint8_t message_type, uint8_t estimated_size);
// Clear all items with proper cleanup
void clear() { void clear() {
cleanup_items_(items.size());
items.clear(); items.clear();
batch_scheduled = false;
batch_start_time = 0; batch_start_time = 0;
} }
// Remove processed items from the front with proper cleanup
void remove_front(size_t count) {
cleanup_items_(count);
items.erase(items.begin(), items.begin() + count);
}
bool empty() const { return items.empty(); } bool empty() const { return items.empty(); }
size_t size() const { return items.size(); }
const BatchItem &operator[](size_t index) const { return items[index]; }
}; };
// DeferredBatch here (16 bytes, 4-byte aligned)
DeferredBatch deferred_batch_; DeferredBatch deferred_batch_;
// ConnectionState enum for type safety
enum class ConnectionState : uint8_t {
WAITING_FOR_HELLO = 0,
CONNECTED = 1,
AUTHENTICATED = 2,
};
// Group 5: Pack all small members together to minimize padding
// This group starts at a 4-byte boundary after DeferredBatch
struct APIFlags {
// Connection state only needs 2 bits (3 states)
uint8_t connection_state : 2;
// Log subscription needs 3 bits (log levels 0-7)
uint8_t log_subscription : 3;
// Boolean flags (1 bit each)
uint8_t remove : 1;
uint8_t state_subscription : 1;
uint8_t sent_ping : 1;
uint8_t service_call_subscription : 1;
uint8_t next_close : 1;
uint8_t batch_scheduled : 1;
uint8_t batch_first_message : 1; // For batch buffer allocation
uint8_t should_try_send_immediately : 1; // True after initial states are sent
#ifdef HAS_PROTO_MESSAGE_DUMP
uint8_t log_only_mode : 1;
#endif
} flags_{}; // 2 bytes total
// 2-byte types immediately after flags_ (no padding between them)
uint16_t client_api_version_major_{0};
uint16_t client_api_version_minor_{0};
// Total: 2 (flags) + 2 + 2 = 6 bytes, then 2 bytes padding to next 4-byte boundary
uint32_t get_batch_delay_ms_() const; uint32_t get_batch_delay_ms_() const;
// Message will use 8 more bytes than the minimum size, and typical // Message will use 8 more bytes than the minimum size, and typical
// MTU is 1500. Sometimes users will see as low as 1460 MTU. // MTU is 1500. Sometimes users will see as low as 1460 MTU.
@ -596,26 +676,79 @@ class APIConnection : public APIServerConnection {
// to send in one go. This is the maximum size of a single packet // to send in one go. This is the maximum size of a single packet
// that can be sent over the network. // that can be sent over the network.
// This is to avoid fragmentation of the packet. // 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_(); bool schedule_batch_();
void process_batch_(); void process_batch_();
void clear_batch_() {
this->deferred_batch_.clear();
this->flags_.batch_scheduled = false;
}
// State for batch buffer allocation #ifdef HAS_PROTO_MESSAGE_DUMP
bool batch_first_message_{false}; // Helper to log a proto message from a MessageCreator object
void log_proto_message_(EntityBase *entity, const MessageCreator &creator, uint8_t message_type) {
this->flags_.log_only_mode = true;
creator(entity, this, MAX_BATCH_PACKET_SIZE, true, message_type);
this->flags_.log_only_mode = false;
}
void log_batch_item_(const DeferredBatch::BatchItem &item) {
// Use the helper to log the message
this->log_proto_message_(item.entity, item.creator, item.message_type);
}
#endif
// Helper method to send a message either immediately or via batching
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)
// 3. Buffer has space available
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_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
this->log_proto_message_(entity, MessageCreator(creator), message_type);
#endif
return true;
}
// If immediate send failed, fall through to batching
}
// Fall back to scheduled batching
return this->schedule_message_(entity, creator, message_type, estimated_size);
}
// Helper function to schedule a deferred message with known message type // Helper function to schedule a deferred message with known message type
bool schedule_message_(EntityBase *entity, MessageCreator creator, uint16_t 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); this->deferred_batch_.add_item(entity, std::move(creator), message_type, estimated_size);
return this->schedule_batch_(); return this->schedule_batch_();
} }
// Overload for function pointers (for info messages and current state reads) // Overload for function pointers (for info messages and current state reads)
bool schedule_message_(EntityBase *entity, MessageCreatorPtr function_ptr, uint16_t message_type) { bool schedule_message_(EntityBase *entity, MessageCreatorPtr function_ptr, uint8_t message_type,
return schedule_message_(entity, MessageCreator(function_ptr), 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, 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_();
}
// Helper function to log API errors with errno
void log_warning_(const char *message, APIError err);
// Specific helper for duplicated error message
void log_socket_operation_failed_(APIError err);
}; };
} // namespace api } // namespace esphome::api
} // namespace esphome
#endif #endif

File diff suppressed because it is too large Load Diff

View File

@ -2,21 +2,23 @@
#include <cstdint> #include <cstdint>
#include <deque> #include <deque>
#include <limits> #include <limits>
#include <span>
#include <utility> #include <utility>
#include <vector> #include <vector>
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#ifdef USE_API #ifdef USE_API
#ifdef USE_API_NOISE
#include "noise/protocol.h"
#endif
#include "api_noise_context.h"
#include "esphome/components/socket/socket.h" #include "esphome/components/socket/socket.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/log.h"
namespace esphome { namespace esphome::api {
namespace api {
// uncomment to log raw packets
//#define HELPER_LOG_PACKETS
// Forward declaration
struct ClientInfo;
class ProtoWriteBuffer; class ProtoWriteBuffer;
@ -29,19 +31,16 @@ struct ReadPacketBuffer {
// Packed packet info structure to minimize memory usage // Packed packet info structure to minimize memory usage
struct PacketInfo { struct PacketInfo {
uint16_t message_type; // 2 bytes uint16_t offset; // Offset in buffer where message starts
uint16_t offset; // 2 bytes (sufficient for packet size ~1460 bytes) uint16_t payload_size; // Size of the message payload
uint16_t payload_size; // 2 bytes (up to 65535 bytes) uint8_t message_type; // Message type (0-255)
uint16_t padding; // 2 byte (for alignment)
PacketInfo(uint16_t type, uint16_t off, uint16_t size) PacketInfo(uint8_t type, uint16_t off, uint16_t size) : offset(off), payload_size(size), message_type(type) {}
: message_type(type), offset(off), payload_size(size), padding(0) {}
}; };
enum class APIError : int { enum class APIError : uint16_t {
OK = 0, OK = 0,
WOULD_BLOCK = 1001, WOULD_BLOCK = 1001,
BAD_HANDSHAKE_PACKET_LEN = 1002,
BAD_INDICATOR = 1003, BAD_INDICATOR = 1003,
BAD_DATA_PACKET = 1004, BAD_DATA_PACKET = 1004,
TCP_NODELAY_FAILED = 1005, TCP_NODELAY_FAILED = 1005,
@ -52,16 +51,19 @@ enum class APIError : int {
BAD_ARG = 1010, BAD_ARG = 1010,
SOCKET_READ_FAILED = 1011, SOCKET_READ_FAILED = 1011,
SOCKET_WRITE_FAILED = 1012, SOCKET_WRITE_FAILED = 1012,
OUT_OF_MEMORY = 1018,
CONNECTION_CLOSED = 1022,
#ifdef USE_API_NOISE
BAD_HANDSHAKE_PACKET_LEN = 1002,
HANDSHAKESTATE_READ_FAILED = 1013, HANDSHAKESTATE_READ_FAILED = 1013,
HANDSHAKESTATE_WRITE_FAILED = 1014, HANDSHAKESTATE_WRITE_FAILED = 1014,
HANDSHAKESTATE_BAD_STATE = 1015, HANDSHAKESTATE_BAD_STATE = 1015,
CIPHERSTATE_DECRYPT_FAILED = 1016, CIPHERSTATE_DECRYPT_FAILED = 1016,
CIPHERSTATE_ENCRYPT_FAILED = 1017, CIPHERSTATE_ENCRYPT_FAILED = 1017,
OUT_OF_MEMORY = 1018,
HANDSHAKESTATE_SETUP_FAILED = 1019, HANDSHAKESTATE_SETUP_FAILED = 1019,
HANDSHAKESTATE_SPLIT_FAILED = 1020, HANDSHAKESTATE_SPLIT_FAILED = 1020,
BAD_HANDSHAKE_ERROR_BYTE = 1021, BAD_HANDSHAKE_ERROR_BYTE = 1021,
CONNECTION_CLOSED = 1022, #endif
}; };
const char *api_error_to_str(APIError err); const char *api_error_to_str(APIError err);
@ -69,12 +71,13 @@ const char *api_error_to_str(APIError err);
class APIFrameHelper { class APIFrameHelper {
public: public:
APIFrameHelper() = default; APIFrameHelper() = default;
explicit APIFrameHelper(std::unique_ptr<socket::Socket> socket) : socket_owned_(std::move(socket)) { explicit APIFrameHelper(std::unique_ptr<socket::Socket> socket, const ClientInfo *client_info)
: socket_owned_(std::move(socket)), client_info_(client_info) {
socket_ = socket_owned_.get(); socket_ = socket_owned_.get();
} }
virtual ~APIFrameHelper() = default; virtual ~APIFrameHelper() = default;
virtual APIError init() = 0; virtual APIError init() = 0;
virtual APIError loop() = 0; virtual APIError loop();
virtual APIError read_packet(ReadPacketBuffer *buffer) = 0; virtual APIError read_packet(ReadPacketBuffer *buffer) = 0;
bool can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); } bool can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); }
std::string getpeername() { return socket_->getpeername(); } std::string getpeername() { return socket_->getpeername(); }
@ -95,13 +98,11 @@ class APIFrameHelper {
} }
return APIError::OK; return APIError::OK;
} }
// Give this helper a name for logging virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0;
void set_log_info(std::string info) { info_ = std::move(info); }
virtual APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) = 0;
// Write multiple protobuf packets in a single operation // Write multiple protobuf packets in a single operation
// packets contains (message_type, offset, length) for each message in the buffer // packets contains (message_type, offset, length) for each message in the buffer
// The buffer contains all messages with appropriate padding before each // The buffer contains all messages with appropriate padding before each
virtual APIError write_protobuf_packets(ProtoWriteBuffer buffer, const std::vector<PacketInfo> &packets) = 0; virtual APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) = 0;
// Get the frame header padding required by this protocol // Get the frame header padding required by this protocol
virtual uint8_t frame_header_padding() = 0; virtual uint8_t frame_header_padding() = 0;
// Get the frame footer size required by this protocol // Get the frame footer size required by this protocol
@ -110,23 +111,35 @@ class APIFrameHelper {
bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); } bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); }
protected: protected:
// Struct for holding parsed frame data
struct ParsedFrame {
std::vector<uint8_t> msg;
};
// Buffer containing data to be sent // Buffer containing data to be sent
struct SendBuffer { struct SendBuffer {
std::vector<uint8_t> data; std::unique_ptr<uint8_t[]> data;
uint16_t offset{0}; // Current offset within the buffer (uint16_t to reduce memory usage) uint16_t size{0}; // Total size of the buffer
uint16_t offset{0}; // Current offset within the buffer
// Using uint16_t reduces memory usage since ESPHome API messages are limited to UINT16_MAX (65535) bytes // Using uint16_t reduces memory usage since ESPHome API messages are limited to UINT16_MAX (65535) bytes
uint16_t remaining() const { return static_cast<uint16_t>(data.size()) - offset; } uint16_t remaining() const { return size - offset; }
const uint8_t *current_data() const { return data.data() + offset; } const uint8_t *current_data() const { return data.get() + offset; }
}; };
// Queue of data buffers to be sent // Common implementation for writing raw data to socket
std::deque<SendBuffer> tx_buf_; APIError write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len);
// Try to send data from the tx buffer
APIError try_send_tx_buf_();
// Helper method to buffer data from IOVs
void buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, uint16_t offset);
// Common socket write error handling
APIError handle_socket_write_error_();
template<typename StateEnum>
APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf,
const std::string &info, StateEnum &state, StateEnum failed_state);
// Pointers first (4 bytes each)
socket::Socket *socket_{nullptr};
std::unique_ptr<socket::Socket> socket_owned_;
// Common state enum for all frame helpers // Common state enum for all frame helpers
// Note: Not all states are used by all implementations // Note: Not all states are used by all implementations
@ -136,7 +149,7 @@ class APIFrameHelper {
// - CLOSED: Used by both Noise and Plaintext // - CLOSED: Used by both Noise and Plaintext
// - FAILED: Used by both Noise and Plaintext // - FAILED: Used by both Noise and Plaintext
// - EXPLICIT_REJECT: Only used by Noise protocol // - EXPLICIT_REJECT: Only used by Noise protocol
enum class State { enum class State : uint8_t {
INITIALIZE = 1, INITIALIZE = 1,
CLIENT_HELLO = 2, // Noise only CLIENT_HELLO = 2, // Noise only
SERVER_HELLO = 3, // Noise only SERVER_HELLO = 3, // Noise only
@ -147,127 +160,29 @@ class APIFrameHelper {
EXPLICIT_REJECT = 8, // Noise only EXPLICIT_REJECT = 8, // Noise only
}; };
// Current state of the frame helper // Containers (size varies, but typically 12+ bytes on 32-bit)
std::deque<SendBuffer> tx_buf_;
std::vector<struct iovec> reusable_iovs_;
std::vector<uint8_t> rx_buf_;
// Pointer to client info (4 bytes on 32-bit)
// Note: The pointed-to ClientInfo object must outlive this APIFrameHelper instance.
const ClientInfo *client_info_{nullptr};
// Group smaller types together
uint16_t rx_buf_len_ = 0;
State state_{State::INITIALIZE}; State state_{State::INITIALIZE};
// Helper name for logging
std::string info_;
// Socket for communication
socket::Socket *socket_{nullptr};
std::unique_ptr<socket::Socket> socket_owned_;
// Common implementation for writing raw data to socket
APIError write_raw_(const struct iovec *iov, int iovcnt);
// Try to send data from the tx buffer
APIError try_send_tx_buf_();
// Helper method to buffer data from IOVs
void buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len);
template<typename StateEnum>
APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf,
const std::string &info, StateEnum &state, StateEnum failed_state);
uint8_t frame_header_padding_{0}; uint8_t frame_header_padding_{0};
uint8_t frame_footer_size_{0}; uint8_t frame_footer_size_{0};
// 5 bytes total, 3 bytes padding
// Reusable IOV array for write_protobuf_packets to avoid repeated allocations
std::vector<struct iovec> reusable_iovs_;
// Receive buffer for reading frame data
std::vector<uint8_t> rx_buf_;
uint16_t rx_buf_len_ = 0;
// Common initialization for both plaintext and noise protocols // Common initialization for both plaintext and noise protocols
APIError init_common_(); APIError init_common_();
// Helper method to handle socket read results
APIError handle_socket_read_result_(ssize_t received);
}; };
#ifdef USE_API_NOISE } // namespace esphome::api
class APINoiseFrameHelper : public APIFrameHelper {
public:
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx)
: APIFrameHelper(std::move(socket)), ctx_(std::move(ctx)) {
// Noise header structure:
// Pos 0: indicator (0x01)
// Pos 1-2: encrypted payload size (16-bit big-endian)
// Pos 3-6: encrypted type (16-bit) + data_len (16-bit)
// Pos 7+: actual payload data
frame_header_padding_ = 7;
}
~APINoiseFrameHelper() override;
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_packets(ProtoWriteBuffer buffer, const std::vector<PacketInfo> &packets) override;
// Get the frame header padding required by this protocol
uint8_t frame_header_padding() override { return frame_header_padding_; }
// Get the frame footer size required by this protocol
uint8_t frame_footer_size() override { return frame_footer_size_; }
protected: #endif // USE_API
APIError state_action_();
APIError try_read_frame_(ParsedFrame *frame);
APIError write_frame_(const uint8_t *data, uint16_t len);
APIError init_handshake_();
APIError check_handshake_finished_();
void send_explicit_handshake_reject_(const std::string &reason);
// Fixed-size header buffer for noise protocol:
// 1 byte for indicator + 2 bytes for message size (16-bit value, not varint)
// Note: Maximum message size is UINT16_MAX (65535), with a limit of 128 bytes during handshake phase
uint8_t rx_header_buf_[3];
uint8_t rx_header_buf_len_ = 0;
std::vector<uint8_t> prologue_;
std::shared_ptr<APINoiseContext> ctx_;
NoiseHandshakeState *handshake_{nullptr};
NoiseCipherState *send_cipher_{nullptr};
NoiseCipherState *recv_cipher_{nullptr};
NoiseProtocolId nid_;
};
#endif // USE_API_NOISE
#ifdef USE_API_PLAINTEXT
class APIPlaintextFrameHelper : public APIFrameHelper {
public:
APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : APIFrameHelper(std::move(socket)) {
// Plaintext header structure (worst case):
// Pos 0: indicator (0x00)
// Pos 1-3: payload size varint (up to 3 bytes)
// Pos 4-5: message type varint (up to 2 bytes)
// Pos 6+: actual payload data
frame_header_padding_ = 6;
}
~APIPlaintextFrameHelper() override = default;
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_packets(ProtoWriteBuffer buffer, const std::vector<PacketInfo> &packets) override;
uint8_t frame_header_padding() override { return frame_header_padding_; }
// Get the frame footer size required by this protocol
uint8_t frame_footer_size() override { return frame_footer_size_; }
protected:
APIError try_read_frame_(ParsedFrame *frame);
// Fixed-size header buffer for plaintext protocol:
// We now store the indicator byte + the two varints.
// To match noise protocol's maximum message size (UINT16_MAX = 65535), we need:
// 1 byte for indicator + 3 bytes for message size varint (supports up to 2097151) + 2 bytes for message type varint
//
// While varints could theoretically be up to 10 bytes each for 64-bit values,
// attempting to process messages with headers that large would likely crash the
// ESP32 due to memory constraints.
uint8_t rx_header_buf_[6]; // 1 byte indicator + 5 bytes for varints (3 for size + 2 for type)
uint8_t rx_header_buf_pos_ = 0;
bool rx_header_parsed_ = false;
uint16_t rx_header_parsed_type_ = 0;
uint16_t rx_header_parsed_len_ = 0;
};
#endif
} // namespace api
} // namespace esphome
#endif

View File

@ -0,0 +1,583 @@
#include "api_frame_helper_noise.h"
#ifdef USE_API
#ifdef USE_API_NOISE
#include "api_connection.h" // For ClientInfo struct
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "proto.h"
#include <cstring>
#include <cinttypes>
namespace esphome::api {
static const char *const TAG = "api.noise";
static const char *const PROLOGUE_INIT = "NoiseAPIInit";
static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit")
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__)
#ifdef HELPER_LOG_PACKETS
#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str())
#define LOG_PACKET_SENDING(data, len) ESP_LOGVV(TAG, "Sending raw: %s", format_hex_pretty(data, len).c_str())
#else
#define LOG_PACKET_RECEIVED(buffer) ((void) 0)
#define LOG_PACKET_SENDING(data, len) ((void) 0)
#endif
/// Convert a noise error code to a readable error
std::string noise_err_to_str(int err) {
if (err == NOISE_ERROR_NO_MEMORY)
return "NO_MEMORY";
if (err == NOISE_ERROR_UNKNOWN_ID)
return "UNKNOWN_ID";
if (err == NOISE_ERROR_UNKNOWN_NAME)
return "UNKNOWN_NAME";
if (err == NOISE_ERROR_MAC_FAILURE)
return "MAC_FAILURE";
if (err == NOISE_ERROR_NOT_APPLICABLE)
return "NOT_APPLICABLE";
if (err == NOISE_ERROR_SYSTEM)
return "SYSTEM";
if (err == NOISE_ERROR_REMOTE_KEY_REQUIRED)
return "REMOTE_KEY_REQUIRED";
if (err == NOISE_ERROR_LOCAL_KEY_REQUIRED)
return "LOCAL_KEY_REQUIRED";
if (err == NOISE_ERROR_PSK_REQUIRED)
return "PSK_REQUIRED";
if (err == NOISE_ERROR_INVALID_LENGTH)
return "INVALID_LENGTH";
if (err == NOISE_ERROR_INVALID_PARAM)
return "INVALID_PARAM";
if (err == NOISE_ERROR_INVALID_STATE)
return "INVALID_STATE";
if (err == NOISE_ERROR_INVALID_NONCE)
return "INVALID_NONCE";
if (err == NOISE_ERROR_INVALID_PRIVATE_KEY)
return "INVALID_PRIVATE_KEY";
if (err == NOISE_ERROR_INVALID_PUBLIC_KEY)
return "INVALID_PUBLIC_KEY";
if (err == NOISE_ERROR_INVALID_FORMAT)
return "INVALID_FORMAT";
if (err == NOISE_ERROR_INVALID_SIGNATURE)
return "INVALID_SIGNATURE";
return to_string(err);
}
/// Initialize the frame helper, returns OK if successful.
APIError APINoiseFrameHelper::init() {
APIError err = init_common_();
if (err != APIError::OK) {
return err;
}
// init prologue
size_t old_size = prologue_.size();
prologue_.resize(old_size + PROLOGUE_INIT_LEN);
std::memcpy(prologue_.data() + old_size, PROLOGUE_INIT, PROLOGUE_INIT_LEN);
state_ = State::CLIENT_HELLO;
return APIError::OK;
}
// Helper for handling handshake frame errors
APIError APINoiseFrameHelper::handle_handshake_frame_error_(APIError aerr) {
if (aerr == APIError::BAD_INDICATOR) {
send_explicit_handshake_reject_("Bad indicator byte");
} else if (aerr == APIError::BAD_HANDSHAKE_PACKET_LEN) {
send_explicit_handshake_reject_("Bad handshake packet len");
}
return aerr;
}
// Helper for handling noise library errors
APIError APINoiseFrameHelper::handle_noise_error_(int err, const char *func_name, APIError api_err) {
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("%s failed: %s", func_name, noise_err_to_str(err).c_str());
return api_err;
}
return APIError::OK;
}
/// Run through handshake messages (if in that phase)
APIError APINoiseFrameHelper::loop() {
// During handshake phase, process as many actions as possible until we can't progress
// socket_->ready() stays true until next main loop, but state_action() will return
// WOULD_BLOCK when no more data is available to read
while (state_ != State::DATA && this->socket_->ready()) {
APIError err = state_action_();
if (err == APIError::WOULD_BLOCK) {
break;
}
if (err != APIError::OK) {
return err;
}
}
// Use base class implementation for buffer sending
return APIFrameHelper::loop();
}
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
*
* @param frame: The struct to hold the frame information in.
* msg_start: points to the start of the payload - this pointer is only valid until the next
* try_receive_raw_ call
*
* @return 0 if a full packet is in rx_buf_
* @return -1 if error, check errno.
*
* errno EWOULDBLOCK: Packet could not be read without blocking. Try again later.
* errno ENOMEM: Not enough memory for reading packet.
* errno API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame.
* errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase.
*/
APIError APINoiseFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
if (frame == nullptr) {
HELPER_LOG("Bad argument for try_read_frame_");
return APIError::BAD_ARG;
}
// read header
if (rx_header_buf_len_ < 3) {
// no header information yet
uint8_t to_read = 3 - rx_header_buf_len_;
ssize_t received = this->socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read);
APIError err = handle_socket_read_result_(received);
if (err != APIError::OK) {
return err;
}
rx_header_buf_len_ += static_cast<uint8_t>(received);
if (static_cast<uint8_t>(received) != to_read) {
// not a full read
return APIError::WOULD_BLOCK;
}
if (rx_header_buf_[0] != 0x01) {
state_ = State::FAILED;
HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]);
return APIError::BAD_INDICATOR;
}
// header reading done
}
// read body
uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2];
if (state_ != State::DATA && msg_size > 128) {
// for handshake message only permit up to 128 bytes
state_ = State::FAILED;
HELPER_LOG("Bad packet len for handshake: %d", msg_size);
return APIError::BAD_HANDSHAKE_PACKET_LEN;
}
// reserve space for body
if (rx_buf_.size() != msg_size) {
rx_buf_.resize(msg_size);
}
if (rx_buf_len_ < msg_size) {
// more data to read
uint16_t to_read = msg_size - rx_buf_len_;
ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read);
APIError err = handle_socket_read_result_(received);
if (err != APIError::OK) {
return err;
}
rx_buf_len_ += static_cast<uint16_t>(received);
if (static_cast<uint16_t>(received) != to_read) {
// not all read
return APIError::WOULD_BLOCK;
}
}
LOG_PACKET_RECEIVED(rx_buf_);
*frame = std::move(rx_buf_);
// consume msg
rx_buf_ = {};
rx_buf_len_ = 0;
rx_header_buf_len_ = 0;
return APIError::OK;
}
/** To be called from read/write methods.
*
* This method runs through the internal handshake methods, if in that state.
*
* If the handshake is still active when this method returns and a read/write can't take place at
* the moment, returns WOULD_BLOCK.
* If an error occurred, returns that error. Only returns OK if the transport is ready for data
* traffic.
*/
APIError APINoiseFrameHelper::state_action_() {
int err;
APIError aerr;
if (state_ == State::INITIALIZE) {
HELPER_LOG("Bad state for method: %d", (int) state_);
return APIError::BAD_STATE;
}
if (state_ == State::CLIENT_HELLO) {
// waiting for client hello
std::vector<uint8_t> frame;
aerr = try_read_frame_(&frame);
if (aerr != APIError::OK) {
return handle_handshake_frame_error_(aerr);
}
// ignore contents, may be used in future for flags
// Resize for: existing prologue + 2 size bytes + frame data
size_t old_size = prologue_.size();
prologue_.resize(old_size + 2 + frame.size());
prologue_[old_size] = (uint8_t) (frame.size() >> 8);
prologue_[old_size + 1] = (uint8_t) frame.size();
std::memcpy(prologue_.data() + old_size + 2, frame.data(), frame.size());
state_ = State::SERVER_HELLO;
}
if (state_ == State::SERVER_HELLO) {
// send server hello
const std::string &name = App.get_name();
const std::string &mac = get_mac_address();
std::vector<uint8_t> msg;
// Calculate positions and sizes
size_t name_len = name.size() + 1; // including null terminator
size_t mac_len = mac.size() + 1; // including null terminator
size_t name_offset = 1;
size_t mac_offset = name_offset + name_len;
size_t total_size = 1 + name_len + mac_len;
msg.resize(total_size);
// chosen proto
msg[0] = 0x01;
// node name, terminated by null byte
std::memcpy(msg.data() + name_offset, name.c_str(), name_len);
// node mac, terminated by null byte
std::memcpy(msg.data() + mac_offset, mac.c_str(), mac_len);
aerr = write_frame_(msg.data(), msg.size());
if (aerr != APIError::OK)
return aerr;
// start handshake
aerr = init_handshake_();
if (aerr != APIError::OK)
return aerr;
state_ = State::HANDSHAKE;
}
if (state_ == State::HANDSHAKE) {
int action = noise_handshakestate_get_action(handshake_);
if (action == NOISE_ACTION_READ_MESSAGE) {
// waiting for handshake msg
std::vector<uint8_t> frame;
aerr = try_read_frame_(&frame);
if (aerr != APIError::OK) {
return handle_handshake_frame_error_(aerr);
}
if (frame.empty()) {
send_explicit_handshake_reject_("Empty handshake message");
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
} else if (frame[0] != 0x00) {
HELPER_LOG("Bad handshake error byte: %u", frame[0]);
send_explicit_handshake_reject_("Bad handshake error byte");
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
}
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_input(mbuf, frame.data() + 1, frame.size() - 1);
err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr);
if (err != 0) {
// Special handling for MAC failure
send_explicit_handshake_reject_(err == NOISE_ERROR_MAC_FAILURE ? "Handshake MAC failure" : "Handshake error");
return handle_noise_error_(err, "noise_handshakestate_read_message", APIError::HANDSHAKESTATE_READ_FAILED);
}
aerr = check_handshake_finished_();
if (aerr != APIError::OK)
return aerr;
} else if (action == NOISE_ACTION_WRITE_MESSAGE) {
uint8_t buffer[65];
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1);
err = noise_handshakestate_write_message(handshake_, &mbuf, nullptr);
APIError aerr_write =
handle_noise_error_(err, "noise_handshakestate_write_message", APIError::HANDSHAKESTATE_WRITE_FAILED);
if (aerr_write != APIError::OK)
return aerr_write;
buffer[0] = 0x00; // success
aerr = write_frame_(buffer, mbuf.size + 1);
if (aerr != APIError::OK)
return aerr;
aerr = check_handshake_finished_();
if (aerr != APIError::OK)
return aerr;
} else {
// bad state for action
state_ = State::FAILED;
HELPER_LOG("Bad action for handshake: %d", action);
return APIError::HANDSHAKESTATE_BAD_STATE;
}
}
if (state_ == State::CLOSED || state_ == State::FAILED) {
return APIError::BAD_STATE;
}
return APIError::OK;
}
void APINoiseFrameHelper::send_explicit_handshake_reject_(const std::string &reason) {
std::vector<uint8_t> data;
data.resize(reason.length() + 1);
data[0] = 0x01; // failure
// Copy error message in bulk
if (!reason.empty()) {
std::memcpy(data.data() + 1, reason.c_str(), reason.length());
}
// temporarily remove failed state
auto orig_state = state_;
state_ = State::EXPLICIT_REJECT;
write_frame_(data.data(), data.size());
state_ = orig_state;
}
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
int err;
APIError aerr;
aerr = state_action_();
if (aerr != APIError::OK) {
return aerr;
}
if (state_ != State::DATA) {
return APIError::WOULD_BLOCK;
}
std::vector<uint8_t> frame;
aerr = try_read_frame_(&frame);
if (aerr != APIError::OK)
return aerr;
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_inout(mbuf, frame.data(), frame.size(), frame.size());
err = noise_cipherstate_decrypt(recv_cipher_, &mbuf);
APIError decrypt_err = handle_noise_error_(err, "noise_cipherstate_decrypt", APIError::CIPHERSTATE_DECRYPT_FAILED);
if (decrypt_err != APIError::OK)
return decrypt_err;
uint16_t msg_size = mbuf.size;
uint8_t *msg_data = frame.data();
if (msg_size < 4) {
state_ = State::FAILED;
HELPER_LOG("Bad data packet: size %d too short", msg_size);
return APIError::BAD_DATA_PACKET;
}
uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1];
uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3];
if (data_len > msg_size - 4) {
state_ = State::FAILED;
HELPER_LOG("Bad data packet: data_len %u greater than msg_size %u", data_len, msg_size);
return APIError::BAD_DATA_PACKET;
}
buffer->container = std::move(frame);
buffer->data_offset = 4;
buffer->data_len = data_len;
buffer->type = type;
return APIError::OK;
}
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,
static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)};
return write_protobuf_packets(buffer, std::span<const PacketInfo>(&packet, 1));
}
APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) {
APIError aerr = state_action_();
if (aerr != APIError::OK) {
return aerr;
}
if (state_ != State::DATA) {
return APIError::WOULD_BLOCK;
}
if (packets.empty()) {
return APIError::OK;
}
std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
uint8_t *buffer_data = raw_buffer->data(); // Cache buffer pointer
this->reusable_iovs_.clear();
this->reusable_iovs_.reserve(packets.size());
uint16_t total_write_len = 0;
// We need to encrypt each packet in place
for (const auto &packet : packets) {
// The buffer already has padding at offset
uint8_t *buf_start = buffer_data + packet.offset;
// Write noise header
buf_start[0] = 0x01; // indicator
// buf_start[1], buf_start[2] to be set after encryption
// Write message header (to be encrypted)
const uint8_t msg_offset = 3;
buf_start[msg_offset] = static_cast<uint8_t>(packet.message_type >> 8); // type high byte
buf_start[msg_offset + 1] = static_cast<uint8_t>(packet.message_type); // type low byte
buf_start[msg_offset + 2] = static_cast<uint8_t>(packet.payload_size >> 8); // data_len high byte
buf_start[msg_offset + 3] = static_cast<uint8_t>(packet.payload_size); // data_len low byte
// payload data is already in the buffer starting at offset + 7
// Make sure we have space for MAC
// The buffer should already have been sized appropriately
// Encrypt the message in place
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_inout(mbuf, buf_start + msg_offset, 4 + packet.payload_size,
4 + packet.payload_size + frame_footer_size_);
int err = noise_cipherstate_encrypt(send_cipher_, &mbuf);
APIError aerr = handle_noise_error_(err, "noise_cipherstate_encrypt", APIError::CIPHERSTATE_ENCRYPT_FAILED);
if (aerr != APIError::OK)
return aerr;
// Fill in the encrypted size
buf_start[1] = static_cast<uint8_t>(mbuf.size >> 8);
buf_start[2] = static_cast<uint8_t>(mbuf.size);
// Add iovec for this encrypted packet
size_t packet_len = static_cast<size_t>(3 + mbuf.size); // indicator + size + encrypted data
this->reusable_iovs_.push_back({buf_start, packet_len});
total_write_len += packet_len;
}
// Send all encrypted packets in one writev call
return this->write_raw_(this->reusable_iovs_.data(), this->reusable_iovs_.size(), total_write_len);
}
APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
uint8_t header[3];
header[0] = 0x01; // indicator
header[1] = (uint8_t) (len >> 8);
header[2] = (uint8_t) len;
struct iovec iov[2];
iov[0].iov_base = header;
iov[0].iov_len = 3;
if (len == 0) {
return this->write_raw_(iov, 1, 3); // Just header
}
iov[1].iov_base = const_cast<uint8_t *>(data);
iov[1].iov_len = len;
return this->write_raw_(iov, 2, 3 + len); // Header + data
}
/** Initiate the data structures for the handshake.
*
* @return 0 on success, -1 on error (check errno)
*/
APIError APINoiseFrameHelper::init_handshake_() {
int err;
memset(&nid_, 0, sizeof(nid_));
// const char *proto = "Noise_NNpsk0_25519_ChaChaPoly_SHA256";
// err = noise_protocol_name_to_id(&nid_, proto, strlen(proto));
nid_.pattern_id = NOISE_PATTERN_NN;
nid_.cipher_id = NOISE_CIPHER_CHACHAPOLY;
nid_.dh_id = NOISE_DH_CURVE25519;
nid_.prefix_id = NOISE_PREFIX_STANDARD;
nid_.hybrid_id = NOISE_DH_NONE;
nid_.hash_id = NOISE_HASH_SHA256;
nid_.modifier_ids[0] = NOISE_MODIFIER_PSK0;
err = noise_handshakestate_new_by_id(&handshake_, &nid_, NOISE_ROLE_RESPONDER);
APIError aerr = handle_noise_error_(err, "noise_handshakestate_new_by_id", APIError::HANDSHAKESTATE_SETUP_FAILED);
if (aerr != APIError::OK)
return aerr;
const auto &psk = ctx_->get_psk();
err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size());
aerr = handle_noise_error_(err, "noise_handshakestate_set_pre_shared_key", APIError::HANDSHAKESTATE_SETUP_FAILED);
if (aerr != APIError::OK)
return aerr;
err = noise_handshakestate_set_prologue(handshake_, prologue_.data(), prologue_.size());
aerr = handle_noise_error_(err, "noise_handshakestate_set_prologue", APIError::HANDSHAKESTATE_SETUP_FAILED);
if (aerr != APIError::OK)
return aerr;
// set_prologue copies it into handshakestate, so we can get rid of it now
prologue_ = {};
err = noise_handshakestate_start(handshake_);
aerr = handle_noise_error_(err, "noise_handshakestate_start", APIError::HANDSHAKESTATE_SETUP_FAILED);
if (aerr != APIError::OK)
return aerr;
return APIError::OK;
}
APIError APINoiseFrameHelper::check_handshake_finished_() {
assert(state_ == State::HANDSHAKE);
int action = noise_handshakestate_get_action(handshake_);
if (action == NOISE_ACTION_READ_MESSAGE || action == NOISE_ACTION_WRITE_MESSAGE)
return APIError::OK;
if (action != NOISE_ACTION_SPLIT) {
state_ = State::FAILED;
HELPER_LOG("Bad action for handshake: %d", action);
return APIError::HANDSHAKESTATE_BAD_STATE;
}
int err = noise_handshakestate_split(handshake_, &send_cipher_, &recv_cipher_);
APIError aerr = handle_noise_error_(err, "noise_handshakestate_split", APIError::HANDSHAKESTATE_SPLIT_FAILED);
if (aerr != APIError::OK)
return aerr;
frame_footer_size_ = noise_cipherstate_get_mac_length(send_cipher_);
HELPER_LOG("Handshake complete!");
noise_handshakestate_free(handshake_);
handshake_ = nullptr;
state_ = State::DATA;
return APIError::OK;
}
APINoiseFrameHelper::~APINoiseFrameHelper() {
if (handshake_ != nullptr) {
noise_handshakestate_free(handshake_);
handshake_ = nullptr;
}
if (send_cipher_ != nullptr) {
noise_cipherstate_free(send_cipher_);
send_cipher_ = nullptr;
}
if (recv_cipher_ != nullptr) {
noise_cipherstate_free(recv_cipher_);
recv_cipher_ = nullptr;
}
}
extern "C" {
// declare how noise generates random bytes (here with a good HWRNG based on the RF system)
void noise_rand_bytes(void *output, size_t len) {
if (!esphome::random_bytes(reinterpret_cast<uint8_t *>(output), len)) {
ESP_LOGE(TAG, "Acquiring random bytes failed; rebooting");
arch_restart();
}
}
}
} // namespace esphome::api
#endif // USE_API_NOISE
#endif // USE_API

View File

@ -0,0 +1,68 @@
#pragma once
#include "api_frame_helper.h"
#ifdef USE_API
#ifdef USE_API_NOISE
#include "noise/protocol.h"
#include "api_noise_context.h"
namespace esphome::api {
class APINoiseFrameHelper : public APIFrameHelper {
public:
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx,
const ClientInfo *client_info)
: APIFrameHelper(std::move(socket), client_info), ctx_(std::move(ctx)) {
// Noise header structure:
// Pos 0: indicator (0x01)
// Pos 1-2: encrypted payload size (16-bit big-endian)
// Pos 3-6: encrypted type (16-bit) + data_len (16-bit)
// Pos 7+: actual payload data
frame_header_padding_ = 7;
}
~APINoiseFrameHelper() override;
APIError init() override;
APIError loop() override;
APIError read_packet(ReadPacketBuffer *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_; }
// Get the frame footer size required by this protocol
uint8_t frame_footer_size() override { return frame_footer_size_; }
protected:
APIError state_action_();
APIError try_read_frame_(std::vector<uint8_t> *frame);
APIError write_frame_(const uint8_t *data, uint16_t len);
APIError init_handshake_();
APIError check_handshake_finished_();
void send_explicit_handshake_reject_(const std::string &reason);
APIError handle_handshake_frame_error_(APIError aerr);
APIError handle_noise_error_(int err, const char *func_name, APIError api_err);
// Pointers first (4 bytes each)
NoiseHandshakeState *handshake_{nullptr};
NoiseCipherState *send_cipher_{nullptr};
NoiseCipherState *recv_cipher_{nullptr};
// Shared pointer (8 bytes on 32-bit = 4 bytes control block pointer + 4 bytes object pointer)
std::shared_ptr<APINoiseContext> ctx_;
// Vector (12 bytes on 32-bit)
std::vector<uint8_t> prologue_;
// NoiseProtocolId (size depends on implementation)
NoiseProtocolId nid_;
// Group small types together
// Fixed-size header buffer for noise protocol:
// 1 byte for indicator + 2 bytes for message size (16-bit value, not varint)
// Note: Maximum message size is UINT16_MAX (65535), with a limit of 128 bytes during handshake phase
uint8_t rx_header_buf_[3];
uint8_t rx_header_buf_len_ = 0;
// 4 bytes total, no padding
};
} // namespace esphome::api
#endif // USE_API_NOISE
#endif // USE_API

View File

@ -0,0 +1,290 @@
#include "api_frame_helper_plaintext.h"
#ifdef USE_API
#ifdef USE_API_PLAINTEXT
#include "api_connection.h" // For ClientInfo struct
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "proto.h"
#include <cstring>
#include <cinttypes>
namespace esphome::api {
static const char *const TAG = "api.plaintext";
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__)
#ifdef HELPER_LOG_PACKETS
#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str())
#define LOG_PACKET_SENDING(data, len) ESP_LOGVV(TAG, "Sending raw: %s", format_hex_pretty(data, len).c_str())
#else
#define LOG_PACKET_RECEIVED(buffer) ((void) 0)
#define LOG_PACKET_SENDING(data, len) ((void) 0)
#endif
/// Initialize the frame helper, returns OK if successful.
APIError APIPlaintextFrameHelper::init() {
APIError err = init_common_();
if (err != APIError::OK) {
return err;
}
state_ = State::DATA;
return APIError::OK;
}
APIError APIPlaintextFrameHelper::loop() {
if (state_ != State::DATA) {
return APIError::BAD_STATE;
}
// Use base class implementation for buffer sending
return APIFrameHelper::loop();
}
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
*
* @param frame: The struct to hold the frame information in.
* msg: store the parsed frame in that struct
*
* @return See APIError
*
* error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame.
*/
APIError APIPlaintextFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
if (frame == nullptr) {
HELPER_LOG("Bad argument for try_read_frame_");
return APIError::BAD_ARG;
}
// read header
while (!rx_header_parsed_) {
// Now that we know when the socket is ready, we can read up to 3 bytes
// into the rx_header_buf_ before we have to switch back to reading
// one byte at a time to ensure we don't read past the message and
// into the next one.
// Read directly into rx_header_buf_ at the current position
// Try to get to at least 3 bytes total (indicator + 2 varint bytes), then read one byte at a time
ssize_t received =
this->socket_->read(&rx_header_buf_[rx_header_buf_pos_], rx_header_buf_pos_ < 3 ? 3 - rx_header_buf_pos_ : 1);
APIError err = handle_socket_read_result_(received);
if (err != APIError::OK) {
return err;
}
// If this was the first read, validate the indicator byte
if (rx_header_buf_pos_ == 0 && received > 0) {
if (rx_header_buf_[0] != 0x00) {
state_ = State::FAILED;
HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]);
return APIError::BAD_INDICATOR;
}
}
rx_header_buf_pos_ += received;
// Check for buffer overflow
if (rx_header_buf_pos_ >= sizeof(rx_header_buf_)) {
state_ = State::FAILED;
HELPER_LOG("Header buffer overflow");
return APIError::BAD_DATA_PACKET;
}
// Need at least 3 bytes total (indicator + 2 varint bytes) before trying to parse
if (rx_header_buf_pos_ < 3) {
continue;
}
// At this point, we have at least 3 bytes total:
// - Validated indicator byte (0x00) stored at position 0
// - At least 2 bytes in the buffer for the varints
// Buffer layout:
// [0]: indicator byte (0x00)
// [1-3]: Message size varint (variable length)
// - 2 bytes would only allow up to 16383, which is less than noise's UINT16_MAX (65535)
// - 3 bytes allows up to 2097151, ensuring we support at least as much as noise
// [2-5]: Message type varint (variable length)
// We now attempt to parse both varints. If either is incomplete,
// we'll continue reading more bytes.
// Skip indicator byte at position 0
uint8_t varint_pos = 1;
uint32_t consumed = 0;
auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos, &consumed);
if (!msg_size_varint.has_value()) {
// not enough data there yet
continue;
}
if (msg_size_varint->as_uint32() > std::numeric_limits<uint16_t>::max()) {
state_ = State::FAILED;
HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum %u", msg_size_varint->as_uint32(),
std::numeric_limits<uint16_t>::max());
return APIError::BAD_DATA_PACKET;
}
rx_header_parsed_len_ = msg_size_varint->as_uint16();
// Move to next varint position
varint_pos += consumed;
auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos, &consumed);
if (!msg_type_varint.has_value()) {
// not enough data there yet
continue;
}
if (msg_type_varint->as_uint32() > std::numeric_limits<uint16_t>::max()) {
state_ = State::FAILED;
HELPER_LOG("Bad packet: message type %" PRIu32 " exceeds maximum %u", msg_type_varint->as_uint32(),
std::numeric_limits<uint16_t>::max());
return APIError::BAD_DATA_PACKET;
}
rx_header_parsed_type_ = msg_type_varint->as_uint16();
rx_header_parsed_ = true;
}
// header reading done
// reserve space for body
if (rx_buf_.size() != rx_header_parsed_len_) {
rx_buf_.resize(rx_header_parsed_len_);
}
if (rx_buf_len_ < rx_header_parsed_len_) {
// more data to read
uint16_t to_read = rx_header_parsed_len_ - rx_buf_len_;
ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read);
APIError err = handle_socket_read_result_(received);
if (err != APIError::OK) {
return err;
}
rx_buf_len_ += static_cast<uint16_t>(received);
if (static_cast<uint16_t>(received) != to_read) {
// not all read
return APIError::WOULD_BLOCK;
}
}
LOG_PACKET_RECEIVED(rx_buf_);
*frame = std::move(rx_buf_);
// consume msg
rx_buf_ = {};
rx_buf_len_ = 0;
rx_header_buf_pos_ = 0;
rx_header_parsed_ = false;
return APIError::OK;
}
APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
APIError aerr;
if (state_ != State::DATA) {
return APIError::WOULD_BLOCK;
}
std::vector<uint8_t> frame;
aerr = try_read_frame_(&frame);
if (aerr != APIError::OK) {
if (aerr == APIError::BAD_INDICATOR) {
// Make sure to tell the remote that we don't
// understand the indicator byte so it knows
// we do not support it.
struct iovec iov[1];
// The \x00 first byte is the marker for plaintext.
//
// The remote will know how to handle the indicator byte,
// but it likely won't understand the rest of the message.
//
// We must send at least 3 bytes to be read, so we add
// a message after the indicator byte to ensures its long
// enough and can aid in debugging.
const char msg[] = "\x00"
"Bad indicator byte";
iov[0].iov_base = (void *) msg;
iov[0].iov_len = 19;
this->write_raw_(iov, 1, 19);
}
return aerr;
}
buffer->container = std::move(frame);
buffer->data_offset = 0;
buffer->data_len = rx_header_parsed_len_;
buffer->type = rx_header_parsed_type_;
return APIError::OK;
}
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));
}
APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) {
if (state_ != State::DATA) {
return APIError::BAD_STATE;
}
if (packets.empty()) {
return APIError::OK;
}
std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
uint8_t *buffer_data = raw_buffer->data(); // Cache buffer pointer
this->reusable_iovs_.clear();
this->reusable_iovs_.reserve(packets.size());
uint16_t total_write_len = 0;
for (const auto &packet : packets) {
// Calculate varint sizes for header layout
uint8_t size_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(packet.payload_size));
uint8_t type_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(packet.message_type));
uint8_t total_header_len = 1 + size_varint_len + type_varint_len;
// Calculate where to start writing the header
// The header starts at the latest possible position to minimize unused padding
//
// Example 1 (small values): total_header_len = 3, header_offset = 6 - 3 = 3
// [0-2] - Unused padding
// [3] - 0x00 indicator byte
// [4] - Payload size varint (1 byte, for sizes 0-127)
// [5] - Message type varint (1 byte, for types 0-127)
// [6...] - Actual payload data
//
// Example 2 (medium values): total_header_len = 4, header_offset = 6 - 4 = 2
// [0-1] - Unused padding
// [2] - 0x00 indicator byte
// [3-4] - Payload size varint (2 bytes, for sizes 128-16383)
// [5] - Message type varint (1 byte, for types 0-127)
// [6...] - Actual payload data
//
// Example 3 (large values): total_header_len = 6, header_offset = 6 - 6 = 0
// [0] - 0x00 indicator byte
// [1-3] - Payload size varint (3 bytes, for sizes 16384-2097151)
// [4-5] - Message type varint (2 bytes, for types 128-32767)
// [6...] - Actual payload data
//
// The message starts at offset + frame_header_padding_
// So we write the header starting at offset + frame_header_padding_ - total_header_len
uint8_t *buf_start = buffer_data + packet.offset;
uint32_t header_offset = frame_header_padding_ - total_header_len;
// Write the plaintext header
buf_start[header_offset] = 0x00; // indicator
// Encode varints directly into buffer
ProtoVarInt(packet.payload_size).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len);
ProtoVarInt(packet.message_type)
.encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len);
// Add iovec for this packet (header + payload)
size_t packet_len = static_cast<size_t>(total_header_len + packet.payload_size);
this->reusable_iovs_.push_back({buf_start + header_offset, packet_len});
total_write_len += packet_len;
}
// Send all packets in one writev call
return write_raw_(this->reusable_iovs_.data(), this->reusable_iovs_.size(), total_write_len);
}
} // namespace esphome::api
#endif // USE_API_PLAINTEXT
#endif // USE_API

View File

@ -0,0 +1,53 @@
#pragma once
#include "api_frame_helper.h"
#ifdef USE_API
#ifdef USE_API_PLAINTEXT
namespace esphome::api {
class APIPlaintextFrameHelper : public APIFrameHelper {
public:
APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket, const ClientInfo *client_info)
: APIFrameHelper(std::move(socket), client_info) {
// Plaintext header structure (worst case):
// Pos 0: indicator (0x00)
// Pos 1-3: payload size varint (up to 3 bytes)
// Pos 4-5: message type varint (up to 2 bytes)
// Pos 6+: actual payload data
frame_header_padding_ = 6;
}
~APIPlaintextFrameHelper() override = default;
APIError init() override;
APIError loop() override;
APIError read_packet(ReadPacketBuffer *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
uint8_t frame_footer_size() override { return frame_footer_size_; }
protected:
APIError try_read_frame_(std::vector<uint8_t> *frame);
// Group 2-byte aligned types
uint16_t rx_header_parsed_type_ = 0;
uint16_t rx_header_parsed_len_ = 0;
// Group 1-byte types together
// Fixed-size header buffer for plaintext protocol:
// We now store the indicator byte + the two varints.
// To match noise protocol's maximum message size (UINT16_MAX = 65535), we need:
// 1 byte for indicator + 3 bytes for message size varint (supports up to 2097151) + 2 bytes for message type varint
//
// While varints could theoretically be up to 10 bytes each for 64-bit values,
// attempting to process messages with headers that large would likely crash the
// ESP32 due to memory constraints.
uint8_t rx_header_buf_[6]; // 1 byte indicator + 5 bytes for varints (3 for size + 2 for type)
uint8_t rx_header_buf_pos_ = 0;
bool rx_header_parsed_ = false;
// 8 bytes total, no padding needed
};
} // namespace esphome::api
#endif // USE_API_PLAINTEXT
#endif // USE_API

View File

@ -3,8 +3,7 @@
#include <cstdint> #include <cstdint>
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
namespace esphome { namespace esphome::api {
namespace api {
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
using psk_t = std::array<uint8_t, 32>; using psk_t = std::array<uint8_t, 32>;
@ -28,5 +27,4 @@ class APINoiseContext {
}; };
#endif // USE_API_NOISE #endif // USE_API_NOISE
} // namespace api } // namespace esphome::api
} // namespace esphome

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -2,11 +2,11 @@
// See script/api_protobuf/api_protobuf.py // See script/api_protobuf/api_protobuf.py
#pragma once #pragma once
#include "api_pb2.h"
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
namespace esphome { #include "api_pb2.h"
namespace api {
namespace esphome::api {
class APIServerConnectionBase : public ProtoService { class APIServerConnectionBase : public ProtoService {
public: public:
@ -17,11 +17,11 @@ class APIServerConnectionBase : public ProtoService {
public: public:
#endif #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 #ifdef HAS_PROTO_MESSAGE_DUMP
this->log_send_message_(T::message_name(), msg.dump()); this->log_send_message_(msg.message_name(), msg.dump());
#endif #endif
return this->send_message_(msg, T::MESSAGE_TYPE); return this->send_message_(msg, message_type);
} }
virtual void on_hello_request(const HelloRequest &value){}; virtual void on_hello_request(const HelloRequest &value){};
@ -60,17 +60,25 @@ class APIServerConnectionBase : public ProtoService {
virtual void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &value){}; virtual void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &value){};
#endif #endif
#ifdef USE_API_HOMEASSISTANT_SERVICES
virtual void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &value){}; virtual void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &value){};
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
virtual void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &value){}; virtual void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &value){};
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
virtual void on_home_assistant_state_response(const HomeAssistantStateResponse &value){}; virtual void on_home_assistant_state_response(const HomeAssistantStateResponse &value){};
#endif
virtual void on_get_time_request(const GetTimeRequest &value){}; virtual void on_get_time_request(const GetTimeRequest &value){};
virtual void on_get_time_response(const GetTimeResponse &value){}; virtual void on_get_time_response(const GetTimeResponse &value){};
#ifdef USE_API_SERVICES
virtual void on_execute_service_request(const ExecuteServiceRequest &value){}; virtual void on_execute_service_request(const ExecuteServiceRequest &value){};
#endif
#ifdef USE_ESP32_CAMERA #ifdef USE_CAMERA
virtual void on_camera_image_request(const CameraImageRequest &value){}; virtual void on_camera_image_request(const CameraImageRequest &value){};
#endif #endif
@ -199,30 +207,36 @@ class APIServerConnectionBase : public ProtoService {
virtual void on_update_command_request(const UpdateCommandRequest &value){}; virtual void on_update_command_request(const UpdateCommandRequest &value){};
#endif #endif
protected: protected:
bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;
}; };
class APIServerConnection : public APIServerConnectionBase { class APIServerConnection : public APIServerConnectionBase {
public: public:
virtual HelloResponse hello(const HelloRequest &msg) = 0; virtual bool send_hello_response(const HelloRequest &msg) = 0;
virtual ConnectResponse connect(const ConnectRequest &msg) = 0; virtual bool send_connect_response(const ConnectRequest &msg) = 0;
virtual DisconnectResponse disconnect(const DisconnectRequest &msg) = 0; virtual bool send_disconnect_response(const DisconnectRequest &msg) = 0;
virtual PingResponse ping(const PingRequest &msg) = 0; virtual bool send_ping_response(const PingRequest &msg) = 0;
virtual DeviceInfoResponse device_info(const DeviceInfoRequest &msg) = 0; virtual bool send_device_info_response(const DeviceInfoRequest &msg) = 0;
virtual void list_entities(const ListEntitiesRequest &msg) = 0; virtual void list_entities(const ListEntitiesRequest &msg) = 0;
virtual void subscribe_states(const SubscribeStatesRequest &msg) = 0; virtual void subscribe_states(const SubscribeStatesRequest &msg) = 0;
virtual void subscribe_logs(const SubscribeLogsRequest &msg) = 0; virtual void subscribe_logs(const SubscribeLogsRequest &msg) = 0;
#ifdef USE_API_HOMEASSISTANT_SERVICES
virtual void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) = 0; virtual void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) = 0;
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0; virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0;
virtual GetTimeResponse get_time(const GetTimeRequest &msg) = 0; #endif
virtual bool send_get_time_response(const GetTimeRequest &msg) = 0;
#ifdef USE_API_SERVICES
virtual void execute_service(const ExecuteServiceRequest &msg) = 0; virtual void execute_service(const ExecuteServiceRequest &msg) = 0;
#endif
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
virtual NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) = 0; virtual bool send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) = 0;
#endif #endif
#ifdef USE_BUTTON #ifdef USE_BUTTON
virtual void button_command(const ButtonCommandRequest &msg) = 0; virtual void button_command(const ButtonCommandRequest &msg) = 0;
#endif #endif
#ifdef USE_ESP32_CAMERA #ifdef USE_CAMERA
virtual void camera_image(const CameraImageRequest &msg) = 0; virtual void camera_image(const CameraImageRequest &msg) = 0;
#endif #endif
#ifdef USE_CLIMATE #ifdef USE_CLIMATE
@ -298,7 +312,7 @@ class APIServerConnection : public APIServerConnectionBase {
virtual void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) = 0; virtual void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) = 0;
#endif #endif
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
virtual BluetoothConnectionsFreeResponse subscribe_bluetooth_connections_free( virtual bool send_subscribe_bluetooth_connections_free_response(
const SubscribeBluetoothConnectionsFreeRequest &msg) = 0; const SubscribeBluetoothConnectionsFreeRequest &msg) = 0;
#endif #endif
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
@ -311,8 +325,7 @@ class APIServerConnection : public APIServerConnectionBase {
virtual void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) = 0; virtual void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) = 0;
#endif #endif
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
virtual VoiceAssistantConfigurationResponse voice_assistant_get_configuration( virtual bool send_voice_assistant_get_configuration_response(const VoiceAssistantConfigurationRequest &msg) = 0;
const VoiceAssistantConfigurationRequest &msg) = 0;
#endif #endif
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
virtual void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) = 0; virtual void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) = 0;
@ -329,17 +342,23 @@ class APIServerConnection : public APIServerConnectionBase {
void on_list_entities_request(const ListEntitiesRequest &msg) override; void on_list_entities_request(const ListEntitiesRequest &msg) override;
void on_subscribe_states_request(const SubscribeStatesRequest &msg) override; void on_subscribe_states_request(const SubscribeStatesRequest &msg) override;
void on_subscribe_logs_request(const SubscribeLogsRequest &msg) override; void on_subscribe_logs_request(const SubscribeLogsRequest &msg) override;
#ifdef USE_API_HOMEASSISTANT_SERVICES
void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &msg) override; void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &msg) override;
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override; void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override;
#endif
void on_get_time_request(const GetTimeRequest &msg) override; void on_get_time_request(const GetTimeRequest &msg) override;
#ifdef USE_API_SERVICES
void on_execute_service_request(const ExecuteServiceRequest &msg) override; void on_execute_service_request(const ExecuteServiceRequest &msg) override;
#endif
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) override; void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) override;
#endif #endif
#ifdef USE_BUTTON #ifdef USE_BUTTON
void on_button_command_request(const ButtonCommandRequest &msg) override; void on_button_command_request(const ButtonCommandRequest &msg) override;
#endif #endif
#ifdef USE_ESP32_CAMERA #ifdef USE_CAMERA
void on_camera_image_request(const CameraImageRequest &msg) override; void on_camera_image_request(const CameraImageRequest &msg) override;
#endif #endif
#ifdef USE_CLIMATE #ifdef USE_CLIMATE
@ -438,5 +457,4 @@ class APIServerConnection : public APIServerConnectionBase {
#endif #endif
}; };
} // namespace api } // namespace esphome::api
} // namespace esphome

View File

@ -1,361 +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 templated version directly takes a message object, calculates its size internally,
* and updates the total_size reference. This eliminates the need for a temporary variable
* at the call site.
*
* @tparam MessageType The type of the nested message (inferred from parameter)
* @param message The nested message object
*/
template<typename MessageType>
static inline void add_message_object(uint32_t &total_size, uint32_t field_id_size, const MessageType &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

@ -16,8 +16,7 @@
#include <algorithm> #include <algorithm>
namespace esphome { namespace esphome::api {
namespace api {
static const char *const TAG = "api"; static const char *const TAG = "api";
@ -31,7 +30,6 @@ APIServer::APIServer() {
} }
void APIServer::setup() { void APIServer::setup() {
ESP_LOGCONFIG(TAG, "Running setup");
this->setup_controller(); this->setup_controller();
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
@ -47,6 +45,11 @@ void APIServer::setup() {
} }
#endif #endif
// Schedule reboot if no clients connect within timeout
if (this->reboot_timeout_ != 0) {
this->schedule_reboot_timeout_();
}
this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections
if (this->socket_ == nullptr) { if (this->socket_ == nullptr) {
ESP_LOGW(TAG, "Could not create socket"); ESP_LOGW(TAG, "Could not create socket");
@ -91,34 +94,42 @@ void APIServer::setup() {
#ifdef USE_LOGGER #ifdef USE_LOGGER
if (logger::global_logger != nullptr) { if (logger::global_logger != nullptr) {
logger::global_logger->add_on_log_callback([this](int level, const char *tag, const char *message) { logger::global_logger->add_on_log_callback(
if (this->shutting_down_) { [this](int level, const char *tag, const char *message, size_t message_len) {
// Don't try to send logs during shutdown if (this->shutting_down_) {
// as it could result in a recursion and // Don't try to send logs during shutdown
// we would be filling a buffer we are trying to clear // as it could result in a recursion and
return; // we would be filling a buffer we are trying to clear
} return;
for (auto &c : this->clients_) { }
if (!c->remove_)
c->try_send_log_message(level, tag, message);
}
});
}
#endif
this->last_connected_ = millis();
#ifdef USE_ESP32_CAMERA
if (esp32_camera::global_esp32_camera != nullptr && !esp32_camera::global_esp32_camera->is_internal()) {
esp32_camera::global_esp32_camera->add_image_callback(
[this](const std::shared_ptr<esp32_camera::CameraImage> &image) {
for (auto &c : this->clients_) { for (auto &c : this->clients_) {
if (!c->remove_) if (!c->flags_.remove && c->get_log_subscription_level() >= level)
c->set_camera_state(image); c->try_send_log_message(level, tag, message, message_len);
} }
}); });
} }
#endif #endif
#ifdef USE_CAMERA
if (camera::Camera::instance() != nullptr && !camera::Camera::instance()->is_internal()) {
camera::Camera::instance()->add_image_callback([this](const std::shared_ptr<camera::CameraImage> &image) {
for (auto &c : this->clients_) {
if (!c->flags_.remove)
c->set_camera_state(image);
}
});
}
#endif
}
void APIServer::schedule_reboot_timeout_() {
this->status_set_warning();
this->set_timeout("api_reboot", this->reboot_timeout_, []() {
if (!global_api_server->is_connected()) {
ESP_LOGE(TAG, "No clients; rebooting");
App.reboot();
}
});
} }
void APIServer::loop() { void APIServer::loop() {
@ -130,71 +141,82 @@ void APIServer::loop() {
auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len); auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len);
if (!sock) if (!sock)
break; break;
ESP_LOGD(TAG, "Accepted %s", sock->getpeername().c_str()); ESP_LOGD(TAG, "Accept %s", sock->getpeername().c_str());
auto *conn = new APIConnection(std::move(sock), this); auto *conn = new APIConnection(std::move(sock), this);
this->clients_.emplace_back(conn); this->clients_.emplace_back(conn);
conn->start(); conn->start();
// Clear warning status and cancel reboot when first client connects
if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) {
this->status_clear_warning();
this->cancel_timeout("api_reboot");
}
} }
} }
if (this->clients_.empty()) {
return;
}
// Process clients and remove disconnected ones in a single pass // Process clients and remove disconnected ones in a single pass
if (!this->clients_.empty()) { // Check network connectivity once for all clients
size_t client_index = 0; if (!network::is_connected()) {
while (client_index < this->clients_.size()) { // Network is down - disconnect all clients
auto &client = this->clients_[client_index]; for (auto &client : this->clients_) {
client->on_fatal_error();
if (client->remove_) { ESP_LOGW(TAG, "%s: Network down; disconnect", client->get_client_combined_info().c_str());
// Handle disconnection
this->client_disconnected_trigger_->trigger(client->client_info_, client->client_peername_);
ESP_LOGV(TAG, "Removing connection to %s", client->client_info_.c_str());
// Swap with the last element and pop (avoids expensive vector shifts)
if (client_index < this->clients_.size() - 1) {
std::swap(this->clients_[client_index], this->clients_.back());
}
this->clients_.pop_back();
// Don't increment client_index since we need to process the swapped element
} else {
// Process active client
client->loop();
client_index++; // Move to next client
}
} }
// Continue to process and clean up the clients below
} }
if (this->reboot_timeout_ != 0) { size_t client_index = 0;
const uint32_t now = millis(); while (client_index < this->clients_.size()) {
if (!this->is_connected()) { auto &client = this->clients_[client_index];
if (now - this->last_connected_ > this->reboot_timeout_) {
ESP_LOGE(TAG, "No client connected; rebooting"); if (!client->flags_.remove) {
App.reboot(); // Common case: process active client
} client->loop();
this->status_set_warning(); client_index++;
} else { continue;
this->last_connected_ = now;
this->status_clear_warning();
} }
// Rare case: handle disconnection
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
this->client_disconnected_trigger_->trigger(client->client_info_.name, client->client_info_.peername);
#endif
ESP_LOGV(TAG, "Remove connection %s", client->client_info_.name.c_str());
// Swap with the last element and pop (avoids expensive vector shifts)
if (client_index < this->clients_.size() - 1) {
std::swap(this->clients_[client_index], this->clients_.back());
}
this->clients_.pop_back();
// Schedule reboot when last client disconnects
if (this->clients_.empty() && this->reboot_timeout_ != 0) {
this->schedule_reboot_timeout_();
}
// Don't increment client_index since we need to process the swapped element
} }
} }
void APIServer::dump_config() { void APIServer::dump_config() {
ESP_LOGCONFIG(TAG, ESP_LOGCONFIG(TAG,
"API Server:\n" "Server:\n"
" Address: %s:%u", " Address: %s:%u",
network::get_use_address().c_str(), this->port_); network::get_use_address().c_str(), this->port_);
#ifdef USE_API_NOISE #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()) { if (!this->noise_ctx_->has_psk()) {
ESP_LOGCONFIG(TAG, " Supports noise encryption: YES"); ESP_LOGCONFIG(TAG, " Supports encryption: YES");
} }
#else #else
ESP_LOGCONFIG(TAG, " Using noise encryption: NO"); ESP_LOGCONFIG(TAG, " Noise encryption: NO");
#endif #endif
} }
bool APIServer::uses_password() const { return !this->password_.empty(); } #ifdef USE_API_PASSWORD
bool APIServer::check_password(const std::string &password) const { bool APIServer::check_password(const std::string &password) const {
// depend only on input password length // depend only on input password length
const char *a = this->password_.c_str(); const char *a = this->password_.c_str();
@ -223,199 +245,139 @@ bool APIServer::check_password(const std::string &password) const {
return result == 0; return result == 0;
} }
#endif
void APIServer::handle_disconnect(APIConnection *conn) {} void APIServer::handle_disconnect(APIConnection *conn) {}
// Macro for entities without extra parameters
#define API_DISPATCH_UPDATE(entity_type, entity_name) \
void APIServer::on_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \
if (obj->is_internal()) \
return; \
for (auto &c : this->clients_) \
c->send_##entity_name##_state(obj); \
}
// Macro for entities with extra parameters (but parameters not used in send)
#define API_DISPATCH_UPDATE_IGNORE_PARAMS(entity_type, entity_name, ...) \
void APIServer::on_##entity_name##_update(entity_type *obj, __VA_ARGS__) { /* NOLINT(bugprone-macro-parentheses) */ \
if (obj->is_internal()) \
return; \
for (auto &c : this->clients_) \
c->send_##entity_name##_state(obj); \
}
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
void APIServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) { API_DISPATCH_UPDATE(binary_sensor::BinarySensor, binary_sensor)
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_binary_sensor_state(obj);
}
#endif #endif
#ifdef USE_COVER #ifdef USE_COVER
void APIServer::on_cover_update(cover::Cover *obj) { API_DISPATCH_UPDATE(cover::Cover, cover)
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_cover_state(obj);
}
#endif #endif
#ifdef USE_FAN #ifdef USE_FAN
void APIServer::on_fan_update(fan::Fan *obj) { API_DISPATCH_UPDATE(fan::Fan, fan)
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_fan_state(obj);
}
#endif #endif
#ifdef USE_LIGHT #ifdef USE_LIGHT
void APIServer::on_light_update(light::LightState *obj) { API_DISPATCH_UPDATE(light::LightState, light)
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_light_state(obj);
}
#endif #endif
#ifdef USE_SENSOR #ifdef USE_SENSOR
void APIServer::on_sensor_update(sensor::Sensor *obj, float state) { API_DISPATCH_UPDATE_IGNORE_PARAMS(sensor::Sensor, sensor, float state)
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_sensor_state(obj);
}
#endif #endif
#ifdef USE_SWITCH #ifdef USE_SWITCH
void APIServer::on_switch_update(switch_::Switch *obj, bool state) { API_DISPATCH_UPDATE_IGNORE_PARAMS(switch_::Switch, switch, bool state)
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_switch_state(obj);
}
#endif #endif
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
void APIServer::on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state) { API_DISPATCH_UPDATE_IGNORE_PARAMS(text_sensor::TextSensor, text_sensor, const std::string &state)
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_text_sensor_state(obj);
}
#endif #endif
#ifdef USE_CLIMATE #ifdef USE_CLIMATE
void APIServer::on_climate_update(climate::Climate *obj) { API_DISPATCH_UPDATE(climate::Climate, climate)
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_climate_state(obj);
}
#endif #endif
#ifdef USE_NUMBER #ifdef USE_NUMBER
void APIServer::on_number_update(number::Number *obj, float state) { API_DISPATCH_UPDATE_IGNORE_PARAMS(number::Number, number, float state)
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_number_state(obj);
}
#endif #endif
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
void APIServer::on_date_update(datetime::DateEntity *obj) { API_DISPATCH_UPDATE(datetime::DateEntity, date)
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_date_state(obj);
}
#endif #endif
#ifdef USE_DATETIME_TIME #ifdef USE_DATETIME_TIME
void APIServer::on_time_update(datetime::TimeEntity *obj) { API_DISPATCH_UPDATE(datetime::TimeEntity, time)
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_time_state(obj);
}
#endif #endif
#ifdef USE_DATETIME_DATETIME #ifdef USE_DATETIME_DATETIME
void APIServer::on_datetime_update(datetime::DateTimeEntity *obj) { API_DISPATCH_UPDATE(datetime::DateTimeEntity, datetime)
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_datetime_state(obj);
}
#endif #endif
#ifdef USE_TEXT #ifdef USE_TEXT
void APIServer::on_text_update(text::Text *obj, const std::string &state) { API_DISPATCH_UPDATE_IGNORE_PARAMS(text::Text, text, const std::string &state)
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_text_state(obj);
}
#endif #endif
#ifdef USE_SELECT #ifdef USE_SELECT
void APIServer::on_select_update(select::Select *obj, const std::string &state, size_t index) { API_DISPATCH_UPDATE_IGNORE_PARAMS(select::Select, select, const std::string &state, size_t index)
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_select_state(obj);
}
#endif #endif
#ifdef USE_LOCK #ifdef USE_LOCK
void APIServer::on_lock_update(lock::Lock *obj) { API_DISPATCH_UPDATE(lock::Lock, lock)
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_lock_state(obj);
}
#endif #endif
#ifdef USE_VALVE #ifdef USE_VALVE
void APIServer::on_valve_update(valve::Valve *obj) { API_DISPATCH_UPDATE(valve::Valve, valve)
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_valve_state(obj);
}
#endif #endif
#ifdef USE_MEDIA_PLAYER #ifdef USE_MEDIA_PLAYER
void APIServer::on_media_player_update(media_player::MediaPlayer *obj) { API_DISPATCH_UPDATE(media_player::MediaPlayer, media_player)
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_media_player_state(obj);
}
#endif #endif
#ifdef USE_EVENT #ifdef USE_EVENT
// Event is a special case - it's the only entity that passes extra parameters to the send method
void APIServer::on_event(event::Event *obj, const std::string &event_type) { void APIServer::on_event(event::Event *obj, const std::string &event_type) {
if (obj->is_internal())
return;
for (auto &c : this->clients_) for (auto &c : this->clients_)
c->send_event(obj, event_type); c->send_event(obj, event_type);
} }
#endif #endif
#ifdef USE_UPDATE #ifdef USE_UPDATE
// Update is a special case - the method is called on_update, not on_update_update
void APIServer::on_update(update::UpdateEntity *obj) { void APIServer::on_update(update::UpdateEntity *obj) {
if (obj->is_internal())
return;
for (auto &c : this->clients_) for (auto &c : this->clients_)
c->send_update_state(obj); c->send_update_state(obj);
} }
#endif #endif
#ifdef USE_ALARM_CONTROL_PANEL #ifdef USE_ALARM_CONTROL_PANEL
void APIServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) { API_DISPATCH_UPDATE(alarm_control_panel::AlarmControlPanel, alarm_control_panel)
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_alarm_control_panel_state(obj);
}
#endif #endif
float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; } float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
void APIServer::set_port(uint16_t port) { this->port_ = port; } void APIServer::set_port(uint16_t port) { this->port_ = port; }
#ifdef USE_API_PASSWORD
void APIServer::set_password(const std::string &password) { this->password_ = password; } void APIServer::set_password(const std::string &password) { this->password_ = password; }
#endif
void APIServer::set_batch_delay(uint32_t batch_delay) { this->batch_delay_ = batch_delay; } void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; }
#ifdef USE_API_HOMEASSISTANT_SERVICES
void APIServer::send_homeassistant_service_call(const HomeassistantServiceResponse &call) { void APIServer::send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
for (auto &client : this->clients_) { for (auto &client : this->clients_) {
client->send_homeassistant_service_call(call); client->send_homeassistant_service_call(call);
} }
} }
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute, void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(std::string)> f) { std::function<void(std::string)> f) {
this->state_subs_.push_back(HomeAssistantStateSubscription{ this->state_subs_.push_back(HomeAssistantStateSubscription{
@ -439,6 +401,7 @@ void APIServer::get_home_assistant_state(std::string entity_id, optional<std::st
const std::vector<APIServer::HomeAssistantStateSubscription> &APIServer::get_state_subs() const { const std::vector<APIServer::HomeAssistantStateSubscription> &APIServer::get_state_subs() const {
return this->state_subs_; return this->state_subs_;
} }
#endif
uint16_t APIServer::get_port() const { return this->port_; } uint16_t APIServer::get_port() const { return this->port_; }
@ -465,10 +428,11 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
ESP_LOGD(TAG, "Noise PSK saved"); ESP_LOGD(TAG, "Noise PSK saved");
if (make_active) { if (make_active) {
this->set_timeout(100, [this, psk]() { 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); this->set_noise_psk(psk);
for (auto &c : this->clients_) { for (auto &c : this->clients_) {
c->send_message(DisconnectRequest()); DisconnectRequest req;
c->send_message(req, DisconnectRequest::MESSAGE_TYPE);
} }
}); });
} }
@ -479,7 +443,7 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
#ifdef USE_HOMEASSISTANT_TIME #ifdef USE_HOMEASSISTANT_TIME
void APIServer::request_time() { void APIServer::request_time() {
for (auto &client : this->clients_) { for (auto &client : this->clients_) {
if (!client->remove_ && client->is_authenticated()) if (!client->flags_.remove && client->is_authenticated())
client->send_time_request(); client->send_time_request();
} }
} }
@ -501,10 +465,12 @@ void APIServer::on_shutdown() {
// Send disconnect requests to all connected clients // Send disconnect requests to all connected clients
for (auto &c : this->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), // If we can't send the disconnect request directly (tx_buffer full),
// schedule it in the batch so it will be sent with the 5ms timer // schedule it at the front of the batch so it will be sent with priority
c->schedule_message_(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);
} }
} }
} }
@ -520,6 +486,5 @@ bool APIServer::teardown() {
return this->clients_.empty(); return this->clients_.empty();
} }
} // namespace api } // namespace esphome::api
} // namespace esphome
#endif #endif

View File

@ -12,12 +12,13 @@
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "list_entities.h" #include "list_entities.h"
#include "subscribe_state.h" #include "subscribe_state.h"
#ifdef USE_API_SERVICES
#include "user_services.h" #include "user_services.h"
#endif
#include <vector> #include <vector>
namespace esphome { namespace esphome::api {
namespace api {
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
struct SavedNoisePsk { struct SavedNoisePsk {
@ -35,13 +36,14 @@ class APIServer : public Component, public Controller {
void dump_config() override; void dump_config() override;
void on_shutdown() override; void on_shutdown() override;
bool teardown() override; bool teardown() override;
#ifdef USE_API_PASSWORD
bool check_password(const std::string &password) const; bool check_password(const std::string &password) const;
bool uses_password() const;
void set_port(uint16_t port);
void set_password(const std::string &password); void set_password(const std::string &password);
#endif
void set_port(uint16_t port);
void set_reboot_timeout(uint32_t reboot_timeout); void set_reboot_timeout(uint32_t reboot_timeout);
void set_batch_delay(uint32_t batch_delay); void set_batch_delay(uint16_t batch_delay);
uint32_t get_batch_delay() const { return batch_delay_; } uint16_t get_batch_delay() const { return batch_delay_; }
// Get reference to shared buffer for API connections // Get reference to shared buffer for API connections
std::vector<uint8_t> &get_shared_buffer_ref() { return shared_write_buffer_; } std::vector<uint8_t> &get_shared_buffer_ref() { return shared_write_buffer_; }
@ -54,7 +56,7 @@ class APIServer : public Component, public Controller {
void handle_disconnect(APIConnection *conn); void handle_disconnect(APIConnection *conn);
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
void on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) override; void on_binary_sensor_update(binary_sensor::BinarySensor *obj) override;
#endif #endif
#ifdef USE_COVER #ifdef USE_COVER
void on_cover_update(cover::Cover *obj) override; void on_cover_update(cover::Cover *obj) override;
@ -104,8 +106,12 @@ class APIServer : public Component, public Controller {
#ifdef USE_MEDIA_PLAYER #ifdef USE_MEDIA_PLAYER
void on_media_player_update(media_player::MediaPlayer *obj) override; void on_media_player_update(media_player::MediaPlayer *obj) override;
#endif #endif
#ifdef USE_API_HOMEASSISTANT_SERVICES
void send_homeassistant_service_call(const HomeassistantServiceResponse &call); void send_homeassistant_service_call(const HomeassistantServiceResponse &call);
#endif
#ifdef USE_API_SERVICES
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
#endif
#ifdef USE_HOMEASSISTANT_TIME #ifdef USE_HOMEASSISTANT_TIME
void request_time(); void request_time();
#endif #endif
@ -122,6 +128,7 @@ class APIServer : public Component, public Controller {
bool is_connected() const; bool is_connected() const;
#ifdef USE_API_HOMEASSISTANT_STATES
struct HomeAssistantStateSubscription { struct HomeAssistantStateSubscription {
std::string entity_id; std::string entity_id;
optional<std::string> attribute; optional<std::string> attribute;
@ -134,27 +141,52 @@ class APIServer : public Component, public Controller {
void get_home_assistant_state(std::string entity_id, optional<std::string> attribute, void get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(std::string)> f); std::function<void(std::string)> f);
const std::vector<HomeAssistantStateSubscription> &get_state_subs() const; const std::vector<HomeAssistantStateSubscription> &get_state_subs() const;
#endif
#ifdef USE_API_SERVICES
const std::vector<UserServiceDescriptor *> &get_user_services() const { return this->user_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_; } Trigger<std::string, std::string> *get_client_connected_trigger() const { return this->client_connected_trigger_; }
#endif
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
Trigger<std::string, std::string> *get_client_disconnected_trigger() const { Trigger<std::string, std::string> *get_client_disconnected_trigger() const {
return this->client_disconnected_trigger_; return this->client_disconnected_trigger_;
} }
#endif
protected: protected:
bool shutting_down_ = false; void schedule_reboot_timeout_();
// Pointers and pointer-like types first (4 bytes each)
std::unique_ptr<socket::Socket> socket_ = nullptr; std::unique_ptr<socket::Socket> socket_ = nullptr;
uint16_t port_{6053}; #ifdef USE_API_CLIENT_CONNECTED_TRIGGER
uint32_t reboot_timeout_{300000};
uint32_t batch_delay_{100};
uint32_t last_connected_{0};
std::vector<std::unique_ptr<APIConnection>> clients_;
std::string password_;
std::vector<uint8_t> shared_write_buffer_; // Shared proto write buffer for all connections
std::vector<HomeAssistantStateSubscription> state_subs_;
std::vector<UserServiceDescriptor *> user_services_;
Trigger<std::string, std::string> *client_connected_trigger_ = new Trigger<std::string, std::string>(); Trigger<std::string, std::string> *client_connected_trigger_ = new Trigger<std::string, std::string>();
#endif
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
Trigger<std::string, std::string> *client_disconnected_trigger_ = new Trigger<std::string, std::string>(); Trigger<std::string, std::string> *client_disconnected_trigger_ = new Trigger<std::string, std::string>();
#endif
// 4-byte aligned types
uint32_t reboot_timeout_{300000};
// Vectors and strings (12 bytes each on 32-bit)
std::vector<std::unique_ptr<APIConnection>> clients_;
#ifdef USE_API_PASSWORD
std::string password_;
#endif
std::vector<uint8_t> shared_write_buffer_; // Shared proto write buffer for all connections
#ifdef USE_API_HOMEASSISTANT_STATES
std::vector<HomeAssistantStateSubscription> state_subs_;
#endif
#ifdef USE_API_SERVICES
std::vector<UserServiceDescriptor *> user_services_;
#endif
// Group smaller types together
uint16_t port_{6053};
uint16_t batch_delay_{100};
bool shutting_down_ = false;
// 5 bytes used, 3 bytes padding
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
std::shared_ptr<APINoiseContext> noise_ctx_ = std::make_shared<APINoiseContext>(); std::shared_ptr<APINoiseContext> noise_ctx_ = std::make_shared<APINoiseContext>();
@ -169,6 +201,5 @@ template<typename... Ts> class APIConnectedCondition : public Condition<Ts...> {
bool check(Ts... x) override { return global_api_server->is_connected(); } bool check(Ts... x) override { return global_api_server->is_connected(); }
}; };
} // namespace api } // namespace esphome::api
} // namespace esphome
#endif #endif

View File

@ -4,9 +4,17 @@ import asyncio
from datetime import datetime from datetime import datetime
import logging import logging
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
import warnings
from aioesphomeapi import APIClient, parse_log_message # Suppress protobuf version warnings
from aioesphomeapi.log_runner import async_run with warnings.catch_warnings():
warnings.filterwarnings(
"ignore", category=UserWarning, message=".*Protobuf gencode version.*"
)
from aioesphomeapi import APIClient, parse_log_message
from aioesphomeapi.log_runner import async_run
import contextlib
from esphome.const import CONF_KEY, CONF_PASSWORD, CONF_PORT, __version__ from esphome.const import CONF_KEY, CONF_PASSWORD, CONF_PORT, __version__
from esphome.core import CORE from esphome.core import CORE
@ -29,8 +37,8 @@ async def async_run_logs(config: dict[str, Any], address: str) -> None:
port: int = int(conf[CONF_PORT]) port: int = int(conf[CONF_PORT])
password: str = conf[CONF_PASSWORD] password: str = conf[CONF_PASSWORD]
noise_psk: str | None = None noise_psk: str | None = None
if CONF_ENCRYPTION in conf: if (encryption := conf.get(CONF_ENCRYPTION)) and (key := encryption.get(CONF_KEY)):
noise_psk = conf[CONF_ENCRYPTION][CONF_KEY] noise_psk = key
_LOGGER.info("Starting log output from %s using esphome API", address) _LOGGER.info("Starting log output from %s using esphome API", address)
cli = APIClient( cli = APIClient(
address, address,
@ -60,7 +68,5 @@ async def async_run_logs(config: dict[str, Any], address: str) -> None:
def run_logs(config: dict[str, Any], address: str) -> None: def run_logs(config: dict[str, Any], address: str) -> None:
"""Run the logs command.""" """Run the logs command."""
try: with contextlib.suppress(KeyboardInterrupt):
asyncio.run(async_run_logs(config, address)) asyncio.run(async_run_logs(config, address))
except KeyboardInterrupt:
pass

View File

@ -3,10 +3,12 @@
#include <map> #include <map>
#include "api_server.h" #include "api_server.h"
#ifdef USE_API #ifdef USE_API
#ifdef USE_API_SERVICES
#include "user_services.h" #include "user_services.h"
namespace esphome { #endif
namespace api { namespace esphome::api {
#ifdef USE_API_SERVICES
template<typename T, typename... Ts> class CustomAPIDeviceService : public UserServiceBase<Ts...> { template<typename T, typename... Ts> class CustomAPIDeviceService : public UserServiceBase<Ts...> {
public: public:
CustomAPIDeviceService(const std::string &name, const std::array<std::string, sizeof...(Ts)> &arg_names, T *obj, CustomAPIDeviceService(const std::string &name, const std::array<std::string, sizeof...(Ts)> &arg_names, T *obj,
@ -19,6 +21,7 @@ template<typename T, typename... Ts> class CustomAPIDeviceService : public UserS
T *obj_; T *obj_;
void (T::*callback_)(Ts...); void (T::*callback_)(Ts...);
}; };
#endif // USE_API_SERVICES
class CustomAPIDevice { class CustomAPIDevice {
public: public:
@ -46,12 +49,14 @@ class CustomAPIDevice {
* @param name The name of the service to register. * @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. * @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> template<typename T, typename... Ts>
void register_service(void (T::*callback)(Ts...), const std::string &name, void register_service(void (T::*callback)(Ts...), const std::string &name,
const std::array<std::string, sizeof...(Ts)> &arg_names) { const std::array<std::string, sizeof...(Ts)> &arg_names) {
auto *service = new CustomAPIDeviceService<T, Ts...>(name, arg_names, (T *) this, callback); // NOLINT auto *service = new CustomAPIDeviceService<T, Ts...>(name, arg_names, (T *) this, callback); // NOLINT
global_api_server->register_user_service(service); global_api_server->register_user_service(service);
} }
#endif
/** Register a custom native API service that will show up in Home Assistant. /** Register a custom native API service that will show up in Home Assistant.
* *
@ -71,11 +76,14 @@ class CustomAPIDevice {
* @param callback The member function to call when the service is triggered. * @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. * @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) { template<typename T> void register_service(void (T::*callback)(), const std::string &name) {
auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback); // NOLINT auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback); // NOLINT
global_api_server->register_user_service(service); global_api_server->register_user_service(service);
} }
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
/** Subscribe to the state (or attribute state) of an entity from Home Assistant. /** Subscribe to the state (or attribute state) of an entity from Home Assistant.
* *
* Usage: * Usage:
@ -127,7 +135,9 @@ class CustomAPIDevice {
auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1); auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1);
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), f); global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), f);
} }
#endif
#ifdef USE_API_HOMEASSISTANT_SERVICES
/** Call a Home Assistant service from ESPHome. /** Call a Home Assistant service from ESPHome.
* *
* Usage: * Usage:
@ -140,7 +150,7 @@ class CustomAPIDevice {
*/ */
void call_homeassistant_service(const std::string &service_name) { void call_homeassistant_service(const std::string &service_name) {
HomeassistantServiceResponse resp; HomeassistantServiceResponse resp;
resp.service = service_name; resp.set_service(StringRef(service_name));
global_api_server->send_homeassistant_service_call(resp); global_api_server->send_homeassistant_service_call(resp);
} }
@ -160,12 +170,12 @@ class CustomAPIDevice {
*/ */
void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) { void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) {
HomeassistantServiceResponse resp; HomeassistantServiceResponse resp;
resp.service = service_name; resp.set_service(StringRef(service_name));
for (auto &it : data) { for (auto &it : data) {
HomeassistantServiceMap kv; resp.data.emplace_back();
kv.key = it.first; auto &kv = resp.data.back();
kv.set_key(StringRef(it.first));
kv.value = it.second; kv.value = it.second;
resp.data.push_back(kv);
} }
global_api_server->send_homeassistant_service_call(resp); global_api_server->send_homeassistant_service_call(resp);
} }
@ -182,7 +192,7 @@ class CustomAPIDevice {
*/ */
void fire_homeassistant_event(const std::string &event_name) { void fire_homeassistant_event(const std::string &event_name) {
HomeassistantServiceResponse resp; HomeassistantServiceResponse resp;
resp.service = event_name; resp.set_service(StringRef(event_name));
resp.is_event = true; resp.is_event = true;
global_api_server->send_homeassistant_service_call(resp); global_api_server->send_homeassistant_service_call(resp);
} }
@ -202,18 +212,18 @@ class CustomAPIDevice {
*/ */
void fire_homeassistant_event(const std::string &service_name, const std::map<std::string, std::string> &data) { void fire_homeassistant_event(const std::string &service_name, const std::map<std::string, std::string> &data) {
HomeassistantServiceResponse resp; HomeassistantServiceResponse resp;
resp.service = service_name; resp.set_service(StringRef(service_name));
resp.is_event = true; resp.is_event = true;
for (auto &it : data) { for (auto &it : data) {
HomeassistantServiceMap kv; resp.data.emplace_back();
kv.key = it.first; auto &kv = resp.data.back();
kv.set_key(StringRef(it.first));
kv.value = it.second; kv.value = it.second;
resp.data.push_back(kv);
} }
global_api_server->send_homeassistant_service_call(resp); global_api_server->send_homeassistant_service_call(resp);
} }
#endif
}; };
} // namespace api } // namespace esphome::api
} // namespace esphome
#endif #endif

View File

@ -2,15 +2,27 @@
#include "api_server.h" #include "api_server.h"
#ifdef USE_API #ifdef USE_API
#ifdef USE_API_HOMEASSISTANT_SERVICES
#include "api_pb2.h" #include "api_pb2.h"
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include <vector> #include <vector>
namespace esphome { namespace esphome::api {
namespace api {
template<typename... X> class TemplatableStringValue : public TemplatableValue<std::string, X...> { 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: public:
TemplatableStringValue() : TemplatableValue<std::string, X...>() {} TemplatableStringValue() : TemplatableValue<std::string, X...>() {}
@ -19,11 +31,14 @@ template<typename... X> class TemplatableStringValue : public TemplatableValue<s
template<typename F, enable_if_t<is_invocable<F, X...>::value, int> = 0> template<typename F, enable_if_t<is_invocable<F, X...>::value, int> = 0>
TemplatableStringValue(F f) 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 { template<typename... Ts> class TemplatableKeyValuePair {
public: public:
// Keys are always string literals from YAML dictionary keys (e.g., "code", "event")
// and never templatable values or lambdas. Only the value parameter can be a lambda/template.
// Using pass-by-value with std::move allows optimal performance for both lvalues and rvalues.
template<typename T> TemplatableKeyValuePair(std::string key, T value) : key(std::move(key)), value(value) {} template<typename T> TemplatableKeyValuePair(std::string key, T value) : key(std::move(key)), value(value) {}
std::string key; std::string key;
TemplatableStringValue<Ts...> value; TemplatableStringValue<Ts...> value;
@ -35,37 +50,39 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
template<typename T> void set_service(T service) { this->service_ = service; } template<typename T> void set_service(T service) { this->service_ = service; }
template<typename T> void add_data(std::string key, T value) { // Keys are always string literals from the Python code generation (e.g., cg.add(var.add_data("tag_id", templ))).
this->data_.push_back(TemplatableKeyValuePair<Ts...>(key, value)); // The value parameter can be a lambda/template, but keys are never templatable.
} // Using pass-by-value allows the compiler to optimize for both lvalues and rvalues.
template<typename T> void add_data(std::string key, T value) { this->data_.emplace_back(std::move(key), value); }
template<typename T> void add_data_template(std::string key, T value) { template<typename T> void add_data_template(std::string key, T value) {
this->data_template_.push_back(TemplatableKeyValuePair<Ts...>(key, value)); this->data_template_.emplace_back(std::move(key), value);
} }
template<typename T> void add_variable(std::string key, T value) { template<typename T> void add_variable(std::string key, T value) {
this->variables_.push_back(TemplatableKeyValuePair<Ts...>(key, value)); this->variables_.emplace_back(std::move(key), value);
} }
void play(Ts... x) override { void play(Ts... x) override {
HomeassistantServiceResponse resp; HomeassistantServiceResponse resp;
resp.service = this->service_.value(x...); std::string service_value = this->service_.value(x...);
resp.set_service(StringRef(service_value));
resp.is_event = this->is_event_; resp.is_event = this->is_event_;
for (auto &it : this->data_) { for (auto &it : this->data_) {
HomeassistantServiceMap kv; resp.data.emplace_back();
kv.key = it.key; auto &kv = resp.data.back();
kv.set_key(StringRef(it.key));
kv.value = it.value.value(x...); kv.value = it.value.value(x...);
resp.data.push_back(kv);
} }
for (auto &it : this->data_template_) { for (auto &it : this->data_template_) {
HomeassistantServiceMap kv; resp.data_template.emplace_back();
kv.key = it.key; auto &kv = resp.data_template.back();
kv.set_key(StringRef(it.key));
kv.value = it.value.value(x...); kv.value = it.value.value(x...);
resp.data_template.push_back(kv);
} }
for (auto &it : this->variables_) { for (auto &it : this->variables_) {
HomeassistantServiceMap kv; resp.variables.emplace_back();
kv.key = it.key; auto &kv = resp.variables.back();
kv.set_key(StringRef(it.key));
kv.value = it.value.value(x...); kv.value = it.value.value(x...);
resp.variables.push_back(kv);
} }
this->parent_->send_homeassistant_service_call(resp); this->parent_->send_homeassistant_service_call(resp);
} }
@ -79,6 +96,6 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
std::vector<TemplatableKeyValuePair<Ts...>> variables_; std::vector<TemplatableKeyValuePair<Ts...>> variables_;
}; };
} // namespace api } // namespace esphome::api
} // namespace esphome #endif
#endif #endif

View File

@ -1,162 +1,93 @@
#include "list_entities.h" #include "list_entities.h"
#ifdef USE_API #ifdef USE_API
#include "api_connection.h" #include "api_connection.h"
#include "api_pb2.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/util.h" #include "esphome/core/util.h"
namespace esphome { namespace esphome::api {
namespace api {
// Generate entity handler implementations using macros
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
bool ListEntitiesIterator::on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) { LIST_ENTITIES_HANDLER(binary_sensor, binary_sensor::BinarySensor, ListEntitiesBinarySensorResponse)
this->client_->send_binary_sensor_info(binary_sensor);
return true;
}
#endif #endif
#ifdef USE_COVER #ifdef USE_COVER
bool ListEntitiesIterator::on_cover(cover::Cover *cover) { LIST_ENTITIES_HANDLER(cover, cover::Cover, ListEntitiesCoverResponse)
this->client_->send_cover_info(cover);
return true;
}
#endif #endif
#ifdef USE_FAN #ifdef USE_FAN
bool ListEntitiesIterator::on_fan(fan::Fan *fan) { LIST_ENTITIES_HANDLER(fan, fan::Fan, ListEntitiesFanResponse)
this->client_->send_fan_info(fan);
return true;
}
#endif #endif
#ifdef USE_LIGHT #ifdef USE_LIGHT
bool ListEntitiesIterator::on_light(light::LightState *light) { LIST_ENTITIES_HANDLER(light, light::LightState, ListEntitiesLightResponse)
this->client_->send_light_info(light);
return true;
}
#endif #endif
#ifdef USE_SENSOR #ifdef USE_SENSOR
bool ListEntitiesIterator::on_sensor(sensor::Sensor *sensor) { LIST_ENTITIES_HANDLER(sensor, sensor::Sensor, ListEntitiesSensorResponse)
this->client_->send_sensor_info(sensor);
return true;
}
#endif #endif
#ifdef USE_SWITCH #ifdef USE_SWITCH
bool ListEntitiesIterator::on_switch(switch_::Switch *a_switch) { LIST_ENTITIES_HANDLER(switch, switch_::Switch, ListEntitiesSwitchResponse)
this->client_->send_switch_info(a_switch);
return true;
}
#endif #endif
#ifdef USE_BUTTON #ifdef USE_BUTTON
bool ListEntitiesIterator::on_button(button::Button *button) { LIST_ENTITIES_HANDLER(button, button::Button, ListEntitiesButtonResponse)
this->client_->send_button_info(button);
return true;
}
#endif #endif
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
bool ListEntitiesIterator::on_text_sensor(text_sensor::TextSensor *text_sensor) { LIST_ENTITIES_HANDLER(text_sensor, text_sensor::TextSensor, ListEntitiesTextSensorResponse)
this->client_->send_text_sensor_info(text_sensor);
return true;
}
#endif #endif
#ifdef USE_LOCK #ifdef USE_LOCK
bool ListEntitiesIterator::on_lock(lock::Lock *a_lock) { LIST_ENTITIES_HANDLER(lock, lock::Lock, ListEntitiesLockResponse)
this->client_->send_lock_info(a_lock);
return true;
}
#endif #endif
#ifdef USE_VALVE #ifdef USE_VALVE
bool ListEntitiesIterator::on_valve(valve::Valve *valve) { LIST_ENTITIES_HANDLER(valve, valve::Valve, ListEntitiesValveResponse)
this->client_->send_valve_info(valve);
return true;
}
#endif #endif
#ifdef USE_CAMERA
bool ListEntitiesIterator::on_end() { return this->client_->send_list_info_done(); } LIST_ENTITIES_HANDLER(camera, camera::Camera, ListEntitiesCameraResponse)
ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(client) {}
bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) {
auto resp = service->encode_list_service_response();
return this->client_->send_message(resp);
}
#ifdef USE_ESP32_CAMERA
bool ListEntitiesIterator::on_camera(esp32_camera::ESP32Camera *camera) {
this->client_->send_camera_info(camera);
return true;
}
#endif #endif
#ifdef USE_CLIMATE #ifdef USE_CLIMATE
bool ListEntitiesIterator::on_climate(climate::Climate *climate) { LIST_ENTITIES_HANDLER(climate, climate::Climate, ListEntitiesClimateResponse)
this->client_->send_climate_info(climate);
return true;
}
#endif #endif
#ifdef USE_NUMBER #ifdef USE_NUMBER
bool ListEntitiesIterator::on_number(number::Number *number) { LIST_ENTITIES_HANDLER(number, number::Number, ListEntitiesNumberResponse)
this->client_->send_number_info(number);
return true;
}
#endif #endif
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
bool ListEntitiesIterator::on_date(datetime::DateEntity *date) { LIST_ENTITIES_HANDLER(date, datetime::DateEntity, ListEntitiesDateResponse)
this->client_->send_date_info(date);
return true;
}
#endif #endif
#ifdef USE_DATETIME_TIME #ifdef USE_DATETIME_TIME
bool ListEntitiesIterator::on_time(datetime::TimeEntity *time) { LIST_ENTITIES_HANDLER(time, datetime::TimeEntity, ListEntitiesTimeResponse)
this->client_->send_time_info(time);
return true;
}
#endif #endif
#ifdef USE_DATETIME_DATETIME #ifdef USE_DATETIME_DATETIME
bool ListEntitiesIterator::on_datetime(datetime::DateTimeEntity *datetime) { LIST_ENTITIES_HANDLER(datetime, datetime::DateTimeEntity, ListEntitiesDateTimeResponse)
this->client_->send_datetime_info(datetime);
return true;
}
#endif #endif
#ifdef USE_TEXT #ifdef USE_TEXT
bool ListEntitiesIterator::on_text(text::Text *text) { LIST_ENTITIES_HANDLER(text, text::Text, ListEntitiesTextResponse)
this->client_->send_text_info(text);
return true;
}
#endif #endif
#ifdef USE_SELECT #ifdef USE_SELECT
bool ListEntitiesIterator::on_select(select::Select *select) { LIST_ENTITIES_HANDLER(select, select::Select, ListEntitiesSelectResponse)
this->client_->send_select_info(select);
return true;
}
#endif #endif
#ifdef USE_MEDIA_PLAYER #ifdef USE_MEDIA_PLAYER
bool ListEntitiesIterator::on_media_player(media_player::MediaPlayer *media_player) { LIST_ENTITIES_HANDLER(media_player, media_player::MediaPlayer, ListEntitiesMediaPlayerResponse)
this->client_->send_media_player_info(media_player);
return true;
}
#endif #endif
#ifdef USE_ALARM_CONTROL_PANEL #ifdef USE_ALARM_CONTROL_PANEL
bool ListEntitiesIterator::on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { LIST_ENTITIES_HANDLER(alarm_control_panel, alarm_control_panel::AlarmControlPanel,
this->client_->send_alarm_control_panel_info(a_alarm_control_panel); ListEntitiesAlarmControlPanelResponse)
return true;
}
#endif #endif
#ifdef USE_EVENT #ifdef USE_EVENT
bool ListEntitiesIterator::on_event(event::Event *event) { LIST_ENTITIES_HANDLER(event, event::Event, ListEntitiesEventResponse)
this->client_->send_event_info(event);
return true;
}
#endif #endif
#ifdef USE_UPDATE #ifdef USE_UPDATE
bool ListEntitiesIterator::on_update(update::UpdateEntity *update) { LIST_ENTITIES_HANDLER(update, update::UpdateEntity, ListEntitiesUpdateResponse)
this->client_->send_update_info(update); #endif
return true;
// Special cases that don't follow the pattern
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, ListEntitiesServicesResponse::MESSAGE_TYPE);
} }
#endif #endif
} // namespace api } // namespace esphome::api
} // namespace esphome
#endif #endif

View File

@ -4,80 +4,89 @@
#ifdef USE_API #ifdef USE_API
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/component_iterator.h" #include "esphome/core/component_iterator.h"
namespace esphome { namespace esphome::api {
namespace api {
class APIConnection; class APIConnection;
// Macro for generating ListEntitiesIterator handlers
// Calls schedule_message_ with try_send_*_info
#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::ESTIMATED_SIZE); \
}
class ListEntitiesIterator : public ComponentIterator { class ListEntitiesIterator : public ComponentIterator {
public: public:
ListEntitiesIterator(APIConnection *client); ListEntitiesIterator(APIConnection *client);
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) override; bool on_binary_sensor(binary_sensor::BinarySensor *entity) override;
#endif #endif
#ifdef USE_COVER #ifdef USE_COVER
bool on_cover(cover::Cover *cover) override; bool on_cover(cover::Cover *entity) override;
#endif #endif
#ifdef USE_FAN #ifdef USE_FAN
bool on_fan(fan::Fan *fan) override; bool on_fan(fan::Fan *entity) override;
#endif #endif
#ifdef USE_LIGHT #ifdef USE_LIGHT
bool on_light(light::LightState *light) override; bool on_light(light::LightState *entity) override;
#endif #endif
#ifdef USE_SENSOR #ifdef USE_SENSOR
bool on_sensor(sensor::Sensor *sensor) override; bool on_sensor(sensor::Sensor *entity) override;
#endif #endif
#ifdef USE_SWITCH #ifdef USE_SWITCH
bool on_switch(switch_::Switch *a_switch) override; bool on_switch(switch_::Switch *entity) override;
#endif #endif
#ifdef USE_BUTTON #ifdef USE_BUTTON
bool on_button(button::Button *button) override; bool on_button(button::Button *entity) override;
#endif #endif
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
bool on_text_sensor(text_sensor::TextSensor *text_sensor) override; bool on_text_sensor(text_sensor::TextSensor *entity) override;
#endif #endif
#ifdef USE_API_SERVICES
bool on_service(UserServiceDescriptor *service) override; bool on_service(UserServiceDescriptor *service) override;
#ifdef USE_ESP32_CAMERA #endif
bool on_camera(esp32_camera::ESP32Camera *camera) override; #ifdef USE_CAMERA
bool on_camera(camera::Camera *entity) override;
#endif #endif
#ifdef USE_CLIMATE #ifdef USE_CLIMATE
bool on_climate(climate::Climate *climate) override; bool on_climate(climate::Climate *entity) override;
#endif #endif
#ifdef USE_NUMBER #ifdef USE_NUMBER
bool on_number(number::Number *number) override; bool on_number(number::Number *entity) override;
#endif #endif
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
bool on_date(datetime::DateEntity *date) override; bool on_date(datetime::DateEntity *entity) override;
#endif #endif
#ifdef USE_DATETIME_TIME #ifdef USE_DATETIME_TIME
bool on_time(datetime::TimeEntity *time) override; bool on_time(datetime::TimeEntity *entity) override;
#endif #endif
#ifdef USE_DATETIME_DATETIME #ifdef USE_DATETIME_DATETIME
bool on_datetime(datetime::DateTimeEntity *datetime) override; bool on_datetime(datetime::DateTimeEntity *entity) override;
#endif #endif
#ifdef USE_TEXT #ifdef USE_TEXT
bool on_text(text::Text *text) override; bool on_text(text::Text *entity) override;
#endif #endif
#ifdef USE_SELECT #ifdef USE_SELECT
bool on_select(select::Select *select) override; bool on_select(select::Select *entity) override;
#endif #endif
#ifdef USE_LOCK #ifdef USE_LOCK
bool on_lock(lock::Lock *a_lock) override; bool on_lock(lock::Lock *entity) override;
#endif #endif
#ifdef USE_VALVE #ifdef USE_VALVE
bool on_valve(valve::Valve *valve) override; bool on_valve(valve::Valve *entity) override;
#endif #endif
#ifdef USE_MEDIA_PLAYER #ifdef USE_MEDIA_PLAYER
bool on_media_player(media_player::MediaPlayer *media_player) override; bool on_media_player(media_player::MediaPlayer *entity) override;
#endif #endif
#ifdef USE_ALARM_CONTROL_PANEL #ifdef USE_ALARM_CONTROL_PANEL
bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) override; bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *entity) override;
#endif #endif
#ifdef USE_EVENT #ifdef USE_EVENT
bool on_event(event::Event *event) override; bool on_event(event::Event *entity) override;
#endif #endif
#ifdef USE_UPDATE #ifdef USE_UPDATE
bool on_update(update::UpdateEntity *update) override; bool on_update(update::UpdateEntity *entity) override;
#endif #endif
bool on_end() override; bool on_end() override;
bool completed() { return this->state_ == IteratorState::NONE; } bool completed() { return this->state_ == IteratorState::NONE; }
@ -86,6 +95,5 @@ class ListEntitiesIterator : public ComponentIterator {
APIConnection *client_; APIConnection *client_;
}; };
} // namespace api } // namespace esphome::api
} // namespace esphome
#endif #endif

View File

@ -3,12 +3,11 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome { namespace esphome::api {
namespace api {
static const char *const TAG = "api.proto"; 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; uint32_t i = 0;
bool error = false; bool error = false;
while (i < length) { while (i < length) {
@ -89,5 +88,4 @@ std::string ProtoMessage::dump() const {
} }
#endif #endif
} // namespace api } // namespace esphome::api
} // namespace esphome

View File

@ -3,15 +3,47 @@
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/string_ref.h"
#include <cassert>
#include <cstring>
#include <vector> #include <vector>
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
#define HAS_PROTO_MESSAGE_DUMP #define HAS_PROTO_MESSAGE_DUMP
#endif #endif
namespace esphome { namespace esphome::api {
namespace api {
/*
* StringRef Ownership Model for API Protocol Messages
* ===================================================
*
* StringRef is used for zero-copy string handling in outgoing (SOURCE_SERVER) messages.
* It holds a pointer and length to existing string data without copying.
*
* CRITICAL: The referenced string data MUST remain valid until message encoding completes.
*
* Safe StringRef Patterns:
* 1. String literals: StringRef("literal") - Always safe (static storage duration)
* 2. Member variables: StringRef(this->member_string_) - Safe if object outlives encoding
* 3. Global/static strings: StringRef(GLOBAL_CONSTANT) - Always safe
* 4. Local variables: Safe ONLY if encoding happens before function returns:
* std::string temp = compute_value();
* msg.set_field(StringRef(temp));
* return this->send_message(msg); // temp is valid during encoding
*
* Unsafe Patterns (WILL cause crashes/corruption):
* 1. Temporaries: msg.set_field(StringRef(obj.get_string())) // get_string() returns by value
* 2. Concatenation: msg.set_field(StringRef(str1 + str2)) // Result is temporary
*
* For unsafe patterns, store in a local variable first:
* std::string temp = get_string(); // or str1 + str2
* msg.set_field(StringRef(temp));
*
* The send_*_response pattern ensures proper lifetime management by encoding
* within the same function scope where temporaries are created.
*/
/// Representation of a VarInt - in ProtoBuf should be 64bit but we only use 32bit /// Representation of a VarInt - in ProtoBuf should be 64bit but we only use 32bit
class ProtoVarInt { class ProtoVarInt {
@ -59,7 +91,6 @@ class ProtoVarInt {
uint32_t as_uint32() const { return this->value_; } uint32_t as_uint32() const { return this->value_; }
uint64_t as_uint64() const { return this->value_; } uint64_t as_uint64() const { return this->value_; }
bool as_bool() 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 { int32_t as_int32() const {
// Not ZigZag encoded // Not ZigZag encoded
return static_cast<int32_t>(this->as_int64()); return static_cast<int32_t>(this->as_int64());
@ -133,15 +164,25 @@ class ProtoVarInt {
uint64_t value_; uint64_t value_;
}; };
// Forward declaration for decode_to_message and encode_to_writer
class ProtoMessage;
class ProtoDecodableMessage;
class ProtoLengthDelimited { class ProtoLengthDelimited {
public: public:
explicit ProtoLengthDelimited(const uint8_t *value, size_t length) : value_(value), length_(length) {} 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_); } 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_); * Decode the length-delimited data into an existing ProtoDecodableMessage instance.
return msg; *
} * 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: protected:
const uint8_t *const value_; const uint8_t *const value_;
@ -166,23 +207,7 @@ class Proto32Bit {
const uint32_t value_; const uint32_t value_;
}; };
class Proto64Bit { // NOTE: Proto64Bit class removed - wire type 1 (64-bit fixed) not supported
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_;
};
class ProtoWriteBuffer { class ProtoWriteBuffer {
public: public:
@ -196,9 +221,9 @@ class ProtoWriteBuffer {
* @param field_id Field number (tag) in the protobuf message * @param field_id Field number (tag) in the protobuf message
* @param type Wire type value: * @param type Wire type value:
* - 0: Varint (int32, int64, uint32, uint64, sint32, sint64, bool, enum) * - 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) * - 2: Length-delimited (string, bytes, embedded messages, packed repeated fields)
* - 5: 32-bit (fixed32, sfixed32, float) * - 5: 32-bit (fixed32, sfixed32, float)
* - Note: Wire type 1 (64-bit fixed) is not supported
* *
* Following https://protobuf.dev/programming-guides/encoding/#structure * Following https://protobuf.dev/programming-guides/encoding/#structure
*/ */
@ -212,12 +237,20 @@ class ProtoWriteBuffer {
this->encode_field_raw(field_id, 2); // type 2: Length-delimited string this->encode_field_raw(field_id, 2); // type 2: Length-delimited string
this->encode_varint_raw(len); this->encode_varint_raw(len);
auto *data = reinterpret_cast<const uint8_t *>(string);
this->buffer_->insert(this->buffer_->end(), data, data + len); // Using resize + memcpy instead of insert provides significant performance improvement:
// ~10-11x faster for 16-32 byte strings, ~3x faster for 64-byte strings
// as it avoids iterator checks and potential element moves that insert performs
size_t old_size = this->buffer_->size();
this->buffer_->resize(old_size + len);
std::memcpy(this->buffer_->data() + old_size, string, len);
} }
void encode_string(uint32_t field_id, const std::string &value, bool force = false) { void encode_string(uint32_t field_id, const std::string &value, bool force = false) {
this->encode_string(field_id, value.data(), value.size(), force); this->encode_string(field_id, value.data(), value.size(), force);
} }
void encode_string(uint32_t field_id, const StringRef &ref, bool force = false) {
this->encode_string(field_id, ref.c_str(), ref.size(), force);
}
void encode_bytes(uint32_t field_id, const uint8_t *data, size_t len, bool force = false) { void encode_bytes(uint32_t field_id, const uint8_t *data, size_t len, bool force = false) {
this->encode_string(field_id, reinterpret_cast<const char *>(data), len, force); this->encode_string(field_id, reinterpret_cast<const char *>(data), len, force);
} }
@ -249,23 +282,10 @@ class ProtoWriteBuffer {
this->write((value >> 16) & 0xFF); this->write((value >> 16) & 0xFF);
this->write((value >> 24) & 0xFF); this->write((value >> 24) & 0xFF);
} }
void encode_fixed64(uint32_t field_id, uint64_t value, bool force = false) { // NOTE: Wire type 1 (64-bit fixed: double, fixed64, sfixed64) is intentionally
if (value == 0 && !force) // not supported to reduce overhead on embedded systems. All ESPHome devices are
return; // 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.
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);
}
void encode_float(uint32_t field_id, float value, bool force = false) { void encode_float(uint32_t field_id, float value, bool force = false) {
if (value == 0.0f && !force) if (value == 0.0f && !force)
return; return;
@ -306,42 +326,471 @@ class ProtoWriteBuffer {
} }
this->encode_uint64(field_id, uvalue, force); this->encode_uint64(field_id, uvalue, force);
} }
template<class C> void encode_message(uint32_t field_id, const C &value, bool force = false) { void encode_message(uint32_t field_id, const ProtoMessage &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());
}
std::vector<uint8_t> *get_buffer() const { return buffer_; } std::vector<uint8_t> *get_buffer() const { return buffer_; }
protected: protected:
std::vector<uint8_t> *buffer_; std::vector<uint8_t> *buffer_;
}; };
// Forward declaration
class ProtoSize;
class ProtoMessage { class ProtoMessage {
public: public:
virtual ~ProtoMessage() = default; virtual ~ProtoMessage() = default;
virtual void encode(ProtoWriteBuffer buffer) const = 0; // Default implementation for messages with no fields
void decode(const uint8_t *buffer, size_t length); virtual void encode(ProtoWriteBuffer buffer) const {}
virtual void calculate_size(uint32_t &total_size) const = 0; // Default implementation for messages with no fields
virtual void calculate_size(ProtoSize &size) const {}
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
std::string dump() const; std::string dump() const;
virtual void dump_to(std::string &out) const = 0; virtual void dump_to(std::string &out) const = 0;
virtual const char *message_name() const { return "unknown"; }
#endif #endif
};
// Base class for messages that support decoding
class ProtoDecodableMessage : public ProtoMessage {
public:
void decode(const uint8_t *buffer, size_t length);
protected: protected:
virtual bool decode_varint(uint32_t field_id, ProtoVarInt value) { return false; } 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_length(uint32_t field_id, ProtoLengthDelimited value) { return false; }
virtual bool decode_32bit(uint32_t field_id, Proto32Bit 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 {
private:
uint32_t total_size_ = 0;
public:
/**
* @brief ProtoSize class for Protocol Buffer serialization size calculation
*
* This class provides methods to calculate the exact byte counts needed
* for encoding various Protocol Buffer field types. The class now uses an
* object-based approach to reduce parameter passing overhead while keeping
* varint calculation methods static for external use.
*
* Implements Protocol Buffer encoding size calculation according to:
* https://protobuf.dev/programming-guides/encoding/
*
* Key features:
* - Object-based approach reduces flash usage by eliminating parameter passing
* - Early-return optimization for zero/default values
* - Static varint methods for external callers
* - Specialized handling for different field types according to protobuf spec
*/
ProtoSize() = default;
uint32_t get_size() const { return total_size_; }
/**
* @brief Calculates the size in bytes needed to encode a uint32_t value as a varint
*
* @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 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
*/
inline void add_int32(uint32_t field_id_size, int32_t value) {
if (value != 0) {
add_int32_force(field_id_size, value);
}
}
/**
* @brief Calculates and adds the size of an int32 field to the total message size (force version)
*/
inline void add_int32_force(uint32_t field_id_size, int32_t value) {
// Always calculate size when forced
// Negative values are encoded as 10-byte varints in protobuf
total_size_ += field_id_size + (value < 0 ? 10 : varint(static_cast<uint32_t>(value)));
}
/**
* @brief Calculates and adds the size of a uint32 field to the total message size
*/
inline void add_uint32(uint32_t field_id_size, uint32_t value) {
if (value != 0) {
add_uint32_force(field_id_size, value);
}
}
/**
* @brief Calculates and adds the size of a uint32 field to the total message size (force version)
*/
inline void add_uint32_force(uint32_t field_id_size, uint32_t value) {
// Always calculate size when force is true
total_size_ += field_id_size + varint(value);
}
/**
* @brief Calculates and adds the size of a boolean field to the total message size
*/
inline void add_bool(uint32_t field_id_size, bool value) {
if (value) {
// 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 (force version)
*/
inline void add_bool_force(uint32_t field_id_size, bool value) {
// Always calculate size when force is true
// Boolean fields always use 1 byte
total_size_ += field_id_size + 1;
}
/**
* @brief Calculates and adds the size of a float field to the total message size
*/
inline void add_float(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
*/
inline void add_fixed32(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
*/
inline void add_sfixed32(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 a sint32 field to the total message size
*
* Sint32 fields use ZigZag encoding, which is more efficient for negative values.
*/
inline void add_sint32(uint32_t field_id_size, int32_t value) {
if (value != 0) {
add_sint32_force(field_id_size, value);
}
}
/**
* @brief Calculates and adds the size of a sint32 field to the total message size (force version)
*
* Sint32 fields use ZigZag encoding, which is more efficient for negative values.
*/
inline void add_sint32_force(uint32_t field_id_size, int32_t value) {
// Always calculate size when force is true
// 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
*/
inline void add_int64(uint32_t field_id_size, int64_t value) {
if (value != 0) {
add_int64_force(field_id_size, value);
}
}
/**
* @brief Calculates and adds the size of an int64 field to the total message size (force version)
*/
inline void add_int64_force(uint32_t field_id_size, int64_t value) {
// Always calculate size when force is true
total_size_ += field_id_size + varint(value);
}
/**
* @brief Calculates and adds the size of a uint64 field to the total message size
*/
inline void add_uint64(uint32_t field_id_size, uint64_t value) {
if (value != 0) {
add_uint64_force(field_id_size, value);
}
}
/**
* @brief Calculates and adds the size of a uint64 field to the total message size (force version)
*/
inline void add_uint64_force(uint32_t field_id_size, uint64_t value) {
// Always calculate size when force is true
total_size_ += field_id_size + varint(value);
}
// NOTE: sint64 support functions (add_sint64_field, add_sint64_field_force) removed
// sint64 type is not supported by ESPHome API to reduce overhead on embedded systems
/**
* @brief Calculates and adds the size of a length-delimited field (string/bytes) to the total message size
*/
inline void add_length(uint32_t field_id_size, size_t len) {
if (len != 0) {
add_length_force(field_id_size, len);
}
}
/**
* @brief Calculates and adds the size of a length-delimited field (string/bytes) to the total message size (repeated
* field version)
*/
inline void add_length_force(uint32_t field_id_size, size_t len) {
// Always calculate size when force is true
// Field ID + length varint + data bytes
total_size_ += field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len);
}
/**
* @brief Adds a pre-calculated size directly to the total
*
* This is used when we can calculate the total size by multiplying the number
* of elements by the bytes per element (for repeated fixed-size types like float, fixed32, etc.)
*
* @param size The pre-calculated total size to add
*/
inline void add_precalculated_size(uint32_t size) { total_size_ += size; }
/**
* @brief Calculates and adds the size of a nested message field to the total message size
*
* 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
*/
inline void add_message_field(uint32_t field_id_size, uint32_t nested_size) {
if (nested_size != 0) {
add_message_field_force(field_id_size, nested_size);
}
}
/**
* @brief Calculates and adds the size of a nested message field to the total message size (force version)
*
* @param nested_size The pre-calculated size of the nested message
*/
inline void add_message_field_force(uint32_t field_id_size, uint32_t nested_size) {
// Always calculate size when force is true
// 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
*/
inline void add_message_object(uint32_t field_id_size, const ProtoMessage &message) {
// Calculate nested message size by creating a temporary ProtoSize
ProtoSize nested_calc;
message.calculate_size(nested_calc);
uint32_t nested_size = nested_calc.get_size();
// Use the base implementation with the calculated nested_size
add_message_field(field_id_size, nested_size);
}
/**
* @brief Calculates and adds the size of a nested message field to the total message size (force version)
*
* @param message The nested message object
*/
inline void add_message_object_force(uint32_t field_id_size, const ProtoMessage &message) {
// Calculate nested message size by creating a temporary ProtoSize
ProtoSize nested_calc;
message.calculate_size(nested_calc);
uint32_t nested_size = nested_calc.get_size();
// Use the base implementation with the calculated nested_size
add_message_field_force(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>
inline void add_repeated_message(uint32_t field_id_size, const std::vector<MessageType> &messages) {
// Skip if the vector is empty
if (messages.empty()) {
return;
}
// Use the force version for all messages in the repeated field
for (const auto &message : messages) {
add_message_object_force(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
ProtoSize msg_size;
value.calculate_size(msg_size);
uint32_t msg_length_bytes = msg_size.get_size();
// 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); template<typename T> const char *proto_enum_to_string(T value);
class ProtoService { class ProtoService {
@ -350,7 +799,9 @@ class ProtoService {
virtual bool is_authenticated() = 0; virtual bool is_authenticated() = 0;
virtual bool is_connection_setup() = 0; virtual bool is_connection_setup() = 0;
virtual void on_fatal_error() = 0; virtual void on_fatal_error() = 0;
#ifdef USE_API_PASSWORD
virtual void on_unauthenticated_access() = 0; virtual void on_unauthenticated_access() = 0;
#endif
virtual void on_no_setup_connection() = 0; virtual void on_no_setup_connection() = 0;
/** /**
* Create a buffer with a reserved size. * Create a buffer with a reserved size.
@ -360,13 +811,14 @@ class ProtoService {
* @return A ProtoWriteBuffer object with the reserved size. * @return A ProtoWriteBuffer object with the reserved size.
*/ */
virtual ProtoWriteBuffer create_buffer(uint32_t reserve_size) = 0; 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 bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) = 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 // 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; ProtoSize size;
msg.calculate_size(msg_size); msg.calculate_size(size);
uint32_t msg_size = size.get_size();
// Create a pre-sized buffer // Create a pre-sized buffer
auto buffer = this->create_buffer(msg_size); auto buffer = this->create_buffer(msg_size);
@ -377,7 +829,30 @@ class ProtoService {
// Send the buffer // Send the buffer
return this->send_buffer(buffer, message_type); return this->send_buffer(buffer, message_type);
} }
// Authentication helper methods
bool check_connection_setup_() {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return false;
}
return true;
}
bool check_authenticated_() {
#ifdef USE_API_PASSWORD
if (!this->check_connection_setup_()) {
return false;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return false;
}
return true;
#else
return this->check_connection_setup_();
#endif
}
}; };
} // namespace api } // namespace esphome::api
} // namespace esphome

View File

@ -3,78 +3,70 @@
#include "api_connection.h" #include "api_connection.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome { namespace esphome::api {
namespace api {
// Generate entity handler implementations using macros
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
bool InitialStateIterator::on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) { INITIAL_STATE_HANDLER(binary_sensor, binary_sensor::BinarySensor)
return this->client_->send_binary_sensor_state(binary_sensor);
}
#endif #endif
#ifdef USE_COVER #ifdef USE_COVER
bool InitialStateIterator::on_cover(cover::Cover *cover) { return this->client_->send_cover_state(cover); } INITIAL_STATE_HANDLER(cover, cover::Cover)
#endif #endif
#ifdef USE_FAN #ifdef USE_FAN
bool InitialStateIterator::on_fan(fan::Fan *fan) { return this->client_->send_fan_state(fan); } INITIAL_STATE_HANDLER(fan, fan::Fan)
#endif #endif
#ifdef USE_LIGHT #ifdef USE_LIGHT
bool InitialStateIterator::on_light(light::LightState *light) { return this->client_->send_light_state(light); } INITIAL_STATE_HANDLER(light, light::LightState)
#endif #endif
#ifdef USE_SENSOR #ifdef USE_SENSOR
bool InitialStateIterator::on_sensor(sensor::Sensor *sensor) { return this->client_->send_sensor_state(sensor); } INITIAL_STATE_HANDLER(sensor, sensor::Sensor)
#endif #endif
#ifdef USE_SWITCH #ifdef USE_SWITCH
bool InitialStateIterator::on_switch(switch_::Switch *a_switch) { return this->client_->send_switch_state(a_switch); } INITIAL_STATE_HANDLER(switch, switch_::Switch)
#endif #endif
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
bool InitialStateIterator::on_text_sensor(text_sensor::TextSensor *text_sensor) { INITIAL_STATE_HANDLER(text_sensor, text_sensor::TextSensor)
return this->client_->send_text_sensor_state(text_sensor);
}
#endif #endif
#ifdef USE_CLIMATE #ifdef USE_CLIMATE
bool InitialStateIterator::on_climate(climate::Climate *climate) { return this->client_->send_climate_state(climate); } INITIAL_STATE_HANDLER(climate, climate::Climate)
#endif #endif
#ifdef USE_NUMBER #ifdef USE_NUMBER
bool InitialStateIterator::on_number(number::Number *number) { return this->client_->send_number_state(number); } INITIAL_STATE_HANDLER(number, number::Number)
#endif #endif
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
bool InitialStateIterator::on_date(datetime::DateEntity *date) { return this->client_->send_date_state(date); } INITIAL_STATE_HANDLER(date, datetime::DateEntity)
#endif #endif
#ifdef USE_DATETIME_TIME #ifdef USE_DATETIME_TIME
bool InitialStateIterator::on_time(datetime::TimeEntity *time) { return this->client_->send_time_state(time); } INITIAL_STATE_HANDLER(time, datetime::TimeEntity)
#endif #endif
#ifdef USE_DATETIME_DATETIME #ifdef USE_DATETIME_DATETIME
bool InitialStateIterator::on_datetime(datetime::DateTimeEntity *datetime) { INITIAL_STATE_HANDLER(datetime, datetime::DateTimeEntity)
return this->client_->send_datetime_state(datetime);
}
#endif #endif
#ifdef USE_TEXT #ifdef USE_TEXT
bool InitialStateIterator::on_text(text::Text *text) { return this->client_->send_text_state(text); } INITIAL_STATE_HANDLER(text, text::Text)
#endif #endif
#ifdef USE_SELECT #ifdef USE_SELECT
bool InitialStateIterator::on_select(select::Select *select) { return this->client_->send_select_state(select); } INITIAL_STATE_HANDLER(select, select::Select)
#endif #endif
#ifdef USE_LOCK #ifdef USE_LOCK
bool InitialStateIterator::on_lock(lock::Lock *a_lock) { return this->client_->send_lock_state(a_lock); } INITIAL_STATE_HANDLER(lock, lock::Lock)
#endif #endif
#ifdef USE_VALVE #ifdef USE_VALVE
bool InitialStateIterator::on_valve(valve::Valve *valve) { return this->client_->send_valve_state(valve); } INITIAL_STATE_HANDLER(valve, valve::Valve)
#endif #endif
#ifdef USE_MEDIA_PLAYER #ifdef USE_MEDIA_PLAYER
bool InitialStateIterator::on_media_player(media_player::MediaPlayer *media_player) { INITIAL_STATE_HANDLER(media_player, media_player::MediaPlayer)
return this->client_->send_media_player_state(media_player);
}
#endif #endif
#ifdef USE_ALARM_CONTROL_PANEL #ifdef USE_ALARM_CONTROL_PANEL
bool InitialStateIterator::on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { INITIAL_STATE_HANDLER(alarm_control_panel, alarm_control_panel::AlarmControlPanel)
return this->client_->send_alarm_control_panel_state(a_alarm_control_panel);
}
#endif #endif
#ifdef USE_UPDATE #ifdef USE_UPDATE
bool InitialStateIterator::on_update(update::UpdateEntity *update) { return this->client_->send_update_state(update); } INITIAL_STATE_HANDLER(update, update::UpdateEntity)
#endif #endif
// Special cases (button and event) are already defined inline in subscribe_state.h
InitialStateIterator::InitialStateIterator(APIConnection *client) : client_(client) {} InitialStateIterator::InitialStateIterator(APIConnection *client) : client_(client) {}
} // namespace api } // namespace esphome::api
} // namespace esphome
#endif #endif

View File

@ -5,76 +5,82 @@
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/component_iterator.h" #include "esphome/core/component_iterator.h"
#include "esphome/core/controller.h" #include "esphome/core/controller.h"
namespace esphome { namespace esphome::api {
namespace api {
class APIConnection; class APIConnection;
// Macro for generating InitialStateIterator handlers
// Calls send_*_state
#define INITIAL_STATE_HANDLER(entity_type, EntityClass) \
bool InitialStateIterator::on_##entity_type(EntityClass *entity) { /* NOLINT(bugprone-macro-parentheses) */ \
return this->client_->send_##entity_type##_state(entity); \
}
class InitialStateIterator : public ComponentIterator { class InitialStateIterator : public ComponentIterator {
public: public:
InitialStateIterator(APIConnection *client); InitialStateIterator(APIConnection *client);
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) override; bool on_binary_sensor(binary_sensor::BinarySensor *entity) override;
#endif #endif
#ifdef USE_COVER #ifdef USE_COVER
bool on_cover(cover::Cover *cover) override; bool on_cover(cover::Cover *entity) override;
#endif #endif
#ifdef USE_FAN #ifdef USE_FAN
bool on_fan(fan::Fan *fan) override; bool on_fan(fan::Fan *entity) override;
#endif #endif
#ifdef USE_LIGHT #ifdef USE_LIGHT
bool on_light(light::LightState *light) override; bool on_light(light::LightState *entity) override;
#endif #endif
#ifdef USE_SENSOR #ifdef USE_SENSOR
bool on_sensor(sensor::Sensor *sensor) override; bool on_sensor(sensor::Sensor *entity) override;
#endif #endif
#ifdef USE_SWITCH #ifdef USE_SWITCH
bool on_switch(switch_::Switch *a_switch) override; bool on_switch(switch_::Switch *entity) override;
#endif #endif
#ifdef USE_BUTTON #ifdef USE_BUTTON
bool on_button(button::Button *button) override { return true; }; bool on_button(button::Button *button) override { return true; };
#endif #endif
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
bool on_text_sensor(text_sensor::TextSensor *text_sensor) override; bool on_text_sensor(text_sensor::TextSensor *entity) override;
#endif #endif
#ifdef USE_CLIMATE #ifdef USE_CLIMATE
bool on_climate(climate::Climate *climate) override; bool on_climate(climate::Climate *entity) override;
#endif #endif
#ifdef USE_NUMBER #ifdef USE_NUMBER
bool on_number(number::Number *number) override; bool on_number(number::Number *entity) override;
#endif #endif
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
bool on_date(datetime::DateEntity *date) override; bool on_date(datetime::DateEntity *entity) override;
#endif #endif
#ifdef USE_DATETIME_TIME #ifdef USE_DATETIME_TIME
bool on_time(datetime::TimeEntity *time) override; bool on_time(datetime::TimeEntity *entity) override;
#endif #endif
#ifdef USE_DATETIME_DATETIME #ifdef USE_DATETIME_DATETIME
bool on_datetime(datetime::DateTimeEntity *datetime) override; bool on_datetime(datetime::DateTimeEntity *entity) override;
#endif #endif
#ifdef USE_TEXT #ifdef USE_TEXT
bool on_text(text::Text *text) override; bool on_text(text::Text *entity) override;
#endif #endif
#ifdef USE_SELECT #ifdef USE_SELECT
bool on_select(select::Select *select) override; bool on_select(select::Select *entity) override;
#endif #endif
#ifdef USE_LOCK #ifdef USE_LOCK
bool on_lock(lock::Lock *a_lock) override; bool on_lock(lock::Lock *entity) override;
#endif #endif
#ifdef USE_VALVE #ifdef USE_VALVE
bool on_valve(valve::Valve *valve) override; bool on_valve(valve::Valve *entity) override;
#endif #endif
#ifdef USE_MEDIA_PLAYER #ifdef USE_MEDIA_PLAYER
bool on_media_player(media_player::MediaPlayer *media_player) override; bool on_media_player(media_player::MediaPlayer *entity) override;
#endif #endif
#ifdef USE_ALARM_CONTROL_PANEL #ifdef USE_ALARM_CONTROL_PANEL
bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) override; bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *entity) override;
#endif #endif
#ifdef USE_EVENT #ifdef USE_EVENT
bool on_event(event::Event *event) override { return true; }; bool on_event(event::Event *event) override { return true; };
#endif #endif
#ifdef USE_UPDATE #ifdef USE_UPDATE
bool on_update(update::UpdateEntity *update) override; bool on_update(update::UpdateEntity *entity) override;
#endif #endif
bool completed() { return this->state_ == IteratorState::NONE; } bool completed() { return this->state_ == IteratorState::NONE; }
@ -82,6 +88,5 @@ class InitialStateIterator : public ComponentIterator {
APIConnection *client_; APIConnection *client_;
}; };
} // namespace api } // namespace esphome::api
} // namespace esphome
#endif #endif

View File

@ -1,8 +1,7 @@
#include "user_services.h" #include "user_services.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome { namespace esphome::api {
namespace api {
template<> bool get_execute_arg_value<bool>(const ExecuteServiceArgument &arg) { return arg.bool_; } template<> bool get_execute_arg_value<bool>(const ExecuteServiceArgument &arg) { return arg.bool_; }
template<> int32_t get_execute_arg_value<int32_t>(const ExecuteServiceArgument &arg) { template<> int32_t get_execute_arg_value<int32_t>(const ExecuteServiceArgument &arg) {
@ -40,5 +39,4 @@ template<> enums::ServiceArgType to_service_arg_type<std::vector<std::string>>()
return enums::SERVICE_ARG_TYPE_STRING_ARRAY; return enums::SERVICE_ARG_TYPE_STRING_ARRAY;
} }
} // namespace api } // namespace esphome::api
} // namespace esphome

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