Compare commits

...

1244 Commits

Author SHA1 Message Date
J. Nick Koston
637513694f Merge remote-tracking branch 'upstream/dev' into remove_posix_tz_parser
# Conflicts:
#	esphome/components/time/posix_tz.cpp
#	esphome/components/time/posix_tz.h
2026-04-01 18:46:04 -10:00
J. Nick Koston
3fbf0f0c01 [api] Simplify encode_to_buffer to single resize call (#15355) 2026-04-02 03:13:09 +00:00
J. Nick Koston
1436d034bf [api] Inline DeferredBatch::add_item to eliminate push_back call barrier (#15353) 2026-04-02 03:11:47 +00:00
J. Nick Koston
08c7b3afbd [esp32_ble_tracker] Reduce scan cycle log spam (#15365) 2026-04-01 16:53:53 -10:00
J. Nick Koston
f36d78e09c [core] Force inline Component::get_component_log_str() (#15363) 2026-04-01 16:15:00 -10:00
J. Nick Koston
be56be5201 [core] Reduce runtime_stats measurement overhead (#15359) 2026-04-01 16:14:45 -10:00
J. Nick Koston
bcc7b8f490 [api] Add send_sensor_state benchmarks (#15352) 2026-04-01 16:12:02 -10:00
J. Nick Koston
27c662e73f [bluetooth_proxy] Replace loop() with set_interval for advertisement flushing (#15347) 2026-04-01 16:11:50 -10:00
Clyde Stubbs
eefbb42be4 [lvgl] Add missing event names (#15362) 2026-04-02 14:16:56 +13:00
dependabot[bot]
b5c4449a16 Bump pillow from 12.1.1 to 12.2.0 (#15361)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-01 14:11:44 -10:00
Boris Krivonog
5cdbbd4887 [mitsubishi_cn105] Add climate component for Mitsubishi A/C units with CN105 connector (Part 1) (#15315)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2026-04-01 11:48:47 -10:00
Clyde Stubbs
bdce47e764 [lvgl] Fixes #4 (#15334) 2026-04-02 10:39:51 +13:00
Jesse Hills
813b142b72 Merge branch 'release' into dev 2026-04-02 09:07:41 +13:00
Jesse Hills
b7dabe236e Merge pull request #15342 from esphome/bump-2026.3.2
2026.3.2
2026-04-02 09:06:55 +13:00
Jonathan Swoboda
2e3ea2152d [esp32_camera] Bump esp32-camera to v2.1.6 (#15349) 2026-04-01 07:13:23 -10:00
J. Nick Koston
ea609d3552 [runtime_stats] Store stats inline on Component to eliminate std::map lookup (#15345) 2026-04-01 07:09:04 -10:00
Gonçalo Pereira
f33fd047ee [hdc2080] Add support for HDC2080 sensor (#9331)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Co-authored-by: Big Mike <mikelawrence@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-04-01 12:09:22 -04:00
tomaszduda23
cc88896280 [debug] add peripherals status (#12053)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-04-01 15:04:22 +00:00
Edward Firmo
fbfb5d401f [nextion] Fix memory leak in reset_() (#15344)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-03-31 22:34:29 -10:00
Rene Guca
212b3e1688 [cover] move time_based_cover to its own subdirectory (#15313)
Co-authored-by: Rene <rene@guca.at>
2026-03-31 21:59:24 -04:00
Kevin Ahrendt
31a70ab299 [resampler] Future-proof resampler task to avoid potential memory leaks (#15186) 2026-03-31 21:44:54 -04:00
Christian H
8f2cf8b8a7 [bmp581_base] Add support for BMP585 (#15277)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-03-31 21:39:41 -04:00
Jesse Hills
600ca01fd3 Bump version to 2026.3.2 2026-04-01 13:18:24 +13:00
J. Nick Koston
65051153ac [esp32_ble_tracker] Restart BLE scan after OTA failure (#15308)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2026-04-01 13:18:24 +13:00
Kevin Ahrendt
514c0c8331 [mixer] Fix memory leak in mixer task on stop/start cycles (#15185) 2026-04-01 13:18:24 +13:00
Edward Firmo
dc634b8c7b [uart] fix baud rate not applied on load_settings() for ESP32 (IDF) (#15341) 2026-04-01 13:18:24 +13:00
Jonathan Swoboda
66a4acafd0 [tormatic] Fix UART stream desync on ESP32 (#15337) 2026-04-01 13:18:24 +13:00
Jonathan Swoboda
3bf45d8fe0 [haier] Fix hOn half-degree temperature setting (#15312) 2026-04-01 13:18:24 +13:00
Keith Burzinski
9cd7c5e700 [thermostat] Fix stale max_runtime_exceeded causing spurious supplemental heating/cooling (#15274) 2026-04-01 13:18:24 +13:00
J. Nick Koston
d79cf1d718 [esp8266] Add enable_scanf_float option (#15284) 2026-04-01 13:18:24 +13:00
J. Nick Koston
3d8a3a91f2 [esp32_ble_server] Fix set_value action with static data lists (#15285) 2026-04-01 13:18:24 +13:00
Jonathan Swoboda
3fd3dcc7e5 [sgp4x] Fix NOx index_offset default (should be 1, not 100) (#15212) 2026-04-01 13:18:23 +13:00
Jonathan Swoboda
7b5a4b466a [uart] Fix debug callback missing peeked byte and reading past end (#15169) 2026-04-01 13:18:23 +13:00
Jonathan Swoboda
92642df419 [wifi] Filter fast_connect by band_mode and use background scan for roaming (#15152) 2026-04-01 13:18:23 +13:00
J. Nick Koston
f5f99071fb [wifi] Fix roaming counter reset from delayed disconnect and successful retry (#15126) 2026-04-01 13:18:23 +13:00
J. Nick Koston
cb15e98765 [datetime] Fix state_as_esptime() returning invalid timestamp (#15128) 2026-04-01 13:18:23 +13:00
Jonathan Swoboda
2f2c7ac393 [sx127x] Fix FIFO read corruption (#15114) 2026-04-01 13:18:23 +13:00
J. Nick Koston
d9788aaefc [wifi] Reduce ESP8266 roaming scan dwell time to match ESP32 (#15127) 2026-04-01 13:18:23 +13:00
J. Nick Koston
f7b410fd0c [wifi] Fix roaming attempt counter reset on disconnect during scan (#15099) 2026-04-01 13:18:23 +13:00
J. Nick Koston
e261b5de65 [time] Point to valid IANA timezone list on validation failure (#15110) 2026-04-01 13:18:23 +13:00
J. Nick Koston
954227b203 [esp32_ble_tracker] Restart BLE scan after OTA failure (#15308)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2026-03-31 23:26:26 +00:00
Kevin Ahrendt
4a23ba7d8a [mixer] Fix memory leak in mixer task on stop/start cycles (#15185) 2026-04-01 12:06:48 +13:00
Edward Firmo
b71c406e70 [uart] fix baud rate not applied on load_settings() for ESP32 (IDF) (#15341) 2026-04-01 12:04:07 +13:00
Jesse Hills
15bcd62f22 [internal_temperature] Move code into platform specific files (#15339) 2026-04-01 11:59:53 +13:00
J. Nick Koston
23dcc5389d [time] Fix strftime %Z and %z returning wrong timezone (#15330) 2026-04-01 11:59:45 +13:00
Jonathan Swoboda
9dca7e0daf [tormatic] Fix UART stream desync on ESP32 (#15337) 2026-03-31 18:01:33 -04:00
Clyde Stubbs
66b6d36a26 [lvgl] Fixes #3 (#15304)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2026-04-01 10:04:10 +13:00
Jonathan Swoboda
2064eef273 [esp32_hosted] Guard against empty firmware URL in perform() (#15338) 2026-03-31 10:53:12 -10:00
dependabot[bot]
64e836f9c8 Bump CodSpeedHQ/action from 4.12.1 to 4.13.0 (#15340)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 10:49:17 -10:00
Bonne Eggleston
2cb987095d [modbus] Share helper functions across modbus components - part B (#14172)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-03-31 10:48:16 -10:00
Clyde Stubbs
da6c4e20fe [lvgl] Fixes #2 (#15161) 2026-04-01 09:29:57 +13:00
Keith Burzinski
26b426bbff [zwave_proxy] Clear Home ID on USB modem disconnect (#15327) 2026-03-31 14:34:16 -05:00
J. Nick Koston
2449aa75af [http_request] Fix crash when esp_http_client_init fails (#15328) 2026-03-31 07:45:23 -10:00
J. Nick Koston
2c9a3051d6 [api] Use memcpy for fixed32 decode on little-endian platforms (#15292) 2026-03-31 07:43:18 -10:00
J. Nick Koston
9b97e95cf3 [binary_sensor] Add on_multi_click integration test (#15329) 2026-03-31 07:42:12 -10:00
J. Nick Koston
c64bc24960 [preferences] Reduce log verbosity for unchanged NVS/FDB writes (#15332) 2026-03-31 07:34:54 -10:00
Jonathan Swoboda
ceb3cb2ae7 [haier] Fix hOn half-degree temperature setting (#15312) 2026-03-31 15:22:29 +00:00
J. Nick Koston
a3913b98ba [wifi] Move LibreTiny WiFi STA state to member variable (#15305) 2026-03-30 17:05:48 -10:00
Guillermo Ruffino
ef65e47bc5 [schema] generator fixes (#15276) 2026-03-31 13:08:50 +13:00
Jonathan Swoboda
53b2a03c80 [multiple] Fix -Wformat and -Wextra warnings across 33 component files (#15321) 2026-03-30 18:56:05 -04:00
dependabot[bot]
58df755d8b Bump requests from 2.33.0 to 2.33.1 (#15324) 2026-03-30 12:27:30 -10:00
Ardumine
c5eb0eb984 [internal_temperature] Add nRF52 Zephyr support (#15297) 2026-03-31 10:50:11 +13:00
Clyde Stubbs
f25fa71235 [lvgl] Fix align_to directives (#15311) 2026-03-31 07:25:15 +11:00
J. Nick Koston
8561a8c495 [core] Suppress component source overflow warnings in testing mode (#15320) 2026-03-30 08:48:04 -10:00
J. Nick Koston
8688ef7125 [benchmark] Fix decode benchmarks being optimized away by compiler (#15293) 2026-03-30 08:24:48 -10:00
J. Nick Koston
46ea61666e [wifi] Replace FreeRTOS queue with LockFreeQueue on ESP-IDF (#15306) 2026-03-30 08:24:34 -10:00
J. Nick Koston
8969eb76e9 [wifi] Avoid redundant SDK calls in WiFi loop on ESP8266 (#15303) 2026-03-30 08:24:17 -10:00
J. Nick Koston
ffee4c22b3 [esp32_ble] Devirtualize BLE event handler dispatch (#15310) 2026-03-30 08:21:58 -10:00
J. Nick Koston
ad3f6ae313 [automation] Remove actions_end_ pointer from ActionList to save RAM (#15283) 2026-03-30 08:20:52 -10:00
Keith Burzinski
b579758c46 [dht] Code clean-up (#15271) 2026-03-30 13:15:37 -05:00
Keith Burzinski
45e6d49d36 [shtcx] Code clean-up (#15261) 2026-03-30 13:15:27 -05:00
Keith Burzinski
ddb188e8f0 [bme68x_bsec2] Fix warning spam, code clean-up (#15258) 2026-03-30 13:15:13 -05:00
Keith Burzinski
1a86e88373 [thermostat] Fix stale max_runtime_exceeded causing spurious supplemental heating/cooling (#15274) 2026-03-30 13:15:02 -05:00
Bonne Eggleston
31574a427b [modbus] Share helper functions across modbus components - part A (#15291)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-30 06:56:47 -10:00
Edward Firmo
1bc6a8d956 [nextion] Fix queue age check using inconsistent time sources (#15317) 2026-03-30 06:54:09 -10:00
J. Nick Koston
d420e7bc23 [modbus_controller] Fix off-by-one bounds check in byte_from_hex_str (#15301) 2026-03-30 08:57:27 -04:00
dependabot[bot]
cd3c2ae77e Bump aioesphomeapi from 44.8.0 to 44.8.1 (#15309)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-29 22:45:46 -10:00
Edward Firmo
95b0e60617 [nextion] Add accessor const qualifiers, return by ref, and deprecate get_wave_chan_id() (#15204)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-03-30 02:20:04 -05:00
Edward Firmo
ffbbe5eab3 [nextion] Fix log level for command processing limit message (#15302) 2026-03-30 01:55:40 -05:00
J. Nick Koston
18168ad7fd [sensor] Use std::array in CalibrateLinearFilter (#15263) 2026-03-29 15:07:15 -10:00
J. Nick Koston
17afbeb87b [binary_sensor] Use std::array in MultiClickTrigger (#15267) 2026-03-29 14:57:15 -10:00
J. Nick Koston
d51b047f63 [sensor] Use std::array in CalibratePolynomialFilter (#15264) 2026-03-29 14:56:04 -10:00
J. Nick Koston
508ec295a4 [sensor] Use std::array in OrFilter (#15262) 2026-03-29 14:55:46 -10:00
J. Nick Koston
66754fa376 [text_sensor] Use std::array in SubstituteFilter (#15266) 2026-03-29 14:24:32 -10:00
J. Nick Koston
4da7f5ecc2 [binary_sensor] Use std::array in AutorepeatFilter (#15268) 2026-03-29 23:50:46 +00:00
J. Nick Koston
29419d9d97 [automation] Use std::array in And/Or/Xor conditions (#15282) 2026-03-29 13:36:08 -10:00
J. Nick Koston
3520ef7480 [text_sensor] Use std::array in MapFilter (#15269) 2026-03-29 22:38:04 +00:00
J. Nick Koston
d6475eaeed [binary_sensor] Remove redundant optional<bool> state_, save 8 bytes per instance (#15095) 2026-03-29 12:15:18 -10:00
J. Nick Koston
a9aaf29d83 [core] Shrink Component from 12 to 8 bytes per instance (#15103) 2026-03-29 12:09:21 -10:00
J. Nick Koston
38fa8925da [ai] Add automation, callback manager, and test grouping docs (#15243) 2026-03-29 12:02:47 -10:00
J. Nick Koston
c2b8ea3361 [web_server_base] Reduce sizeof(WebServerBase) by 4 bytes (#15251) 2026-03-29 18:02:29 -04:00
J. Nick Koston
584807b039 [ld2410] Fix flaky integration test race condition (#15299) 2026-03-29 11:58:03 -10:00
J. Nick Koston
5da3253f4b [esp8266] Add enable_scanf_float option (#15284) 2026-03-29 11:57:52 -10:00
J. Nick Koston
2a97eca00b [sensor] Use std::array in ValueList/FilterOut/ThrottleWithPriority filters (#15265) 2026-03-29 11:55:52 -10:00
J. Nick Koston
1f3fd60d29 [version] Remove duplicate build_info_data.h include (#15288) 2026-03-29 11:55:39 -10:00
J. Nick Koston
8a802ca666 [benchmark] Add BLE raw advertisement proto encode benchmarks (#15289) 2026-03-29 11:54:07 -10:00
J. Nick Koston
a91e6d92f6 [core] Remove dead get_loop_priority code (#15242) 2026-03-29 17:32:43 -04:00
Tobias Stanzel
d9adb078aa [tm1637] Add buffer manipulation methods (#13686)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-03-29 14:41:00 -03:00
J. Nick Koston
7a7c33fdb1 [esp32_ble_server] Fix set_value action with static data lists (#15285) 2026-03-28 15:38:06 -10:00
Jonathan Swoboda
b6abfec82e [core] Fix area/device hash collision validation not running (#15259) 2026-03-27 22:22:24 -04:00
Jonathan Swoboda
47774fb644 [modbus_controller] Fix wrong enum in function_code_to_register (#15253) 2026-03-27 19:55:57 -04:00
Jonathan Swoboda
34410e92b7 [as5600] Remove dead angle/position sensor code (#15254) 2026-03-27 19:55:40 -04:00
Edward Firmo
a99f051e19 [nextion] Replace queue name string literals with short Nextion-native identifiers (#15215)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-03-27 13:49:00 -10:00
Keith Burzinski
f6c63c62e4 [tmp117] Code clean-up (#15260) 2026-03-27 17:59:26 -05:00
Jonathan Swoboda
76d75850a3 [sgp4x] Remove dead voc_baseline config option (#15250) 2026-03-27 17:35:12 -04:00
Jonathan Swoboda
68d9f657ad [bl0940] Fix energy reference default using wrong constant in legacy mode (#15249) 2026-03-27 21:32:37 +00:00
Jonathan Swoboda
24b8a95340 [pid] Remove unused PIDSimulator class (#15247) 2026-03-27 17:24:15 -04:00
Jonathan Swoboda
d245b9f123 [sm2135] Fix copy-paste error in setup pin mode (#15248) 2026-03-27 17:24:03 -04:00
Edward Firmo
a2dee21e8e [nextion] Replace std::deque queues with std::list (#15211) 2026-03-27 10:24:19 -10:00
dependabot[bot]
3016cd3636 Bump github/codeql-action from 4.34.1 to 4.35.1 (#15245)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-27 09:29:08 -10:00
Jonathan Swoboda
7532e1f957 [multiple] Fix uninitialized members and error constant types (#15235) 2026-03-27 14:58:41 -04:00
Jonathan Swoboda
f0db0c1054 [esp32] Add ESP-IDF 5.5.4 and 6.0.0 version mappings (#15241) 2026-03-27 14:48:08 -04:00
Jonathan Swoboda
05c15f4241 [remote_base] Fix gobox uint64_t format specifier (#15237) 2026-03-27 08:44:40 -10:00
Jonathan Swoboda
951ad91cb2 [atm90e32] Fix phase angle precision loss and remove unused member (#15238) 2026-03-27 08:39:30 -10:00
Jonathan Swoboda
53bd57f3c2 [pid] Fix inverted debug log conditions and broken smoothing formula (#15240) 2026-03-27 08:37:54 -10:00
Jonathan Swoboda
4b9467cd0c [esp32_ble_client] Fix wrong union member in OPEN_EVT handler (#15236) 2026-03-27 08:37:33 -10:00
Jonathan Swoboda
0a607b9c93 [esp32_ble_server] Fix wrong union member in STOP_EVT handler (#15239) 2026-03-27 08:36:16 -10:00
Jonathan Swoboda
810c046cc6 [multiple] Fix misc hardware register bugs (#15208) 2026-03-27 14:25:38 -04:00
J. Nick Koston
5a8d6931a8 [factory_reset] Migrate FastBootTrigger to callback automation (#15232) 2026-03-27 08:24:35 -10:00
J. Nick Koston
0d67f91fac [rf_bridge] Migrate triggers to callback automation (#15231) 2026-03-27 08:24:25 -10:00
J. Nick Koston
f9d41bd36a [modbus_controller] Migrate triggers to callback automation (#15230) 2026-03-27 08:24:15 -10:00
J. Nick Koston
39509265bc [haier] Migrate triggers to callback automation (#15229) 2026-03-27 08:24:03 -10:00
J. Nick Koston
2f3c21c7c1 [ezo] Migrate triggers to callback automation (#15228) 2026-03-27 08:23:50 -10:00
J. Nick Koston
d77bf23c76 [nextion] Migrate triggers to callback automation (#15227) 2026-03-27 08:23:37 -10:00
J. Nick Koston
f5cd1e5e76 [ld2450] Fix flaky integration test race condition (#15226) 2026-03-27 08:23:26 -10:00
J. Nick Koston
a73c67e476 [ltr501] Migrate triggers to callback automation (#15225) 2026-03-27 08:23:17 -10:00
J. Nick Koston
a95f9f41fb [ltr_als_ps] Migrate triggers to callback automation (#15224) 2026-03-27 08:22:58 -10:00
J. Nick Koston
6ffb5af60c [fingerprint_grow] Migrate triggers to callback automation (#15223) 2026-03-27 08:22:47 -10:00
J. Nick Koston
a5416df615 [sim800l] Migrate triggers to callback automation (#15222) 2026-03-27 08:22:36 -10:00
J. Nick Koston
985477f2cf [pn7150][pn7160] Migrate triggers to callback automation (#15221) 2026-03-27 08:22:25 -10:00
J. Nick Koston
a4a8fa3027 [pn532] Migrate PN532OnFinishedWriteTrigger to callback automation (#15220) 2026-03-27 08:22:14 -10:00
J. Nick Koston
623408bbfe [hlk_fm22x] Migrate triggers to callback automation (#15219) 2026-03-27 08:22:02 -10:00
J. Nick Koston
514df6c99a [dfplayer] Migrate FinishedPlaybackTrigger to callback automation (#15218) 2026-03-27 08:21:52 -10:00
J. Nick Koston
54283a2599 [rotary_encoder] Migrate triggers to callback automation (#15217) 2026-03-27 08:21:41 -10:00
J. Nick Koston
4493d2efb6 [online_image] Migrate triggers to callback automation (#15216) 2026-03-27 08:21:27 -10:00
J. Nick Koston
83b3187126 [rtttl] Migrate FinishedPlaybackTrigger to callback automation (#15202) 2026-03-27 08:21:16 -10:00
J. Nick Koston
a2d452684a [ld2450] Migrate LD2450DataTrigger to callback automation (#15201)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 08:21:03 -10:00
J. Nick Koston
2e42547d32 [media_player] Migrate triggers to callback automation (#15200)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 08:20:46 -10:00
J. Nick Koston
dea8fdd906 [lock] Migrate LockStateTrigger to callback automation (#15199) 2026-03-27 08:20:35 -10:00
J. Nick Koston
b41634e19a [alarm_control_panel] Migrate triggers to callback automation (#15198) 2026-03-27 08:20:24 -10:00
J. Nick Koston
b0f6a94df5 [sml] Migrate DataTrigger to callback automation (#15233) 2026-03-27 08:20:11 -10:00
J. Nick Koston
1e65165e48 [safe_mode] Migrate SafeModeTrigger to callback automation (#15197) 2026-03-27 08:19:58 -10:00
Jonathan Swoboda
73e939ffb5 [sgp4x] Fix NOx index_offset default (should be 1, not 100) (#15212) 2026-03-27 14:13:24 -04:00
Diorcet Yann
2d9922496c [git] Add support for subpath to computed destination directory (#15135) 2026-03-27 12:02:45 -04:00
Edward Firmo
6feb2d04df [nextion] Replace static std::string COMMAND_DELIMITER with constexpr (#15195)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-03-26 17:36:35 -10:00
J. Nick Koston
90dafa3fa4 [logger] Warn when VERBOSE/VERY_VERBOSE logging is active (#15189) 2026-03-27 01:59:58 +00:00
J. Nick Koston
e77cdb5971 [light] Validate effect names during config validation instead of codegen (#15107) 2026-03-26 15:13:44 -10:00
J. Nick Koston
90e6c0d7c7 [core] Remove indirection from ControllerRegistry dispatch (#15173) 2026-03-26 15:09:16 -10:00
J. Nick Koston
240e53afce [fan] Add benchmarks for fan component (#15210) 2026-03-26 14:35:09 -10:00
J. Nick Koston
fa8a609bcc [automation] Eliminate trigger trampolines with deduplicated forwarder structs (#15174) 2026-03-26 13:50:50 -10:00
dependabot[bot]
6aafb521c1 Bump ruff from 0.15.7 to 0.15.8 (#15192)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-26 19:59:21 +00:00
Edward Firmo
81f0aa1168 [nextion] Replace or/and operators and missing this-> (#15191) 2026-03-26 09:54:50 -10:00
dependabot[bot]
3152642571 Bump codecov/codecov-action from 5.5.3 to 6.0.0 (#15194)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-26 09:48:06 -10:00
dependabot[bot]
1e2c410abf Bump cryptography from 46.0.5 to 46.0.6 (#15193)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-26 09:47:18 -10:00
J. Nick Koston
a008c27fcf [climate] Avoid duplicate get_traits() in publish_state (#15181)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-26 15:01:08 -04:00
Clyde Stubbs
1edf952dda [font] Add unit tests verifying correct processing of glyphs (#15178) 2026-03-26 14:59:06 -04:00
Edward Firmo
d9ada4536c [nextion] Fix leading space in pressed color string commands (#15190) 2026-03-26 14:58:12 -04:00
Jonathan Swoboda
bf89a191f0 [wifi] Guard coex_background_scan with CONFIG_SOC_WIFI_SUPPORTED (#15187) 2026-03-26 13:39:35 -04:00
Jonathan Swoboda
c2456409bd [core] Improve clean-all with no arguments (#15184) 2026-03-26 13:39:19 -04:00
J. Nick Koston
02e23eb386 [benchmark] Add light call and publish benchmarks (#15176) 2026-03-26 07:33:10 -10:00
J. Nick Koston
6898284361 [benchmark] Add cover publish_state and call benchmarks (#15179) 2026-03-26 07:32:54 -10:00
J. Nick Koston
f3a31be6d0 [benchmark] Add climate publish_state and call benchmarks (#15180) 2026-03-26 07:32:39 -10:00
Daniel Kent
9260401747 [bmp581] Add SPI support for BMP581 (#13124)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-03-26 12:11:46 -04:00
J. Nick Koston
176e9d43c9 update tests 2026-03-25 20:54:12 -10:00
J. Nick Koston
c55330c27e Merge remote-tracking branch 'upstream/dev' into remove_posix_tz_parser
# Conflicts:
#	esphome/components/api/api_pb2_dump.cpp
2026-03-25 20:33:26 -10:00
J. Nick Koston
8a6b009173 [light] Move normal state logging to VERBOSE (#15177) 2026-03-26 15:53:33 +13:00
Keith Burzinski
676ac9d8b8 [infrared][ir_rf_proxy] Add receiver_frequency config for IR receiver demodulation frequency (#15156)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-26 15:30:46 +13:00
J. Nick Koston
29e263ad7d [esp32] Wrap vfprintf to fix printf stub on picolibc (IDF 6) (#15172) 2026-03-25 19:43:01 -04:00
Jonathan Swoboda
a075f63b59 [uart] Fix debug callback missing peeked byte and reading past end (#15169) 2026-03-25 16:50:37 -04:00
J. Nick Koston
ec60da893f [core] Move state logging to client-side formatting, console to VERBOSE (#15155) 2026-03-25 19:45:06 +00:00
dependabot[bot]
d8fbce365a Bump requests from 2.32.5 to 2.33.0 (#15170)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-25 09:38:20 -10:00
Jonathan Swoboda
f6c5767a83 [inkplate] Use atomic GPIO write to prevent ISR race (#15166) 2026-03-25 14:10:28 -04:00
Jonathan Swoboda
19615f2eae [bme68x_bsec2] Fix uninitialized bme68x_conf in measurement duration calculation (#15168) 2026-03-25 14:10:04 -04:00
Jonathan Swoboda
c42c6745b9 [mcp9600] Fix setup success check using OR instead of AND (#15165) 2026-03-25 08:06:48 -10:00
Edward Firmo
65d0a91fcc [nextion] Add defined keys to defines.h (#14971)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-25 18:01:52 +00:00
J. Nick Koston
a22d47c719 [api] Add --no-states flag to esphome logs command (#15160) 2026-03-25 07:36:53 -10:00
J. Nick Koston
010516aef2 [benchmark] Add sensor publish_state benchmarks (#15034) 2026-03-25 07:33:17 -10:00
Jonathan Swoboda
a15389318f [audio] Bump esp-audio-libs to 2.0.4 (#15164) 2026-03-25 11:57:33 -04:00
Edward Firmo
5d67868ac6 [nextion] Fix inline doc parameter types for page and touch callbacks (#14972) 2026-03-25 10:39:46 -04:00
Clyde Stubbs
e0d8000007 [ai] Add instructions regarding constructor parameters (#15091)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-25 10:34:34 -04:00
Frédéric Metrich
b66ff374a2 [esp32] Fix GPIO strapping pins and add USB-JTAG warnings (#15105)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-03-25 14:26:33 +00:00
Brandon der Blätter
6c981e83db [hub75] Add SCAN_1_8_32PX_FULL wiring option (#15130) 2026-03-25 09:52:50 -04:00
Clyde Stubbs
2355fcb44e [lvgl] Update function and type names (#15109)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-25 09:51:51 -04:00
Piotr Szulc
f5bbff0b05 [core] Add CONF_LIBRETINY constant to const.py (#15141)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-03-25 07:40:39 -04:00
Clyde Stubbs
c45c9da771 [lvgl] Various 9.5 fixes (#15157) 2026-03-25 20:51:23 +11:00
dependabot[bot]
7a40759567 Bump aioesphomeapi from 44.7.0 to 44.8.0 (#15159)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-25 08:55:12 +00:00
J. Nick Koston
af5b98c635 [time] Remove dummy placeholder values for recalc_timestamp_utc() (#15129) 2026-03-25 01:07:28 +00:00
J. Nick Koston
690dc324c9 [logger] Move task log buffer storage to BSS (#15153) 2026-03-25 00:52:37 +00:00
Jonathan Swoboda
26e78c840c [wifi] Filter fast_connect by band_mode and use background scan for roaming (#15152) 2026-03-24 20:21:04 -04:00
J. Nick Koston
9c9ae190ee [core] Use compile-time HasElse parameter in IfAction (#15134) 2026-03-24 14:13:59 -10:00
J. Nick Koston
238adbe008 [wifi] Fix roaming counter reset from delayed disconnect and successful retry (#15126) 2026-03-24 14:04:17 -10:00
J. Nick Koston
f457b995f7 [datetime] Fix state_as_esptime() returning invalid timestamp (#15128) 2026-03-24 14:03:56 -10:00
J. Nick Koston
b6aec4fa25 [ethernet] Add W5100 support for RP2040 (#15131) 2026-03-24 14:03:30 -10:00
J. Nick Koston
9fb5b6aa15 [light] Replace initial_state storage with flash-resident callback (#15133) 2026-03-24 14:03:18 -10:00
J. Nick Koston
752fe30332 [api] Add descriptive message to status warning when waiting for client (#15148) 2026-03-24 20:01:59 -04:00
Jonathan Swoboda
4ff85e2a1e [core] Fix clean-all to handle custom build paths (#15146)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2026-03-24 19:48:17 -04:00
Diorcet Yann
13baf26050 [core] get_log_str: fix false-positive error on null-terminated strings with stricter compilers (#15136)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
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@home-assistant.io>
2026-03-24 20:26:21 +00:00
Jonathan Swoboda
8751f348c8 [sx127x] Fix FIFO read corruption (#15114) 2026-03-24 10:04:27 -10:00
Fabian Bläse
22bc47da23 [light] Fix incorrect mode change handling on transition to off (#15147) 2026-03-24 19:57:58 +00:00
Jonathan Swoboda
55df21db51 [esp32] Default CPU frequency to maximum supported (#15143) 2026-03-24 15:44:28 -04:00
Jonathan Swoboda
3cd50f0495 [ci] Block new CONF_ constants from being added to esphome/const.py (#15145) 2026-03-24 09:31:08 -10:00
Diorcet Yann
b3390d40fb [core] Fix cg.add_define propagation to dependencies in native ESP-IDF builds (#15137) 2026-03-24 14:31:42 -04:00
Javier Peletier
7eddf429ea [substitutions] speed up config loading: substitutions pass and !include redesign (package refactor part 4) (#12126)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-23 23:57:22 -10:00
J. Nick Koston
793813790a [api] Precompute tag bytes for forced varint and length-delimited fields (#15067) 2026-03-24 01:52:39 +00:00
J. Nick Koston
fe2c4e47bf [sensor] Deprecate .raw_state, guard raw_callback_ behind USE_SENSOR_FILTER (#15094)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-03-24 00:40:02 +00:00
Javier Peletier
df4318505f [substitutions] refactor substitute() as a pure function (package refactor part 3) (#15031)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-23 14:28:04 -10:00
J. Nick Koston
69911c3db1 [wifi] Reduce ESP8266 roaming scan dwell time to match ESP32 (#15127) 2026-03-23 13:58:36 -10:00
J. Nick Koston
8ad8f89e50 [light] Reorder LightState fields to eliminate padding (#15112) 2026-03-23 13:56:53 -10:00
J. Nick Koston
a3d9854704 [gpio] Remove redundant last_state_ and pack GPIOBinarySensor fields (#15113) 2026-03-23 13:56:36 -10:00
J. Nick Koston
13d3968d9b [api] Avoid heap allocation in PSK update timeout lambda (#14921)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-23 13:41:09 -10:00
J. Nick Koston
382de7ca90 [api] Store dump strings in PROGMEM to save RAM on ESP8266 (#14982) 2026-03-23 13:40:53 -10:00
J. Nick Koston
a0d0516b22 [benchmark] Add noise handshake benchmark (#15039) 2026-03-23 13:40:41 -10:00
J. Nick Koston
0fb31726f6 [esp32] Add sram1_as_iram option and bootloader version detection (#14874) 2026-03-23 13:39:29 -10:00
Clyde Stubbs
e6a73cab8f [number] Add sensor platform (#15125) 2026-03-24 12:04:53 +13:00
Javier Peletier
bf6000ef3d [substitutions] substitutions pass and !include redesign (package refactor part 2b) (#14918)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-23 12:50:28 -10:00
dependabot[bot]
332118db56 Bump pytest-cov from 7.0.0 to 7.1.0 (#15123)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-23 11:44:26 -10:00
Clyde Stubbs
6956bf7e53 [text] Add text_sensor for read-only view of text component (#15090) 2026-03-24 10:24:25 +13:00
Daniel Kent
11b829dda1 [spa06_spi] Add SPA06-003 Temperature and Pressure Sensor - SPI support (Part 3 of 3) (#14523)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-03-23 15:59:17 -04:00
J. Nick Koston
1e16b30380 [ethernet] Add ENC28J60 SPI Ethernet support (#14945) 2026-03-23 19:18:58 +00:00
Daniel Kent
4c1363b104 [spi] Add LOG_SPI_DEVICE macro (#15118) 2026-03-23 15:07:40 -04:00
J. Nick Koston
9da0c5bc85 [wifi] Fix roaming attempt counter reset on disconnect during scan (#15099) 2026-03-23 08:47:15 -10:00
J. Nick Koston
4b0c711f77 [ci] Ban std::bind in new C++ code (#14969) 2026-03-23 08:23:35 -10:00
J. Nick Koston
9385f16128 [text_sensor] Guard raw_callback_ behind USE_TEXT_SENSOR_FILTER, save 4 bytes per instance (#15097) 2026-03-23 08:23:22 -10:00
J. Nick Koston
36d2e58b11 [api] Make ProtoDecodableMessage::decode() non-virtual (#15076) 2026-03-23 08:23:08 -10:00
J. Nick Koston
03d6b36fe0 [gpio] Compile out interlock fields when unused (#15111) 2026-03-23 08:22:38 -10:00
J. Nick Koston
3b5b51b4f0 [time] Point to valid IANA timezone list on validation failure (#15110) 2026-03-23 08:22:25 -10:00
Clyde Stubbs
e8c5dfca3e [lvgl] Various fixes (#15098) 2026-03-23 12:09:30 -04:00
Kevin Ahrendt
5a984b54cf [audio] Bump microOpus to avoid creating an extra opus-staged directory (#14974) 2026-03-23 08:31:05 -04:00
Simone Rossetto
43879964bd [wireguard] bump esp_wireguard to 0.4.4 for mbedtls 4.0+ compatibility (#15104)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-22 23:03:19 -10:00
J. Nick Koston
5560c9eef7 [test] Fix flakey ld2412 integration test race condition (#15100) 2026-03-22 21:10:51 -10:00
J. Nick Koston
f4097d5a95 [api] Devirtualize API command dispatch (#15044) 2026-03-23 19:57:40 +13:00
Keith Burzinski
225330413a [uart] Rename FlushResult to UARTFlushResult with UART_FLUSH_RESULT_ prefix (#15101)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 01:55:14 -05:00
J. Nick Koston
e67b5a78d0 [esp32] Patch DRAM segment for testing mode to fix grouped component test overflow (#15102) 2026-03-23 01:51:40 -05:00
J. Nick Koston
baf365404c [network] Inline get_use_address() to eliminate function call overhead (#14942) 2026-03-23 04:18:43 +00:00
J. Nick Koston
0de2c758aa [scheduler] Use placement-new for std::function move in set_timer_common_ (#14757) 2026-03-23 16:31:27 +13:00
J. Nick Koston
597bb18543 [benchmark] Add binary sensor publish and sensor filter benchmarks (#15035) 2026-03-23 16:30:57 +13:00
Jesse Hills
ebdf20adc0 Merge branch 'release' into dev 2026-03-23 16:10:17 +13:00
Jesse Hills
7ecdf6db2e Merge pull request #15084 from esphome/bump-2026.3.1
2026.3.1
2026-03-23 16:09:32 +13:00
J. Nick Koston
8a3b5a8def [core] Fix placement new storage name for templated types (#15096) 2026-03-23 16:09:23 +13:00
J. Nick Koston
98d9fd76b3 [mqtt] Fix const-correctness for trigger constructors (#15093) 2026-03-22 16:27:20 -10:00
J. Nick Koston
6992219e34 [core] Attribute placement new storage symbols to components (#15092) 2026-03-22 16:27:07 -10:00
J. Nick Koston
fbe3e7d99c [api] Emit raw tag+value writes for forced fixed32 key fields (#15051) 2026-03-22 15:28:46 -10:00
J. Nick Koston
9cdc17566a [combination] Use FixedVector and parent pointer to enable inline Callback storage (#14947) 2026-03-22 15:06:45 -10:00
Kamil Cukrowski
cd05462e9f [core] Use placement new allocation for Pvariables (#15079)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-22 14:42:04 -10:00
J. Nick Koston
83d02c602a [logger] Fix dummy_main.cpp Logger constructor for clang-tidy (#15088)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 13:25:04 +13:00
J. Nick Koston
e85065b1c4 [logger] Fix dummy_main.cpp Logger constructor for clang-tidy (#15088)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 14:06:00 -10:00
J. Nick Koston
d0e705d948 [core] Inline Application::loop() to eliminate stack frame (#15041) 2026-03-22 12:46:28 -10:00
J. Nick Koston
2c06464f7b [packet_transport] Use FixedVector and parent pointer to enable inline Callback storage (#14946) 2026-03-22 12:41:54 -10:00
J. Nick Koston
84727b1f71 [esp32] Validate eFuse MAC reads and reject garbage MACs (#15049) 2026-03-22 12:41:01 -10:00
J. Nick Koston
aef987dccf [core] Fix Callback::create memcpy from function reference (#14995) 2026-03-22 12:37:46 -10:00
J. Nick Koston
b2b61bea6a [web_server_idf] Inline send() to reduce httpd task stack depth (#15045) 2026-03-22 12:33:06 -10:00
J. Nick Koston
30f66be1da [esp32] Mention ignore_pin_validation_error in flash pin error message (#14998) 2026-03-22 12:32:42 -10:00
J. Nick Koston
6caa9ee227 [logger] Move log level lookup tables to PROGMEM (#15003) 2026-03-22 12:32:08 -10:00
J. Nick Koston
9152f77cdd [core] Reduce automation call chain stack depth (#15042) 2026-03-22 12:31:48 -10:00
J. Nick Koston
4d09eb2cec [tests] Fix flaky ld24xx integration tests by disabling API batching (#15050) 2026-03-22 12:29:28 -10:00
J. Nick Koston
5cc4f6e85a [logger] Add task_log_buffer_zephyr.cpp to platform source filter (#15081) 2026-03-22 12:29:12 -10:00
J. Nick Koston
6d16c57747 [sht4x] Add missing hal.h include for millis() on ESP-IDF (#15087) 2026-03-23 11:23:21 +13:00
J. Nick Koston
27f3a5f5f4 [sht4x] Add missing hal.h include for millis() on ESP-IDF (#15087) 2026-03-22 11:54:54 -10:00
J. Nick Koston
45c0e6ef7f [logger] Fix unit test Logger constructor call (#15086) 2026-03-23 09:52:46 +13:00
J. Nick Koston
593dbc9e67 [logger] Fix unit test and benchmark Logger constructor calls (#15085) 2026-03-23 09:50:58 +13:00
J. Nick Koston
daafa8faa3 [wifi] Inline trivial WiFiAP and WiFiComponent accessors (#15075) 2026-03-22 10:36:18 -10:00
Jesse Hills
320474b62d Bump version to 2026.3.1 2026-03-23 09:28:58 +13:00
Jason Kölker
a3c483edf3 [pmsx003] Keep active-mode reads aligned (#14832) 2026-03-23 09:28:58 +13:00
J. Nick Koston
036be63f7b [logger] Fix race condition in task log buffer initialization (#15071)
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>
2026-03-23 09:28:58 +13:00
Jonathan Swoboda
bbfe324dd6 [ultrasonic] Fix ISR edge detection with debounce and trigger filtering (#15014) 2026-03-23 09:28:57 +13:00
J. Nick Koston
de3292c828 [light] Fix gamma LUT quantizing small brightness to zero (#15060) 2026-03-23 09:28:57 +13:00
J. Nick Koston
67ab2e143c [uart] Fix RTL87xx compilation failure due to SUCCESS macro collision (#15054) 2026-03-23 09:28:57 +13:00
J. Nick Koston
9abc112f76 [sht4x] Fix heater causing measurement jitter (#15030) 2026-03-23 09:28:50 +13:00
J. Nick Koston
b5880df93c [light] Fix constant_brightness broken by gamma LUT refactor (#15048)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-23 09:27:59 +13:00
J. Nick Koston
2352c732de [mqtt] Rate-limit component resends to prevent task WDT on reconnect (#15061) 2026-03-23 09:27:59 +13:00
Samuel Sieb
77264de3f6 [analog_threshhold] add missing header (#15058) 2026-03-23 09:27:59 +13:00
J. Nick Koston
42da281854 [time] Fix timezone_offset() and recalc_timestamp_local() always returning UTC (#14996) 2026-03-23 09:27:59 +13:00
J. Nick Koston
06cc5a29a7 [core] Add copy() method to StringRef for std::string compatibility (#15028) 2026-03-23 09:27:59 +13:00
J. Nick Koston
98b4e1ea15 [web_server] Increase httpd task stack size to prevent stack overflow (#14997)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-23 09:27:59 +13:00
Jonathan Swoboda
0bf6e1e839 [esp32_touch] Fix initial state never published when sensor untouched (#15032) 2026-03-23 09:27:59 +13:00
J. Nick Koston
3fe84eadef [wifi] Fix ESP8266 power_save_mode mapping (LIGHT/HIGH were swapped) (#15029) 2026-03-23 09:27:59 +13:00
J. Nick Koston
12eed0d384 [api] Increase noise handshake timeout to 60s for slow WiFi environments (#15022) 2026-03-23 09:27:59 +13:00
dependabot[bot]
28e8250b69 Bump aioesphomeapi from 44.6.1 to 44.6.2 (#15027)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-23 09:27:59 +13:00
Keith Roehrenbeck
0297260a57 [ld2450] Fix zone target counts including untracked ghost targets (#15026) 2026-03-23 09:27:59 +13:00
J. Nick Koston
d4f7cb984c [uart] Fix UART0 default pin IOMUX loopback on ESP32 (#14978) 2026-03-23 09:27:59 +13:00
Clyde Stubbs
08187a01b1 [sdl] Fix get_width()/height() when rotation used (#14950) 2026-03-23 09:27:59 +13:00
J. Nick Koston
daf3502e15 [logger] Fix ESP8266 crash with VERY_VERBOSE log level (#14980) 2026-03-23 09:27:59 +13:00
J. Nick Koston
08cab43548 [time] Fix lookup of top-level IANA timezone keys like UTC and GMT (#14952) 2026-03-23 09:27:59 +13:00
dependabot[bot]
5cbe936256 Bump aioesphomeapi from 44.6.0 to 44.6.1 (#14954)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-23 09:27:59 +13:00
Jonathan Swoboda
729d3d4bc2 [openthread] Guard InstanceLock against uninitialized semaphore (#14940)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 09:27:58 +13:00
Jonathan Swoboda
8af0991590 [ble_client] Fix RSSI sensor reporting same value for all clients (#14939)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 09:27:58 +13:00
J. Nick Koston
99d968f80a [http_request] Prevent double update task launch (#14910) 2026-03-23 09:27:58 +13:00
dependabot[bot]
705d548435 Bump aioesphomeapi from 44.5.2 to 44.6.0 (#14927)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-23 09:27:58 +13:00
Jason Kölker
2b6d63fd09 [pmsx003] Keep active-mode reads aligned (#14832) 2026-03-23 09:21:08 +13:00
J. Nick Koston
c917b8ce06 [logger] Fix race condition in task log buffer initialization (#15071)
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>
2026-03-23 09:20:28 +13:00
Jonathan Swoboda
12b10d8b89 [ultrasonic] Fix ISR edge detection with debounce and trigger filtering (#15014) 2026-03-22 16:19:46 -04:00
J. Nick Koston
6a77b8b1f4 [light] Fix gamma LUT quantizing small brightness to zero (#15060) 2026-03-23 09:19:28 +13:00
J. Nick Koston
ba4be2a904 [uart] Fix RTL87xx compilation failure due to SUCCESS macro collision (#15054) 2026-03-23 09:17:59 +13:00
J. Nick Koston
ca0523b86c [sht4x] Fix heater causing measurement jitter (#15030) 2026-03-23 09:16:46 +13:00
J. Nick Koston
5e68282519 [light] Fix constant_brightness broken by gamma LUT refactor (#15048)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-23 09:14:52 +13:00
Clyde Stubbs
a0d5525312 [lvgl] Meter fixes (#15073) 2026-03-22 19:01:49 +11:00
J. Nick Koston
c48fd0738b [mqtt] Rate-limit component resends to prevent task WDT on reconnect (#15061) 2026-03-21 15:33:42 -10:00
J. Nick Koston
8224da3460 [core] Inline Component::get_component_log_str() (#15068) 2026-03-21 15:32:24 -10:00
Clyde Stubbs
dd82a91d8f [lvgl] Don't animate page change when not requested (#15069) 2026-03-22 11:13:17 +11:00
J. Nick Koston
86ec218f75 [benchmark] Add plaintext API frame write benchmarks (#15036) 2026-03-21 13:15:35 -10:00
Samuel Sieb
2a6ec597b4 [analog_threshhold] add missing header (#15058) 2026-03-21 18:13:08 +00:00
dependabot[bot]
8dd69207ea Bump aioesphomeapi from 44.6.2 to 44.7.0 (#15052)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-21 10:24:56 +00:00
J. Nick Koston
d203a46ef8 [api] Enable HAVE_WEAK_SYMBOLS and HAVE_INLINE_ASM for libsodium (#15038) 2026-03-21 04:17:37 +00:00
J. Nick Koston
1920d8a887 [benchmark] Add noise encryption benchmarks (#15037) 2026-03-20 17:35:17 -10:00
J. Nick Koston
95dea59382 [core] Use SplitMix32 PRNG for random_uint32() (#14984) 2026-03-20 15:25:54 -10:00
J. Nick Koston
f3cddcee21 [core] Store parent pointers as members to enable inline Callback storage (#14923) 2026-03-20 15:25:40 -10:00
J. Nick Koston
21e384cafd [esp32] Disable PicolibC Newlib compatibility shim on IDF 6.0+ (#15008)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-20 15:10:18 -10:00
J. Nick Koston
32db055b98 [number] Clean up NumberCall::perform() increment/decrement logic (#15000) 2026-03-20 15:09:28 -10:00
J. Nick Koston
2c87260046 [core] Optimize Component::is_ready() with bitmask check (#15005) 2026-03-20 15:09:13 -10:00
J. Nick Koston
51ccad8461 [preferences] Shorten TAG strings across all platforms (#15004) 2026-03-20 15:09:01 -10:00
J. Nick Koston
7f500c4b6e [modbus] Fix size_t format warning in clear_rx_buffer_ (#15002) 2026-03-20 15:08:46 -10:00
J. Nick Koston
564d155cb6 [wifi] Use LOG_STR_LITERAL for scan complete log on ESP8266 (#15001) 2026-03-20 15:08:33 -10:00
J. Nick Koston
edf5542559 [analyze-memory] Attribute extern C symbols to components via source file mapping (#15006) 2026-03-20 15:05:17 -10:00
J. Nick Koston
51335e8830 [ledc] Fix deprecated intr_type warning on ESP-IDF 6.0+ (#15009) 2026-03-20 15:01:30 -10:00
J. Nick Koston
391ffe34f8 [rp2040] Fix get_mac_address_raw to use ethernet MAC when WiFi unavailable (#15033) 2026-03-20 15:01:11 -10:00
J. Nick Koston
12ead0408a [gpio] Use constexpr uint32_t timer ID for interlock timeout (#15010) 2026-03-20 15:00:56 -10:00
Daniel Kent
2d39cc2540 [spa06_i2c] Add SPA06-003 Temperature and Pressure Sensor - I2C support (Part 2 of 3) (#14522)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-03-21 00:38:04 +00:00
J. Nick Koston
a9a8f4cb3b [time] Fix timezone_offset() and recalc_timestamp_local() always returning UTC (#14996) 2026-03-20 13:58:14 -10:00
J. Nick Koston
8fa2e75afa [core] Add copy() method to StringRef for std::string compatibility (#15028) 2026-03-20 13:58:02 -10:00
J. Nick Koston
0b01f9fc42 [web_server] Increase httpd task stack size to prevent stack overflow (#14997)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-20 13:57:51 -10:00
Jonathan Swoboda
ed8c062d9f [esp32_touch] Fix initial state never published when sensor untouched (#15032) 2026-03-20 19:53:02 -04:00
J. Nick Koston
5e516e78e4 [wifi] Fix ESP8266 power_save_mode mapping (LIGHT/HIGH were swapped) (#15029) 2026-03-20 12:13:49 -10:00
J. Nick Koston
896b6ec8c9 [api] Increase noise handshake timeout to 60s for slow WiFi environments (#15022) 2026-03-20 17:06:23 -05:00
dependabot[bot]
9e7cdaf475 Bump aioesphomeapi from 44.6.1 to 44.6.2 (#15027)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-20 11:10:40 -10:00
dependabot[bot]
a3fd1d5d00 Bump github/codeql-action from 4.33.0 to 4.34.1 (#15023)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-20 10:41:45 -10:00
dependabot[bot]
7257bed1e9 Bump CodSpeedHQ/action from 4.11.1 to 4.12.1 (#15024)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-20 10:41:20 -10:00
Javier Peletier
5a9977cf5c [lvgl] Fix arc indicator widget not registered in widget_map (#14986) 2026-03-21 07:35:41 +11:00
Keith Roehrenbeck
12b3aec567 [ld2450] Fix zone target counts including untracked ghost targets (#15026) 2026-03-20 10:11:57 -10:00
J. Nick Koston
d59c006ff9 [uart] Fix UART0 default pin IOMUX loopback on ESP32 (#14978) 2026-03-19 20:56:51 -10:00
J. Nick Koston
02ada93ea5 [wifi] Reject WiFi config on RP2040/RP2350 boards without CYW43 chip (#14990) 2026-03-19 18:40:33 -10:00
Kent Gibson
6e87f8eb4e [template] alarm_control_panel collapse SensorDataStore and bypassed_sensor_indicies into SensorInfo (#14852)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-20 02:06:58 +00:00
Clyde Stubbs
7df550f2a9 Ensure lvgl libs available when editing for host (#14987) 2026-03-20 01:52:52 +00:00
J. Nick Koston
9f7342dfaf Merge branch 'dev' into remove_posix_tz_parser 2026-03-19 15:39:47 -10:00
Clyde Stubbs
b02f0e3c5f [sdl] Fix get_width()/height() when rotation used (#14950) 2026-03-20 12:39:10 +11:00
J. Nick Koston
151f71e033 [ci] Add libretiny and zephyr to memory impact platform filter (#14985) 2026-03-19 14:12:15 -10:00
J. Nick Koston
7ac001e994 [mhz19] Fix unused function warning for detection_range_to_log_string (#14981) 2026-03-19 14:12:03 -10:00
J. Nick Koston
de177d2445 [logger] Fix ESP8266 crash with VERY_VERBOSE log level (#14980) 2026-03-19 14:11:49 -10:00
J. Nick Koston
a9cb7143dc [core] Inline calculate_looping_components_ into header (#14944) 2026-03-19 14:11:17 -10:00
J. Nick Koston
902258b56e [preferences] Compile out loop() when flash_write_interval is non-zero (#14943) 2026-03-19 14:11:06 -10:00
dependabot[bot]
c2a96ea293 Bump ruff from 0.15.6 to 0.15.7 (#14977)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-19 10:54:53 -10:00
J. Nick Koston
37a3c3ab3a [core] Replace std::bind with placeholders to lambdas (#14962) 2026-03-19 08:50:38 -10:00
J. Nick Koston
a8ed781f3e [time] Fix lookup of top-level IANA timezone keys like UTC and GMT (#14952) 2026-03-19 08:44:35 -10:00
J. Nick Koston
63f0d054b7 [core] Replace std::bind with lambda in DelayAction (#14968) 2026-03-19 08:44:16 -10:00
J. Nick Koston
e7dcf54a77 [http_request] Replace std::bind with lambdas in HttpRequestSendAction (#14966) 2026-03-19 08:44:02 -10:00
J. Nick Koston
1ba5504944 [mqtt] Replace std::bind with lambda in MQTTPublishJsonAction (#14965) 2026-03-19 08:43:47 -10:00
J. Nick Koston
5637116378 [mqtt] Replace std::bind with lambdas in CustomMQTTDevice (#14964) 2026-03-19 08:43:22 -10:00
J. Nick Koston
cdc4ba6295 [api] Replace std::bind with lambdas in CustomAPIDevice (#14963) 2026-03-19 08:43:06 -10:00
J. Nick Koston
d1aa1881bb [core] Replace std::bind with lambdas across 13 components (#14961) 2026-03-19 08:42:26 -10:00
J. Nick Koston
14107ec452 [bme68x_bsec2] Store trigger time as member to avoid std::function SBO overflow (#14960)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-03-19 08:42:08 -10:00
J. Nick Koston
2ca6681896 [xiaomi_rtcgq02lm] Drop unused capture from timeout lambdas (#14959) 2026-03-19 08:41:15 -10:00
J. Nick Koston
40a65d36b4 [nau7802] Replace std::bind with lambda to fit std::function SBO (#14958) 2026-03-19 08:41:01 -10:00
J. Nick Koston
16ec237ac6 [wireguard] Replace std::bind with inline lambdas (#14957) 2026-03-19 08:40:32 -10:00
J. Nick Koston
0afcdbfe73 [binary_sensor] Replace std::bind with inline lambda in MultiClickTrigger (#14956) 2026-03-19 08:40:18 -10:00
aanban
b9439036d4 [remote_base] add support for brennenstuhl comfort-line switches (#9407)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-03-19 09:19:24 -04:00
CFlix
cb23f9453f [absolute_humidity] loop() improvement (#14684)
Co-authored-by: DAVe3283 <DAVe3283+GitHub@gmail.com>
2026-03-19 09:09:00 -04:00
Daniel Kent
0858ecbb8e [spa06_base] Add SPA06-003 Temperature and Pressure Sensor (Part 1 of 3) (#14521)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-03-19 08:05:12 -04:00
luar123
96da6dd075 [esp32] Add custom partitions and refactor partition table generation (#7682)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 07:41:11 -04:00
dependabot[bot]
2c31bdc6a2 Bump aioesphomeapi from 44.6.0 to 44.6.1 (#14954)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-18 23:43:50 -10:00
Javier Peletier
0a3393bed3 [core] Disable LeakSanitizer in C++ unit tests (#14712) 2026-03-18 23:13:36 -10:00
Javier Peletier
c2c50ceea7 [substitutions] substitutions pass and !include redesign (package refactor part 2a) (#14917)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-18 23:10:01 -10:00
Clyde Stubbs
2341d510d3 [lvgl] Migrate to library v9.5.0 (#12312)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-03-19 20:31:33 +13:00
Kevin Ahrendt
9d6f2f71e8 [speaker_source] Reshuffle playlist on repeat all restart (#14773)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-18 19:51:17 -10:00
Nate Clark
e1334cf57f [mqtt] Support JSON payload with code for alarm control panel commands (#14731)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-19 05:40:48 +00:00
J. Nick Koston
a1aff7cadf [preferences] Devirtualize preference backend and manager classes (#14825) 2026-03-18 18:42:05 -10:00
J. Nick Koston
2271ac6470 [api] Extract overflow buffer from frame helper into APIOverflowBuffer (#14871) 2026-03-18 18:41:45 -10:00
J. Nick Koston
8fe36cde23 [core] Replace std::function with lightweight Callback in CallbackManager (#14853) 2026-03-18 18:41:05 -10:00
Jesse Hills
fdd5956c1e Merge branch 'release' into dev 2026-03-19 17:34:17 +13:00
Jesse Hills
609003c897 Merge pull request #14941 from esphome/bump-2026.3.0
2026.3.0
2026-03-19 17:33:27 +13:00
Jonathan Swoboda
403ba262c6 [openthread] Guard InstanceLock against uninitialized semaphore (#14940)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 21:23:47 -04:00
Jonathan Swoboda
f8be27ce6d [ble_client] Fix RSSI sensor reporting same value for all clients (#14939)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 20:10:42 -04:00
Jesse Hills
2c10adba85 Bump version to 2026.3.0 2026-03-19 13:09:03 +13:00
J. Nick Koston
a50d70c8d3 [core] Remove call_loop_ wrapper and call loop() directly (#14931) 2026-03-18 14:08:03 -10:00
J. Nick Koston
4d86049c21 [ota] Pack deferred state args into uint32 to avoid heap allocation (#14922)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-18 14:06:55 -10:00
J. Nick Koston
44037c4f9b [http_request] Prevent double update task launch (#14910) 2026-03-18 14:06:41 -10:00
J. Nick Koston
5856d05701 [core] Devirtualize PollingComponent::set_update_interval (#14938) 2026-03-18 14:05:57 -10:00
J. Nick Koston
a2a048e3bf [ld2412] Inline trivial gate threshold number setters (#14937) 2026-03-18 13:02:07 -10:00
Kevin Ahrendt
9f4c773963 [media_source] Add request helpers for smart sources (#14936) 2026-03-18 12:34:16 -10:00
Kevin Ahrendt
ef0eef8117 [const] Move shared volume constants to components/const (#14935) 2026-03-18 21:57:33 +00:00
Jonathan Swoboda
097e6eb41f [i2s_audio] Remove legacy I2S driver support (#14932) 2026-03-18 11:42:56 -10:00
Jonathan Swoboda
73a49493a2 [vbus][shelly_dimmer][st7789v][modbus_controller] Fix integer overflows, off-by-one, and coordinate swap (#14916) 2026-03-18 16:43:42 -04:00
Jonathan Swoboda
4a93d5b544 [vl53l0x][ld2420][ble_client][inkplate] Fix state corruption, crash, OOB read, and shift UB (#14919) 2026-03-18 16:42:53 -04:00
Jonathan Swoboda
cc0655a904 [bedjet][light][i2s_audio][ld2412] Fix uninitialized pointers, div-by-zero, and buffer validation (#14925) 2026-03-18 16:42:13 -04:00
Jesse Hills
a859cb3cce Merge branch 'beta' into dev 2026-03-19 09:20:26 +13:00
Jesse Hills
9e4e2d78dc Merge pull request #14926 from esphome/bump-2026.3.0b5
2026.3.0b5
2026-03-19 09:19:50 +13:00
Jonathan Swoboda
47909d5299 [hub75] Bump esp-hub75 to 0.3.5 (#14915) 2026-03-18 09:47:14 -10:00
dependabot[bot]
16667bf5be Bump aioesphomeapi from 44.5.2 to 44.6.0 (#14927)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-18 09:39:26 -10:00
dependabot[bot]
ef3afe3e21 Bump codecov/codecov-action from 5.5.2 to 5.5.3 (#14928)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-18 09:33:29 -10:00
dependabot[bot]
3a47317fc8 Bump actions/cache from 5.0.3 to 5.0.4 in /.github/actions/restore-python (#14930)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-18 09:33:15 -10:00
dependabot[bot]
89066e3e20 Bump actions/cache from 5.0.3 to 5.0.4 (#14929)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-18 09:33:00 -10:00
Jesse Hills
af9366fdd4 Bump version to 2026.3.0b5 2026-03-19 08:19:26 +13:00
J. Nick Koston
448402ca2c [http_request] Fix data race on update_info_ strings in update task (#14909) 2026-03-19 08:19:26 +13:00
Jonathan Swoboda
fc67551edc [tc74][apds9960] Fix signed temperature and FIFO register address (#14907) 2026-03-19 08:19:26 +13:00
Jonathan Swoboda
98d3dce672 [voice_assistant][micro_wake_word] Fix null deref and missing error return (#14906) 2026-03-19 08:19:26 +13:00
Jonathan Swoboda
4cb93d4df8 [sensor][ee895][hdc2010] Fix misc bugs found during component scan (#14890) 2026-03-19 08:19:18 +13:00
Jonathan Swoboda
91e66cfd9d [gree] Fix IR checksum for YAA/YAC/YAC1FB9/GENERIC models (#14888) 2026-03-19 08:17:03 +13:00
Jonathan Swoboda
6cf32af33f [seeed_mr24hpc1] Fix frame parser length handling bugs (#14863) 2026-03-19 08:17:03 +13:00
J. Nick Koston
9a80c980cb [scheduler] Early exit cancel path after first match (#14902) 2026-03-18 07:48:26 -10:00
J. Nick Koston
c9e6c85e6a [scheduler] Inline fast-path checks into header (#14905) 2026-03-18 07:48:11 -10:00
J. Nick Koston
e88c9ba066 [core] Inline progmem_read functions on non-ESP8266 platforms (#14913) 2026-03-18 07:47:42 -10:00
J. Nick Koston
45be290392 [ci] Bump Python to 3.14 in sync-device-classes workflow (#14912) 2026-03-18 07:47:17 -10:00
J. Nick Koston
3f28ab88ca [http_request] Fix data race on update_info_ strings in update task (#14909) 2026-03-18 07:46:18 -10:00
Jonathan Swoboda
1d07f37d62 [opentherm] Migrate from legacy timer API to GPTimer API (#14859) 2026-03-18 09:22:28 -04:00
Jonathan Swoboda
16c5224341 [tc74][apds9960] Fix signed temperature and FIFO register address (#14907) 2026-03-18 07:48:43 -04:00
Jesse Hills
e83372e2f3 Merge branch 'beta' into dev 2026-03-18 16:22:02 +13:00
Jesse Hills
6b9be033d6 Merge pull request #14904 from esphome/bump-2026.3.0b4
2026.3.0b4
2026-03-18 16:21:28 +13:00
Jonathan Swoboda
2531fb1a02 [voice_assistant][micro_wake_word] Fix null deref and missing error return (#14906) 2026-03-17 23:12:13 -04:00
J. Nick Koston
3e845d387a [tests] Fix test_show_logs_serial taking 30s due to unmocked serial port wait (#14903) 2026-03-17 14:44:17 -10:00
Jesse Hills
5cc03d9bef Bump version to 2026.3.0b4 2026-03-18 13:35:21 +13:00
J. Nick Koston
0fa96b6e1e [scheduler] Fix UB in cross-thread counter/vector reads, add atomic fast-path (#14880)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-03-18 13:35:21 +13:00
J. Nick Koston
be2e4a5278 [mqtt] Fix data race on inbound event queue (#14891)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2026-03-18 13:35:21 +13:00
J. Nick Koston
80bd6489cf [esp32_ble_server] Remove vestigial semaphore from BLECharacteristic (#14900) 2026-03-18 13:35:21 +13:00
J. Nick Koston
ccf672d7ee [esp32_ble] Fix EventPool/LockFreeQueue sizing off-by-one (#14892) 2026-03-18 13:35:20 +13:00
J. Nick Koston
6154b673c2 [usb_uart] Fix EventPool/LockFreeQueue sizing off-by-one (#14895) 2026-03-18 13:35:20 +13:00
J. Nick Koston
3bde7ec978 [usb_host] Fix EventPool/LockFreeQueue sizing off-by-one (#14896) 2026-03-18 13:35:20 +13:00
J. Nick Koston
8caa11dcf4 [usb_cdc_acm] Fix EventPool/LockFreeQueue sizing off-by-one (#14894) 2026-03-18 13:35:20 +13:00
J. Nick Koston
1b70df2c1f [espnow] Fix EventPool/LockFreeQueue sizing off-by-one (#14893) 2026-03-18 13:35:20 +13:00
J. Nick Koston
4122fa5ddd [core] Add back deprecated set_internal() for external projects (#14887) 2026-03-18 13:35:20 +13:00
Jonathan Swoboda
c5d42b0569 [speaker] Fix media playlist using announcement delay (#14889) 2026-03-18 13:35:20 +13:00
J. Nick Koston
37f9541f32 [api] Fix ProtoMessage protected destructor compile error on host platform (#14882) 2026-03-18 13:35:20 +13:00
Diorcet Yann
8bbfadb59a [core] Small improvements (#14884) 2026-03-18 13:35:20 +13:00
J. Nick Koston
b9e8da92c7 [scheduler] Fix UB in cross-thread counter/vector reads, add atomic fast-path (#14880)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-03-18 00:19:31 +00:00
Javier Peletier
0c5f055d45 [core] cpp tests: Allow customizing code generation during tests (#14681)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-18 00:16:01 +00:00
J. Nick Koston
342020e1d3 [mqtt] Fix data race on inbound event queue (#14891)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2026-03-17 13:49:24 -10:00
J. Nick Koston
62f9bc79c4 [ci] Add CodSpeed badge to README (#14901) 2026-03-17 13:48:21 -10:00
Jonathan Swoboda
53bfb02a21 [sensor][ee895][hdc2010] Fix misc bugs found during component scan (#14890) 2026-03-17 19:46:26 -04:00
J. Nick Koston
83484a8828 [esp32_ble_server] Remove vestigial semaphore from BLECharacteristic (#14900) 2026-03-17 13:38:41 -10:00
J. Nick Koston
ece235218f [debug][bme680_bsec] Use fnv1_hash_extend to avoid temporary string allocations (#14876) 2026-03-17 13:27:46 -10:00
J. Nick Koston
f3409acfa8 [core] Document EventPool sizing requirement with LockFreeQueue (#14897) 2026-03-17 13:08:58 -10:00
J. Nick Koston
77b7201eb8 [ci] Run CodSpeed benchmarks on push to dev for baseline (#14899)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-17 13:08:45 -10:00
J. Nick Koston
6b91df8d75 [esp32_ble][esp32_ble_server] Inline is_active/is_running and remove STL bloat (#14875) 2026-03-17 13:05:16 -10:00
J. Nick Koston
1670f04a87 [core] Add CodSpeed C++ benchmarks for protobuf, main loop, and helpers (#14878) 2026-03-17 12:29:38 -10:00
J. Nick Koston
1adf05e2d5 [esp32_ble] Fix EventPool/LockFreeQueue sizing off-by-one (#14892) 2026-03-17 22:24:02 +00:00
J. Nick Koston
a94bb74d04 [usb_uart] Fix EventPool/LockFreeQueue sizing off-by-one (#14895) 2026-03-18 11:18:31 +13:00
J. Nick Koston
c19c75220b [usb_host] Fix EventPool/LockFreeQueue sizing off-by-one (#14896) 2026-03-18 11:17:59 +13:00
J. Nick Koston
97382ed814 [usb_cdc_acm] Fix EventPool/LockFreeQueue sizing off-by-one (#14894) 2026-03-18 11:17:43 +13:00
J. Nick Koston
5f06679d78 [espnow] Fix EventPool/LockFreeQueue sizing off-by-one (#14893) 2026-03-18 11:16:44 +13:00
Jonathan Swoboda
851e8b6c0d [gree] Fix IR checksum for YAA/YAC/YAC1FB9/GENERIC models (#14888) 2026-03-17 16:28:13 -04:00
J. Nick Koston
9a729608d5 [core] Add back deprecated set_internal() for external projects (#14887) 2026-03-17 19:58:05 +00:00
Jonathan Swoboda
53fa346ddc [speaker] Fix media playlist using announcement delay (#14889) 2026-03-17 19:18:49 +00:00
J. Nick Koston
b3210de374 [core] Extract shared C++ build helpers from cpp_unit_test.py (#14883) 2026-03-17 08:53:36 -10:00
J. Nick Koston
82ccc37ba1 [ethernet] Mark EthernetComponent as final (#14842) 2026-03-17 08:14:52 -10:00
J. Nick Koston
3826e95506 [api] Fix ProtoMessage protected destructor compile error on host platform (#14882) 2026-03-17 08:14:36 -10:00
Jonathan Swoboda
b083491e74 [microphone] Switch IDF test to new I2S driver (#14886) 2026-03-17 13:46:32 -04:00
Diorcet Yann
73ca0ff106 [core] Small improvements (#14884) 2026-03-17 09:22:31 -04:00
Jesse Hills
bba11b3b1e Merge branch 'beta' into dev 2026-03-17 14:14:03 +13:00
Jesse Hills
a40d97f346 Merge pull request #14873 from esphome/bump-2026.3.0b3
2026.3.0b3
2026-03-17 14:13:29 +13:00
Jesse Hills
d6c67d5c35 Bump version to 2026.3.0b3 2026-03-17 11:45:03 +13:00
Jonathan Swoboda
0816b27398 [core] Support both dot and dash separators in Version.parse (#14858) 2026-03-17 11:45:03 +13:00
Jonathan Swoboda
9133582aa0 [as3935] Fix ENERGY_MASK dropping bit 4 of lightning energy MMSB (#14861) 2026-03-17 11:45:02 +13:00
Jonathan Swoboda
f36b0fcb61 [am43] Fix battery update throttle using wrong type (#14864) 2026-03-17 11:45:02 +13:00
Jonathan Swoboda
bb0a5dc8a8 [lilygo_t5_47] Fix Y coordinate mapping and clamp touch point count (#14865) 2026-03-17 11:45:02 +13:00
Jonathan Swoboda
0c260e483e [gpio][dallas_temp] Fix one_wire read64() and DS18S20 division by zero (#14866) 2026-03-17 11:45:02 +13:00
tomaszduda23
b8ce907976 [ble_nus] fix uart debug (#14850) 2026-03-17 11:45:02 +13:00
dependabot[bot]
ffce637ea5 Bump aioesphomeapi from 44.5.1 to 44.5.2 (#14849)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-17 11:45:02 +13:00
J. Nick Koston
d6fba39037 [runtime_image] Add esp-dsp dependency for JPEGDEC SIMD on ESP32 (#14840)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-03-17 11:45:02 +13:00
J. Nick Koston
5d5c2723b2 [fastled] Include esp_lcd IDF component for ESP32-S3 compatibility (#14839) 2026-03-17 11:45:02 +13:00
guillempages
06d1498c47 [runtime_image] Update jpegdec lib version (#14726)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-03-17 11:45:02 +13:00
Fabrice
2142bc1b76 [mipi_rgb] Make h- and v-sync pins optional (#14870) 2026-03-17 09:25:11 +11:00
KamilCuk
f81e04b036 [web_server] Fix wrong printf format specifier (#14836) 2026-03-16 11:30:31 -10:00
Jonathan Swoboda
c3327d0b43 [i2s_audio] Fix ESP-IDF 6.0 compatibility for I2S port types (#14818)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2026-03-16 16:04:20 -04:00
Jonathan Swoboda
8577c26358 [i2c] Handle ESP_ERR_INVALID_RESPONSE as NACK for IDF 6.0 (#14867) 2026-03-16 20:03:09 +00:00
Jonathan Swoboda
80730fd012 [seeed_mr24hpc1] Fix frame parser length handling bugs (#14863) 2026-03-16 09:57:53 -10:00
dependabot[bot]
5ee3e94ca1 Bump actions/create-github-app-token from 2.2.1 to 3.0.0 (#14868)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 09:57:33 -10:00
dependabot[bot]
037f75e0ff Bump github/codeql-action from 4.32.6 to 4.33.0 (#14869)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 09:57:17 -10:00
Jonathan Swoboda
c47f4fbc1c [core] Support both dot and dash separators in Version.parse (#14858) 2026-03-16 09:45:16 -10:00
Jonathan Swoboda
2f86e48a83 [as3935] Fix ENERGY_MASK dropping bit 4 of lightning energy MMSB (#14861) 2026-03-16 09:44:55 -10:00
Jonathan Swoboda
0bbba75757 [am43] Fix battery update throttle using wrong type (#14864) 2026-03-16 09:42:13 -10:00
Jonathan Swoboda
9362d9745e [ci] Fix clang-tidy hash check 403 error on fork PRs (#14860) 2026-03-16 09:41:21 -10:00
Jonathan Swoboda
c8f708c13c [lilygo_t5_47] Fix Y coordinate mapping and clamp touch point count (#14865) 2026-03-16 09:40:24 -10:00
Jonathan Swoboda
05590a3a21 [gpio][dallas_temp] Fix one_wire read64() and DS18S20 division by zero (#14866) 2026-03-16 09:39:26 -10:00
Jonathan Swoboda
cdf2867baf [hub75] Bump esp-hub75 to 0.3.4 (#14862) 2026-03-16 15:05:56 -04:00
J. Nick Koston
b142557979 [ethernet] Add RP2040 W5500 Ethernet support (#14820) 2026-03-16 18:26:06 +00:00
J. Nick Koston
db405c483e [core] Cache errno to avoid duplicate __errno() calls (#14751)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 07:35:34 -10:00
J. Nick Koston
808c7b67b3 [core] Inline WarnIfComponentBlockingGuard::finish() into header (#14798)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 07:35:19 -10:00
J. Nick Koston
7131eafc09 [logger] Reduce per-message overhead by inlining hot path helpers (#14851) 2026-03-16 07:35:04 -10:00
J. Nick Koston
7b4af76a61 [core] Inline Mutex on all embedded platforms (#14756)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-16 07:33:10 -10:00
J. Nick Koston
2cd93daa5e [api] Optimize plaintext varint encoding and devirtualize write_protobuf_packet (#14758) 2026-03-16 07:32:58 -10:00
J. Nick Koston
f86bb2bdb0 [ethernet] Add IDF 6.0 registry component dependencies (#14847)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-16 08:02:59 -04:00
tomaszduda23
414182fe6d [ble_nus] fix uart debug (#14850) 2026-03-15 21:08:05 -10:00
dependabot[bot]
2ee0df1da3 Bump aioesphomeapi from 44.5.1 to 44.5.2 (#14849)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 05:43:38 +00:00
Jonathan Swoboda
e1252e32d1 [deep_sleep] Fix ESP-IDF 6.0 GPIO wakeup API rename (#14846) 2026-03-15 19:10:30 -10:00
Jonathan Swoboda
1183ef825b [usb_host] Fix ESP-IDF 6.0 compatibility for external USB host component (#14844) 2026-03-15 19:09:55 -10:00
Keith Burzinski
c09edb94c1 [tinyusb] Fix regression from bump to 2.x in #14796 (#14848) 2026-03-16 00:04:07 -05:00
J. Nick Koston
9948adc6a0 [runtime_image] Add esp-dsp dependency for JPEGDEC SIMD on ESP32 (#14840)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-03-15 18:15:01 -10:00
J. Nick Koston
ccb467b219 [fastled] Include esp_lcd IDF component for ESP32-S3 compatibility (#14839) 2026-03-15 18:14:41 -10:00
J. Nick Koston
1377776d21 [ethernet] Restructure for multi-platform support (#14819) 2026-03-15 15:17:21 -10:00
J. Nick Koston
29501ef4f8 [core] Mark leaf Component subclasses as final (#14833)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 15:13:34 -10:00
J. Nick Koston
d97c23b8e3 [core] Add no-arg status_set_warning() to allow linker GC of const char* overload (#14821) 2026-03-15 15:13:10 -10:00
Bonne Eggleston
92d5e7b18c [tests] Fix integration helper to match entities exactly (#14837)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-15 13:02:23 -10:00
Jesse Hills
15ce4b3616 Merge branch 'beta' into dev 2026-03-16 11:46:15 +13:00
Jesse Hills
254e1f3abb Merge pull request #14834 from esphome/bump-2026.3.0b2
2026.3.0b2
2026-03-16 11:45:42 +13:00
Jesse Hills
deb6b97eea Bump version to 2026.3.0b2 2026-03-16 09:25:21 +13:00
J. Nick Koston
22ea2764d4 [debug] Fix shared buffer between reset reason and wakeup cause (#14813) 2026-03-16 09:25:21 +13:00
J. Nick Koston
632dbc8fe8 [core] Inline LwIPLock as no-op on platforms without lwIP core locking (#14787) 2026-03-16 09:25:21 +13:00
leccelecce
98d9871620 [online_image] Log download duration in milliseconds instead of seconds (#14803) 2026-03-16 09:25:21 +13:00
J. Nick Koston
a064eceb9b [template] Fix misleading 'Text value too long to save' warning (#14753) 2026-03-16 09:25:21 +13:00
J. Nick Koston
49107f2174 [api] Increase log Nagle coalescing on all platforms except ESP8266 (#14752) 2026-03-16 09:25:21 +13:00
J. Nick Koston
e9c2659147 [select] Fix -Wmaybe-uninitialized warnings on ESP8266 (#14759) 2026-03-16 09:25:20 +13:00
Kjell Braden
18b54f075e [runtime_image] fix BMP parsing (#14762) 2026-03-16 09:25:20 +13:00
J. Nick Koston
45e40223ac [rp2040] Fix compiler warnings in crash_handler and mdns (#14739) 2026-03-16 09:25:20 +13:00
J. Nick Koston
1ab1534028 [mdns] Fix RP2040 mDNS not restarting after WiFi reconnect (#14737)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-16 09:25:20 +13:00
J. Nick Koston
039efdb02a [i2c] Fix RP2040 I2C bus selection based on pin assignment (#14745) 2026-03-16 09:25:20 +13:00
J. Nick Koston
b0447dc521 [light] Fix binary light spamming 'brightness not supported' warning with strobe effect (#14735) 2026-03-16 09:25:20 +13:00
J. Nick Koston
aacbaab5f8 [wifi] Reject EAP/WPA2 Enterprise config on unsupported platforms (#14746) 2026-03-16 09:25:20 +13:00
J. Nick Koston
dc5032f72f [water_heater] Set OPERATION_MODE feature flag when modes are configured (#14748) 2026-03-16 09:25:20 +13:00
J. Nick Koston
c263c2c382 [captive_portal] Fix captive portal inaccessible when web_server auth is configured (#14734)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-16 09:25:20 +13:00
J. Nick Koston
910784ca84 [debug] Fix missing reset reason for RP2040/RP2350 (#14740) 2026-03-16 09:25:20 +13:00
J. Nick Koston
0b99e8f08d [rp2040] Use full flash for sketch in testing mode (#14747)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 09:25:20 +13:00
J. Nick Koston
93be539789 [light] Fix ambiguous set_effect overload for const char* (#14732) 2026-03-16 09:25:20 +13:00
Brian Kaufman
390bb0451f [OTA] Stage exact uploaded size for ESP8266 web OTA (gzip fix) (#14741) 2026-03-16 09:25:20 +13:00
J. Nick Koston
14c3e2d9d9 [api] Fix heap-buffer-overflow in protobuf message dump for StringRef (#14721) 2026-03-16 09:25:20 +13:00
J. Nick Koston
23c7e0f803 [uart] Allow hardware UART with single pin on RP2040 (#14725) 2026-03-16 09:25:20 +13:00
J. Nick Koston
cb4d1d1b5e [api] Fix undefined behavior in noise handshake with empty rx buffer (#14722) 2026-03-16 09:25:20 +13:00
J. Nick Koston
2ba807efe8 [adc] Fix PICO_VSYS_PIN compile error on RP2350 boards (#14724) 2026-03-16 09:25:20 +13:00
J. Nick Koston
c8cf9b74b1 [ota][socket] Fix ESP8266/RP2040 OTA timeout by using SO_RCVTIMEO instead of polling (#14675) 2026-03-16 09:25:19 +13:00
J. Nick Koston
33475703da [time] Fix settimeofday() failure on ESP8266 (#14707)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-16 09:25:19 +13:00
J. Nick Koston
1b7d0f9c0b [esp32_ble_client] Fix disconnect race that causes stuck connections (#14211)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 09:25:19 +13:00
J. Nick Koston
1d881ef6f4 [socket] Fast path for TCP_NODELAY bypasses lwip_setsockopt overhead (#14693) 2026-03-16 09:25:19 +13:00
J. Nick Koston
3a838d897f [socket] Fix use-after-free in LWIP PCB close/abort path (#14706) 2026-03-16 09:25:19 +13:00
Matthias König
da130c900f [mqtt] Fixed permission denied error for client certificates on Windows (#13525)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-03-16 09:25:19 +13:00
Kevin Ahrendt
440734dadf [audio] Bump microOpus to v0.3.5 (#14727) 2026-03-16 09:25:19 +13:00
Brian Kaufman
df2ddc47ec [web_server] use DETAIL_ALL in update_all_json_generator (#14711) 2026-03-16 09:25:19 +13:00
Keith Burzinski
4b1c4ba5c0 [ledc] Fix high-pressure crash & recovery (#14720) 2026-03-16 09:25:19 +13:00
Adam DeMuri
6002badb3c [modbus] Fix buffer overflow in modbus (#14719)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-16 09:25:19 +13:00
J. Nick Koston
e8f51fec88 [rp2040] Fix crash handler design flaws (#14716) 2026-03-16 09:25:19 +13:00
Keith Burzinski
7cec2d3029 [ethernet] ESP32-S3 Ethernet compilation fix (#14717) 2026-03-16 09:25:19 +13:00
J. Nick Koston
2b0c471ed7 [esp32] Add crash handler to capture and report backtrace across reboots (#14709) 2026-03-16 09:25:19 +13:00
Keith Burzinski
064bd13ebb [ethernet] ESP32-P4 Ethernet compilation fix (#14714)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-03-16 09:25:19 +13:00
Jonathan Swoboda
2627490a11 [esp32_hosted] Bump esp_hosted to 2.12.1 (#14708)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 09:25:19 +13:00
dependabot[bot]
4219d6d367 Bump tornado from 6.5.4 to 6.5.5 (#14704)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 09:25:19 +13:00
Jonathan Swoboda
33f9ad9cee [esp32] Support non-numeric version extras in IDF version string (#14826) 2026-03-15 14:58:12 -04:00
Jonathan Swoboda
18a082de30 [ci] Support URL and version extras in generate-esp32-boards.py (#14828)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-03-15 14:58:01 -04:00
Jonathan Swoboda
7f418d969e [multiple] Fix implicit int-to-gpio_num_t conversions for GCC 15 (#14830) 2026-03-15 14:57:52 -04:00
Jonathan Swoboda
fe9f19d9ed [mqtt] Fix ESP-IDF 6.0 compatibility for external MQTT component (#14822) 2026-03-15 09:30:12 -04:00
J. Nick Koston
d37f8876d7 [bthome_mithermometer][xiaomi_ble] Migrate CCM to PSA AEAD API for ESP-IDF 6.0 (#14816) 2026-03-14 13:39:07 -10:00
J. Nick Koston
d7c42bc9ec [debug] Fix ESP-IDF 6.0 compatibility for wakeup cause API (#14812) 2026-03-14 13:38:51 -10:00
J. Nick Koston
efc508a82b [dlms_meter] Migrate GCM to PSA AEAD API for ESP-IDF 6.0 (#14817) 2026-03-14 13:18:40 -10:00
J. Nick Koston
0edc0fd9c8 [esp32_ble_tracker] Migrate to PSA Crypto API for ESP-IDF 6.0 (#14811) 2026-03-14 13:17:56 -10:00
J. Nick Koston
cc4c13930f [hmac_sha256] Migrate to PSA Crypto MAC API for ESP-IDF 6.0 (#14814) 2026-03-14 13:17:43 -10:00
J. Nick Koston
234ca7c951 [debug] Fix shared buffer between reset reason and wakeup cause (#14813) 2026-03-14 13:17:32 -10:00
J. Nick Koston
447c4669b1 [esp32] Disable SHA-512 in mbedTLS on IDF 6.0+ and add idf_version() helper (#14810) 2026-03-14 11:26:20 -10:00
J. Nick Koston
27942f1973 [helpers] Replace deprecated std::is_trivial in FixedRingBuffer (#14808) 2026-03-14 11:05:39 -10:00
J. Nick Koston
158a119a5a [sha256] Migrate to PSA Crypto API for ESP-IDF 6.0 (#14809) 2026-03-14 10:43:04 -10:00
Jonathan Swoboda
b126f3af3b [ledc] Fix ESP-IDF 6.0 compatibility for peripheral reset (#14790)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 15:02:13 -04:00
Jonathan Swoboda
d4e1e32a30 [mipi_dsi] Fix ESP-IDF 6.0 compatibility for use_dma2d flag (#14792)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 15:02:06 -04:00
Jonathan Swoboda
271b423b22 [psram] Fix ESP-IDF 6.0 compatibility for PSRAM sdkconfig options (#14794)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 15:01:58 -04:00
Jonathan Swoboda
417858f098 [psram] Add ESP32-C61 PSRAM support (#14795)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 15:01:49 -04:00
Jonathan Swoboda
c52042e023 [tinyusb][usb_cdc_acm] Bump esp_tinyusb to 2.1.1 (#14796)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 15:01:29 -04:00
Jonathan Swoboda
f12531e7e0 [esp32_camera] Bump esp32-camera to 2.1.5 (#14806)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 08:32:17 -10:00
J. Nick Koston
ca279110c9 [output] Inline trivial FloatOutput accessors (#14786)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-14 08:31:50 -10:00
J. Nick Koston
f2968e0449 [api] Reduce API code size with buffer and nodelay optimizations (#14797) 2026-03-14 08:13:50 -10:00
J. Nick Koston
0043be6165 [core] Inline trivial EntityBase accessors (#14782)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-14 08:13:01 -10:00
J. Nick Koston
0716c9f722 [core] Inline LwIPLock as no-op on platforms without lwIP core locking (#14787) 2026-03-14 08:12:04 -10:00
leccelecce
fcf5637aa5 [online_image] Log download duration in milliseconds instead of seconds (#14803) 2026-03-14 09:15:54 -04:00
J. Nick Koston
5e3c44d48f [rp2040] Add CI check for boards.py freshness (#14754)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-13 13:28:55 -10:00
J. Nick Koston
d6d3bbbad8 [scheduler] Use integer math for interval offset calculation (#14755) 2026-03-13 13:28:34 -10:00
Jonathan Swoboda
86b7933081 [esp32_rmt_led_strip][remote_transmitter][remote_receiver] Fix ESP-IDF 6.0 RMT compatibility (#14783)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 19:24:41 -04:00
J. Nick Koston
7cceb72cc3 [api] Inline force-variant ProtoSize calc methods (#14781) 2026-03-13 13:23:41 -10:00
J. Nick Koston
56f7b3e61b [ci] Only run integration tests for changed components (#14776) 2026-03-13 13:20:35 -10:00
J. Nick Koston
22062d79a2 [analyze-memory] Add function call frequency analysis (#14779) 2026-03-13 13:20:17 -10:00
Jonathan Swoboda
ab3b677113 [adc] Fix ESP-IDF 6.0 compatibility for ADC_ATTEN_DB_12 (#14784)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 23:11:18 +00:00
Jonathan Swoboda
cdb445f69d [mipi_dsi] Fix ESP-IDF 6.0 compatibility for LCD color format (#14785)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 23:00:28 +00:00
Thomas SAMTER
1eed1adfa0 [pid] Replace std::deque with FixedRingBuffer (#14733)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-13 11:38:45 -10:00
J. Nick Koston
a6c08576be [sensor] Use FixedRingBuffer in SlidingWindowFilter, add window_size limit (#14736) 2026-03-13 10:17:40 -10:00
dependabot[bot]
f41aa8b18c Bump ruff from 0.15.5 to 0.15.6 (#14774)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-13 19:35:10 +00:00
Jonathan Swoboda
6700347a48 [wifi] Fix ESP-IDF 6.0 compatibility (#14766)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 14:47:12 -04:00
Jonathan Swoboda
b147830ef9 [core] Fix std::isnan conflict with picolibc on ESP-IDF 6.0 (#14768)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 14:24:39 -04:00
J. Nick Koston
bd844fcd0a [template] Fix misleading 'Text value too long to save' warning (#14753) 2026-03-13 07:37:44 -10:00
J. Nick Koston
8936be628f [api] Increase log Nagle coalescing on all platforms except ESP8266 (#14752) 2026-03-13 07:37:30 -10:00
J. Nick Koston
5920fa97e4 [select] Fix -Wmaybe-uninitialized warnings on ESP8266 (#14759) 2026-03-13 09:20:50 -04:00
Kjell Braden
326769e43c [runtime_image] fix BMP parsing (#14762) 2026-03-13 09:18:42 -04:00
Thomas SAMTER
7524590bcf [const] Add CONF_CLIMATE_ID for climate component sub-entities (#14764) 2026-03-13 09:17:11 -04:00
Michael Kerscher
15ec46abfe [vbus] add DeltaSol CS4 (Citrin Solar 1.3) (#12477) 2026-03-12 22:31:16 -07:00
J. Nick Koston
920af91db6 [rp2040] Fix compiler warnings in crash_handler and mdns (#14739) 2026-03-13 01:37:46 +00:00
J. Nick Koston
a744261934 [mdns] Fix RP2040 mDNS not restarting after WiFi reconnect (#14737)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-13 01:12:22 +00:00
J. Nick Koston
59c1368440 [i2c] Fix RP2040 I2C bus selection based on pin assignment (#14745) 2026-03-12 14:53:46 -10:00
J. Nick Koston
7e8e085a04 [light] Fix binary light spamming 'brightness not supported' warning with strobe effect (#14735) 2026-03-12 14:49:07 -10:00
J. Nick Koston
22b25724ae [wifi] Reject EAP/WPA2 Enterprise config on unsupported platforms (#14746) 2026-03-12 14:48:55 -10:00
J. Nick Koston
89719cf4b2 [water_heater] Set OPERATION_MODE feature flag when modes are configured (#14748) 2026-03-12 14:48:41 -10:00
J. Nick Koston
e15b19b223 [captive_portal] Fix captive portal inaccessible when web_server auth is configured (#14734)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-12 14:48:29 -10:00
J. Nick Koston
2ca13972b9 [debug] Fix missing reset reason for RP2040/RP2350 (#14740) 2026-03-12 14:48:06 -10:00
J. Nick Koston
7bb4e75459 [rp2040] Use full flash for sketch in testing mode (#14747)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 14:47:16 -10:00
J. Nick Koston
fd8e510745 [light] Fix ambiguous set_effect overload for const char* (#14732) 2026-03-12 18:28:25 -05:00
Brian Kaufman
25c74c8f99 [OTA] Stage exact uploaded size for ESP8266 web OTA (gzip fix) (#14741) 2026-03-12 13:23:29 -10:00
J. Nick Koston
05d285ba86 [api] Fix heap-buffer-overflow in protobuf message dump for StringRef (#14721) 2026-03-12 07:16:53 -10:00
J. Nick Koston
186ca4e458 [uart] Allow hardware UART with single pin on RP2040 (#14725) 2026-03-12 07:16:38 -10:00
J. Nick Koston
618312f0ee [api] Fix undefined behavior in noise handshake with empty rx buffer (#14722) 2026-03-12 07:16:23 -10:00
J. Nick Koston
70d188202a [adc] Fix PICO_VSYS_PIN compile error on RP2350 boards (#14724) 2026-03-12 07:16:08 -10:00
J. Nick Koston
4a21afe7ce [ota][socket] Fix ESP8266/RP2040 OTA timeout by using SO_RCVTIMEO instead of polling (#14675) 2026-03-12 07:15:48 -10:00
J. Nick Koston
fd1d016795 [time] Fix settimeofday() failure on ESP8266 (#14707)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-12 07:15:34 -10:00
J. Nick Koston
03c091adfc [esp32_ble_client] Fix disconnect race that causes stuck connections (#14211)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 07:15:21 -10:00
J. Nick Koston
a3a88acfcf [socket] Fast path for TCP_NODELAY bypasses lwip_setsockopt overhead (#14693) 2026-03-12 07:15:04 -10:00
J. Nick Koston
07f8ae6c82 [socket] Fix use-after-free in LWIP PCB close/abort path (#14706) 2026-03-12 07:14:49 -10:00
Matthias König
25c30ac5bb [mqtt] Fixed permission denied error for client certificates on Windows (#13525)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-03-12 12:00:08 -04:00
guillempages
a76767a0ab [runtime_image] Update jpegdec lib version (#14726)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-03-12 10:15:20 -04:00
Kevin Ahrendt
511d185772 [audio] Bump microOpus to v0.3.5 (#14727) 2026-03-12 08:56:01 -04:00
Brian Kaufman
c4c19c8a6c [web_server] use DETAIL_ALL in update_all_json_generator (#14711) 2026-03-11 23:07:26 -10:00
Massimo Antonello
fe2d60ccec [one_wire] allow changing address at runtime (#12150) 2026-03-12 01:52:58 -07:00
Keith Burzinski
657890695f [ledc] Fix high-pressure crash & recovery (#14720) 2026-03-12 03:16:02 -05:00
Adam DeMuri
8a5f008aee [modbus] Fix buffer overflow in modbus (#14719)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-11 22:00:26 -10:00
J. Nick Koston
f8a22b87b8 [rp2040] Fix crash handler design flaws (#14716) 2026-03-12 18:23:01 +13:00
Keith Burzinski
7f38d95424 [ethernet] ESP32-S3 Ethernet compilation fix (#14717) 2026-03-11 23:48:27 -05:00
Javier Peletier
bb7d96b954 [const] Add UNIT_METER_PER_SECOND, UNIT_MILLILITRE, UNIT_POUND to const.py (#14713) 2026-03-11 16:31:17 -10:00
J. Nick Koston
8daa946afa [esp32] Add crash handler to capture and report backtrace across reboots (#14709) 2026-03-12 14:00:20 +13:00
Keith Burzinski
ddc40f44fa [ethernet] ESP32-P4 Ethernet compilation fix (#14714)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-03-11 19:56:25 -05:00
Jonathan Swoboda
409640c0ee [esp32_hosted] Bump esp_hosted to 2.12.1 (#14708)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 16:30:44 -04:00
Jesse Hills
822c9161c6 Merge branch 'beta' into dev 2026-03-12 09:15:50 +13:00
Jesse Hills
ad198fd77b Merge pull request #14702 from esphome/bump-2026.3.0b1
2026.3.0b1
2026-03-12 09:15:11 +13:00
dependabot[bot]
a060f175ad Bump actions/download-artifact from 8.0.0 to 8.0.1 (#14705)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-11 09:28:46 -10:00
dependabot[bot]
73f305ff9c Bump tornado from 6.5.4 to 6.5.5 (#14704)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-11 09:28:19 -10:00
Jesse Hills
b6ff7185e7 [ci] Dont run codeowners workflows on release or beta PRs (#14703) 2026-03-12 08:04:07 +13:00
J. Nick Koston
928f6f1866 [ci] Add PR title check for unescaped angle brackets (#14701) 2026-03-12 07:57:43 +13:00
Jesse Hills
02f7aee680 Bump version to 2026.3.0b1 2026-03-12 07:34:53 +13:00
Jesse Hills
e7c3277eeb Bump version to 2026.4.0-dev 2026-03-12 07:34:53 +13:00
Kevin Ahrendt
bef5e4de9c [speaker_source] Add announcement pipeline (#14654) 2026-03-11 08:29:17 -10:00
Jonathan Swoboda
04bcd9f56b [dashboard] Use sys.executable for dashboard subprocess commands (#14698)
Co-authored-by: Jonathan Swoboda <swoboda1337@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-03-11 14:25:36 -04:00
Jonathan Swoboda
03c0ce704b Bump pyupgrade to v3.21.2 for Python 3.14 compatibility (#14699)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 08:22:48 -10:00
Kevin Ahrendt
b27165a842 [speaker_source] Add shuffle support (#14653) 2026-03-11 08:11:00 -10:00
Big Mike
3d4ebe74ce [sensirion_common] Use SmallBufferWithHeapFallback helper (#14270)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-11 08:00:42 -10:00
Kevin Ahrendt
4e16f270a3 [speaker_source] Add playlist management (#14652) 2026-03-11 07:47:58 -10:00
Jonathan Swoboda
c52a48ed38 [multiple] Convert static function locals to member variables (#14689)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2026-03-11 07:11:46 +00:00
Robert Resch
236f6b1935 [micronova] Add command queue (#12268)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: edenhaus <edenhaus@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2026-03-11 19:52:43 +13:00
J. Nick Koston
e8e700a683 [socket] Fix RP2040 heap corruption from malloc in lwip accept callback (#14687) 2026-03-11 19:51:54 +13:00
Adam DeMuri
4df3d3554e Enable the address and behavior sanitizers for C++ component unit tests (#13490) 2026-03-10 19:44:05 -10:00
J. Nick Koston
4fbdf00bcb Merge branch 'dev' into remove_posix_tz_parser 2026-03-10 18:45:54 -10:00
Keith Burzinski
d0f37ae694 [logger] Fix UART selection not applied before pre_setup() (#14690) 2026-03-11 04:31:27 +00:00
Keith Burzinski
6561c9bc95 [core] Fix waiting for port indefinitely (#14688) 2026-03-10 17:32:29 -10:00
J. Nick Koston
794098de99 [rp2040] Add HardFault crash handler with backtrace (#14685) 2026-03-10 16:40:45 -10:00
CFlix
b84d773bec [bme280] Change communication error message to include "no response" hint. (#14686) 2026-03-10 20:24:46 -04:00
Thomas Rupprecht
dcbf3c8728 [esp32] gpio type improvements (#14517) 2026-03-10 18:18:35 -04:00
J. Nick Koston
30c8c68703 [socket] Fix RP2040 TCP race condition between lwip callbacks and main loop (#14679) 2026-03-10 11:22:23 -10:00
CFlix
9513edc468 [dew_point] Add dew_point sensor component (#14441)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-03-10 17:17:13 -04:00
J. Nick Koston
6356e3def9 [core] Warn on crystal frequency mismatch during serial upload (#14582)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 20:42:38 +00:00
Jonathan Swoboda
8d988723cd [config] Allow !extend/!remove on components without id in schema (#14682)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 16:38:50 -04:00
Kevin Ahrendt
8ca6ee4349 [speaker_source] Add new media player (#14649)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-10 20:25:26 +00:00
mahumpula
780e009bf4 [runtime_image] Add support for 8bit BMPs and fix existing issues (#10733)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-03-10 16:23:49 -04:00
Jonathan Swoboda
04d80cfb75 [esp32_hosted] Bump esp_wifi_remote and esp_hosted versions (#14680)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 15:17:30 -04:00
J. Nick Koston
9404eadaf8 [rp2040_ble] Add BLE component for RP2040/RP2350 (#14603) 2026-03-10 09:12:28 -10:00
J. Nick Koston
4d2ef09a29 [log] Detect early log calls before logger init and optimize hot path (#14538) 2026-03-10 09:12:10 -10:00
J. Nick Koston
89bb5d9e42 [core] Require explicit synchronous= for register_action (#14606) 2026-03-10 09:11:45 -10:00
J. Nick Koston
9dd3ec258c [scheduler] Replace unique_ptr with raw pointers, add leak detection (#14620) 2026-03-10 09:11:28 -10:00
J. Nick Koston
c709010c4c [api] Replace std::vector<uint8_t> with APIBuffer to skip zero-fill (#14593) 2026-03-10 09:11:12 -10:00
J. Nick Koston
6e468936ec [api] Inline ProtoVarInt::parse fast path and return consumed in struct (#14638)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 09:10:55 -10:00
J. Nick Koston
2c7ef4f758 [rp2040] Use picotool for BOOTSEL upload and improve upload UX (#14483) 2026-03-10 09:10:33 -10:00
Diorcet Yann
06a127f64b [core] ESP-IDF compilation fixes (#14541) 2026-03-10 11:52:48 -04:00
Anunay Kulshrestha
fba21e6dd4 [bl0940] Fix reset_calibration() declaration missing from header (#14676)
Co-authored-by: Claude <noreply@anthropic.com>
2026-03-10 10:44:19 -04:00
J. Nick Koston
4b50d14496 [serial_proxy] Reduce loop() overhead by disabling when idle and splitting read path (#14673) 2026-03-10 02:10:03 -05:00
Javier Peletier
e82f0f4432 [cpptests] support testing platform components (#13075)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-10 02:41:02 +00:00
Tobias Stanzel
00f809f5f0 [sen6x] fix memory leak issue (#14623)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-09 21:45:20 -04:00
Jonathan Swoboda
c31ac662bd [multiple] Fix crashes from malformed external input (#14643)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-09 20:39:58 -04:00
J. Nick Koston
d6ce5dda81 [ci] Skip YAML anchor keys in integration fixture component extraction (#14670) 2026-03-09 22:54:56 +00:00
J. Nick Koston
dadbdd0f7b [ci] Make codeowner label update non-fatal for fork PRs (#14668) 2026-03-09 12:34:31 -10:00
Jonathan Swoboda
d96be88ff5 [multiple] Fix reliability issues in 5 components (#14655)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-09 18:32:57 -04:00
Jonathan Swoboda
d2686b49be [canbus] Fix multiple MCP component bugs (#14461)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:15:33 -04:00
Keith Burzinski
468ce74c8e [api][serial_proxy] Fix dangling pointer (#14640) 2026-03-09 17:04:47 -05:00
Jonathan Swoboda
b3fc43c13c [multiple] Fix wrong behavior in sensor calculations and drivers (#14644)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:00:17 -04:00
Jonathan Swoboda
308e8e78cd [ble_scanner] Escape special characters in JSON output (#14664)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:59:36 -04:00
Jonathan Swoboda
470d9160a5 [demo] Fix alarm control panel auth bypass when code is omitted (#14645)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:57:02 -04:00
Jonathan Swoboda
9902447834 [multiple] Fix minor bugs in 8 components (#14650)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:51:50 -04:00
Jonathan Swoboda
7c1b9f0cb4 [multiple] Fix wrong behavior in 5 components (#14647)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:22:06 -04:00
Jonathan Swoboda
fecedeb018 [multiple] Fix crashes from malformed external input (batch 2) (#14651)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:20:09 -04:00
Jonathan Swoboda
9418f35cc3 [multiple] Remove unnecessary heap allocations in 4 components (#14656)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:18:44 -04:00
Jonathan Swoboda
08a0608a48 [wifi][captive_portal][heatpumpir][es8388] Fix wrong behavior in 4 components (#14657)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:18:21 -04:00
Jonathan Swoboda
b721cd48e5 [hmc5883l][mmc5603][honeywellabp2][xgzp68xx][max9611] Fix uninitialized members (#14659)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:18:07 -04:00
Jonathan Swoboda
75f55adbfa [api][at581x][vl53l0x] Fix bounds check issues in 3 components (#14660)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:17:31 -04:00
Jonathan Swoboda
a379e5a635 [runtime_image][st7701s] Fix BMP decoder and LCD init bugs (#14663)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:16:29 -04:00
dependabot[bot]
019db74582 Bump setuptools from 82.0.0 to 82.0.1 (#14665)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-09 20:44:27 +00:00
Jonathan Swoboda
31f4b4d00d [multiple] Fix undefined behavior across components (#14639)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 07:33:08 -04:00
Jonathan Swoboda
0db9137d91 [multiple] Add division by zero guards (#14634)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2026-03-09 00:10:48 -04:00
Clyde Stubbs
f3ca86b670 [ci-custom] Directions on constant hoisting (#14637) 2026-03-08 23:48:03 -04:00
J. Nick Koston
088a8a4338 [ci] Match symbols with changed signatures in memory impact analysis (#14600)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 17:23:58 -10:00
Jonathan Swoboda
5d3893368d [multiple] Add array bounds checks (#14635)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 23:16:32 -04:00
Jonathan Swoboda
5b9cab02be [multiple] Add default initializers to uninitialized member variables (#14636)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 22:37:54 -04:00
Edward Firmo
cac751e9e8 [nextion] Add configurable HTTP parameters for TFT upload (#14234)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-09 01:29:41 +00:00
J. Nick Koston
6ba5c9a705 [api] Skip state_action_() call in noise data path (#14629)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 01:22:39 +00:00
J. Nick Koston
c681dc8872 [socket] Add socket wake support for RP2040 (#14498) 2026-03-08 15:11:24 -10:00
J. Nick Koston
d0285cdc41 [core] Pack entity flags into configure_entity_() and protect setters (#14564)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 15:11:15 -10:00
J. Nick Koston
9547a54fac [const] Move CONF_ENABLE_FULL_PRINTF to const.py (#14633) 2026-03-08 14:52:40 -10:00
J. Nick Koston
b05dbfccd3 [api] Bump noise-c to 0.1.11 (#14632) 2026-03-08 14:52:21 -10:00
J. Nick Koston
aef2d74e41 [ld2450] Add integration tests with mock UART (#14611) 2026-03-08 14:32:59 -10:00
J. Nick Koston
e1c849d5d2 [esp8266] Wrap printf/vprintf/fprintf to eliminate _vfiprintf_r (~1.6 KB flash) (#14621) 2026-03-08 14:32:47 -10:00
J. Nick Koston
c11ad7f0e6 [rp2040] Wrap printf/vprintf/fprintf to eliminate _vfprintf_r (~9.2 KB flash) (#14622) 2026-03-08 14:32:35 -10:00
J. Nick Koston
88536ff72b [modbus] Fix timeout for non-hardware UARTs (e.g., USB UART) (#14614)
Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
2026-03-08 14:31:42 -10:00
J. Nick Koston
93d7ec4d72 [esp32_ble] Inline ble_addr_to_uint64 to eliminate call overhead (#14591) 2026-03-08 14:07:59 -10:00
J. Nick Koston
66a5ad0d75 [core] Skip zero-initialization of StaticVector data array (#14592) 2026-03-08 14:06:55 -10:00
J. Nick Koston
771404668d [api] Inline fast path of try_to_clear_buffer (#14630) 2026-03-08 14:01:01 -10:00
J. Nick Koston
76c567a71c [scheduler] Use std::atomic<uint8_t> instead of std::atomic<bool> for remove flag (#14626) 2026-03-08 14:00:04 -10:00
J. Nick Koston
e7730cff00 [esp32_ble] Optimize BLE event hot path performance (#14627) 2026-03-08 13:59:40 -10:00
Jonathan Swoboda
d5dc4a39cb [i2s_audio] Fix mono sample swap and block 8-bit mono on ESP32 (#14516)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-08 12:10:43 -10:00
Keith Burzinski
50b3f9d25c [mixer_speaker] Add task debounce (#14581) 2026-03-08 18:09:06 -04:00
J. Nick Koston
ad5811280a [ci] Add medium-pr label for PRs with ≤100 lines changed (#14628) 2026-03-08 10:59:43 -10:00
tomaszduda23
9be1876fae [ble_nus] make ble_nus timeout shorter than watchdog (#14619)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2026-03-08 10:52:16 -10:00
dependabot[bot]
1b3a7f0b6a Bump aioesphomeapi from 44.5.0 to 44.5.1 (#14624)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-08 18:18:14 +00:00
Diorcet Yann
3f143d9f19 [ethernet] Fix commit 3f700bac1c (#14618) 2026-03-08 09:50:32 -04:00
Oliver Kleinecke
a9b5f95c76 [usb_uart] ch34x chip-type & port-count enumeration (#14544) 2026-03-08 21:24:39 +11:00
Keith Burzinski
0c4a44566f [serial_proxy] New component (#13944)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-08 03:55:49 -05:00
tomaszduda23
2c705810cd [nrf52] allow to update OTA via cmd (#12344)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-08 08:40:52 +00:00
J. Nick Koston
a530aeec22 [api] Inline varint and encode_varint_raw fast paths for hot loop performance (#14607)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 22:09:12 -10:00
dependabot[bot]
d9e76da806 Bump aioesphomeapi from 44.4.0 to 44.5.0 (#14617)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-08 07:59:25 +00:00
tomaszduda23
e4b89a69d4 [nrf52, ota] ble and serial OTA based on mcumgr (#11932)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-03-07 20:32:20 -10:00
Keith Burzinski
04cff1c916 [usb_uart] Return flush result, expose timeout via config (#14616) 2026-03-08 00:04:14 -06:00
Keith Burzinski
5e842a8b20 [uart] Return flush result, expose timeout via config (#14608)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-03-08 05:23:13 +00:00
J. Nick Koston
be6c3c52ac [api] Add force proto field option to skip zero checks on hot path (#14610)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 18:59:13 -10:00
Jonathan Swoboda
9fea8fe01b [vbus][rf_bridge][sensirion_common] Add buffer size guards (#14597)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 18:50:36 -10:00
J. Nick Koston
d55fe9a34b [api] Fix value-initialization of DeviceInfoResponse (#14615)
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
2026-03-07 18:34:35 -10:00
J. Nick Koston
66919ef969 [i2s_audio] Include legacy driver IDF component when use_legacy is set (#14613) 2026-03-07 22:33:54 -06:00
dependabot[bot]
ea7cfffdda Bump aioesphomeapi from 44.3.1 to 44.4.0 (#14609)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-08 02:40:13 +00:00
J. Nick Koston
888f3d804b [ld2420] Add integration tests with mock UART (#14471) 2026-03-07 13:22:50 -10:00
J. Nick Koston
545395a6f0 [ci] Add RP2350 to PR template test environment (#14599) 2026-03-07 13:16:19 -10:00
Oliver Kleinecke
f2dfb5e1dc [uart][usb_uart] Add debug_prefix option to distinguish multiple defined uarts in log (#14525) 2026-03-08 10:16:12 +11:00
Diorcet Yann
3f700bac1c [component] Fix components for compatibility with stricter compilers (#14545)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-03-07 18:50:44 +00:00
J. Nick Koston
a0cd35c5fc [core] Inline status_clear_warning/error fast path (#14571) 2026-03-07 07:27:08 -10:00
J. Nick Koston
e7b8ec18f1 [api] Inline APIServer::is_connected() for common no-arg path (#14574) 2026-03-07 07:26:50 -10:00
J. Nick Koston
77f2c371b2 [api] Single-pass protobuf encode for BLE proxy advertisements (#14575) 2026-03-07 07:26:34 -10:00
J. Nick Koston
45f20d9c06 [core] Merge set_name + set_entity_strings into configure_entity_ (#14444)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 07:26:01 -10:00
J. Nick Koston
f57fa4cc8d [bluetooth_proxy] Add BLE connection parameters API (#14577) 2026-03-07 07:25:33 -10:00
J. Nick Koston
abc870006c [captive_portal] Enable support for RP2040 (#14505) 2026-03-07 07:25:13 -10:00
puddly
15ffbb0b05 [uart] Fully enable raw mode with host serial (#14573) 2026-03-07 11:51:02 -05:00
Simon Redman
8b62c35ea7 [uart] Add error message when initializing UART with unsupported configuration (#13229) 2026-03-07 11:41:37 -05:00
tomaszduda23
0e106d843c [nrf52][zephyr] support for multi on rate callbacks (#14557) 2026-03-07 11:18:21 -05:00
rwrozelle
cbebb81196 [openthread] move esp functions into correct file (#14588) 2026-03-07 11:12:27 -05:00
J. Nick Koston
05ae69b766 [api] Sync api.proto from aioesphomeapi (#14579) 2026-03-06 19:00:37 -10:00
dependabot[bot]
9b489c9eba Bump aioesphomeapi from 44.2.0 to 44.3.1 (#14580)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-07 03:52:51 +00:00
Ricardo Sanz
df11e2765e [climate][haier][template][core] Relocate CONF_CURRENT_TEMPERATURE to general const file (#14503) 2026-03-07 01:00:52 +00:00
AndreKR
f53ee70caa [http_request] Make TLS buffer configurable on ESP8266 (#14009)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-03-06 14:29:20 -10:00
Jonathan Swoboda
d8deb2255d [mipi_rgb] Fix byte order and dirty bounds in fill() (#14537)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 12:18:09 -10:00
dependabot[bot]
035f985693 Bump ruff from 0.15.4 to 0.15.5 (#14565)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-06 22:16:36 +00:00
dependabot[bot]
086c1bb505 Bump docker/build-push-action from 6.19.2 to 7.0.0 in /.github/actions/build-image (#14567)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-06 12:12:44 -10:00
dependabot[bot]
c26c5935b6 Bump github/codeql-action from 4.32.5 to 4.32.6 (#14566)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-06 12:12:19 -10:00
Jonathan Swoboda
de7572bd3e [lightwaverf] Fix ISR safety issues (#14563)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:04:12 -10:00
Jonathan Swoboda
5777908da7 [iaqcore][scd30][sen21231][beken_spi_led_strip] Fix uninitialized variables and missing error checks (#14568)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:03:53 -10:00
Jonathan Swoboda
587bf68091 [ltr501][pvvx_mithermometer][smt100] Convert static locals to instance members (#14569)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-06 11:03:30 -10:00
Jonathan Swoboda
2c83c6a79f [shelly_dimmer][lvgl][seeed_mr60fda2][packet_transport] Fix buffer bounds checks (#14534)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 10:47:56 -10:00
Jonathan Swoboda
4f4b2bfdec [bmp581_base][bl0906] Fix 24-bit sign extension bugs (#14558)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 20:14:35 +00:00
Jonathan Swoboda
0469612d07 [multiple] Fix assorted medium-severity bugs (#14555)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 15:02:17 -05:00
Jonathan Swoboda
8f3db96291 [esp32_ble_server][weikai][ade7880] Fix copy-paste bugs (#14552)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 09:50:26 -10:00
Jonathan Swoboda
a9cceebb33 [pid][nextion][pn532_i2c][pipsolar] Fix copy-paste and logic bugs (#14551)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 09:48:50 -10:00
Jonathan Swoboda
9ab5f5d451 [light] Fix unsigned underflow in addressable scan effect (#14546)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 09:42:05 -10:00
Jonathan Swoboda
219d5170e0 [noblex] Fix IR receive losing decoded bytes between calls (#14533)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 14:15:54 -05:00
Jonathan Swoboda
7b8ba9bf20 [multiple] Fix cast/operator precedence bugs (#14560)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 09:14:12 -10:00
Jonathan Swoboda
3db436e48e [esp32_ble_server][espnow][time] Fix logic bugs (#14553)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 09:05:34 -10:00
Jonathan Swoboda
3c7956e72d [multiple] Add default initializers to uninitialized member variables (#14556)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 09:04:00 -10:00
J. Nick Koston
42dbb51022 [api] Devirtualize protobuf encode/calculate_size (#14449)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-06 19:03:54 +00:00
Jonathan Swoboda
9654140c00 [tm1638][rp2040_pio_led_strip][atm90e32] Fix bounds checks and off-by-one (#14559)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 09:00:46 -10:00
J. Nick Koston
8a915dcbbe [core] Move device class strings to PROGMEM on ESP8266 (#14443)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-06 08:34:27 -10:00
Thomas Rupprecht
b2378e830e [rtttl] Add AudioStreamInfo and set volume (#14439)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-03-06 18:11:52 +00:00
J. Nick Koston
65b7c73bf3 [sgp4x] Fix undefined behavior from mutating entity config at runtime (#14562)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 18:02:34 +00:00
J. Nick Koston
6e3bc7b1dd [ci] Use pull_request_target for codeowner approved label workflow (#14561) 2026-03-06 07:33:05 -10:00
J. Nick Koston
82629c397f [hlk_fm22x] Fix oversized response rejection breaking GET_ALL_FACE_IDS (#14506) 2026-03-06 07:01:50 -10:00
J. Nick Koston
a16b8fc0ac [rp2040] Fix Pico W LED pin and auto-generate board definitions for arduino-pico 5.5.x (#14528) 2026-03-06 07:00:31 -10:00
J. Nick Koston
74e4b69654 [core] Replace Application name/friendly_name std::string with StringRef (#14532)
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
2026-03-06 06:58:13 -10:00
J. Nick Koston
07e51886f3 [core] Move entity icon strings to PROGMEM on ESP8266 (#14437) 2026-03-06 06:57:52 -10:00
tomaszduda23
e59a2b3ede [nrf52] prepare for usb cdc (#14174)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-03-06 16:25:44 +00:00
Thomas Rupprecht
5084c32f3c [esp32] Fix ESP32-S3 pin validation error message (#14540) 2026-03-06 07:22:11 -05:00
Thomas Rupprecht
c0b7f41397 [esp32] Fix wrong variable usage in P4 pin validation error msg (#14539) 2026-03-06 07:21:44 -05:00
Jonathan Swoboda
6c07c15c50 [mipi_dsi][e131] Fix semaphore cast, missing return, and light count overread (#14530)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 07:18:56 -05:00
Jonathan Swoboda
666fb7cf39 [sx127x][sx126x][max6956] Fix null deref, unterminated string, and pin bounds check (#14529)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 07:18:28 -05:00
tomaszduda23
a2c0d70c2c [ble_nus] Add uart support (#14320)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-05 21:00:17 -10:00
Jonathan Swoboda
80fe54ed69 [bluetooth_proxy] Add null checks for api_connection (#14536)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 23:30:39 -05:00
Jonathan Swoboda
44870323da [host] Add null checks for getenv and fopen in preferences (#14531)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 20:47:47 -05:00
Gnuspice
58ab630965 [ethernet] add get_eth_handle() function (#14527) 2026-03-05 23:37:07 +00:00
Kevin Ahrendt
64098122e7 [audio_file] Add media source platform (#14436)
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>
2026-03-05 12:30:13 -10:00
Jonathan Swoboda
8a8f6824a2 [openthread][ethernet][wifi] Add IPv6 address array bounds assert (#14488)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 12:29:44 -10:00
tomaszduda23
b2c12d88fe [uart] init tx_pin, rx_pin, flow control, rx_buffer_size (#14524) 2026-03-05 12:24:11 -10:00
Jonathan Swoboda
e8b1dce67b [st7735][st7789v][st7920] Fix display buffer overflows and dead code (#14511)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 16:28:41 -05:00
J. Nick Koston
fbf63d8e3b [rp2040] Update arduino-pico to 5.5.1 and fix WiFi AP fallback (#14500) 2026-03-05 21:23:00 +00:00
Kevin Ahrendt
06d6322fe3 [audio] Extract detect_audio_file_type helper (#14507)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-05 21:19:45 +00:00
Jonathan Swoboda
de14e7055e [cse7761][ads1115][tmp1075][matrix_keypad][seeed_mr60bha2] Fix assorted bugs (#14518)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-05 21:10:26 +00:00
Jonathan Swoboda
3392e4d73b [usb_uart][nextion][feedback][whirlpool][packet_transport][he60r][hc8][runtime_stats] Fix millis() wrapping bugs (#14474)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-05 16:08:58 -05:00
Bonne Eggleston
b0be02e16d [modbus] Fix timing bugs and better adhere to spec (#8032)
Co-authored-by: brambo123 <52667932+brambo123@users.noreply.github.com>
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-05 20:54:17 +00:00
Jonathan Swoboda
d11e7cab46 [xiaomi_ble][pvvx_mithermometer][atc_mithermometer] Add BLE service data bounds checks (#14514)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 20:18:54 +00:00
J. Nick Koston
e25d740968 [wifi] Cache is_connected() for cheap inline access (#14463)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-03-05 09:58:58 -10:00
Jonathan Swoboda
291679126f [nfc] Fix off-by-one in NDEF message parsing (#14485)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 09:55:54 -10:00
dependabot[bot]
99a805cba6 Bump the docker-actions group across 1 directory with 2 updates (#14520)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-05 09:52:10 -10:00
Jonathan Swoboda
bb37887a8c [wled][lcd_base][touchscreen][ee895] Fix off-by-one, buffer overrun, empty deref, and uninitialized pointers (#14513)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-05 09:51:32 -10:00
Kevin Ahrendt
5c5ea8824e [audio_file] New component for embedding files into firmware (#14434)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
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>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-05 09:51:08 -10:00
Jonathan Swoboda
22d90d702d [usb_cdc_acm][scd4x][pulse_counter][mopeka_std_check][ruuvi_ble] Fix assorted one-liner bugs (#14495)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 09:47:55 -10:00
Jonathan Swoboda
9961c8180a [alpha3][mpu6886][emc2101] Fix copy-paste bugs (#14492)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-05 09:47:32 -10:00
Jonathan Swoboda
d6f3186b3d [haier][bedjet][vbus][lightwaverf] Fix buffer overflow bugs (#14493)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-05 09:47:10 -10:00
Jonathan Swoboda
05ddc85412 [rc522][sml][kamstrup_kmp] Fix buffer bounds checks (#14515)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-05 09:46:56 -10:00
Jonathan Swoboda
6f0460b0ee [sim800l][tormatic][tx20] Fix OOB access, div-by-zero, and off-by-one (#14512)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-05 09:46:47 -10:00
Olivier ARCHER
44d314d069 [GPS] fix component Python declaration to match C++ implementation (#14519) 2026-03-05 09:22:37 -10:00
J. Nick Koston
e210e414bd [ota] Devirtualize OTA backend calls (#14473) 2026-03-05 19:15:02 +00:00
Jonathan Swoboda
cce7a09fa9 [pn532_spi] Fix preamble check logic and OOB access when full_len is zero (#14486)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 14:09:34 -05:00
Jonathan Swoboda
e1d0c6da09 [dfplayer][ufire_ise][ufire_ec][qmp6988][atm90e26] Fix wrong operators and masks (#14491)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-05 14:09:23 -05:00
rwrozelle
9518d88a2a [openthread] static log level code quality improvement (#14456)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-05 08:35:20 -10:00
Jonathan Swoboda
4a5d8449fd [sht4x][grove_tb6612fng] Fix logic bugs (#14497)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 08:33:12 -10:00
Jonathan Swoboda
3df4ef9362 [ssd1322][ssd1325][ssd1327] Fix nibble mask bug in grayscale draw_pixel (#14496)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 08:31:26 -10:00
Jonathan Swoboda
01f4275202 [veml7700] Fix initial settling timeout using raw enum instead of milliseconds (#14487)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 13:16:33 -05:00
Jonathan Swoboda
a061397469 [dfrobot_sen0395][sx1509] Fix structural bugs (#14494)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 13:16:06 -05:00
J. Nick Koston
2777d35990 [api] Devirtualize frame helper calls when protocol is fixed at compile time (#14468) 2026-03-05 07:21:44 -10:00
J. Nick Koston
c49c23d5d9 [network] Inline network::is_connected() and ethernet is_connected() (#14464)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-05 07:21:04 -10:00
rwrozelle
0e2a10c5f0 [openthread] Cache is_connected() for cheap inline access (#14484)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-04 17:34:13 -10:00
Clyde Stubbs
f5c37bf486 [packet_transport] Minimise heap allocations (#14482) 2026-03-05 14:24:01 +11:00
J. Nick Koston
0ff5270632 [ci] Fix codeowner approval label workflow for fork PRs (#14490)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 16:57:19 -10:00
J. Nick Koston
5df4fd0a27 [tests] Fix flaky uart_mock integration tests (#14476) 2026-03-04 15:51:51 -10:00
Brandon Harvey
c0143ac6d6 [ai] Add docs note about keeping component index pages in sync (#14465)
Co-authored-by: Brandon Harvey <bharvey88@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-04 15:46:40 -10:00
Jonathan Swoboda
c8e7f78a25 [zwave_proxy] Fix uint8_t overflow for buffer index and frame end (#14480)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 18:32:50 -05:00
Jonathan Swoboda
b6d7e8e14d [sgp30] Fix serial number truncation from 48-bit to 24-bit (#14478)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 18:18:28 -05:00
Jonathan Swoboda
55103c0652 [ds2484] Fix read64() using uint8_t accumulator instead of uint64_t (#14479)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 18:18:14 -05:00
J. Nick Koston
61ea6c3b2f [ci] Add missing issues: write permission to codeowner approval workflow (#14477)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-04 17:46:26 -05:00
Jonathan Swoboda
e11a91411b [esp32_improv][rf_bridge][esp32_ble_server][display][lvgl][pipsolar] Fix unsigned integer underflows (#14466)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
2026-03-04 16:36:52 -05:00
Jonathan Swoboda
0c883b80c4 [inkplate][ezo_pmp][ezo][packet_transport] Fix use-after-free bugs (#14467)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 16:05:49 -05:00
Kevin Ahrendt
4928e678d1 [mixer][resampler][speaker] Use core static task manager (#14454) 2026-03-04 14:37:22 -05:00
Jonathan Swoboda
22fc3aab39 [ld2420] Fix buffer overflows in simple mode, energy mode, and calibration (#14458)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 08:19:46 -10:00
Jonathan Swoboda
c37ab1de84 [fingerprint_grow] Fix OOB write and uint16 overflow (#14462)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 12:58:52 -05:00
Jonathan Swoboda
246a8bff0c [pn7160][pn7150][pn532] Fix tag purge skipping, NDEF bounds check, and NDEF length byte order (#14460)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 12:58:42 -05:00
Jonathan Swoboda
9abba79c54 [remote_base][remote_receiver] Fix OOB access in pronto comparison and RMT buffer allocation (#14459)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 12:58:24 -05:00
Jonathan Swoboda
5ba880f19b [sx127x] Fix preamble MSB register always written as zero (#14457)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 12:57:45 -05:00
J. Nick Koston
b2e8544c58 [ld2412] Add integration tests with mock UART (#14448) 2026-03-04 07:18:31 -10:00
J. Nick Koston
ac19d05db2 [core] Call loop() directly in main loop, bypass call() indirection (#14451) 2026-03-04 07:17:41 -10:00
J. Nick Koston
065773ed4c [runtime_stats] Use micros() for accurate per-component timing (#14452) 2026-03-04 07:17:28 -10:00
JiriPrchal
37146ff565 [integration] Add set method to publish and save sensor value (#13316)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-03-04 09:00:09 -05:00
Tilman Vogel
cba34e770e [core] improve help text for --device option, mention OTA (#14445)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2026-03-03 21:18:36 -05:00
Kevin Ahrendt
8911d9d28f [media_source] Clarify threading contract (#14433) 2026-03-03 20:42:36 -05:00
J. Nick Koston
9371159a7e [core] Replace custom esphome::optional with std::optional (#14368)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 01:14:05 +00:00
Kevin Ahrendt
43a6fe9b6c [core] add a StaticTask helper to manage task lifecycles (#14446) 2026-03-04 01:06:36 +00:00
Jesse Hills
989330d6bc [globals] Fix handling of string booleans in yaml (#14447) 2026-03-03 22:54:40 +00:00
Jonathan Swoboda
ee78d7a0c0 [tests] Fix integration test race condition in PlatformIO cache init (#14435)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 17:42:41 -05:00
Thomas Rupprecht
96793a99ce [rtttl] add new codeowner (#14440) 2026-03-03 10:55:56 -10:00
Clyde Stubbs
380c0db020 [usb_uart] Don't claim interrupt interface for ch34x (#14431) 2026-03-03 14:49:38 -06:00
J. Nick Koston
95544dddf8 [ci] Add code-owner-approved label workflow (#14421) 2026-03-03 07:11:47 -10:00
J. Nick Koston
b209c903bb [core] Inline trivial Component state accessors (#14425) 2026-03-03 07:05:15 -10:00
J. Nick Koston
4f69c487da [bk72xx] Fix ~100ms loop stalls by raising main task priority (#14420) 2026-03-03 07:04:12 -10:00
J. Nick Koston
78602ccacb [ci] Add lint check to prevent powf in core and base entity platforms (#14126) 2026-03-03 07:03:50 -10:00
J. Nick Koston
1f1b20f4fe [core] Pack entity string properties into PROGMEM-indexed uint8_t fields (#14171) 2026-03-03 07:03:24 -10:00
J. Nick Koston
d53ff7892a [socket] Cache lwip_sock pointers and inline ready() chain (#14408) 2026-03-03 07:03:02 -10:00
rwrozelle
b6f0bb9b6b [speaker] Add off on capability to media player (#9295)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org>
2026-03-03 10:59:01 -05:00
Clyde Stubbs
cfde0613bb [const][uart][usb_uart][weikai][core] Move constants to components/const (#14430) 2026-03-03 07:53:18 -05:00
Jesse Hills
903c67c994 Revert "[wifi] Revert cyw43_wifi_link_status change for RP2040"
This reverts commit 1b5bf2c848.
2026-03-03 22:16:54 +13:00
Jesse Hills
d8d479cef7 Merge branch 'release' into dev 2026-03-03 22:15:06 +13:00
schrob
60d66ca2dc [openthread] Add tx power option (#14200)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-03-02 23:28:01 -05:00
J. Nick Koston
db15b94cd7 [core] Inline HighFrequencyLoopRequester::is_high_frequency() (#14423) 2026-03-03 03:17:20 +00:00
Cody Cutrer
ae49b67321 [ld2450] Clear all related sensors when a target is not being tracked (#13602)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-02 15:47:40 -10:00
melak
c77241940b [lps22] Add support for the LPS22DF variant (#14397) 2026-03-02 15:24:00 -10:00
Kevin Ahrendt
97d713ee64 [media_source] Add new Media Source platform component (#14417)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-02 15:16:38 -10:00
Jesse Hills
bc04a1a0ff Merge branch 'release' into dev 2026-03-03 11:35:53 +13:00
Jonathan Swoboda
7a87348855 [ci] Skip PR title check for dependabot PRs (#14418)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 16:49:14 -05:00
J. Nick Koston
2e623fd6c3 [tests] Fix flaky log assertion race in oversized payload tests (#14414) 2026-03-02 11:48:50 -10:00
dependabot[bot]
727fa07377 Bump github/codeql-action from 4.32.4 to 4.32.5 (#14416)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-02 11:44:53 -10:00
Lino Schmidt
5510b45f3b [const] Move CONF_WATCHDOG (#14415) 2026-03-02 11:43:06 -10:00
J. Nick Koston
3615a7b90c [core] Eliminate __udivdi3 in millis() on ESP32 and RP2040 (#14409) 2026-03-02 11:42:25 -10:00
J. Nick Koston
d1de50c0e5 [core] Add ESP8266 support to wake_loop_any_context() (#14392) 2026-03-02 21:11:04 +00:00
Jonathan Swoboda
38f671a923 [uart] Fix flow_control_pin inverted flag ignored on ESP-IDF (#14410)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 08:52:06 +13:00
J. Nick Koston
cb232d8288 [core] Fix compile-time loop() detection for multiple inheritance (#14411) 2026-03-02 19:11:47 +00:00
J. Nick Koston
2fa244715d [socket] Fix pre-existing bugs found during socket devirtualization review (#14404) 2026-03-02 08:54:54 -10:00
netixx
b9b1af1c3d [mcp23016] Fix register access to use 16-bit paired transactions (#13676)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-03-02 17:38:28 +00:00
J. Nick Koston
a1d91ac779 [core] Compile-time detection of loop() overrides (#14405) 2026-03-02 06:59:23 -10:00
J. Nick Koston
585e195044 [socket] Devirtualize socket abstraction layer (#14398) 2026-03-02 06:58:31 -10:00
J. Nick Koston
39572d9628 [light] Resolve effect names to indices at codegen time (#14265) 2026-03-02 06:58:15 -10:00
J. Nick Koston
f278250740 [core] Deduplicate ControllerRegistry notify dispatch loop (#14394) 2026-03-02 06:57:59 -10:00
J. Nick Koston
54f410901f [web_server] Avoid temporary std::string allocations in request parameter parsing (#14366)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 06:57:39 -10:00
J. Nick Koston
00242443e1 [light] Replace powf gamma with pre-computed lookup tables (LUT) (#14123)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 06:57:23 -10:00
J. Nick Koston
82da4935b6 [core] Auto-wrap static strings in PROGMEM on ESP8266 via TemplatableValue (#13885) 2026-03-02 06:57:08 -10:00
J. Nick Koston
1c5fd8bbd4 [core] Move millis_64 rollover tracking out of Scheduler (#14360)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 06:56:51 -10:00
Copilot
77a7cbcffd Use cached files on network errors in external_files (#14055)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: jesserockz <3060199+jesserockz@users.noreply.github.com>
2026-03-02 11:41:20 -05:00
Bonne Eggleston
3160457ca6 Create integration tests for modbus (#14395)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-01 22:51:27 -10:00
schrob
590ee81f7a [openthread] Disable default enabled OT console build (#14390)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-02 06:16:36 +00:00
J. Nick Koston
9d4357c619 [core] Wake main loop from ISR in enable_loop_soon_any_context() (#14383) 2026-03-01 18:20:14 -10:00
J. Nick Koston
80a2acca4f [ld2410] Add UART mock integration test for LD2410 component (#14377) 2026-03-01 18:19:32 -10:00
J. Nick Koston
f68a3ed15d [api] Remove virtual destructor from ProtoMessage (#14393) 2026-03-01 18:09:00 -10:00
schrob
6d3d8970a6 [openthread] Disable default enabled OT diag code (#14399) 2026-03-01 22:47:02 -05:00
Thomas Rupprecht
073ca63f60 [rtttl] improve comments Part 2 (#13971)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-03-01 22:44:02 -05:00
J. Nick Koston
251e0129a2 Merge remote-tracking branch 'upstream/dev' into remove_posix_tz_parser 2026-03-01 17:02:06 -10:00
J. Nick Koston
0e18e4461e [time,api] Send pre-parsed timezone struct over protobuf (#14233)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 15:52:53 +13:00
J. Nick Koston
3e7424b307 [preferences] Reduce heap churn with small inline buffer optimization (#13259) 2026-03-02 14:22:55 +13:00
J. Nick Koston
48b5cae6c4 [api] Use StringRef for user service string arguments (#13974) 2026-03-02 10:32:44 +13:00
J. Nick Koston
a1760a1980 [improv_serial] Add missing USE_IMPROV_SERIAL define to fix WiFi scan filtering (#14359) 2026-03-02 09:23:10 +13:00
J. Nick Koston
19bbd39e33 [uart] Enable wake-on-RX by default on ESP32 (#14391) 2026-02-28 21:06:46 -06:00
J. Nick Koston
3f97b3b706 [core] Extract set_status_flag_ helper to deduplicate status_set methods (#14384)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-28 14:27:06 -10:00
J. Nick Koston
82e620bcf5 [ld2450] Single-pass zone target counting (#14387) 2026-02-28 14:26:55 -10:00
J. Nick Koston
80e0761bf1 [ld2450] Use integer dedup for direction text sensor updates (#14386) 2026-02-28 14:26:31 -10:00
J. Nick Koston
c0781d3680 [ld2450] Use atan2f for angle calculation (#14388) 2026-02-28 14:26:08 -10:00
J. Nick Koston
b7cb65ec49 [ci] Fix TypeError in ci-custom.py when POST lint checks fail (#14378) 2026-02-28 14:23:20 -10:00
J. Nick Koston
fdbfac15db [uart] Replace wake-on-RX task+queue with direct ISR callback (#14382) 2026-03-01 00:21:08 +00:00
J. Nick Koston
28424d6acd [ld2410][ld2412] Fix signed char causing incorrect distance values (#14380)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-28 23:50:12 +00:00
J. Nick Koston
b679b04d14 [core] Move CONF_STOP_BITS, CONF_DATA_BITS, CONF_PARITY to const.py (#14379) 2026-02-28 13:27:33 -10:00
Jonathan Swoboda
b7d651dd17 [core] Defer entity automation codegen to prevent sibling ID deadlocks (#14381)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-28 13:10:53 -10:00
J. Nick Koston
757e8d90e6 [core] Inline set_component_state_ and use it in Application (#14369) 2026-02-28 07:20:34 -10:00
J. Nick Koston
7d52a9587f [api] Outline keepalive ping logic from APIConnection::loop() (#14374) 2026-02-28 07:20:20 -10:00
J. Nick Koston
067d773aac [core] Make register_component protected, remove runtime checks (#14371) 2026-02-28 07:19:55 -10:00
Clyde Stubbs
089d1e55e7 [mipi_dsi] Fix Waveshare P4 7B board config (#14372) 2026-02-28 20:37:04 +11:00
Raymond Richmond
6c0998f220 [gt911] Support for interrupt signal via IO Expander (#14358) 2026-02-28 18:26:06 +11:00
J. Nick Koston
49cc389bf0 [esp32] Wrap printf/vprintf/fprintf to eliminate _vfprintf_r (~11 KB flash) (#14362)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 17:28:05 -10:00
J. Nick Koston
8480e8df9f [uart] Revert UART0 default pin workarounds (fixed in ESP-IDF 5.5.2) (#14363) 2026-02-27 17:27:51 -10:00
J. Nick Koston
e7d4f2608b [sen6x] Fix test sensor ID collisions with sen5x (#14367) 2026-02-27 16:01:17 -10:00
Ryan Wagoner
d1b4813197 [web_server] Add climate preset, fan mode, and humidity support (#14061) 2026-02-27 14:20:13 -10:00
Jonathan Swoboda
298ee7b92e [gps] Fix codegen deadlock when automations reference sibling sensors (#14365)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-27 19:08:42 -05:00
J. Nick Koston
5c56b99742 [ci] Fix C++ unit tests missing time component dependency (#14364) 2026-02-27 13:19:11 -10:00
Martin Ebner
b9d70dcda2 [sen6x] Add SEN6x sensor support (#12553)
Co-authored-by: Martin Ebner <martinebner@me.com>
Co-authored-by: Tobias Stanzel <tobi.stanzel@gmail.com>
Co-authored-by: Big Mike <mike@bigmike.land>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-02-27 16:41:28 -05:00
dependabot[bot]
5e3857abf7 Bump click from 8.1.7 to 8.3.1 (#11955)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-27 16:25:36 -05:00
Laura Wratten
bb567827a1 [sht3xd] Allow sensors that don't support serial number read (#14224)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-02-27 16:23:32 -05:00
J. Nick Koston
280f874edc [rp2040] Use native time_us_64() for millis_64() (#14356)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 11:18:02 -10:00
Jonathan Swoboda
f6755aabae [ci] Add PR title format check (#14345)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-27 15:18:07 -05:00
J. Nick Koston
52af4bced0 [component] Devirtualize call_dump_config (#14355) 2026-02-27 10:01:23 -10:00
J. Nick Koston
63e757807e [zephyr] Use native k_uptime_get() for millis_64() (#14350)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 10:01:09 -10:00
dependabot[bot]
edd63e3d2d Bump actions/download-artifact from 7.0.0 to 8.0.0 (#14327)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-27 14:43:10 -05:00
dependabot[bot]
32133e2f46 Bump ruff from 0.15.3 to 0.15.4 (#14357)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-27 14:42:20 -05:00
Clyde Stubbs
2255c68377 [esp32] Enable execute_from_psram for P4 (#14329) 2026-02-28 06:40:55 +11:00
J. Nick Koston
9c1d1a0d9f [color] Use integer math in Color::gradient to reduce code size (#14354) 2026-02-27 19:25:13 +00:00
J. Nick Koston
8698b01bc7 [host] Use native clock_gettime for millis_64() (#14340)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 18:54:13 +00:00
J. Nick Koston
3411ce2150 [core] Fix Application asm label for Mach-O using __USER_LABEL_PREFIX__ (#14334)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-27 08:49:57 -10:00
J. Nick Koston
29e1e8bdfd [wifi] Add LibreTiny component test configs (#14351) 2026-02-27 18:45:20 +00:00
J. Nick Koston
317dd5b2da [ci] Skip memory impact target branch build when tests don't exist (#14316) 2026-02-27 08:42:08 -10:00
Michael Cassaniti
4ae7633418 [safe_mode] Add feature to explicitly mark a boot as successful (#14306)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-02-27 13:23:02 -05:00
J. Nick Koston
c3a0eeceec [wifi] Use direct SDK APIs for LibreTiny SSID retrieval (#14349) 2026-02-27 18:17:17 +00:00
J. Nick Koston
4fe173b644 [wifi] Remove stale TODO comment for ESP8266 callback deferral (#14347) 2026-02-27 17:56:57 +00:00
J. Nick Koston
1c7f769ec7 [core] Add millis_64() HAL function with native ESP32 implementation (#14339) 2026-02-27 07:48:21 -10:00
deirdreobyrne
72ca514cc2 [esp32_hosted] Add configurable SDIO clock frequency (#14319)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Deirdre <obyrne@rk1.lan>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-02-27 17:25:53 +00:00
J. Nick Koston
20314b4d63 [mdns] Update espressif/mdns to v1.10.0 (#14338) 2026-02-27 17:20:08 +00:00
Kevin Ahrendt
017d1b2872 [audio] Bump microOpus to v0.3.4 (#14346) 2026-02-27 12:12:50 -05:00
Jonathan Swoboda
ef9fc87351 [zigbee] Fix codegen ordering for basic/identify attribute lists (#14343)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-27 11:17:04 -05:00
J. Nick Koston
0f7ac1726d [core] Extend fast select optimization to LibreTiny platforms (#14254)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 06:03:37 -10:00
whitty
bd3f8e006c [esp32_ble] allow setting of min/max key_size and auth_req_mode (#7138)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-02-27 06:02:29 -10:00
dependabot[bot]
07406c96e1 Bump actions/upload-artifact from 6.0.0 to 7.0.0 (#14326)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-26 21:35:15 -10:00
Jonathan Swoboda
4044520ccc [esp32_touch] Migrate to new unified touch sensor driver (esp_driver_touch_sens) (#14033)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-02-26 20:38:36 -10:00
Keith Burzinski
656389f215 [usb_uart] Performance, correctness and reliability improvements (#14333) 2026-02-26 23:41:35 -06:00
J. Nick Koston
04db37a34a [esp8266] Remove forced scanf linkage to save ~8KB flash (#13678)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 17:38:38 -10:00
J. Nick Koston
8f0a555b31 Merge branch 'posix_tz_proto' into remove_posix_tz_parser 2026-02-26 15:20:11 -10:00
J. Nick Koston
15846137a6 [rp2040] Update arduino-pico framework from 3.9.4 to 5.5.0 (#14328) 2026-02-26 15:17:52 -10:00
J. Nick Koston
50e7571f4c [web_server_idf] Prefer make_unique_for_overwrite for noninit recv buffer (#14279) 2026-02-26 15:17:25 -10:00
J. Nick Koston
32aad0f582 Merge remote-tracking branch 'upstream/dev' into posix_tz_proto 2026-02-26 15:15:50 -10:00
J. Nick Koston
1ccfcfc8d8 [time] Eliminate libc timezone bloat (~9.5KB flash ESP32, ~2% RAM on ESP8266) (#13635)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 15:12:44 -10:00
George Joseph
527d4964f6 [mipi_dsi] Add more Waveshare panels and comments (#14023) 2026-02-27 11:38:07 +11:00
esphomebot
67ba68a1a0 Update webserver local assets to 20260226-220330 (#14330)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-02-26 22:21:40 +00:00
lyubomirtraykov
8bd474fd01 [api] Add DEFROSTING to ClimateAction (#13976)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-02-26 10:27:18 -10:00
Oliver Kleinecke
54edc46c7f [esp_ldo] Add channels 1&2 support and passthrough mode (#14177) 2026-02-27 07:12:52 +11:00
J. Nick Koston
08035261b8 [libretiny] Use C++17 nested namespace syntax (#14325) 2026-02-26 10:02:36 -10:00
J. Nick Koston
e8b45e53fd [libretiny] Use -Os optimization for ESPHome source on BK72xx (SDK remains at -O1) (#14322) 2026-02-26 10:02:25 -10:00
Jonathan Swoboda
d325890148 [cc1101] Transition through IDLE in begin_tx/begin_rx for reliable state changes (#14321)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-26 14:48:05 -05:00
dependabot[bot]
8da1e3ce21 Bump ruff from 0.15.2 to 0.15.3 (#14323)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-02-26 19:32:53 +00:00
dependabot[bot]
c149be20fc Bump aioesphomeapi from 44.1.0 to 44.2.0 (#14324)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-26 19:31:47 +00:00
J. Nick Koston
4c3bb1596e [wifi] Use memcpy-based insertion sort for scan results (#13960) 2026-02-27 08:14:46 +13:00
J. Nick Koston
1912dcf03d [core] Use placement new for global Application instance (#14052) 2026-02-27 08:07:42 +13:00
J. Nick Koston
ae16c3bae7 Add socket compile tests for libretiny platforms (#14314) 2026-02-26 08:25:36 -10:00
J. Nick Koston
be000eab4e [ci] Add undocumented C++ API change checkbox and auto-label (#14317) 2026-02-26 08:02:52 -10:00
J. Nick Koston
a05d0202e6 [core] ESP32: massively reduce main loop socket polling overhead by replacing select() (#14249) 2026-02-26 06:21:27 -10:00
Jonathan Swoboda
6c253f0c71 [sprinkler] Fix millis overflow and underflow bugs (#14299)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-02-26 08:40:43 -05:00
J. Nick Koston
962cbfb9d8 [safe_mode] Mark SafeModeComponent and SafeModeTrigger as final (#14282) 2026-02-25 22:14:53 -05:00
J. Nick Koston
d52f8c9c6f [web_server] Mark classes as final (#14283) 2026-02-25 22:14:33 -05:00
J. Nick Koston
ee4d67930f [api] Mark ListEntitiesIterator and InitialStateIterator as final (#14284) 2026-02-25 22:14:16 -05:00
J. Nick Koston
cced0a82b5 [ota] Mark OTA backend and component leaf classes as final (#14287) 2026-02-25 22:14:04 -05:00
J. Nick Koston
478a876b01 [mdns] Mark MDNSComponent as final (#14290) 2026-02-25 22:13:51 -05:00
J. Nick Koston
789da5fdf8 [logger] Mark Logger and LoggerMessageTrigger as final (#14291) 2026-02-25 22:13:44 -05:00
Jesse Hills
bd08a56210 Merge branch 'release' into dev 2026-02-26 15:17:16 +13:00
Jonathan Swoboda
0d5b7df77d [sensor] Fix delta filter percentage mode regression (#14302)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-25 19:32:02 -05:00
Jonathan Swoboda
534857db9c [wled] Fix millis overflow in blank timeout (#14300)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2026-02-25 18:01:49 -05:00
Jonathan Swoboda
0a81a7a50b [mcp2515] Fix millis overflow in set_mode_ timeout (#14298)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-25 18:01:32 -05:00
Jonathan Swoboda
23ef233b60 [gp8403] Fix enum size mismatch in voltage register write (#14296)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-25 17:21:50 -05:00
Jonathan Swoboda
24fb74f78b [ld2420] Fix buffer overflows in command response parsing (#14297)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-25 17:21:33 -05:00
Jonathan Swoboda
2e167835ea [pn532] Replace millis zero sentinel with optional (#14295)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-25 15:15:49 -05:00
Jonathan Swoboda
a60e5c5c4f [lightwaverf] Fix millis overflow in send timeout check (#14294)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-25 15:11:52 -05:00
Jonathan Swoboda
3dcc9ab765 [ble_presence] Fix millis overflow in presence timeout check (#14293)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-25 15:08:04 -05:00
Jonathan Swoboda
d61e2f9c29 [light] Fix millis overflow in transition progress and flash timing (#14292)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-25 15:06:13 -05:00
Jonathan Swoboda
5dffceda59 [hmc5883l] Fix wrong gain for 88uT range (#14281)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-25 14:35:27 -05:00
Jonathan Swoboda
d1a636a5c3 [rtttl] Fix speaker playback bugs (#14280)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-25 14:34:38 -05:00
Jonathan Swoboda
3f558f63d8 [bl0942] Fix millis overflow in packet timeout check (#14285)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-25 19:28:47 +00:00
Jonathan Swoboda
df77213f2c [shelly_dimmer] Fix millis overflow in ACK timeout check (#14288)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-25 19:27:00 +00:00
Jonathan Swoboda
e601162cdd [lcd_base] Fix millis() truncation to uint8_t (#14289)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-25 19:21:00 +00:00
Jonathan Swoboda
62da60df47 [ld2420] Fix sizeof vs value bug in register memcpy (#14286)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-25 19:19:20 +00:00
J. Nick Koston
8bb577de64 [api] Split ProtoVarInt::parse into 32-bit and 64-bit phases (#14039) 2026-02-25 12:23:13 -06:00
Thomas Rupprecht
ede8235aae [core] more accurate check for leap year and valid day_of_month (#14197)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-25 16:46:28 +00:00
Szpadel
37a0cec53d [ac_dimmer] Use a shared ESP32 GPTimer for multiple dimmers (#13523)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-02-25 16:12:03 +00:00
esphomebot
78ab63581b Update webserver local assets to 20260225-155043 (#14275)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-02-25 16:09:45 +00:00
J. Nick Koston
1beeb9ab5c [web_server] Fix uptime display overflow after ~24.8 days (#13739) 2026-02-25 08:54:32 -07:00
Jonathan Swoboda
228874a52b [config] Improve dimensions validation and fix online_image resize aspect ratio (#14274)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-25 10:45:50 -05:00
Big Mike
bb05cfb711 [sensirion_common] Move sen5x's sensirion_convert_to_string_in_place() function to sensirion_common (#14269) 2026-02-25 07:34:58 -05:00
J. Nick Koston
b134c4679c [light] Replace std::lerp with lightweight lerp_fast in LightColorValues::lerp (#14238) 2026-02-24 22:33:57 -06:00
Jonathan Swoboda
2e705a919f [pid] Fix deadband threshold conversion for Fahrenheit (#14268)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-24 23:26:00 -05:00
J. Nick Koston
1dac501b04 [light] Add additional light effect test cases (#14266) 2026-02-24 21:39:51 -06:00
Jesse Hills
905e81330e Don't get stuck forever on a failed component can_proceed (#14267) 2026-02-25 03:28:19 +00:00
J. Nick Koston
3460a8c922 [dlms_meter/kamstrup_kmp] Replace powf with pow10_int (#14125)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 13:44:50 +13:00
J. Nick Koston
2ff876c629 [core] Use custom deleter for SchedulerItem unique_ptr to prevent destructor inlining (#14258)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-02-25 13:18:44 +13:00
J. Nick Koston
08dc487b5b [core] Pass std::function by rvalue reference in scheduler (#14260) 2026-02-25 13:08:07 +13:00
J. Nick Koston
4dc6b12ec5 [api] Pass std::function by rvalue reference in state subscriptions (#14261) 2026-02-25 12:56:43 +13:00
J. Nick Koston
cca4777f64 [web_server_idf] Pass std::function by rvalue reference (#14262) 2026-02-25 12:51:01 +13:00
Andrew Rankin
af00d601be [esp32_ble_server] add max_clients option for multi-client support (#14239) 2026-02-25 08:19:13 +11:00
Jonathan Swoboda
fe3c2ba555 [http_request.ota] Percent-encode credentials in URL (#14257)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-24 14:15:22 -05:00
J. Nick Koston
6554ad7c7e [core] Prevent inlining of mark_matching_items_removed_locked_ on Thumb-1 (#14256) 2026-02-24 12:08:51 -06:00
Clyde Stubbs
4abbed0cd4 [mipi_dsi] Allow transform disable; fix warnings (#14216) 2026-02-24 08:33:33 -05:00
H. Árkosi Róbert
72263eda85 [version] text sensor add option hide_hash to restore the pre-2026.1 behavior (#14251)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-02-24 07:31:58 -06:00
Jonathan Swoboda
abf7074518 [esp32] Improve ESP32-P4 engineering sample warning message (#14252)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-24 08:27:48 -05:00
J. Nick Koston
ad2da0af52 [network] Use C++17 nested namespace syntax (#14248) 2026-02-24 02:00:21 +00:00
J. Nick Koston
7d9d90d3f8 [cse7766] Use C++17 nested namespace syntax (#14247) 2026-02-23 20:50:22 -05:00
J. Nick Koston
70e47f301d [ethernet] Use C++17 nested namespace syntax (#14246) 2026-02-23 20:50:11 -05:00
J. Nick Koston
1614eb9c9c [i2c] Use C++17 nested namespace syntax (#14245) 2026-02-23 20:50:00 -05:00
J. Nick Koston
a694003fe3 [usb_host] Use C++17 nested namespace syntax (#14244) 2026-02-23 20:49:48 -05:00
J. Nick Koston
500aa7bf1d [text_sensor] Use C++17 nested namespace syntax (#14243) 2026-02-23 20:49:35 -05:00
J. Nick Koston
63c1496115 [text] Use C++17 nested namespace syntax (#14242) 2026-02-23 20:49:25 -05:00
J. Nick Koston
843d06df3f [switch] Use C++17 nested namespace syntax (#14241) 2026-02-23 20:49:15 -05:00
J. Nick Koston
30cc51eac9 [version] Use C++17 nested namespace syntax (#14240) 2026-02-23 20:49:00 -05:00
Jonathan Swoboda
ebf1047da7 [core] Move build_info_data.h out of application.h to fix incremental rebuilds (#14230)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-23 18:51:56 -05:00
J. Nick Koston
869678953d [core] Add pow10_int helper, replace powf in normalize_accuracy and sensor filters (#14114)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 12:03:24 +13:00
J. Nick Koston
0aa43b0c4f tweak 2026-02-23 16:51:46 -06:00
J. Nick Koston
28c6fbdc9e [api] Mark timezone string field as deprecated in GetTimeResponse
parsed_timezone struct should be used instead. The string field will be
removed before 2027.1.0.
2026-02-23 16:50:53 -06:00
J. Nick Koston
b4817c424d [api] Skip timezone update when parsed struct is not populated
Old clients (before 2026.3.0) send only the timezone string without the
parsed_timezone struct, so all fields default to zero. Without this check,
the device would overwrite its codegen-configured timezone with UTC.

Keep the codegen timezone when the struct is unpopulated (all zeros).
For actual UTC this also skips, which is harmless since UTC is the default.
2026-02-23 16:49:29 -06:00
J. Nick Koston
199288b813 [time] Fix test namespace for RecalcTimestampLocal and TimezoneOffset tests
Move tests that use make_us_central(), set_global_tz(), ParsedTimezone,
and DSTRuleType into esphome::time::testing namespace where those symbols
are declared.
2026-02-23 16:41:20 -06:00
J. Nick Koston
8374ccf7b5 [time] Remove C++ POSIX TZ string parser (bridge code)
Remove the runtime POSIX TZ string parser and all associated bridge code
now that timezone data is sent as pre-parsed structs via protobuf.

Removed:
- parse_posix_tz() and internal parsing helpers (skip_tz_name, parse_offset,
  parse_dst_rule, parse_uint, parse_transition_time)
- RealTimeClock::set_timezone() overloads and apply_timezone_()
- API connection fallback path for string-based timezone

Kept:
- All conversion functions (epoch_to_local_tm, is_in_dst, calculate_dst_transition)
- Internal helpers used by conversion functions
- localtime_r/localtime overrides
- Tests for all permanent functions
2026-02-23 16:34:14 -06:00
J. Nick Koston
a757838408 [time] Wrap codegen timezone fields in scope block
Fixes redeclaration error when multiple time platforms are in the
same build by wrapping the local ParsedTimezone variable in a scope block.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 15:49:35 -06:00
J. Nick Koston
4b400aa79a avoid ram increase 2026-02-23 15:42:39 -06:00
James Myatt
4a52900352 [nfc] Fix logging tag for nfc helpers (#14235) 2026-02-23 21:16:32 +00:00
J. Nick Koston
ba11722e77 [time] Skip POSIX TZ validation for empty timezone strings
Empty timezone strings are valid (meaning UTC/no timezone).
The parse_posix_tz_python() validation should only run on
non-empty strings.
2026-02-23 15:00:25 -06:00
J. Nick Koston
49ddaa2002 Merge branch 'posix_tz' into posix_tz_proto 2026-02-23 14:49:09 -06:00
J. Nick Koston
1a99abc629 [time] Add context to test file about bridge code removal timeline 2026-02-23 14:45:37 -06:00
J. Nick Koston
f95d8a33e2 Merge branch 'posix_tz' into posix_tz_proto 2026-02-23 14:44:14 -06:00
J. Nick Koston
de01d766f1 [time] Mark posix_tz parser as bridge code to remove before 2026.9.0
The C++ POSIX TZ string parser is only needed for backward compatibility
with older Home Assistant clients that send the timezone as a string.
Once all clients send the pre-parsed ParsedTimezone protobuf struct,
the parser and its helpers can be removed entirely.

See https://github.com/esphome/backlog/issues/91
2026-02-23 14:43:57 -06:00
J. Nick Koston
db6db5fb10 merge proto 2026-02-23 14:25:57 -06:00
J. Nick Koston
9e8efe15d3 Merge branch 'dev' into posix_tz 2026-02-23 14:25:26 -06:00
tomaszduda23
02c37bb6d6 [nrf52,logger] generate crash magic in python (#14173) 2026-02-23 20:23:40 +00:00
dependabot[bot]
918bbfb0d3 Bump aioesphomeapi from 44.0.0 to 44.1.0 (#14232)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-23 20:22:32 +00:00
tomaszduda23
063c6a9e45 [esp32,core] Move CONF_ENABLE_OTA_ROLLBACK to core (#14231) 2026-02-23 20:06:20 +00:00
Jonathan Swoboda
daee71a2c1 [http_request] Retry update check on startup until network is ready (#14228)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-23 14:21:29 -05:00
Jonathan Swoboda
0d32a5321c [remote_transmitter/remote_receiver] Rename _esp32.cpp to _rmt.cpp (#14226)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-23 13:46:53 -05:00
J. Nick Koston
5e95b9b36c Merge branch 'dev' into posix_tz 2026-02-23 12:45:16 -06:00
J. Nick Koston
e199145f1c [core] Avoid expensive modulo in LockFreeQueue for non-power-of-2 sizes (#14221) 2026-02-23 12:20:55 -06:00
Joshua Sing
1f945a334a [hdc302x] Add new component (#10160)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-02-23 12:01:23 -05:00
Jonathan Swoboda
fb6c7d81d5 [core] Fix multiline log continuations without leading whitespace (#14217)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 05:08:40 +00:00
J. Nick Koston
417f4535af [logger] Use subtraction-based line number formatting to avoid division (#14219) 2026-02-23 04:25:20 +00:00
schrob
ee94bc4715 [openthread] Refactor to optimize and match code rules (#14156) 2026-02-22 22:43:42 -05:00
schrob
6801604533 [openthread] Add Thread version DEBUG trace (#14196) 2026-02-22 22:40:40 -05:00
schrob
5c388a5200 [openthread_info] Optimize: Devirtualize/unify (#14208) 2026-02-22 22:39:36 -05:00
J. Nick Koston
d239a2400d [text_sensor] Conditionally compile filter infrastructure (#14213) 2026-02-22 21:36:21 -06:00
J. Nick Koston
93ce582ad3 [sensor] Conditionally compile filter infrastructure (#14214)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 21:35:51 -06:00
J. Nick Koston
6e70987451 [binary_sensor] Conditionally compile filter infrastructure (#14215) 2026-02-22 21:35:30 -06:00
schrob
263fff0ba2 Move CONF_OUTPUT_POWER into const.py (#14201) 2026-02-22 22:35:00 -05:00
J. Nick Koston
ded457c2c1 [libretiny] Tune oversized lwIP defaults for ESPHome (#14186)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-23 13:52:46 +13:00
J. Nick Koston
b539a5aa51 [water_heater] Fix device_id missing from state responses (#14212) 2026-02-22 23:07:56 +00:00
J. Nick Koston
5fddce6638 [logger] Make tx_buffer_ compile-time sized (#14205) 2026-02-23 12:02:05 +13:00
J. Nick Koston
ee1f521325 [http_request] Replace std::list<Header> with std::vector in perform() chain (#14027) 2026-02-23 11:59:54 +13:00
J. Nick Koston
ede2da2fbc [core] Conditionally compile get_loop_priority with USE_LOOP_PRIORITY (#14210) 2026-02-23 10:57:23 +13:00
J. Nick Koston
509f06afac [network] Improve IPAddress::str() deprecation warning with usage example (#14195) 2026-02-23 10:56:09 +13:00
J. Nick Koston
1753074eef [web_server_base] Remove unnecessary Component inheritance and modernize (#14204) 2026-02-23 10:01:07 +13:00
Edward Firmo
e013b48675 [nextion] Add error log for failed HTTP status during TFT upload (#14190) 2026-02-21 23:44:06 -06:00
J. Nick Koston
49e4ae54be [bme68x_bsec2] Fix compilation on ESP32 Arduino (#14194) 2026-02-22 05:22:59 +00:00
J. Nick Koston
d5c9c56fdf [platformio] Add exponential backoff and session reset to download retries (#14191) 2026-02-21 19:41:43 -06:00
J. Nick Koston
a468261523 [scheduler] De-template and consolidate scheduler helper functions (#14164) 2026-02-21 19:41:26 -06:00
J. Nick Koston
462ac29563 [scheduler] Use relaxed memory ordering for atomic reads under lock (#14140) 2026-02-21 17:29:41 -06:00
J. Nick Koston
6f198adb0c [scheduler] Reduce lock acquisitions in process_defer_queue_ (#14107) 2026-02-21 14:29:50 -06:00
J. Nick Koston
e521522b38 [haier] Fix uninitialized HonSettings causing API connection failures (#14188) 2026-02-21 13:54:43 -06:00
J. Nick Koston
5a07908dfa [api] Fix build error when lambda returns StringRef in homeassistant.event data (#14187) 2026-02-21 13:54:20 -06:00
J. Nick Koston
9571a979eb [ci] Suggest StringRef instead of std::string_view (#14183) 2026-02-21 13:53:45 -06:00
tomaszduda23
48ba007c22 [nrf52] print line number after crash in logs (#14165) 2026-02-21 14:16:20 -05:00
Clyde Stubbs
6ff17fbf7c [epaper_spi] Fix color mapping for weact (#14134) 2026-02-22 04:17:54 +11:00
J. Nick Koston
416b97311b [mqtt] Remove broken ESP8266 ssl_fingerprints option (#14182) 2026-02-21 11:12:35 -06:00
J. Nick Koston
7fb09da7cf [dsmr] Add deprecated std::string overload for set_decryption_key (#14180) 2026-02-21 11:08:13 -06:00
Sxt Fov
6ecb01dedc [cc1101] actions to change general and tuner settings (#14141)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-02-21 08:45:15 -05:00
Clyde Stubbs
518f08b909 [mipi_dsi] Disallow swap_xy (#14124) 2026-02-21 19:51:13 +11:00
Rodrigo Martín
2eac106f11 [mqtt] add missing precision in HA autodiscovery (#14010) 2026-02-20 23:20:27 -05:00
J. Nick Koston
f77da803c9 [api] Write protobuf encode output to pre-sized buffer directly (#14018) 2026-02-20 21:39:18 -06:00
J. Nick Koston
f8f98bf428 [logger] Reduce UART driver heap waste on ESP32 (#14168) 2026-02-21 03:16:49 +00:00
J. Nick Koston
abe37c9841 [uptime] Use scheduler millis_64() for rollover-safe uptime tracking (#14170) 2026-02-21 03:08:49 +00:00
J. Nick Koston
8589f80d8f [api,ota,captive_portal] Fix fd leaks and clean up socket_ip_loop_monitored setup paths (#14167) 2026-02-20 20:59:49 -06:00
J. Nick Koston
0e38acd67a [api] Warn when clients connect with outdated API version (#14145) 2026-02-20 19:21:56 -06:00
J. Nick Koston
a3f279c1cf [usb_host] Implement disable_loop/enable_loop pattern for USB components (#14163) 2026-02-20 19:21:14 -06:00
J. Nick Koston
35037d1a5b [core] Deduplicate base64 encode/decode logic (#14143)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 19:20:58 -06:00
J. Nick Koston
d206c75b0b [logger] Fix loop disable optimization using wrong preprocessor guard (#14158) 2026-02-20 19:20:44 -06:00
tomaszduda23
1d3054ef5e [nrf52,logger] Early debug (#11685) 2026-02-20 16:12:50 -06:00
Jonathan Swoboda
db6aa58f40 [max7219digit] Fix typo in action names (#14162)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 16:06:46 -05:00
Pawelo
48115eca18 [safe_mode] Extract RTC_KEY constant for shared use (#14121)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2026-02-20 14:08:31 -06:00
dependabot[bot]
edfc3e3501 Bump ruff from 0.15.1 to 0.15.2 (#14159)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-02-20 19:32:41 +00:00
dependabot[bot]
1a37632891 Bump pylint from 4.0.4 to 4.0.5 (#14160)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-20 19:27:45 +00:00
dependabot[bot]
b85a49cdb3 Bump github/codeql-action from 4.32.3 to 4.32.4 (#14161)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-20 13:27:15 -06:00
Jonathan Swoboda
9c0eed8a67 [e131] Remove dead LWIP TCP code path from loop() (#14155)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-02-20 17:03:39 +00:00
Jonathan Swoboda
887375ebef Merge branch 'release' into dev 2026-02-20 11:24:25 -05:00
Jonathan Swoboda
403235e2d4 [wifi] Add band_mode configuration for ESP32-C5 dual-band WiFi (#14148)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 11:20:29 -05:00
Jonathan Swoboda
9ce01fc369 [esp32] Add engineering_sample option for ESP32-P4 (#14139)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 11:20:05 -05:00
J. Nick Koston
b0a35559b3 [esp32] Bump ESP-IDF to 5.5.3.1, revert GATTS workaround (#14147) 2026-02-20 10:19:01 -06:00
Jonathan Swoboda
efe54e3b5e [ld2410/ld2450] Replace header sync with buffer size increase for frame resync (#14138)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 23:25:25 -05:00
Jonathan Swoboda
5af871acce [ld2420] Increase MAX_LINE_LENGTH to allow footer-based resync (#14137)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 22:36:12 -05:00
Jonathan Swoboda
a2f0607c1e [ld2410] Add frame header synchronization to readline_() (#14136)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 03:04:38 +00:00
Jonathan Swoboda
b67b2cc3ab [ld2450] Add frame header synchronization to fix initialization regression (#14135)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-02-20 02:56:20 +00:00
Jonathan Swoboda
7a2a149061 [esp32] Bump ESP-IDF to 5.5.3 (#14122)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 21:43:29 -05:00
J. Nick Koston
afbc45bf32 [e131] Drain all queued packets per loop iteration (#14133) 2026-02-19 20:35:42 -06:00
J. Nick Koston
c1265a9490 [core] Use constexpr for hand-written PROGMEM arrays in C++ (#14129)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:54:57 -06:00
J. Nick Koston
94712b3961 [esp8266][web_server] Use constexpr for PROGMEM arrays in codegen (#14128)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:54:44 -06:00
J. Nick Koston
d29288547e [core] Use constexpr for PROGMEM arrays (#14127)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:54:33 -06:00
Jonathan Swoboda
1b4de55efd [pulse_counter] Fix PCNT glitch filter calculation off by 1000x (#14132)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 00:12:37 +00:00
J. Nick Koston
cceb109303 [uart] Always call pin setup for UART0 default pins on ESP-IDF (#14130) 2026-02-19 23:48:18 +00:00
Jonathan Swoboda
17a810b939 [wifi] Sync output_power with PHY max TX power to prevent brownout (#14118)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 15:14:48 -05:00
J. Nick Koston
4aa8f57d36 [json] Add SerializationBuffer for stack-first JSON serialization (#13625) 2026-02-19 14:08:44 -06:00
Jonathan Swoboda
f2c98d6126 [safe_mode] Log brownout as reset reason on OTA rollback (#14113)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 17:45:04 +00:00
J. Nick Koston
7a5c3cee0d [esp32_ble] Enable CONFIG_BT_RELEASE_IRAM on ESP32-C2 (#14109)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 17:41:00 +00:00
Jonathan Swoboda
9aa17984df [pulse_counter] Fix build failure when use_pcnt is false (#14111)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 17:25:26 +00:00
Jonathan Swoboda
da616e0557 [ethernet] Improve clk_mode deprecation warning with actionable YAML (#14104)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 17:00:05 +00:00
Kevin Ahrendt
d2026b4cd7 [audio] Disable FLAC CRC validation to improve decoding efficiency (#14108) 2026-02-19 11:56:34 -05:00
Jonathan Swoboda
ed74790eed [i2c] Remove deprecated stop parameter overloads and readv/writev methods (#14106)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 16:56:06 +00:00
Jonathan Swoboda
bf2e22da4f [esp32] Remove deprecated add_idf_component() parameters and IDF component refresh option (#14105)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 16:55:03 +00:00
Jonathan Swoboda
bd50b80882 [opentherm] Remove deprecated opentherm_version config option (#14103)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 11:34:40 -05:00
Kevin Ahrendt
b11ad26c4f [audio] Support decoding audio directly from flash (#14098)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-19 11:20:19 -05:00
J. Nick Koston
f7459670d3 [core] Optimize WarnIfComponentBlockingGuard::finish() hot path (#14040)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 16:10:22 +00:00
Jonathan Swoboda
5304750215 [socket] Fix IPv6 compilation error on host platform (#14101)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 16:00:34 +00:00
J. Nick Koston
a8171da003 [web_server] Reduce set_json_id flash and stack usage (#14029) 2026-02-19 09:38:57 -06:00
J. Nick Koston
916cf0d8b7 [e131] Replace std::map with std::vector for universe tracking (#14087) 2026-02-19 09:28:00 -06:00
J. Nick Koston
0484b2852d [e131] Fix E1.31 on ESP8266 and RP2040 by restoring WiFiUDP support (#14086) 2026-02-19 09:27:05 -06:00
J. Nick Koston
b5a8e1c94c [ci] Update lint message to recommend constexpr over static const (#14099) 2026-02-19 09:06:46 -06:00
dependabot[bot]
01a46f665f Bump esptool from 5.1.0 to 5.2.0 (#14058)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-19 09:42:22 -05:00
J. Nick Koston
535980b9bd [cse7761] Use constexpr for compile-time constants (#14081) 2026-02-19 08:40:41 -06:00
J. Nick Koston
b0085e21f7 [core] Devirtualize call_loop() and mark_failed() in Component (#14083) 2026-02-19 08:40:23 -06:00
J. Nick Koston
6daca09794 [logger] Replace LogListener virtual interface with LogCallback struct (#14084) 2026-02-19 08:40:08 -06:00
J. Nick Koston
7b53a98950 [socket] Log error when UDP socket requested on LWIP TCP-only platforms (#14089) 2026-02-19 08:39:44 -06:00
Rodrigo Martín
4cc1e6a910 [esp32_ble_server] add test for lambda characteristic (#14091) 2026-02-19 09:23:22 -05:00
Marc Hörsken
4d05e4d576 [esp32_camera] Add support for sensors without JPEG support (#9496)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-02-18 21:52:38 -06:00
Kevin Ahrendt
eefad194d0 [audio, speaker] Add support for decoding Ogg Opus files (#13967) 2026-02-18 21:51:33 -06:00
Kevin Ahrendt
ba7134ee3f [mdns] add Sendspin advertisement support (#14013)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-02-18 21:51:16 -06:00
Kevin Ahrendt
264c8faedd [media_player] Add more commands to support Sendspin (#12258)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2026-02-18 21:51:01 -06:00
Kevin Ahrendt
3c227eeca4 [audio] Add support for sinking via an arbitrary callback (#14035) 2026-02-18 21:50:39 -06:00
J. Nick Koston
c8598fe620 [bluetooth_proxy] Use constexpr for remaining compile-time constants (#14080) 2026-02-18 21:34:25 -06:00
J. Nick Koston
2f9b76f129 [pn7160] Use constexpr for compile-time constants (#14078) 2026-02-18 21:33:39 -06:00
J. Nick Koston
9a8b00a428 [nfc] Use constexpr for compile-time constants (#14077) 2026-02-18 21:33:23 -06:00
J. Nick Koston
eaf0d03a37 [ld2420] Use constexpr for compile-time constants (#14079) 2026-02-18 21:32:37 -06:00
J. Nick Koston
e7f2021864 [http_request] Replace std::map with std::vector in action template (#14026) 2026-02-18 21:32:24 -06:00
J. Nick Koston
dff9780d3a [core] Use constexpr for compile-time constants (#14071) 2026-02-19 03:19:48 +00:00
J. Nick Koston
20239d1bb3 [remote_base] Use constexpr for compile-time constants (#14076) 2026-02-19 03:16:09 +00:00
J. Nick Koston
ee7d63f73a [packet_transport] Use constexpr for compile-time constants (#14074) 2026-02-18 21:09:49 -06:00
J. Nick Koston
76c151c6e6 [api] Use constexpr for compile-time constant (#14072) 2026-02-18 21:07:38 -06:00
J. Nick Koston
9c9365c146 [bluetooth_proxy][esp32_ble_client][esp32_ble_server] Use constexpr for compile-time constants (#14073) 2026-02-18 21:07:06 -06:00
J. Nick Koston
7e118178b3 [web_server] Fix water_heater JSON key names and move traits to DETAIL_ALL (#14064) 2026-02-18 21:00:24 -06:00
J. Nick Koston
66d2ac8cb9 [web_server] Move climate static traits to DETAIL_ALL only (#14066) 2026-02-18 21:00:09 -06:00
J. Nick Koston
e4c233b6ce [mqtt] Use constexpr for compile-time constants (#14075) 2026-02-18 20:59:31 -06:00
J. Nick Koston
be853afc24 [core] Conditionally compile setup_priority override infrastructure (#14057) 2026-02-18 20:57:56 -06:00
J. Nick Koston
565443b710 [pulse_counter] Fix compilation on ESP32-C6/C5/H2/P4 (#14070) 2026-02-18 19:08:53 -06:00
J. Nick Koston
3b869f1720 [web_server] Double socket allocation to prevent connection exhaustion (#14067) 2026-02-18 19:01:37 -06:00
J. Nick Koston
5f82017a31 [udp] Register socket consumption for CONFIG_LWIP_MAX_SOCKETS (#14068) 2026-02-18 19:01:00 -06:00
J. Nick Koston
bd055e75b9 [core] Shrink Application::dump_config_at_ from size_t to uint16_t (#14053)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 16:49:37 -06:00
J. Nick Koston
d90754dc0a [http_request] Replace heavy STL containers with std::vector for headers (#14024) 2026-02-18 16:49:19 -06:00
J. Nick Koston
387f615dae [api] Add handshake timeout to prevent connection slot exhaustion (#14050) 2026-02-18 16:48:30 -06:00
J. Nick Koston
02e310f2c9 [core] Remove unnecessary IRAM_ATTR from yield(), delay(), feed_wdt(), and arch_feed_wdt() (#14063) 2026-02-18 16:48:13 -06:00
Jesse Hills
d83738df87 Merge branch 'release' into dev 2026-02-19 11:43:58 +13:00
J. Nick Koston
09fc028895 [core] Remove dead global_state variable (#14060) 2026-02-18 15:16:26 -06:00
J. Nick Koston
82cfa00a97 [tlc59208f] Make mode constants inline constexpr (#14043) 2026-02-18 15:04:30 -06:00
J. Nick Koston
4a038978d2 [pca9685] Make mode constants inline constexpr (#14042) 2026-02-18 15:04:14 -06:00
Jesse Hills
bd38041d04 Merge branch 'beta' into dev 2026-02-19 09:05:23 +13:00
Jonathan Swoboda
9cd7b0b32b [external_components] Clean up incomplete clone on failed ref fetch (#14051)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 18:09:33 +00:00
dependabot[bot]
f73bcc0e7b Bump cryptography from 45.0.1 to 46.0.5 (#14049)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-18 09:08:12 -06:00
dependabot[bot]
652c669777 Bump pillow from 11.3.0 to 12.1.1 (#14048)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-18 09:08:02 -06:00
J. Nick Koston
fb89900c64 [core] Make setup_priority and component state constants constexpr (#14041) 2026-02-18 08:22:36 -06:00
J. Nick Koston
fb35ddebb9 [display] Make COLOR_OFF and COLOR_ON inline constexpr (#14044) 2026-02-18 08:22:07 -06:00
Jesse Hills
a3d7e76992 Merge branch 'beta' into dev 2026-02-18 13:29:11 +13:00
dependabot[bot]
5bb863f7da Bump actions/stale from 10.1.1 to 10.2.0 (#14036)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-17 13:24:39 -06:00
Rodrigo Martín
81ed70325c [esp32_ble_server] fix infinitely large characteristic value (#14011) 2026-02-17 07:45:21 -10:00
schrob
e826d71bd8 [openthread] Fix compiler format warning (#14030) 2026-02-17 10:16:57 -05:00
J. Nick Koston
4cd3f6c36a [api] Remove unused reserve from APIServer constructor (#14017) 2026-02-17 16:30:57 +13:00
Jesse Hills
6b4b8cb2f9 Merge branch 'beta' into dev 2026-02-17 16:22:46 +13:00
J. Nick Koston
0c4827d348 [json, core] Remove stored RAMAllocator, make constructors constexpr (#14000) 2026-02-16 08:09:53 -06:00
J. Nick Koston
81872d9822 [camera, camera_encoder] Remove stored RAMAllocator member (#13997) 2026-02-16 08:09:26 -06:00
J. Nick Koston
ffb9a00e26 [online_image] Remove stored RAMAllocator member from DownloadBuffer (#13999) 2026-02-16 08:09:13 -06:00
J. Nick Koston
f2c827f9a2 [runtime_image] Remove stored RAMAllocator member (#13998) 2026-02-16 08:08:43 -06:00
Cornelius A. Ludmann
f2cb5db9e0 [epaper_spi] Add Waveshare 7.5in e-Paper (H) (#13991) 2026-02-16 13:44:30 +11:00
Kevin Ahrendt
066419019f [audio] Support reallocating non-empty AudioTransferBuffer (#13979) 2026-02-15 16:09:35 -05:00
Pawelo
15da6d0a0b [epaper_spi] Add WeAct 3-color e-paper display support (#13894) 2026-02-16 07:58:51 +11:00
Jonathan Swoboda
6303bc3e35 [esp32_rmt] Handle ESP32 variants without RMT hardware (#14001)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 13:23:06 -05:00
Jonathan Swoboda
0f4dc6702d [fan] Fix preset_mode not restored on boot (#14002)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 12:11:50 -05:00
Jonathan Swoboda
f48c8a6444 [combination] Fix 'coeffecient' typo with backward-compatible deprecation (#14004)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 12:11:36 -05:00
dependabot[bot]
38404b2013 Bump ruff from 0.15.0 to 0.15.1 (#13980)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-02-14 15:11:17 -07:00
AndreKR
5a6d64814a [http_request] Improve TLS logging on ESP8266 (#13985) 2026-02-14 10:08:26 -07:00
J. Nick Koston
36776b40c2 [wifi] Fix ESP8266 DHCP state corruption from premature dhcp_renew() (#13983)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 08:21:04 -07:00
Jesse Hills
58c3ba7ac6 Merge branch 'beta' into dev 2026-02-14 16:03:25 +13:00
dependabot[bot]
931b47673c Bump github/codeql-action from 4.32.2 to 4.32.3 (#13981)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-13 16:22:26 -06:00
J. Nick Koston
79d9fbf645 [nfc] Replace constant std::vector with static constexpr std::array (#13978) 2026-02-13 16:22:05 -06:00
J. Nick Koston
f24e7709ac [core] Make LOG_ENTITY_ICON a no-op when icons are compiled out (#13973) 2026-02-13 16:21:50 -06:00
Kevin Ahrendt
903971de12 [runtime_image, online_image] Create runtime_image component to decode images (#10212) 2026-02-13 11:25:43 -05:00
J. Nick Koston
b04e427f01 [usb_host] Extract cold path from loop(), replace std::string with buffer API (#13957)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-13 06:39:00 -06:00
J. Nick Koston
e0c03b2dfa [api] Fix ESP8266 noise API handshake deadlock and prompt socket cleanup (#13972) 2026-02-12 18:20:58 -06:00
J. Nick Koston
7dff631dcb [core] Flatten single-callsite vector realloc functions (#13970) 2026-02-12 18:20:39 -06:00
J. Nick Koston
36aba385af [web_server] Flatten deq_push_back_with_dedup_ to inline vector realloc (#13968) 2026-02-12 18:20:21 -06:00
Jonathan Swoboda
136d17366f [docker] Suppress git detached HEAD advice (#13962)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 16:12:17 -05:00
Jonathan Swoboda
db7870ef5f [alarm_control_panel] Fix flaky integration test race condition (#13964)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 16:04:39 -05:00
dependabot[bot]
bbc88d92ea Bump docker/build-push-action from 6.19.1 to 6.19.2 in /.github/actions/build-image (#13965)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-12 14:31:43 -06:00
Jesse Hills
1604b5d6e4 Merge branch 'beta' into dev 2026-02-13 07:11:49 +13:00
J. Nick Koston
7fd535179e [helpers] Add heap warnings to format_hex_pretty, deprecate ethernet/web_server std::string APIs (#13959) 2026-02-12 17:47:44 +00:00
Lukáš Maňas
e3a457e402 [pulse_meter] Fix early edge detection (#12360)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-02-12 17:20:54 +00:00
J. Nick Koston
0dcff82bb4 [wifi] Deprecate wifi_ssid() in favor of wifi_ssid_to() (#13958) 2026-02-12 17:14:36 +00:00
J. Nick Koston
cde8b66719 [web_server] Switch from getParam to arg API to eliminate heap allocations (#13942) 2026-02-12 11:04:41 -06:00
J. Nick Koston
0e1433329d [api] Extract cold code from APIServer::loop() hot path (#13902) 2026-02-12 11:04:23 -06:00
J. Nick Koston
60fef5e656 [analyze_memory] Fix mDNS packet buffer miscategorized as wifi_config (#13949)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 10:26:54 -06:00
J. Nick Koston
725e774fe7 [web_server] Guard icon JSON field with USE_ENTITY_ICON (#13948) 2026-02-12 10:26:36 -06:00
J. Nick Koston
9aa98ed6c6 [uart] Remove redundant mutex, fix flush race, conditional event queue (#13955) 2026-02-12 10:26:10 -06:00
Guillermo Ruffino
7b251dcc31 [schema-gen] fix Windows: ensure UTF-8 encoding when reading component files (#13952) 2026-02-12 11:23:59 -05:00
schrob
8a08c688f6 [mipi_spi] Add Waveshare 1.83 v2 panel (#13680) 2026-02-12 23:25:51 +11:00
J. Nick Koston
9c185b42c3 Reword comment to avoid ci-custom scanf lint false positive
The regex matches `scanf (` in comments too since `\s*\(` matches the
space before the parenthesized size note.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 06:23:30 -06:00
J. Nick Koston
1fe95d8f82 Merge branch 'dev' into posix_tz 2026-02-12 05:40:07 -06:00
Jesse Hills
d6461251f9 Bump version to 2026.3.0-dev 2026-02-12 23:04:19 +13:00
J. Nick Koston
849df4b2a8 no host 2026-01-30 03:26:03 -06:00
J. Nick Koston
5f7582ffdb override localtime() to use our timezone
By providing our own localtime() and localtime_r() implementations,
user lambdas calling ::localtime() continue to work correctly without
needing migration. This eliminates the breaking change while still
achieving the memory savings.
2026-01-30 03:25:21 -06:00
J. Nick Koston
dcd0f53027 fix clang-tidy warnings
- Add NOLINT for intentional global mutable state
- Simplify boolean return in parse_posix_tz
- Add USE_TIME_TIMEZONE define for tests
- Add NOLINT for Google Test SetUp/TearDown methods
2026-01-30 02:51:36 -06:00
J. Nick Koston
b5e073bf7f clarify comment about days_to_year_start 2026-01-30 01:52:05 -06:00
J. Nick Koston
cde2199b64 more cover 2026-01-30 01:46:57 -06:00
J. Nick Koston
a1eef9870c cleanup 2026-01-30 01:28:23 -06:00
J. Nick Koston
19e9ab253e cleanup 2026-01-30 01:24:48 -06:00
J. Nick Koston
e3a99f12e4 more edge cases 2026-01-30 01:22:32 -06:00
J. Nick Koston
d31a860bf2 fix, macos and linux disagree on ambig time 2026-01-30 01:18:16 -06:00
J. Nick Koston
cfea3472bd cleanups 2026-01-30 01:11:31 -06:00
J. Nick Koston
31859a3eb5 fix 2026-01-30 01:10:43 -06:00
J. Nick Koston
9f3e5f990f cleanups 2026-01-30 01:09:30 -06:00
J. Nick Koston
f317f58545 cleanups 2026-01-30 01:09:06 -06:00
J. Nick Koston
01c23eace3 cleanups 2026-01-30 01:06:46 -06:00
J. Nick Koston
9b8556c2b2 fix 2026-01-30 01:03:42 -06:00
J. Nick Koston
9628c213b5 make human readable 2026-01-30 01:01:21 -06:00
J. Nick Koston
07a71c412d make human readable 2026-01-30 01:00:07 -06:00
J. Nick Koston
0d736e4143 fix 2026-01-30 00:41:53 -06:00
J. Nick Koston
a93e3b6fa0 ambig time 2026-01-30 00:38:29 -06:00
J. Nick Koston
22ab20ba4c aioesphomeapi and esphome both always have M format, it was overkill 2026-01-30 00:36:17 -06:00
J. Nick Koston
6ee51b0159 remove crazy over definsive edge cases that the bot wants -- they never happen and just make things larger 2026-01-30 00:25:42 -06:00
J. Nick Koston
e2b3186731 remove crazy over definsive edge cases that the bot wants -- they never happen and just make things larger 2026-01-30 00:23:09 -06:00
J. Nick Koston
31aa58c45d bot review 2026-01-30 00:12:46 -06:00
J. Nick Koston
a757cb3c91 bot review 2026-01-30 00:03:28 -06:00
J. Nick Koston
91ad54d864 bot review 2026-01-30 00:03:13 -06:00
J. Nick Koston
3703755e03 more fixes 2026-01-29 23:59:39 -06:00
J. Nick Koston
c1d380dee4 more fixes 2026-01-29 23:58:07 -06:00
J. Nick Koston
b2120609b9 bot review 2026-01-29 23:54:14 -06:00
J. Nick Koston
9e6e8a7ecb bot review 2026-01-29 23:51:50 -06:00
J. Nick Koston
de06b36544 bot review 2026-01-29 23:50:37 -06:00
J. Nick Koston
695df9b979 bot review 2026-01-29 23:49:07 -06:00
J. Nick Koston
aa91cdd984 no setz 2026-01-29 23:47:28 -06:00
J. Nick Koston
284a9cdab6 must set TZ 2026-01-29 23:41:41 -06:00
J. Nick Koston
77ebfc8687 aioesphomeapi and esphome both always have M format, it was overkill 2026-01-29 23:34:59 -06:00
J. Nick Koston
899f2bbac5 aioesphomeapi and esphome both always have M format, it was overkill 2026-01-29 23:34:49 -06:00
J. Nick Koston
bb35e7b4b5 bad feedback from copilot 2026-01-29 23:31:09 -06:00
J. Nick Koston
64e4edd70f bad feedback from copilot 2026-01-29 23:30:33 -06:00
J. Nick Koston
300b7169ad cleanup 2026-01-29 23:29:10 -06:00
J. Nick Koston
1353dbc31e cleanup 2026-01-29 23:28:35 -06:00
J. Nick Koston
300eea034b handle trailing garbage 2026-01-29 23:26:53 -06:00
J. Nick Koston
90a06b5249 Merge branch 'dev' into posix_tz 2026-01-29 19:20:14 -10:00
J. Nick Koston
1b7b307d08 simplify 2026-01-29 22:57:17 -06:00
J. Nick Koston
a946aefbed more cover 2026-01-29 22:54:56 -06:00
J. Nick Koston
8708f96de4 less ram 2026-01-29 22:53:29 -06:00
J. Nick Koston
bd056b3b9e improve readability 2026-01-29 22:47:54 -06:00
J. Nick Koston
5d49c81e2d more cover 2026-01-29 22:42:33 -06:00
J. Nick Koston
bec7d6d223 tweak 2026-01-29 22:31:23 -06:00
J. Nick Koston
973105f2e5 tweak 2026-01-29 22:28:09 -06:00
J. Nick Koston
53fb876738 tests 2026-01-29 22:17:36 -06:00
J. Nick Koston
d2bc168f39 tweak 2026-01-29 22:07:34 -06:00
J. Nick Koston
34ec72ad49 tweak 2026-01-29 22:05:23 -06:00
J. Nick Koston
85c814b712 tweak 2026-01-29 22:02:46 -06:00
J. Nick Koston
fc951baebc tweak 2026-01-29 21:59:46 -06:00
J. Nick Koston
a1cdfe71de tweak 2026-01-29 21:54:40 -06:00
J. Nick Koston
c1971955a3 tweak 2026-01-29 21:53:43 -06:00
J. Nick Koston
e1df75fc9b tweak 2026-01-29 21:53:06 -06:00
J. Nick Koston
ea83330ab9 tweak 2026-01-29 21:52:24 -06:00
J. Nick Koston
4cdf0224ba tweak 2026-01-29 21:48:46 -06:00
J. Nick Koston
47f029b713 cover 2026-01-29 21:38:59 -06:00
J. Nick Koston
d45a20af83 tweak 2026-01-29 21:25:46 -06:00
J. Nick Koston
d37c37ef62 tweak 2026-01-29 21:19:00 -06:00
J. Nick Koston
aad3764806 posix_tz 2026-01-29 21:14:42 -06:00
1827 changed files with 73617 additions and 30839 deletions

View File

@@ -124,6 +124,28 @@ This document provides essential context for AI models interacting with this pro
* **Indentation:** Use spaces (two per indentation level), not tabs
* **Type aliases:** Prefer `using type_t = int;` over `typedef int type_t;`
* **Line length:** Wrap lines at no more than 120 characters
* **Constructor parameters vs setters:** Component properties that are both **required** and **invariant**
(never change after construction) should be constructor parameters rather than set via setter methods.
This makes the dependency explicit and prevents use of the object in an incompletely-initialized state.
In code generation, when calling `cg.new_Pvariable()` or the relevant helper function to create the component, pass these as arguments.
```cpp
// Good - required invariant dependency as constructor parameter
class SourceTextSensor : public text_sensor::TextSensor, public Component {
public:
explicit SourceTextSensor(text::Text *source) : source_(source) {}
protected:
text::Text *source_;
};
```
```cpp
// Bad - required invariant dependency as setter
class SourceTextSensor : public text_sensor::TextSensor, public Component {
public:
void set_source(text::Text *source) { this->source_ = source; }
protected:
text::Text *source_{nullptr};
};
```
* **Component Structure:**
* **Standard Files:**
@@ -217,6 +239,123 @@ This document provides essential context for AI models interacting with this pro
var = await switch.new_switch(config)
```
* **Automations (Triggers, Actions, Conditions):**
Automations have three building blocks: **Triggers** (fire when something happens), **Actions** (do something), and **Conditions** (check if something is true).
* **Triggers -- Callback method (preferred):**
Use `build_callback_automation()` for simple triggers. This eliminates the need for a C++ Trigger class by using a lightweight pointer-sized forwarder struct registered directly as a callback. No `CONF_TRIGGER_ID` in the schema.
**Python:**
```python
from esphome import automation
CONFIG_SCHEMA = cv.Schema({
cv.GenerateID(): cv.declare_id(MyComponent),
cv.Optional(CONF_ON_STATE): automation.validate_automation({}),
}).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
for conf in config.get(CONF_ON_STATE, []):
await automation.build_callback_automation(
var, "add_on_state_callback", [(bool, "x")], conf
)
```
`build_callback_automation` arguments: `parent`, `callback_method` (C++ method name), `args` (template args as `[(type, name)]` tuples), `config`, and optional `forwarder` (defaults to `TriggerForwarder<Ts...>`).
For boolean filtering (e.g. `on_press`/`on_release`), use built-in forwarders with `args=[]`:
```python
for conf_key, forwarder in (
(CONF_ON_PRESS, automation.TriggerOnTrueForwarder),
(CONF_ON_RELEASE, automation.TriggerOnFalseForwarder),
):
for conf in config.get(conf_key, []):
await automation.build_callback_automation(
var, "add_on_state_callback", [], conf, forwarder=forwarder
)
```
**C++ -- no trigger class needed.** The callback registration method must be templatized to accept both `std::function` and lightweight forwarder structs (which avoid heap allocation):
```cpp
class MyComponent : public Component {
public:
// Must be a template -- accepts both std::function and pointer-sized forwarder structs
template<typename F> void add_on_state_callback(F &&callback) {
this->state_callback_.add(std::forward<F>(callback));
}
protected:
// Use CallbackManager when callbacks are always registered (e.g. core components)
CallbackManager<void(bool)> state_callback_;
// Use LazyCallbackManager when callbacks are often not registered -- saves 8 bytes
// (nullptr vs empty std::vector) per instance when no callbacks are added
// LazyCallbackManager<void(bool)> state_callback_;
};
```
* **Triggers -- Trigger class method:**
Use `build_automation()` with a `Trigger<Ts...>` subclass only when the forwarder needs **mutable state beyond a single `Automation*` pointer** (e.g. edge detection tracking previous state, timing logic).
**Python:**
```python
TurnOnTrigger = my_ns.class_("TurnOnTrigger", automation.Trigger.template())
CONFIG_SCHEMA = cv.Schema({
cv.Optional(CONF_ON_TURN_ON): automation.validate_automation(
{cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TurnOnTrigger)}
),
})
async def to_code(config):
for conf in config.get(CONF_ON_TURN_ON, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
```
**C++:**
```cpp
class TurnOnTrigger : public Trigger<> {
public:
explicit TurnOnTrigger(MyComponent *parent) : last_on_{false} {
parent->add_on_state_callback([this](bool state) {
if (state && !this->last_on_)
this->trigger();
this->last_on_ = state;
});
}
protected:
bool last_on_;
};
```
* **Actions:**
```cpp
template<typename... Ts> class MyAction : public Action<Ts...> {
public:
explicit MyAction(MyComponent *parent) : parent_(parent) {}
void play(const Ts &...) override { this->parent_->do_something(); }
protected:
MyComponent *parent_;
};
```
Register with `@automation.register_action("my_component.do_something", MyAction, schema, synchronous=True)`. Use `synchronous=True` for actions that run to completion inside `play()` without deferring. Use `synchronous=False` if the action may suspend/defer execution (e.g. `delay`, `wait_until`, `script.wait`) or store trigger arguments for later use.
* **Conditions:**
```cpp
template<typename... Ts> class MyCondition : public Condition<Ts...> {
public:
explicit MyCondition(MyComponent *parent) : parent_(parent) {}
bool check(const Ts &...) override { return this->parent_->is_active(); }
protected:
MyComponent *parent_;
};
```
Register with `@automation.register_condition("my_component.is_active", MyCondition, schema)`.
* **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)`.
@@ -252,10 +391,39 @@ This document provides essential context for AI models interacting with this pro
* **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
├── test_build_components/
└── common/ # Shared bus packages (uart, i2c, spi, etc.)
│ ├── uart/ # UART at default baud rate
│ ├── uart_115200/ # UART at 115200 baud
│ ├── i2c/ # I2C bus
│ └── spi/ # SPI bus
└── components/[component]/
├── common.yaml # Component-only config (no bus definitions)
├── test.esp32-idf.yaml
├── test.esp8266-ard.yaml
└── test.rp2040-ard.yaml
```
Run them using `script/test_build_components`. Use `-c <component>` to test specific components and `-t <target>` for specific platforms.
* **Test Grouping with Packages:** Components that use shared bus packages can be grouped together in CI to reduce build count. **Never define buses (uart, i2c, spi, modbus) directly in test YAML files** — always use packages from `test_build_components/common/`:
```yaml
# test.esp32-idf.yaml — use packages for buses
packages:
uart: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml
<<: !include common.yaml
```
```yaml
# common.yaml — component config only, NO bus definitions
my_component:
id: my_instance
sensor:
- platform: my_component
name: My Sensor
```
Components that define buses directly are flagged as "NEEDS MIGRATION" and cannot be grouped, increasing CI build time.
* **Testing All Components Together:** To verify that all components can be tested together without ID conflicts or configuration issues, use:
```bash
./script/test_component_grouping.py -e config --all
@@ -286,6 +454,7 @@ This document provides essential context for AI models interacting with this pro
* **Documentation Contributions:**
* Documentation is hosted in the separate `esphome/esphome-docs` repository.
* The contribution workflow is the same as for the codebase.
* When editing a component's documentation page, also update the corresponding component index page to ensure both pages remain in sync.
* **Best Practices:**
* **Component Development:** Keep dependencies minimal, provide clear error messages, and write comprehensive docstrings and tests.
@@ -394,6 +563,30 @@ This document provides essential context for AI models interacting with this pro
Note: Avoiding heap allocation after `setup()` is always required regardless of component type. The prioritization above is about the effort spent on container optimization (e.g., migrating from `std::vector` to `StaticVector`).
**Callback Managers:**
ESPHome provides two callback manager types in `esphome/core/helpers.h` for the observer pattern. Both support `std::function`, lambdas, and lightweight forwarder structs via their templatized `add()` method.
| Type | Idle overhead (32-bit) | When to use |
|------|----------------------|-------------|
| `CallbackManager<void(Ts...)>` | 12 bytes (empty `std::vector`) | Callbacks are always or almost always registered |
| `LazyCallbackManager<void(Ts...)>` | 4 bytes (`nullptr`) | Callbacks are often not registered (common case) |
`LazyCallbackManager` is a drop-in replacement for `CallbackManager` that defers allocation until the first callback is added. Prefer it for entity-level callbacks where most instances have no subscribers.
**Important:** Registration methods that add to a callback manager **must always be templatized** to accept both `std::function` and pointer-sized forwarder structs (used by `build_callback_automation`). Never use `std::function` in the method signature:
```cpp
// Bad -- forces heap allocation for forwarder structs
void add_on_state_callback(std::function<void(bool)> &&callback) {
this->state_callback_.add(std::move(callback));
}
// Good -- accepts any callable without forcing std::function wrapping
template<typename F> void add_on_state_callback(F &&callback) {
this->state_callback_.add(std::forward<F>(callback));
}
```
* **State Management:** Use `CORE.data` for component state that needs to persist during configuration generation. Avoid module-level mutable globals.
**Bad Pattern (Module-Level Globals):**

View File

@@ -1 +1 @@
ce05c28e9dc0b12c4f6e7454986ffea5123ac974a949da841be698c535f2083e
f31f13994768b5b07e29624406c9b053bf4bb26e1623ac2bc1e9d4a9477502d6

View File

@@ -6,8 +6,9 @@
- [ ] Bugfix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Developer breaking change (an API change that could break external components)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) — [policy](https://developers.esphome.io/contributing/code/#what-constitutes-a-c-breaking-change)
- [ ] Developer breaking change (an API change that could break external components) — [policy](https://developers.esphome.io/contributing/code/#what-is-considered-public-c-api)
- [ ] Undocumented C++ API change (removal or change of undocumented public methods that lambda users may depend on) — [policy](https://developers.esphome.io/contributing/code/#c-user-expectations)
- [ ] Code quality improvements to existing code or addition of tests
- [ ] Other
@@ -24,7 +25,7 @@
- [ ] ESP32
- [ ] ESP32 IDF
- [ ] ESP8266
- [ ] RP2040
- [ ] RP2040/RP2350
- [ ] BK72xx
- [ ] RTL87xx
- [ ] LN882x

View File

@@ -47,7 +47,7 @@ runs:
- name: Build and push to ghcr by digest
id: build-ghcr
uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6.19.1
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
env:
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false
@@ -73,7 +73,7 @@ runs:
- name: Build and push to dockerhub by digest
id: build-dockerhub
uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6.19.1
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
env:
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false

View File

@@ -22,7 +22,7 @@ runs:
python-version: ${{ inputs.python-version }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
# yamllint disable-line rule:line-length

View File

@@ -14,6 +14,7 @@ module.exports = {
'chained-pr',
'core',
'small-pr',
'medium-pr',
'dashboard',
'github-actions',
'by-code-owner',
@@ -27,6 +28,7 @@ module.exports = {
'new-feature',
'breaking-change',
'developer-breaking-change',
'undocumented-api-change',
'code-quality',
'deprecated-component'
],

View File

@@ -1,5 +1,13 @@
const fs = require('fs');
const { DOCS_PR_PATTERNS } = require('./constants');
const {
COMPONENT_REGEX,
detectComponents,
hasCoreChanges,
hasDashboardChanges,
hasGitHubActionsChanges,
} = require('../detect-tags');
const { loadCodeowners, getEffectiveOwners } = require('../codeowners');
// Strategy: Merge branch detection
async function detectMergeBranch(context) {
@@ -20,15 +28,13 @@ async function detectMergeBranch(context) {
// Strategy: Component and platform labeling
async function detectComponentPlatforms(changedFiles, 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]}`);
}
for (const comp of detectComponents(changedFiles)) {
labels.add(`component: ${comp}`);
}
for (const file of changedFiles) {
const platformMatch = file.match(targetPlatformRegex);
if (platformMatch) {
labels.add(`platform: ${platformMatch[1]}`);
@@ -90,20 +96,14 @@ async function detectNewPlatforms(prFiles, apiData) {
// Strategy: Core files detection
async function detectCoreChanges(changedFiles) {
const labels = new Set();
const coreFiles = changedFiles.filter(file =>
file.startsWith('esphome/core/') ||
(file.startsWith('esphome/') && file.split('/').length === 2)
);
if (coreFiles.length > 0) {
if (hasCoreChanges(changedFiles)) {
labels.add('core');
}
return labels;
}
// Strategy: PR size detection
async function detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, TOO_BIG_THRESHOLD) {
async function detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, MEDIUM_PR_THRESHOLD, TOO_BIG_THRESHOLD) {
const labels = new Set();
if (totalChanges <= SMALL_PR_THRESHOLD) {
@@ -111,6 +111,11 @@ async function detectPRSize(prFiles, totalAdditions, totalDeletions, totalChange
return labels;
}
if (totalChanges <= MEDIUM_PR_THRESHOLD) {
labels.add('medium-pr');
return labels;
}
const testAdditions = prFiles
.filter(file => file.filename.startsWith('tests/'))
.reduce((sum, file) => sum + (file.additions || 0), 0);
@@ -131,80 +136,33 @@ async function detectPRSize(prFiles, totalAdditions, totalDeletions, totalChange
// Strategy: Dashboard changes
async function detectDashboardChanges(changedFiles) {
const labels = new Set();
const dashboardFiles = changedFiles.filter(file =>
file.startsWith('esphome/dashboard/') ||
file.startsWith('esphome/components/dashboard_import/')
);
if (dashboardFiles.length > 0) {
if (hasDashboardChanges(changedFiles)) {
labels.add('dashboard');
}
return labels;
}
// Strategy: GitHub Actions changes
async function detectGitHubActionsChanges(changedFiles) {
const labels = new Set();
const githubActionsFiles = changedFiles.filter(file =>
file.startsWith('.github/workflows/')
);
if (githubActionsFiles.length > 0) {
if (hasGitHubActionsChanges(changedFiles)) {
labels.add('github-actions');
}
return labels;
}
// Strategy: Code owner detection
async function detectCodeOwner(github, context, changedFiles) {
const labels = new Set();
const { owner, repo } = context.repo;
try {
const { data: codeownersFile } = await github.rest.repos.getContent({
owner,
repo,
path: 'CODEOWNERS',
});
const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8');
const codeownersPatterns = loadCodeowners();
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;
}
}
// Check if PR author is a codeowner of any changed file
const effective = getEffectiveOwners(changedFiles, codeownersPatterns);
if (effective.users.has(prAuthor)) {
labels.add('by-code-owner');
}
} catch (error) {
console.log('Failed to read or parse CODEOWNERS file:', error.message);
@@ -238,6 +196,7 @@ async function detectPRTemplateCheckboxes(context) {
{ 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\] Developer breaking change \(an API change that could break external components\)/i, label: 'developer-breaking-change' },
{ pattern: /- \[x\] Undocumented C\+\+ API change \(removal or change of undocumented public methods that lambda users may depend on\)/i, label: 'undocumented-api-change' },
{ pattern: /- \[x\] Code quality improvements to existing code or addition of tests/i, label: 'code-quality' }
];
@@ -258,7 +217,7 @@ async function detectDeprecatedComponents(github, context, changedFiles) {
const { owner, repo } = context.repo;
// Compile regex once for better performance
const componentFileRegex = /^esphome\/components\/([^\/]+)\//;
const componentFileRegex = COMPONENT_REGEX;
// Get files that are modified or added in components directory
const componentFiles = changedFiles.filter(file => componentFileRegex.test(file));

View File

@@ -35,6 +35,7 @@ async function fetchApiData() {
module.exports = async ({ github, context }) => {
// Environment variables
const SMALL_PR_THRESHOLD = parseInt(process.env.SMALL_PR_THRESHOLD);
const MEDIUM_PR_THRESHOLD = parseInt(process.env.MEDIUM_PR_THRESHOLD);
const MAX_LABELS = parseInt(process.env.MAX_LABELS);
const TOO_BIG_THRESHOLD = parseInt(process.env.TOO_BIG_THRESHOLD);
const COMPONENT_LABEL_THRESHOLD = parseInt(process.env.COMPONENT_LABEL_THRESHOLD);
@@ -120,7 +121,7 @@ module.exports = async ({ github, context }) => {
detectNewComponents(prFiles),
detectNewPlatforms(prFiles, apiData),
detectCoreChanges(changedFiles),
detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, TOO_BIG_THRESHOLD),
detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, MEDIUM_PR_THRESHOLD, TOO_BIG_THRESHOLD),
detectDashboardChanges(changedFiles),
detectGitHubActionsChanges(changedFiles),
detectCodeOwner(github, context, changedFiles),

227
.github/scripts/codeowners.js vendored Normal file
View File

@@ -0,0 +1,227 @@
// Shared CODEOWNERS parsing and matching utilities.
//
// Used by:
// - codeowner-review-request.yml
// - codeowner-approved-label-update.yml
// - auto-label-pr/detectors.js (detectCodeOwner)
/**
* Convert a CODEOWNERS glob pattern to a RegExp.
*
* Handles **, *, and ? wildcards after escaping regex-special characters.
*/
function globToRegex(pattern) {
let regexStr = pattern
.replace(/([.+^=!:${}()|[\]\\])/g, '\\$1')
.replace(/\*\*/g, '\x00GLOBSTAR\x00') // protect ** from next replace
.replace(/\*/g, '[^/]*') // single star
.replace(/\x00GLOBSTAR\x00/g, '.*') // restore globstar
.replace(/\?/g, '.');
return new RegExp('^' + regexStr + '$');
}
/**
* Parse raw CODEOWNERS file content into an array of
* { pattern, regex, owners } objects.
*
* Each `owners` entry is the raw string from the file (e.g. "@user" or
* "@esphome/core").
*/
function parseCodeowners(content) {
const lines = content
.split('\n')
.map(line => line.trim())
.filter(line => line && !line.startsWith('#'));
const patterns = [];
for (const line of lines) {
const parts = line.split(/\s+/);
if (parts.length < 2) continue;
const pattern = parts[0];
const owners = parts.slice(1);
const regex = globToRegex(pattern);
patterns.push({ pattern, regex, owners });
}
return patterns;
}
/**
* Fetch and parse the CODEOWNERS file via the GitHub API.
*
* @param {object} github - octokit instance from actions/github-script
* @param {string} owner - repo owner
* @param {string} repo - repo name
* @param {string} [ref] - git ref (SHA / branch) to read from
* @returns {Array<{pattern: string, regex: RegExp, owners: string[]}>}
*/
async function fetchCodeowners(github, owner, repo, ref) {
const params = { owner, repo, path: 'CODEOWNERS' };
if (ref) params.ref = ref;
const { data: file } = await github.rest.repos.getContent(params);
const content = Buffer.from(file.content, 'base64').toString('utf8');
return parseCodeowners(content);
}
/**
* Classify raw owner strings into individual users and teams.
*
* @param {string[]} rawOwners - e.g. ["@user1", "@esphome/core"]
* @returns {{ users: string[], teams: string[] }}
* users login names without "@"
* teams team slugs without the "org/" prefix
*/
function classifyOwners(rawOwners) {
const users = [];
const teams = [];
for (const o of rawOwners) {
const clean = o.startsWith('@') ? o.slice(1) : o;
if (clean.includes('/')) {
teams.push(clean.split('/')[1]);
} else {
users.push(clean);
}
}
return { users, teams };
}
/**
* For each file, find its effective codeowners using GitHub's
* "last match wins" semantics, then union across all files.
*
* @param {string[]} files - list of file paths
* @param {Array} codeownersPatterns - from parseCodeowners / fetchCodeowners
* @returns {{ users: Set<string>, teams: Set<string>, matchedFileCount: number }}
*/
function getEffectiveOwners(files, codeownersPatterns) {
const users = new Set();
const teams = new Set();
let matchedFileCount = 0;
for (const file of files) {
// Last matching pattern wins for each file
let effectiveOwners = null;
for (const { regex, owners } of codeownersPatterns) {
if (regex.test(file)) {
effectiveOwners = owners;
}
}
if (effectiveOwners) {
matchedFileCount++;
const classified = classifyOwners(effectiveOwners);
for (const u of classified.users) users.add(u);
for (const t of classified.teams) teams.add(t);
}
}
return { users, teams, matchedFileCount };
}
/**
* Read and parse the CODEOWNERS file from disk.
*
* Use this when the repo is already checked out (avoids an API call).
*
* @param {string} [repoRoot='.'] - path to the repo root
* @returns {Array<{pattern: string, regex: RegExp, owners: string[]}>}
*/
function loadCodeowners(repoRoot = '.') {
const fs = require('fs');
const path = require('path');
const content = fs.readFileSync(path.join(repoRoot, 'CODEOWNERS'), 'utf8');
return parseCodeowners(content);
}
/** Possible label actions returned by determineLabelAction. */
const LabelAction = Object.freeze({
ADD: 'add',
REMOVE: 'remove',
NONE: 'none',
});
/**
* Determine what label action is needed for a PR based on codeowner approvals.
*
* Checks changed files against CODEOWNERS patterns, reviews, and current labels
* to decide if the label should be added, removed, or left unchanged.
*
* @param {object} github - octokit instance from actions/github-script
* @param {string} owner - repo owner
* @param {string} repo - repo name
* @param {number} pr_number - pull request number
* @param {Array} codeownersPatterns - from loadCodeowners / fetchCodeowners
* @param {string} labelName - label to manage
* @returns {Promise<LabelAction>}
*/
async function determineLabelAction(github, owner, repo, pr_number, codeownersPatterns, labelName) {
// Get the list of changed files in this PR
const prFiles = await github.paginate(
github.rest.pulls.listFiles,
{ owner, repo, pull_number: pr_number }
);
const changedFiles = prFiles.map(file => file.filename);
console.log(`Found ${changedFiles.length} changed files`);
if (changedFiles.length === 0) {
console.log('No changed files found');
return LabelAction.NONE;
}
// Get effective owners using last-match-wins semantics
const effective = getEffectiveOwners(changedFiles, codeownersPatterns);
const componentCodeowners = effective.users;
console.log(`Component-specific codeowners: ${Array.from(componentCodeowners).join(', ') || '(none)'}`);
// Get current labels
const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({
owner, repo, issue_number: pr_number
});
const hasLabel = currentLabels.some(label => label.name === labelName);
if (componentCodeowners.size === 0) {
console.log('No component-specific codeowners found');
return hasLabel ? LabelAction.REMOVE : LabelAction.NONE;
}
// Get all reviews and find latest per user
const reviews = await github.paginate(
github.rest.pulls.listReviews,
{ owner, repo, pull_number: pr_number }
);
const latestReviewByUser = new Map();
for (const review of reviews) {
if (!review.user || review.user.type === 'Bot' || review.state === 'COMMENTED') continue;
latestReviewByUser.set(review.user.login, review);
}
// Check if any component-specific codeowner has an active approval
let hasCodeownerApproval = false;
for (const [login, review] of latestReviewByUser) {
if (review.state === 'APPROVED' && componentCodeowners.has(login)) {
console.log(`Codeowner '${login}' has approved`);
hasCodeownerApproval = true;
break;
}
}
if (hasCodeownerApproval && !hasLabel) return LabelAction.ADD;
if (!hasCodeownerApproval && hasLabel) return LabelAction.REMOVE;
console.log(`Label already ${hasLabel ? 'present' : 'absent'}, no change needed`);
return LabelAction.NONE;
}
module.exports = {
globToRegex,
parseCodeowners,
fetchCodeowners,
loadCodeowners,
classifyOwners,
getEffectiveOwners,
LabelAction,
determineLabelAction
};

66
.github/scripts/detect-tags.js vendored Normal file
View File

@@ -0,0 +1,66 @@
/**
* Shared tag detection from changed file paths.
* Used by pr-title-check and auto-label-pr workflows.
*/
const COMPONENT_REGEX = /^esphome\/components\/([^\/]+)\//;
/**
* Detect component names from changed files.
* @param {string[]} changedFiles - List of changed file paths
* @returns {Set<string>} Set of component names
*/
function detectComponents(changedFiles) {
const components = new Set();
for (const file of changedFiles) {
const match = file.match(COMPONENT_REGEX);
if (match) {
components.add(match[1]);
}
}
return components;
}
/**
* Detect if core files were changed.
* Core files are in esphome/core/ or top-level esphome/ directory.
* @param {string[]} changedFiles - List of changed file paths
* @returns {boolean}
*/
function hasCoreChanges(changedFiles) {
return changedFiles.some(file =>
file.startsWith('esphome/core/') ||
(file.startsWith('esphome/') && file.split('/').length === 2)
);
}
/**
* Detect if dashboard files were changed.
* @param {string[]} changedFiles - List of changed file paths
* @returns {boolean}
*/
function hasDashboardChanges(changedFiles) {
return changedFiles.some(file =>
file.startsWith('esphome/dashboard/') ||
file.startsWith('esphome/components/dashboard_import/')
);
}
/**
* Detect if GitHub Actions files were changed.
* @param {string[]} changedFiles - List of changed file paths
* @returns {boolean}
*/
function hasGitHubActionsChanges(changedFiles) {
return changedFiles.some(file =>
file.startsWith('.github/workflows/')
);
}
module.exports = {
COMPONENT_REGEX,
detectComponents,
hasCoreChanges,
hasDashboardChanges,
hasGitHubActionsChanges,
};

View File

@@ -12,6 +12,7 @@ permissions:
env:
SMALL_PR_THRESHOLD: 30
MEDIUM_PR_THRESHOLD: 100
MAX_LABELS: 15
TOO_BIG_THRESHOLD: 1000
COMPONENT_LABEL_THRESHOLD: 10
@@ -26,7 +27,7 @@ jobs:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v2
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}

View File

@@ -62,7 +62,7 @@ jobs:
run: git diff
- if: failure()
name: Archive artifacts
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: generated-proto-files
path: |

View File

@@ -40,7 +40,7 @@ jobs:
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()
- if: failure() && github.event.pull_request.head.repo.full_name == github.repository
name: Request changes
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
@@ -53,7 +53,7 @@ jobs:
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()
- if: success() && github.event.pull_request.head.repo.full_name == github.repository
name: Dismiss review
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:

View File

@@ -49,7 +49,7 @@ jobs:
with:
python-version: "3.11"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Set TAG
run: |

View File

@@ -47,7 +47,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
# yamllint disable-line rule:line-length
@@ -106,6 +106,7 @@ jobs:
script/build_codeowners.py --check
script/build_language_schema.py --check
script/generate-esp32-boards.py --check
script/generate-rp2040-boards.py --check
pytest:
name: Run pytest
@@ -153,12 +154,12 @@ jobs:
. venv/bin/activate
pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
- name: Save Python virtual environment cache
if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
@@ -170,6 +171,8 @@ jobs:
- common
outputs:
integration-tests: ${{ steps.determine.outputs.integration-tests }}
integration-tests-run-all: ${{ steps.determine.outputs.integration-tests-run-all }}
integration-test-files: ${{ steps.determine.outputs.integration-test-files }}
clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
clang-tidy-mode: ${{ steps.determine.outputs.clang-tidy-mode }}
python-linters: ${{ steps.determine.outputs.python-linters }}
@@ -182,6 +185,7 @@ jobs:
cpp-unit-tests-run-all: ${{ steps.determine.outputs.cpp-unit-tests-run-all }}
cpp-unit-tests-components: ${{ steps.determine.outputs.cpp-unit-tests-components }}
component-test-batches: ${{ steps.determine.outputs.component-test-batches }}
benchmarks: ${{ steps.determine.outputs.benchmarks }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -194,7 +198,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Restore components graph cache
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: .temp/components_graph.json
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
@@ -210,6 +214,8 @@ jobs:
# Extract individual fields
echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT
echo "integration-tests-run-all=$(echo "$output" | jq -r '.integration_tests_run_all')" >> $GITHUB_OUTPUT
echo "integration-test-files=$(echo "$output" | jq -c '.integration_test_files')" >> $GITHUB_OUTPUT
echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT
echo "clang-tidy-mode=$(echo "$output" | jq -r '.clang_tidy_mode')" >> $GITHUB_OUTPUT
echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT
@@ -222,9 +228,10 @@ jobs:
echo "cpp-unit-tests-run-all=$(echo "$output" | jq -r '.cpp_unit_tests_run_all')" >> $GITHUB_OUTPUT
echo "cpp-unit-tests-components=$(echo "$output" | jq -c '.cpp_unit_tests_components')" >> $GITHUB_OUTPUT
echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT
echo "benchmarks=$(echo "$output" | jq -r '.benchmarks')" >> $GITHUB_OUTPUT
- name: Save components graph cache
if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: .temp/components_graph.json
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
@@ -246,7 +253,7 @@ jobs:
python-version: "3.13"
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
@@ -261,9 +268,20 @@ jobs:
- name: Register matcher
run: echo "::add-matcher::.github/workflows/matchers/pytest.json"
- name: Run integration tests
env:
INTEGRATION_TEST_FILES: ${{ needs.determine-jobs.outputs.integration-test-files }}
INTEGRATION_TESTS_RUN_ALL: ${{ needs.determine-jobs.outputs.integration-tests-run-all }}
run: |
. venv/bin/activate
pytest -vv --no-cov --tb=native -n auto tests/integration/
if [[ "$INTEGRATION_TESTS_RUN_ALL" == "true" ]]; then
echo "Running all integration tests"
pytest -vv --no-cov --tb=native -n auto tests/integration/
else
# Parse JSON array into bash array to avoid shell expansion issues
mapfile -t test_files < <(echo "$INTEGRATION_TEST_FILES" | jq -r '.[]')
echo "Running ${#test_files[@]} specific integration tests"
pytest -vv --no-cov --tb=native -n auto "${test_files[@]}"
fi
cpp-unit-tests:
name: Run C++ unit tests
@@ -292,6 +310,40 @@ jobs:
script/cpp_unit_test.py $ARGS
fi
benchmarks:
name: Run CodSpeed benchmarks
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
if: >-
(github.event_name == 'push' && github.ref_name == 'dev') ||
(github.event_name == 'pull_request' && needs.determine-jobs.outputs.benchmarks == 'true')
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Build benchmarks
id: build
run: |
. venv/bin/activate
export BENCHMARK_LIB_CONFIG=$(python script/setup_codspeed_lib.py)
# --build-only prints BUILD_BINARY=<path> to stdout
BINARY=$(script/cpp_benchmark.py --all --build-only | grep '^BUILD_BINARY=' | tail -1 | cut -d= -f2-)
echo "binary=$BINARY" >> $GITHUB_OUTPUT
- name: Run CodSpeed benchmarks
uses: CodSpeedHQ/action@d872884a306dd4853acf0f584f4b706cf0cc72a2 # v4
with:
run: ${{ steps.build.outputs.binary }}
mode: simulation
clang-tidy-single:
name: ${{ matrix.name }}
runs-on: ubuntu-24.04
@@ -335,14 +387,14 @@ jobs:
- name: Cache platformio
if: github.ref == 'refs/heads/dev'
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
@@ -414,14 +466,14 @@ jobs:
- name: Cache platformio
if: github.ref == 'refs/heads/dev'
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
@@ -503,14 +555,14 @@ jobs:
- name: Cache platformio
if: github.ref == 'refs/heads/dev'
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
@@ -686,7 +738,7 @@ jobs:
ram_usage: ${{ steps.extract.outputs.ram_usage }}
flash_usage: ${{ steps.extract.outputs.flash_usage }}
cache_hit: ${{ steps.cache-memory-analysis.outputs.cache-hit }}
skip: ${{ steps.check-script.outputs.skip }}
skip: ${{ steps.check-script.outputs.skip || steps.check-tests.outputs.skip }}
steps:
- name: Check out target branch
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -705,10 +757,39 @@ jobs:
echo "::warning::ci_memory_impact_extract.py not found on target branch, skipping memory impact analysis"
fi
# All remaining steps only run if script exists
# Check if test files exist on the target branch for the requested
# components and platform. When a PR adds new test files for a platform,
# the target branch won't have them yet, so skip instead of failing.
# This check must be done here (not in determine-jobs.py) because
# determine-jobs runs on the PR branch and cannot see what the target
# branch has.
- name: Check for test files on target branch
id: check-tests
if: steps.check-script.outputs.skip != 'true'
run: |
components='${{ toJSON(fromJSON(needs.determine-jobs.outputs.memory_impact).components) }}'
platform="${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}"
found=false
for component in $(echo "$components" | jq -r '.[]'); do
# Check for test files matching the platform (test.platform.yaml or test-*.platform.yaml)
for f in tests/components/${component}/test*.${platform}.yaml; do
if [ -f "$f" ]; then
found=true
break 2
fi
done
done
if [ "$found" = false ]; then
echo "skip=true" >> $GITHUB_OUTPUT
echo "::warning::No test files found on target branch for platform ${platform}, skipping memory impact analysis"
else
echo "skip=false" >> $GITHUB_OUTPUT
fi
# All remaining steps only run if script and tests exist
- name: Generate cache key
id: cache-key
if: steps.check-script.outputs.skip != 'true'
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true'
run: |
# Get the commit SHA of the target branch
target_sha=$(git rev-parse HEAD)
@@ -735,14 +816,14 @@ jobs:
- name: Restore cached memory analysis
id: cache-memory-analysis
if: steps.check-script.outputs.skip != 'true'
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true'
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: memory-analysis-target.json
key: ${{ steps.cache-key.outputs.cache-key }}
- name: Cache status
if: steps.check-script.outputs.skip != 'true'
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true'
run: |
if [ "${{ steps.cache-memory-analysis.outputs.cache-hit }}" == "true" ]; then
echo "✓ Cache hit! Using cached memory analysis results."
@@ -752,21 +833,21 @@ jobs:
fi
- name: Restore Python
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache platformio
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.platformio
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
- name: Build, compile, and analyze memory
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
id: build
run: |
. venv/bin/activate
@@ -800,15 +881,15 @@ jobs:
--platform "$platform"
- name: Save memory analysis to cache
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success'
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success'
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: memory-analysis-target.json
key: ${{ steps.cache-key.outputs.cache-key }}
- name: Extract memory usage for outputs
id: extract
if: steps.check-script.outputs.skip != 'true'
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true'
run: |
if [ -f memory-analysis-target.json ]; then
ram=$(jq -r '.ram_bytes' memory-analysis-target.json)
@@ -822,7 +903,7 @@ jobs:
fi
- name: Upload memory analysis JSON
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: memory-analysis-target
path: memory-analysis-target.json
@@ -848,7 +929,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache platformio
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.platformio
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
@@ -886,7 +967,7 @@ jobs:
--platform "$platform"
- name: Upload memory analysis JSON
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: memory-analysis-pr
path: memory-analysis-pr.json
@@ -916,13 +997,13 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Download target analysis JSON
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: memory-analysis-target
path: ./memory-analysis
continue-on-error: true
- name: Download PR analysis JSON
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: memory-analysis-pr
path: ./memory-analysis

View File

@@ -0,0 +1,81 @@
# Adds/removes a 'code-owner-approved' label when a component-specific
# codeowner approves (or dismisses) a PR.
#
# Uses pull_request_target so that fork PRs do not require workflow approval.
# The label is reconciled on every PR update; for review events specifically,
# this means the label is applied on the next push after a codeowner review.
name: Codeowner Approved Label
on:
pull_request_target:
types: [opened, synchronize, reopened, ready_for_review]
branches-ignore:
- release
- beta
permissions:
issues: write
pull-requests: read
contents: read
jobs:
codeowner-approved:
name: Run
if: ${{ github.repository == 'esphome/esphome' }}
runs-on: ubuntu-latest
steps:
- name: Checkout base branch
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.base.sha }}
sparse-checkout: |
.github/scripts/codeowners.js
CODEOWNERS
- name: Check codeowner approval and update label
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
with:
script: |
const { loadCodeowners, determineLabelAction, LabelAction } = require('./.github/scripts/codeowners.js');
const owner = context.repo.owner;
const repo = context.repo.repo;
const pr_number = parseInt(process.env.PR_NUMBER, 10);
const LABEL_NAME = 'code-owner-approved';
console.log(`Processing PR #${pr_number} for codeowner approval label`);
const codeownersPatterns = loadCodeowners();
const action = await determineLabelAction(
github, owner, repo, pr_number, codeownersPatterns, LABEL_NAME
);
if (action === LabelAction.NONE) {
console.log('No label change needed');
return;
}
try {
if (action === LabelAction.ADD) {
await github.rest.issues.addLabels({
owner, repo, issue_number: pr_number, labels: [LABEL_NAME]
});
console.log(`Added '${LABEL_NAME}' label`);
} else if (action === LabelAction.REMOVE) {
await github.rest.issues.removeLabel({
owner, repo, issue_number: pr_number, name: LABEL_NAME
});
console.log(`Removed '${LABEL_NAME}' label`);
}
} catch (error) {
if (error.status === 403) {
console.log(`Warning: insufficient permissions to update label (expected for fork PRs)`);
} else if (error.status === 404) {
console.log(`Label '${LABEL_NAME}' not present, nothing to remove`);
} else {
throw error;
}
}

View File

@@ -13,6 +13,9 @@ on:
# Needs to be pull_request_target to get write permissions
pull_request_target:
types: [opened, reopened, synchronize, ready_for_review]
branches-ignore:
- release
- beta
permissions:
pull-requests: write
@@ -24,10 +27,17 @@ jobs:
if: ${{ github.repository == 'esphome/esphome' && !github.event.pull_request.draft }}
runs-on: ubuntu-latest
steps:
- name: Checkout base branch
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.base.sha }}
- name: Request reviews from component codeowners
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
const { loadCodeowners, getEffectiveOwners } = require('./.github/scripts/codeowners.js');
const owner = context.repo.owner;
const repo = context.repo.repo;
const pr_number = context.payload.pull_request.number;
@@ -38,12 +48,15 @@ jobs:
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
});
// Get the list of changed files in this PR (with pagination)
const files = await github.paginate(
github.rest.pulls.listFiles,
{
owner,
repo,
pull_number: pr_number
}
);
const changedFiles = files.map(file => file.filename);
console.log(`Found ${changedFiles.length} changed files`);
@@ -53,32 +66,10 @@ jobs:
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 from the checked-out base branch
const codeownersPatterns = loadCodeowners();
// 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 + '$');
}
console.log(`Parsed ${codeownersPatterns.length} codeowner patterns`);
// Helper function to create comment body
function createCommentBody(reviewersList, teamsList, matchedFileCount, isSuccessful = true) {
@@ -93,50 +84,11 @@ jobs:
}
}
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);
}
}
}
}
}
// Match changed files against CODEOWNERS patterns using last-match-wins semantics
const effective = getEffectiveOwners(changedFiles, codeownersPatterns);
const matchedOwners = effective.users;
const matchedTeams = effective.teams;
const matchedFileCount = effective.matchedFileCount;
if (matchedOwners.size === 0 && matchedTeams.size === 0) {
console.log('No codeowners found for any changed files');
@@ -170,11 +122,14 @@ jobs:
}
// 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 reviews = await github.paginate(
github.rest.pulls.listReviews,
{
owner,
repo,
pull_number: pr_number
}
);
const reviewedUsers = new Set();
reviews.forEach(review => {
@@ -247,7 +202,7 @@ jobs:
}
const totalReviewers = reviewersList.length + teamsList.length;
console.log(`Requesting reviews from ${reviewersList.length} users and ${teamsList.length} teams for ${fileMatches.size} matched files`);
console.log(`Requesting reviews from ${reviewersList.length} users and ${teamsList.length} teams for ${matchedFileCount} matched files`);
// Request reviews
try {
@@ -279,7 +234,7 @@ jobs:
// 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);
const commentBody = createCommentBody(reviewersList, teamsList, matchedFileCount, true);
await github.rest.issues.createComment({
owner,
@@ -297,7 +252,7 @@ jobs:
// 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);
const commentBody = createCommentBody(reviewersList, teamsList, matchedFileCount, false);
try {
await github.rest.issues.createComment({

View File

@@ -58,7 +58,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -86,6 +86,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
with:
category: "/language:${{matrix.language}}"

93
.github/workflows/pr-title-check.yml vendored Normal file
View File

@@ -0,0 +1,93 @@
name: PR Title Check
on:
pull_request:
types: [opened, edited, synchronize, reopened]
permissions:
contents: read
pull-requests: read
jobs:
check:
name: Validate PR title
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
const {
detectComponents,
hasCoreChanges,
hasDashboardChanges,
hasGitHubActionsChanges,
} = require('./.github/scripts/detect-tags.js');
const title = context.payload.pull_request.title;
const author = context.payload.pull_request.user.login;
// Skip bot PRs (e.g. dependabot) - they have their own title format
if (author === 'dependabot[bot]') {
return;
}
// Block titles starting with "word:" or "word(scope):" patterns
const commitStylePattern = /^\w+(\(.*?\))?[!]?\s*:/;
if (commitStylePattern.test(title)) {
core.setFailed(
`PR title should not start with a "prefix:" style format.\n` +
`Please use the format: [component] Brief description\n`
);
return;
}
// Get changed files to detect tags
const files = await github.paginate(github.rest.pulls.listFiles, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
});
const filenames = files.map(f => f.filename);
// Detect tags from changed files using shared logic
const tags = new Set();
for (const comp of detectComponents(filenames)) {
tags.add(comp);
}
if (hasCoreChanges(filenames)) tags.add('core');
if (hasDashboardChanges(filenames)) tags.add('dashboard');
if (hasGitHubActionsChanges(filenames)) tags.add('ci');
if (tags.size === 0) {
return;
}
// Check for angle brackets not wrapped in backticks.
// Astro docs MDX treats bare < as JSX component opening tags.
const stripped = title.replace(/`[^`]*`/g, '');
if (/[<>]/.test(stripped)) {
core.setFailed(
'PR title contains `<` or `>` not wrapped in backticks.\n' +
'Astro docs MDX interprets bare `<` as JSX components.\n' +
'Please wrap angle brackets with backticks, e.g.: [component] Add `<feature>` support'
);
return;
}
// Check title starts with [tag] prefix
const bracketPattern = /^\[\w+\]/;
if (!bracketPattern.test(title)) {
const suggestion = [...tags].map(c => `[${c}]`).join('');
// Skip if the suggested prefix would be too long for a readable title
if (suggestion.length > 40) {
return;
}
core.setFailed(
`PR modifies: ${[...tags].join(', ')}\n` +
`Title must start with a [tag] prefix.\n` +
`Suggested: ${suggestion} <description>`
);
}

View File

@@ -99,15 +99,15 @@ jobs:
python-version: "3.11"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Log in to docker hub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -138,7 +138,7 @@ jobs:
# version: ${{ needs.init.outputs.tag }}
- name: Upload digests
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: digests-${{ matrix.platform.arch }}
path: /tmp/digests
@@ -171,24 +171,24 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Download digests
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: digests-*
path: /tmp/digests
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Log in to docker hub
if: matrix.registry == 'dockerhub'
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry
if: matrix.registry == 'ghcr'
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -221,7 +221,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
@@ -256,7 +256,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
@@ -287,7 +287,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}

View File

@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Stale
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
with:
debug-only: ${{ github.ref != 'refs/heads/dev' }} # Dry-run when not run on dev branch
remove-stale-when-updated: true

View File

@@ -24,7 +24,7 @@ jobs:
- name: Setup Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: 3.13
python-version: "3.14"
- name: Install Home Assistant
run: |

View File

@@ -11,7 +11,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.15.0
rev: v0.15.8
hooks:
# Run the linter.
- id: ruff
@@ -37,7 +37,7 @@ repos:
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/asottile/pyupgrade
rev: v3.20.0
rev: v3.21.2
hooks:
- id: pyupgrade
args: [--py311-plus]

View File

@@ -54,6 +54,8 @@ esphome/components/atm90e32/* @circuitsetup @descipher
esphome/components/audio/* @kahrendt
esphome/components/audio_adc/* @kbx81
esphome/components/audio_dac/* @kbx81
esphome/components/audio_file/* @kahrendt
esphome/components/audio_file/media_source/* @kahrendt
esphome/components/axs15231/* @clydebarrow
esphome/components/b_parasite/* @rbaron
esphome/components/ballu/* @bazuchan
@@ -90,6 +92,7 @@ esphome/components/bmp3xx_i2c/* @latonita
esphome/components/bmp3xx_spi/* @latonita
esphome/components/bmp581_base/* @danielkent-net @kahrendt
esphome/components/bmp581_i2c/* @danielkent-net @kahrendt
esphome/components/bmp581_spi/* @danielkent-net @kahrendt
esphome/components/bp1658cj/* @Cossid
esphome/components/bp5758d/* @Cossid
esphome/components/bthome_mithermometer/* @nagyrobi
@@ -130,6 +133,7 @@ esphome/components/dashboard_import/* @esphome/core
esphome/components/datetime/* @jesserockz @rfdarter
esphome/components/debug/* @esphome/core
esphome/components/delonghi/* @grob6000
esphome/components/dew_point/* @CFlix
esphome/components/dfplayer/* @glmnet
esphome/components/dfrobot_sen0395/* @niklasweber
esphome/components/dht/* @OttoWinter
@@ -213,6 +217,8 @@ esphome/components/hbridge/light/* @DotNetDann
esphome/components/hbridge/switch/* @dwmw2
esphome/components/hc8/* @omartijn
esphome/components/hdc2010/* @optimusprimespace @ssieb
esphome/components/hdc2080/* @G-Pereira @jesserockz
esphome/components/hdc302x/* @joshuasing
esphome/components/he60r/* @clydebarrow
esphome/components/heatpumpir/* @rob-deutsch
esphome/components/hitachi_ac424/* @sourabhjaiswal
@@ -240,7 +246,6 @@ esphome/components/hyt271/* @Philippe12
esphome/components/i2c/* @esphome/core
esphome/components/i2c_device/* @gabest11
esphome/components/i2s_audio/* @jesserockz
esphome/components/i2s_audio/media_player/* @jesserockz
esphome/components/i2s_audio/microphone/* @jesserockz
esphome/components/i2s_audio/speaker/* @jesserockz @kahrendt
esphome/components/iaqcore/* @yozik04
@@ -315,6 +320,7 @@ esphome/components/mcp9808/* @k7hpn
esphome/components/md5/* @esphome/core
esphome/components/mdns/* @esphome/core
esphome/components/media_player/* @jesserockz
esphome/components/media_source/* @kahrendt
esphome/components/micro_wake_word/* @jesserockz @kahrendt
esphome/components/micronova/* @edenhaus @jorre05
esphome/components/microphone/* @jesserockz @kahrendt
@@ -325,6 +331,7 @@ esphome/components/mipi_dsi/* @clydebarrow
esphome/components/mipi_rgb/* @clydebarrow
esphome/components/mipi_spi/* @clydebarrow
esphome/components/mitsubishi/* @RubyBailey
esphome/components/mitsubishi_cn105/* @crnjan
esphome/components/mixer/speaker/* @kahrendt
esphome/components/mlx90393/* @functionpointer
esphome/components/mlx90614/* @jesserockz
@@ -406,11 +413,13 @@ esphome/components/restart/* @esphome/core
esphome/components/rf_bridge/* @jesserockz
esphome/components/rgbct/* @jesserockz
esphome/components/rp2040/* @jesserockz
esphome/components/rp2040_ble/* @bdraco
esphome/components/rp2040_pio_led_strip/* @Papa-DMan
esphome/components/rp2040_pwm/* @jesserockz
esphome/components/rpi_dpi_rgb/* @clydebarrow
esphome/components/rtl87xx/* @kuba2k2
esphome/components/rtttl/* @glmnet
esphome/components/rtttl/* @glmnet @ximex
esphome/components/runtime_image/* @clydebarrow @guillempages @kahrendt
esphome/components/runtime_stats/* @bdraco
esphome/components/rx8130/* @beormund
esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti
@@ -427,8 +436,10 @@ esphome/components/select/* @esphome/core
esphome/components/sen0321/* @notjj
esphome/components/sen21231/* @shreyaskarnik
esphome/components/sen5x/* @martgras
esphome/components/sen6x/* @martgras @mebner86 @mikelawrence @tuct
esphome/components/sensirion_common/* @martgras
esphome/components/sensor/* @esphome/core
esphome/components/serial_proxy/* @kbx81
esphome/components/sfa30/* @ghsensdev
esphome/components/sgp40/* @SenexCrenshaw
esphome/components/sgp4x/* @martgras @SenexCrenshaw
@@ -449,8 +460,12 @@ esphome/components/sn74hc165/* @jesserockz
esphome/components/socket/* @esphome/core
esphome/components/sonoff_d1/* @anatoly-savchenkov
esphome/components/sound_level/* @kahrendt
esphome/components/spa06_base/* @danielkent-net
esphome/components/spa06_i2c/* @danielkent-net
esphome/components/spa06_spi/* @danielkent-net
esphome/components/speaker/* @jesserockz @kahrendt
esphome/components/speaker/media_player/* @kahrendt @synesthesiam
esphome/components/speaker_source/* @kahrendt
esphome/components/spi/* @clydebarrow @esphome/core
esphome/components/spi_device/* @clydebarrow
esphome/components/spi_led_strip/* @clydebarrow
@@ -581,6 +596,7 @@ esphome/components/xl9535/* @mreditor97
esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68
esphome/components/xxtea/* @clydebarrow
esphome/components/zephyr/* @tomaszduda23
esphome/components/zephyr_mcumgr/ota/* @tomaszduda23
esphome/components/zhlt01/* @cfeenstra1024
esphome/components/zigbee/* @tomaszduda23
esphome/components/zio_ultrasonic/* @kahrendt

View File

@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version
# control system is used.
PROJECT_NUMBER = 2026.2.4
PROJECT_NUMBER = 2026.4.0-dev
# 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

View File

@@ -1,4 +1,4 @@
# ESPHome [![Discord Chat](https://img.shields.io/discord/429907082951524364.svg)](https://discord.gg/KhAMKrd) [![GitHub release](https://img.shields.io/github/release/esphome/esphome.svg)](https://GitHub.com/esphome/esphome/releases/)
# ESPHome [![Discord Chat](https://img.shields.io/discord/429907082951524364.svg)](https://discord.gg/KhAMKrd) [![GitHub release](https://img.shields.io/github/release/esphome/esphome.svg)](https://GitHub.com/esphome/esphome/releases/) [![CodSpeed](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json)](https://codspeed.io/esphome/esphome)
<a href="https://esphome.io/">
<picture>

View File

@@ -1,6 +1,7 @@
# PYTHON_ARGCOMPLETE_OK
import argparse
from collections.abc import Callable
from contextlib import suppress
from datetime import datetime
import functools
import getpass
@@ -9,6 +10,8 @@ import logging
import os
from pathlib import Path
import re
import shutil
import subprocess
import sys
import time
from typing import Protocol
@@ -23,6 +26,7 @@ import esphome.codegen as cg
from esphome.config import iter_component_configs, read_config, strip_default_ids
from esphome.const import (
ALLOWED_NAME_CHARS,
ARGUMENT_HELP_DEVICE,
CONF_API,
CONF_BAUD_RATE,
CONF_BROKER,
@@ -43,7 +47,9 @@ from esphome.const import (
CONF_SUBSTITUTIONS,
CONF_TOPIC,
ENV_NOGITIGNORE,
KEY_CORE,
KEY_NATIVE_IDF,
KEY_TARGET_PLATFORM,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_RP2040,
@@ -55,7 +61,11 @@ from esphome.helpers import get_bool_env, indent, is_ip_address
from esphome.log import AnsiFore, color, setup_log
from esphome.types import ConfigType
from esphome.util import (
PICOTOOL_PACKAGE,
detect_rp2040_bootsel,
get_picotool_path,
get_serial_ports,
is_picotool_usb_permission_error,
list_yaml_files,
run_external_command,
run_external_process,
@@ -64,9 +74,26 @@ from esphome.util import (
_LOGGER = logging.getLogger(__name__)
ESPHOME_COMMAND = [sys.executable, "-m", "esphome"]
# Maximum buffer size for serial log reading to prevent unbounded memory growth
SERIAL_BUFFER_MAX_SIZE = 65536
_RP2040_BOOTSEL_INSTRUCTIONS = (
"To enter BOOTSEL mode:\n"
" 1. Unplug the device\n"
" 2. Hold the BOOT/BOOTSEL button\n"
" 3. Plug in the USB cable while holding the button\n"
" 4. Release the button - the device should appear as a USB drive (RPI-RP2)\n"
"Then run the upload command again."
)
_RP2040_UDEV_HINT = (
"You may need to add a udev rule for RP2040 devices. "
"See: https://github.com/raspberrypi/picotool"
"/blob/master/udev/60-picotool.rules"
)
# Special non-component keys that appear in configs
_NON_COMPONENT_KEYS = frozenset(
{
@@ -162,6 +189,7 @@ class PortType(StrEnum):
NETWORK = "NETWORK"
MQTT = "MQTT"
MQTTIP = "MQTTIP"
BOOTSEL = "BOOTSEL"
# Magic MQTT port types that require special handling
@@ -240,6 +268,19 @@ def choose_upload_log_host(
(f"{port.path} ({port.description})", port.path) for port in get_serial_ports()
]
# Add RP2040 BOOTSEL device option when uploading
bootsel_permission_error = False
if (
purpose == Purpose.UPLOADING
and CORE.data.get(KEY_CORE, {}).get(KEY_TARGET_PLATFORM) == PLATFORM_RP2040
and (picotool := _find_picotool()) is not None
):
bootsel = detect_rp2040_bootsel(picotool)
if bootsel.device_count > 0:
options.append(("RP2040 BOOTSEL (via picotool)", "BOOTSEL"))
elif bootsel.permission_error:
bootsel_permission_error = True
if purpose == Purpose.LOGGING:
if has_mqtt_logging():
mqtt_config = CORE.config[CONF_MQTT]
@@ -257,6 +298,25 @@ def choose_upload_log_host(
if has_mqtt_ip_lookup():
options.append(("Over The Air (MQTT IP lookup)", "MQTTIP"))
# Show helpful BOOTSEL instructions for RP2040 when no BOOTSEL device is found
if (
purpose == Purpose.UPLOADING
and CORE.data.get(KEY_CORE, {}).get(KEY_TARGET_PLATFORM) == PLATFORM_RP2040
and not any(get_port_type(opt[1]) == PortType.BOOTSEL for opt in options)
):
if bootsel_permission_error:
_LOGGER.warning(
"An RP2040 device in BOOTSEL mode was detected but could "
"not be accessed due to USB permissions."
)
if sys.platform.startswith("linux"):
_LOGGER.warning(_RP2040_UDEV_HINT)
if not options:
raise EsphomeError(
f"No RP2040 device found. {_RP2040_BOOTSEL_INSTRUCTIONS}"
)
_LOGGER.info("Tip: %s", _RP2040_BOOTSEL_INSTRUCTIONS)
if check_default is not None and check_default in [opt[1] for opt in options]:
return [check_default]
return [choose_prompt(options, purpose=purpose)]
@@ -403,10 +463,13 @@ def get_port_type(port: str) -> PortType:
Returns:
PortType.SERIAL for serial ports (/dev/ttyUSB0, COM1, etc.)
PortType.BOOTSEL for RP2040 BOOTSEL upload via picotool
PortType.MQTT for MQTT logging
PortType.MQTTIP for MQTT IP lookup
PortType.NETWORK for IP addresses, hostnames, or mDNS names
"""
if port == "BOOTSEL":
return PortType.BOOTSEL
if port.startswith("/") or port.startswith("COM"):
return PortType.SERIAL
if port == "MQTT":
@@ -431,6 +494,14 @@ def run_miniterm(config: ConfigType, port: str, args) -> int:
return 1
_LOGGER.info("Starting log output from %s with baud rate %s", port, baud_rate)
process_stacktrace = None
try:
module = importlib.import_module("esphome.components." + CORE.target_platform)
process_stacktrace = getattr(module, "process_stacktrace")
except AttributeError:
pass
backtrace_state = False
ser = serial.Serial()
ser.baudrate = baud_rate
@@ -472,9 +543,14 @@ def run_miniterm(config: ConfigType, port: str, args) -> int:
)
safe_print(parser.parse_line(line, time_str))
backtrace_state = platformio_api.process_stacktrace(
config, line, backtrace_state=backtrace_state
)
if process_stacktrace:
backtrace_state = process_stacktrace(
config, line, backtrace_state
)
else:
backtrace_state = platformio_api.process_stacktrace(
config, line, backtrace_state=backtrace_state
)
except serial.SerialException:
_LOGGER.error("Serial port closed!")
return 0
@@ -614,6 +690,47 @@ def _check_and_emit_build_info() -> None:
)
def _get_configured_xtal_freq() -> int | None:
"""Read the configured crystal frequency from the sdkconfig file."""
sdkconfig_path = CORE.relative_build_path(f"sdkconfig.{CORE.name}")
if not sdkconfig_path.is_file():
return None
with suppress(OSError, ValueError):
content = sdkconfig_path.read_text()
for line in content.splitlines():
if line.startswith("CONFIG_XTAL_FREQ="):
return int(line.split("=", 1)[1])
return None
def _make_crystal_freq_callback(
configured_freq: int,
) -> Callable[[str], str | None]:
"""Create a callback that checks esptool crystal frequency output."""
crystal_re = re.compile(r"Crystal frequency:\s+(\d+)\s*MHz")
def check_crystal_line(line: str) -> str | None:
if not (match := crystal_re.search(line)):
return None
detected = int(match.group(1))
if detected == configured_freq:
return None
return (
f"\n\033[33mWARNING: Crystal frequency mismatch! "
f"Device reports {detected}MHz but firmware is configured "
f"for {configured_freq}MHz.\n"
f"UART logging and other clock-dependent features will not "
f"work correctly.\n"
f"Set the correct crystal frequency with sdkconfig_options:\n"
f" esp32:\n"
f" framework:\n"
f" sdkconfig_options:\n"
f" CONFIG_XTAL_FREQ_{detected}: 'y'\033[0m\n\n"
)
return check_crystal_line
def upload_using_esptool(
config: ConfigType, port: str, file: str, speed: int
) -> str | int:
@@ -642,6 +759,14 @@ def upload_using_esptool(
mcu = get_esp32_variant().lower()
line_callbacks: list[Callable[[str], str | None]] = []
if (
CORE.is_esp32
and file is None
and (configured_freq := _get_configured_xtal_freq()) is not None
):
line_callbacks.append(_make_crystal_freq_callback(configured_freq))
def run_esptool(baud_rate):
cmd = [
"esptool",
@@ -666,9 +791,13 @@ def upload_using_esptool(
if os.environ.get("ESPHOME_USE_SUBPROCESS") is None:
import esptool
return run_external_command(esptool.main, *cmd) # pylint: disable=no-member
return run_external_command(
esptool.main, # pylint: disable=no-member
*cmd,
line_callbacks=line_callbacks,
)
return run_external_process(*cmd)
return run_external_process(*cmd, line_callbacks=line_callbacks)
rc = run_esptool(first_baudrate)
if rc == 0 or first_baudrate == 115200:
@@ -681,15 +810,140 @@ def upload_using_esptool(
return run_esptool(115200)
def upload_using_platformio(config: ConfigType, port: str):
def upload_using_platformio(config: ConfigType, port: str) -> int:
from esphome import platformio_api
# RP2040 platform-raspberrypi build recipe expects firmware.bin.signed for
# the upload target, but 'nobuild' skips the build phase that creates it.
# Create it here so the upload doesn't fail.
if CORE.data.get(KEY_CORE, {}).get(KEY_TARGET_PLATFORM) == PLATFORM_RP2040:
idedata = platformio_api.get_idedata(config)
build_dir = Path(idedata.firmware_elf_path).parent
firmware_bin = build_dir / "firmware.bin"
signed_bin = build_dir / "firmware.bin.signed"
if firmware_bin.is_file() and not signed_bin.is_file():
shutil.copy2(firmware_bin, signed_bin)
upload_args = ["-t", "upload", "-t", "nobuild"]
if port is not None:
upload_args += ["--upload-port", port]
return platformio_api.run_platformio_cli_run(config, CORE.verbose, *upload_args)
def _find_picotool() -> Path | None:
"""Find the picotool binary from PlatformIO packages."""
from esphome import platformio_api
try:
idedata = platformio_api.get_idedata(CORE.config)
except Exception: # noqa: BLE001 # pylint: disable=broad-except
return None
return get_picotool_path(idedata.cc_path)
def upload_using_picotool(config: ConfigType) -> int:
"""Upload firmware to RP2040 in BOOTSEL mode using picotool.
Uses picotool to load the ELF firmware directly via USB, avoiding
the mass storage copy approach that causes "disk not ejected properly"
warnings on macOS.
"""
from esphome import platformio_api
idedata = platformio_api.get_idedata(config)
firmware_elf = Path(idedata.firmware_elf_path)
if not firmware_elf.is_file():
_LOGGER.error(
"Firmware ELF file not found at %s. "
"Make sure the project has been compiled first.",
firmware_elf,
)
return 1
picotool = get_picotool_path(idedata.cc_path)
if picotool is None:
_LOGGER.error(
"picotool not found. Ensure the RP2040 PlatformIO platform "
"is installed (%s).",
PICOTOOL_PACKAGE,
)
return 1
_LOGGER.info("Uploading firmware to RP2040 via picotool...")
try:
# Don't capture stdout — let picotool write directly to the terminal
# so progress bars display in real-time with \r updates.
# Capture stderr only so we can detect permission errors.
result = subprocess.run(
[str(picotool), "load", "-v", "-x", str(firmware_elf)],
stderr=subprocess.PIPE,
timeout=60,
check=False,
)
except subprocess.TimeoutExpired:
_LOGGER.error("picotool upload timed out after 60 seconds.")
return 1
except OSError as err:
_LOGGER.error("Failed to run picotool: %s", err)
return 1
if result.returncode != 0:
stderr = result.stderr.decode("utf-8", errors="replace").strip()
if stderr:
for line in stderr.splitlines():
safe_print(line)
if is_picotool_usb_permission_error(stderr):
msg = "Permission denied accessing USB device."
if sys.platform.startswith("linux"):
msg += f" {_RP2040_UDEV_HINT}"
_LOGGER.error(msg)
else:
_LOGGER.error("picotool upload failed (exit code %d).", result.returncode)
return 1
return 0
def _wait_for_serial_port(
port: str | None = None,
timeout: float = 30.0,
known_ports: set[str] | None = None,
) -> None:
"""Wait for a serial port to appear, e.g. after a device reboot.
USB-CDC devices disappear briefly after flashing while the device
reboots and re-enumerates on the USB bus.
If port is given, wait for that specific path. If known_ports is
given, wait for a new port that wasn't in the set. Otherwise wait
for any serial port to appear.
"""
def _port_found() -> bool:
if port is not None:
if os.name == "posix":
return os.path.exists(port)
return any(p.path == port for p in get_serial_ports())
ports = get_serial_ports()
if known_ports is not None:
return any(p.path not in known_ports for p in ports)
return bool(ports)
if _port_found():
return
if port is not None:
_LOGGER.info("Waiting for %s to come online...", port)
else:
_LOGGER.info("Waiting for device to reboot...")
start = time.monotonic()
while time.monotonic() - start < timeout:
time.sleep(0.05)
if _port_found():
time.sleep(0.05)
return
def check_permissions(port: str):
if os.name == "posix" and get_port_type(port) == PortType.SERIAL:
# Check if we can open selected serial port
@@ -719,7 +973,15 @@ def upload_program(
except AttributeError:
pass
if get_port_type(host) == PortType.SERIAL:
port_type = get_port_type(host)
if port_type == PortType.BOOTSEL:
exit_code = upload_using_picotool(config)
# Return None for device - BOOTSEL can't be used for logging,
# so command_run will show the interactive chooser for log source
return exit_code, None
if port_type == PortType.SERIAL:
check_permissions(host)
exit_code = 1
@@ -773,6 +1035,7 @@ def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int
port_type = get_port_type(port)
if port_type == PortType.SERIAL:
_wait_for_serial_port(port)
check_permissions(port)
return run_miniterm(config, port, args)
@@ -783,7 +1046,11 @@ def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int
):
from esphome.components.api.client import run_logs
return run_logs(config, network_devices)
return run_logs(
config,
network_devices,
subscribe_states=not getattr(args, "no_states", False),
)
if port_type in (PortType.NETWORK, PortType.MQTT) and has_mqtt_logging():
from esphome import mqtt
@@ -911,6 +1178,9 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
purpose=Purpose.UPLOADING,
)
# Snapshot current serial ports before upload so we can detect new ones
pre_upload_ports = {p.path for p in get_serial_ports()}
exit_code, successful_device = upload_program(config, args, devices)
if exit_code == 0:
_LOGGER.info("Successfully uploaded program.")
@@ -921,6 +1191,19 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
if args.no_logs:
return 0
# After BOOTSEL upload, wait for a new serial port to appear
# so it shows up in the log chooser
if (
successful_device is None
and CORE.data.get(KEY_CORE, {}).get(KEY_TARGET_PLATFORM) == PLATFORM_RP2040
):
_wait_for_serial_port(known_ports=pre_upload_ports)
# If exactly one new serial port appeared, use it directly
serial_ports = get_serial_ports()
new_ports = [p for p in serial_ports if p.path not in pre_upload_ports]
if len(new_ports) == 1:
successful_device = new_ports[0].path
# For logs, prefer the device we successfully uploaded to
devices = choose_upload_log_host(
default=successful_device,
@@ -1030,9 +1313,8 @@ def command_update_all(args: ArgsProtocol) -> int | None:
files = list_yaml_files(args.configuration)
def build_command(f):
if CORE.dashboard:
return ["esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA"]
return ["esphome", "run", f, "--no-logs", "--device", "OTA"]
dashboard = ["--dashboard"] if CORE.dashboard else []
return [*ESPHOME_COMMAND, *dashboard, "run", f, "--no-logs", "--device", "OTA"]
return run_multiple_configs(files, build_command)
@@ -1181,7 +1463,7 @@ def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None:
new_path.write_text(new_raw, encoding="utf-8")
rc = run_external_process("esphome", "config", str(new_path))
rc = run_external_process(*ESPHOME_COMMAND, "config", str(new_path))
if rc != 0:
print(color(AnsiFore.BOLD_RED, "Rename failed. Reverting changes."))
new_path.unlink()
@@ -1199,7 +1481,7 @@ def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None:
cli_args.insert(0, "--dashboard")
try:
rc = run_external_process("esphome", *cli_args)
rc = run_external_process(*ESPHOME_COMMAND, *cli_args)
except KeyboardInterrupt:
rc = 1
if rc != 0:
@@ -1354,7 +1636,7 @@ def parse_args(argv):
parser_upload.add_argument(
"--device",
action="append",
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.",
help=ARGUMENT_HELP_DEVICE,
)
parser_upload.add_argument(
"--upload_speed",
@@ -1377,7 +1659,7 @@ def parse_args(argv):
parser_logs.add_argument(
"--device",
action="append",
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.",
help=ARGUMENT_HELP_DEVICE,
)
parser_logs.add_argument(
"--reset",
@@ -1386,6 +1668,11 @@ def parse_args(argv):
help="Reset the device before starting serial logs.",
default=os.getenv("ESPHOME_SERIAL_LOGGING_RESET"),
)
parser_logs.add_argument(
"--no-states",
action="store_true",
help="Do not show entity state changes in log output.",
)
parser_discover = subparsers.add_parser(
"discover",
@@ -1407,7 +1694,7 @@ def parse_args(argv):
parser_run.add_argument(
"--device",
action="append",
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.",
help=ARGUMENT_HELP_DEVICE,
)
parser_run.add_argument(
"--upload_speed",
@@ -1596,7 +1883,7 @@ def run_esphome(argv):
# argv[0] is the program path, skip it since we prefix with "esphome"
def build_command(f):
return (
["esphome"]
[*ESPHOME_COMMAND]
+ [arg for arg in argv[1:] if arg not in args.configuration]
+ [str(f)]
)

View File

@@ -1,6 +1,6 @@
"""Memory usage analyzer for ESPHome compiled binaries."""
from collections import defaultdict
from collections import Counter, defaultdict
from dataclasses import dataclass, field
import logging
from pathlib import Path
@@ -40,6 +40,15 @@ _READELF_SECTION_PATTERN = re.compile(
r"\s*\[\s*\d+\]\s+([\.\w]+)\s+\w+\s+[\da-fA-F]+\s+[\da-fA-F]+\s+([\da-fA-F]+)"
)
# Regex for extracting call targets from objdump disassembly
# Matches direct call instructions across architectures:
# Xtensa: call0/call4/call8/call12/callx0/callx4/callx8/callx12 <addr> <symbol>
# ARM: bl/blx <addr> <symbol>
# Captures the mangled symbol name inside angle brackets.
_CALL_TARGET_PATTERN = re.compile(
r"\t(?:call(?:0|4|8|12)|callx(?:0|4|8|12)|blx?)\s+[\da-fA-F]+ <([^>]+)>"
)
# Component category prefixes
_COMPONENT_PREFIX_ESPHOME = "[esphome]"
_COMPONENT_PREFIX_EXTERNAL = "[external]"
@@ -47,6 +56,10 @@ _COMPONENT_PREFIX_LIB = "[lib]"
_COMPONENT_CORE = f"{_COMPONENT_PREFIX_ESPHOME}core"
_COMPONENT_API = f"{_COMPONENT_PREFIX_ESPHOME}api"
# Placement new storage suffix (generated by codegen Pvariable)
_PSTORAGE_SUFFIX = "__pstorage"
# C++ namespace prefixes
_NAMESPACE_ESPHOME = "esphome::"
_NAMESPACE_STD = "std::"
@@ -192,20 +205,27 @@ class MemoryAnalyzer:
self._cswtch_symbols: list[tuple[str, int, str, str]] = []
# Library symbol mapping: symbol_name -> library_name
self._lib_symbol_map: dict[str, str] = {}
# Source file symbol mapping: symbol_name -> component_name
# Used for extern "C" and other symbols without C++ namespace
self._source_symbol_map: dict[str, str] = {}
# Library dir to name mapping: "lib641" -> "espsoftwareserial",
# "espressif__mdns" -> "mdns"
self._lib_hash_to_name: dict[str, str] = {}
# Heuristic category to library redirect: "mdns_lib" -> "[lib]mdns"
self._heuristic_to_lib: dict[str, str] = {}
# Function call counts: mangled_name -> call_count
self._function_call_counts: Counter[str] = Counter()
def analyze(self) -> dict[str, ComponentMemory]:
"""Analyze the ELF file and return component memory usage."""
self._parse_sections()
self._parse_symbols()
self._scan_libraries()
self._scan_source_symbols()
self._categorize_symbols()
self._analyze_cswtch_symbols()
self._analyze_sdk_libraries()
self._analyze_function_calls()
return dict(self.components)
def _parse_sections(self) -> None:
@@ -316,6 +336,13 @@ class MemoryAnalyzer:
# Demangle C++ names if needed
demangled = self._demangle_symbol(symbol_name)
# Check for placement new storage symbols (generated by codegen)
# Format: {component}__{id}__pstorage
if demangled.endswith(_PSTORAGE_SUFFIX) and (
component := self._match_pstorage_component(demangled)
):
return component
# Check for special component classes first (before namespace pattern)
# This handles cases like esphome::ESPHomeOTAComponent which should map to ota
if _NAMESPACE_ESPHOME in demangled:
@@ -351,6 +378,11 @@ class MemoryAnalyzer:
if lib_name := self._lib_symbol_map.get(symbol_name):
return f"{_COMPONENT_PREFIX_LIB}{lib_name}"
# Check source file mapping (catches extern "C" functions in ESPHome sources)
# Must be before heuristic patterns since source attribution is authoritative
if component := self._source_symbol_map.get(symbol_name):
return component
# Check against symbol patterns
for component, patterns in SYMBOL_PATTERNS.items():
if any(pattern in symbol_name for pattern in patterns):
@@ -378,14 +410,33 @@ class MemoryAnalyzer:
# Track uncategorized symbols for analysis
return "other"
def _match_pstorage_component(self, symbol_name: str) -> str | None:
"""Match a __pstorage symbol to its ESPHome component.
Symbol format: {component}__{id}__pstorage
The component namespace is embedded by codegen before the double underscore.
"""
prefix = symbol_name[: -len(_PSTORAGE_SUFFIX)]
# Extract component namespace before the first double underscore
dunder_pos = prefix.find("__")
if dunder_pos == -1:
return None
component_name = prefix[:dunder_pos]
if component_name in get_esphome_components():
return f"{_COMPONENT_PREFIX_ESPHOME}{component_name}"
if component_name in self.external_components:
return f"{_COMPONENT_PREFIX_EXTERNAL}{component_name}"
return None
def _batch_demangle_symbols(self, symbols: list[str]) -> None:
"""Batch demangle C++ symbol names for efficiency."""
if not symbols:
return
_LOGGER.info("Demangling %d symbols", len(symbols))
self._demangle_cache = batch_demangle(symbols, objdump_path=self.objdump_path)
_LOGGER.info("Successfully demangled %d symbols", len(self._demangle_cache))
demangled = batch_demangle(symbols, objdump_path=self.objdump_path)
self._demangle_cache.update(demangled)
_LOGGER.info("Successfully demangled %d symbols", len(demangled))
def _demangle_symbol(self, symbol: str) -> str:
"""Get demangled C++ symbol name from cache."""
@@ -640,6 +691,7 @@ class MemoryAnalyzer:
return None
symbol_map: dict[str, str] = {}
source_symbol_map: dict[str, str] = {}
current_symbol: str | None = None
section_prefixes = (".text.", ".rodata.", ".data.", ".bss.", ".literal.")
@@ -675,9 +727,18 @@ class MemoryAnalyzer:
if dir_key in source_path:
symbol_map[current_symbol] = lib_name
break
else:
# Map ESPHome source files to components for extern "C"
# and other symbols without C++ namespace
component = self._source_file_to_component(source_path)
if component.startswith(
(_COMPONENT_PREFIX_ESPHOME, _COMPONENT_PREFIX_EXTERNAL)
):
source_symbol_map[current_symbol] = component
current_symbol = None
self._source_symbol_map = source_symbol_map
return symbol_map or None
def _scan_libraries(self) -> None:
@@ -728,6 +789,112 @@ class MemoryAnalyzer:
len(libraries),
)
def _scan_source_symbols(self) -> None:
"""Scan ESPHome source object files to map extern "C" symbols to components.
When no linker map file is available, this uses ``nm`` to scan ``.o`` files
under ``src/esphome/`` and build a symbol-to-component mapping. This catches
``extern "C"`` functions and other symbols that lack C++ namespace prefixes.
Skips scanning if ``_source_symbol_map`` was already populated by
``_parse_map_file()``.
"""
if self._source_symbol_map or not self.nm_path:
return
obj_dir = self._find_object_files_dir()
if obj_dir is None:
return
# Find ESPHome source object files
esphome_src_dir = obj_dir / "src" / "esphome"
if not esphome_src_dir.is_dir():
return
obj_files = sorted(esphome_src_dir.rglob("*.o"))
if not obj_files:
return
# Run nm with --print-file-name to get file:symbol mapping
result = run_tool(
[self.nm_path, "--print-file-name", "-g", "--defined-only"]
+ [str(f) for f in obj_files],
)
if result is None or result.returncode != 0:
_LOGGER.debug("nm scan of source objects failed")
return
self._source_symbol_map = self._parse_nm_source_output(result.stdout, obj_dir)
if self._source_symbol_map:
_LOGGER.info(
"Built source symbol map from nm: %d symbols",
len(self._source_symbol_map),
)
def _parse_nm_source_output(self, output: str, base_dir: Path) -> dict[str, str]:
"""Parse nm output to map non-namespaced symbols to ESPHome components.
Extracts global defined symbols from ESPHome source object files that
don't use C++ namespacing (e.g. ``extern "C"`` functions).
Args:
output: Raw stdout from ``nm --print-file-name -g --defined-only``
or ``nm --print-file-name -S``.
base_dir: Build directory for computing relative paths.
Returns:
Dict mapping symbol names to component names.
"""
source_map: dict[str, str] = {}
for line in output.splitlines():
# Format: /path/to/file.o: addr type name
# or: /path/to/file.o: addr size type name (with -S)
colon_idx = line.rfind(".o:")
if colon_idx == -1:
continue
file_path = line[: colon_idx + 2]
fields = line[colon_idx + 3 :].split()
if len(fields) < 3:
continue
# With -S flag, format is: addr size type name
# Without -S flag: addr type name
# type is a single char; size is hex digits
# Detect by checking if fields[1] is a single uppercase letter (type)
if len(fields[1]) == 1 and fields[1].isalpha():
# addr type name
sym_type = fields[1]
symbol_name = fields[2]
elif len(fields) >= 4:
# addr size type name
sym_type = fields[2]
symbol_name = fields[3]
else:
continue
# Only global defined symbols (uppercase type)
if not sym_type.isupper() or sym_type == "U":
continue
# Skip symbols already in esphome:: namespace
if symbol_name.startswith("_ZN7esphome"):
continue
# Make path relative to base_dir for _source_file_to_component
try:
rel_path = str(Path(file_path).relative_to(base_dir))
except ValueError:
continue
component = self._source_file_to_component(rel_path)
if component.startswith(
(_COMPONENT_PREFIX_ESPHOME, _COMPONENT_PREFIX_EXTERNAL)
):
source_map[symbol_name] = component
return source_map
def _find_object_files_dir(self) -> Path | None:
"""Find the directory containing object files for this build.
@@ -1011,6 +1178,43 @@ class MemoryAnalyzer:
total_size,
)
def _analyze_function_calls(self) -> None:
"""Count function call sites by parsing disassembly output.
Parses direct call instructions (call0/call8/bl/blx) from objdump -d
to count how many times each function is called. This helps identify
inlining candidates — frequently called small functions benefit most
from inlining.
"""
result = run_tool(
[self.objdump_path, "-d", str(self.elf_path)],
timeout=60,
)
if result is None or result.returncode != 0:
_LOGGER.debug("Failed to disassemble ELF for function call analysis")
return
self._function_call_counts = Counter(
match.group(1)
for line in result.stdout.splitlines()
if (match := _CALL_TARGET_PATTERN.search(line))
)
# Demangle any call targets not already in the cache
missing = [
name
for name in self._function_call_counts
if name not in self._demangle_cache
]
if missing:
self._batch_demangle_symbols(missing)
_LOGGER.debug(
"Function call analysis: %d unique targets, %d total calls",
len(self._function_call_counts),
sum(self._function_call_counts.values()),
)
def get_unattributed_ram(self) -> tuple[int, int, int]:
"""Get unattributed RAM sizes (SDK/framework overhead).

View File

@@ -15,6 +15,7 @@ from . import (
_COMPONENT_PREFIX_ESPHOME,
_COMPONENT_PREFIX_EXTERNAL,
_COMPONENT_PREFIX_LIB,
_PSTORAGE_SUFFIX,
RAM_SECTIONS,
MemoryAnalyzer,
)
@@ -23,6 +24,17 @@ if TYPE_CHECKING:
from . import ComponentMemory
def _format_pstorage_name(name: str) -> str:
"""Format a __pstorage symbol as 'storage for {id}'."""
if not name.endswith(_PSTORAGE_SUFFIX):
return name
prefix = name[: -len(_PSTORAGE_SUFFIX)]
# Strip component namespace prefix: {component}__{id} -> {id}
dunder_pos = prefix.find("__")
var_id = prefix[dunder_pos + 2 :] if dunder_pos != -1 else prefix
return f"storage for {var_id}"
class MemoryAnalyzerCLI(MemoryAnalyzer):
"""Memory analyzer with CLI-specific report generation."""
@@ -148,11 +160,14 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
If section is one of the RAM sections (.data or .bss), a label like
" [data]" or " [bss]" is appended. For non-RAM sections or when
section is None, no section label is added.
Placement new storage symbols are formatted as "storage for {id}".
"""
display_name = _format_pstorage_name(demangled)
section_label = ""
if section in RAM_SECTIONS:
section_label = f" [{section[1:]}]" # .data -> [data], .bss -> [bss]
return f"{demangled} ({size:,} B){section_label}"
return f"{display_name} ({size:,} B){section_label}"
def _add_top_symbols(self, lines: list[str]) -> None:
"""Add a section showing the top largest symbols in the binary."""
@@ -175,11 +190,13 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
for i, (_, demangled, size, section, component) in enumerate(top_symbols):
# Format section label
section_label = f"[{section[1:]}]" if section else ""
# Truncate demangled name if too long
# Format storage symbols readably
display_name = _format_pstorage_name(demangled)
# Truncate if too long
demangled_display = (
f"{demangled[:truncate_limit]}..."
if len(demangled) > self.COL_TOP_SYMBOL_NAME
else demangled
f"{display_name[:truncate_limit]}..."
if len(display_name) > self.COL_TOP_SYMBOL_NAME
else display_name
)
lines.append(
f"{i + 1:>2}. {size:>7,} B {section_label:<8} {demangled_display:<{self.COL_TOP_SYMBOL_NAME}} {component}"
@@ -231,6 +248,110 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
lines.append(f" {size:>6,} B {sym_name}")
lines.append("")
# Number of top called functions to show
TOP_CALLS_LIMIT: int = 50
# Number of inlining candidates to show
INLINE_CANDIDATES_LIMIT: int = 25
# Maximum function size in bytes to consider for inlining
INLINE_SIZE_THRESHOLD: int = 16
def _build_symbol_sizes(self) -> dict[str, int]:
"""Build a size lookup from all component symbols: mangled_name -> size."""
return {
symbol: size
for symbols in self._component_symbols.values()
for symbol, _, size, _ in symbols
}
def _format_call_row(
self, index: int, mangled: str, count: int, symbol_sizes: dict[str, int]
) -> str:
"""Format a single row for call frequency tables."""
demangled = self._demangle_cache.get(mangled, mangled)
if len(demangled) > 80:
demangled = f"{demangled[:77]}..."
size = symbol_sizes.get(mangled)
size_str = f"{size:>5,} B" if size is not None else " ?"
return f"{index:>3} {count:>5} {size_str} {demangled}"
def _add_call_table_header(self, lines: list[str]) -> None:
"""Add the header row for call frequency tables."""
lines.append(f"{'#':>3} {'Calls':>5} {'Size':>7} Function")
lines.append(f"{'---':>3} {'-----':>5} {'-------':>7} {'-' * 60}")
def _add_function_call_analysis(self, lines: list[str]) -> None:
"""Add function call frequency analysis section.
Shows the most frequently called functions by call site count.
"""
self._add_section_header(lines, "Top Called Functions")
symbol_sizes = self._build_symbol_sizes()
# Sort by call count descending
sorted_calls = sorted(
self._function_call_counts.items(), key=lambda x: x[1], reverse=True
)
self._add_call_table_header(lines)
for i, (mangled, count) in enumerate(sorted_calls[: self.TOP_CALLS_LIMIT]):
lines.append(self._format_call_row(i + 1, mangled, count, symbol_sizes))
total_calls = sum(self._function_call_counts.values())
lines.append("")
lines.append(
f"Total: {len(self._function_call_counts)} unique targets, "
f"{total_calls:,} call sites"
)
lines.append("")
def _add_inline_candidates(self, lines: list[str]) -> None:
"""Add inlining candidates section.
Shows frequently called functions that are small enough to benefit
from inlining (< 16 bytes). These are the best candidates for
reducing call overhead.
"""
self._add_section_header(
lines,
f"Inlining Candidates (<{self.INLINE_SIZE_THRESHOLD} B, by call count)",
)
symbol_sizes = self._build_symbol_sizes()
# Filter to small functions with known size, sort by call count
candidates = sorted(
(
(mangled, count)
for mangled, count in self._function_call_counts.items()
if mangled in symbol_sizes
and symbol_sizes[mangled] < self.INLINE_SIZE_THRESHOLD
),
key=lambda x: x[1],
reverse=True,
)
if not candidates:
lines.append("No candidates found.")
lines.append("")
return
self._add_call_table_header(lines)
for i, (mangled, count) in enumerate(
candidates[: self.INLINE_CANDIDATES_LIMIT]
):
lines.append(self._format_call_row(i + 1, mangled, count, symbol_sizes))
lines.append("")
lines.append(
f"Showing top {min(len(candidates), self.INLINE_CANDIDATES_LIMIT)} "
f"of {len(candidates)} functions under "
f"{self.INLINE_SIZE_THRESHOLD} B"
)
lines.append("")
def generate_report(self, detailed: bool = False) -> str:
"""Generate a formatted memory report."""
components = sorted(
@@ -469,15 +590,16 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
lines.append(f"Total size: {comp_mem.flash_total:,} B")
lines.append("")
# Show all symbols above threshold for better visibility
# Show symbols above threshold, always include storage symbols
large_symbols = [
(sym, dem, size, sec)
for sym, dem, size, sec in sorted_symbols
if size > self.SYMBOL_SIZE_THRESHOLD
or dem.endswith(_PSTORAGE_SUFFIX)
]
lines.append(
f"{comp_name} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B ({len(large_symbols)} symbols):"
f"{comp_name} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B & storage ({len(large_symbols)} symbols):"
)
for i, (symbol, demangled, size, section) in enumerate(large_symbols):
lines.append(
@@ -500,7 +622,10 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
# Sort by size descending
sorted_ram_syms = sorted(ram_syms, key=lambda x: x[2], reverse=True)
large_ram_syms = [
s for s in sorted_ram_syms if s[2] > self.RAM_SYMBOL_SIZE_THRESHOLD
s
for s in sorted_ram_syms
if s[2] > self.RAM_SYMBOL_SIZE_THRESHOLD
or s[1].endswith(_PSTORAGE_SUFFIX)
]
lines.append(f"{name} ({mem.ram_total:,} B total RAM):")
@@ -518,13 +643,14 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
for symbol, demangled, size, section in large_ram_syms[:10]:
# Format section label consistently by stripping leading dot
section_label = section.lstrip(".") if section else ""
display_name = _format_pstorage_name(demangled)
# Add ellipsis if name is truncated
demangled_display = (
f"{demangled[:70]}..." if len(demangled) > 70 else demangled
)
lines.append(
f" {size:>6,} B [{section_label}] {demangled_display}"
display_name = (
f"{display_name[:70]}..."
if len(display_name) > 70
else display_name
)
lines.append(f" {size:>6,} B [{section_label}] {display_name}")
if len(large_ram_syms) > 10:
lines.append(f" ... and {len(large_ram_syms) - 10} more")
lines.append("")
@@ -533,6 +659,11 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
if self._cswtch_symbols:
self._add_cswtch_analysis(lines)
# Function call frequency analysis
if self._function_call_counts:
self._add_function_call_analysis(lines)
self._add_inline_candidates(lines)
lines.append(
"Note: This analysis covers symbols in the ELF file. Some runtime allocations may not be included."
)

View File

@@ -256,7 +256,7 @@ SYMBOL_PATTERNS = {
"ipv6_stack": ["nd6_", "ip6_", "mld6_", "icmp6_", "icmp6_input"],
# Order matters! More specific categories must come before general ones.
# mdns must come before bluetooth to avoid "_mdns_disable_pcb" matching "ble_" pattern
"mdns_lib": ["mdns"],
"mdns_lib": ["mdns", "packet$"],
# memory_mgmt must come before wifi_stack to catch mmu_hal_* symbols
"memory_mgmt": [
"mem_",
@@ -408,7 +408,6 @@ SYMBOL_PATTERNS = {
],
"arduino_core": [
"pinMode",
"resetPins",
"millis",
"micros",
"delay(", # More specific - Arduino delay function with parenthesis
@@ -794,7 +793,6 @@ SYMBOL_PATTERNS = {
"s_dp",
"s_ni",
"s_reg_dump",
"packet$",
"d_mult_table",
"K",
"fcstab",

View File

@@ -1,3 +1,5 @@
import logging
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import (
@@ -57,8 +59,42 @@ def maybe_conf(conf, *validators):
return validate
def register_action(name: str, action_type: MockObjClass, schema: cv.Schema):
return ACTION_REGISTRY.register(name, action_type, schema)
_LOGGER = logging.getLogger(__name__)
def register_action(
name: str,
action_type: MockObjClass,
schema: cv.Schema,
*,
synchronous: bool | None = None,
):
"""Register an action type.
All callers must pass ``synchronous`` explicitly.
``synchronous=True`` — the action never defers ``play_next_()`` to a
later point (callback, timer, or ``loop()``). Trigger arguments are
only used during the initial call, so string args can use non-owning
StringRef for zero-copy access.
``synchronous=False`` — the action defers ``play_next_()`` via a
callback, timer, or ``Component::loop()``. Trigger arguments must
outlive the initial call, so string args use owning std::string to
prevent dangling references.
"""
if synchronous is None:
_LOGGER.warning(
"register_action('%s', ...) is missing the synchronous= parameter. "
"Defaulting to synchronous=False (safe but prevents StringRef "
"optimization). Check the C++ class: use synchronous=False if "
"play_next_() is deferred to a callback, timer, or loop(); "
"use synchronous=True if play_next_() always runs before the "
"initial play/play_complex call returns",
name,
)
synchronous = False
return ACTION_REGISTRY.register(name, action_type, schema, synchronous=synchronous)
def register_condition(name: str, condition_type: MockObjClass, schema: cv.Schema):
@@ -101,6 +137,9 @@ UpdateComponentAction = cg.esphome_ns.class_("UpdateComponentAction", Action)
SuspendComponentAction = cg.esphome_ns.class_("SuspendComponentAction", Action)
ResumeComponentAction = cg.esphome_ns.class_("ResumeComponentAction", Action)
Automation = cg.esphome_ns.class_("Automation")
TriggerForwarder = cg.esphome_ns.class_("TriggerForwarder")
TriggerOnTrueForwarder = cg.esphome_ns.class_("TriggerOnTrueForwarder")
TriggerOnFalseForwarder = cg.esphome_ns.class_("TriggerOnFalseForwarder")
LambdaCondition = cg.esphome_ns.class_("LambdaCondition", Condition)
StatelessLambdaCondition = cg.esphome_ns.class_("StatelessLambdaCondition", Condition)
@@ -211,7 +250,9 @@ async def and_condition_to_code(
args: TemplateArgsType,
) -> MockObj:
conditions = await build_condition_list(config, template_arg, args)
return cg.new_Pvariable(condition_id, template_arg, conditions)
return cg.new_Pvariable(
condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions
)
@register_condition("or", OrCondition, validate_condition_list)
@@ -222,7 +263,9 @@ async def or_condition_to_code(
args: TemplateArgsType,
) -> MockObj:
conditions = await build_condition_list(config, template_arg, args)
return cg.new_Pvariable(condition_id, template_arg, conditions)
return cg.new_Pvariable(
condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions
)
@register_condition("all", AndCondition, validate_condition_list)
@@ -233,7 +276,9 @@ async def all_condition_to_code(
args: TemplateArgsType,
) -> MockObj:
conditions = await build_condition_list(config, template_arg, args)
return cg.new_Pvariable(condition_id, template_arg, conditions)
return cg.new_Pvariable(
condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions
)
@register_condition("any", OrCondition, validate_condition_list)
@@ -244,7 +289,9 @@ async def any_condition_to_code(
args: TemplateArgsType,
) -> MockObj:
conditions = await build_condition_list(config, template_arg, args)
return cg.new_Pvariable(condition_id, template_arg, conditions)
return cg.new_Pvariable(
condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions
)
@register_condition("not", NotCondition, validate_potentially_and_condition)
@@ -266,7 +313,9 @@ async def xor_condition_to_code(
args: TemplateArgsType,
) -> MockObj:
conditions = await build_condition_list(config, template_arg, args)
return cg.new_Pvariable(condition_id, template_arg, conditions)
return cg.new_Pvariable(
condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions
)
@register_condition("lambda", LambdaCondition, cv.returning_lambda)
@@ -335,7 +384,10 @@ async def component_is_idle_condition_to_code(
@register_action(
"delay", DelayAction, cv.templatable(cv.positive_time_period_milliseconds)
"delay",
DelayAction,
cv.templatable(cv.positive_time_period_milliseconds),
synchronous=False,
)
async def delay_action_to_code(
config: ConfigType,
@@ -366,6 +418,7 @@ async def delay_action_to_code(
cv.has_at_least_one_key(CONF_THEN, CONF_ELSE),
cv.has_at_least_one_key(CONF_CONDITION, CONF_ANY, CONF_ALL),
),
synchronous=True,
)
async def if_action_to_code(
config: ConfigType,
@@ -373,13 +426,16 @@ async def if_action_to_code(
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
has_else = CONF_ELSE in config
# Prepend HasElse bool to template arguments: IfAction<HasElse, Ts...>
if_template_arg = cg.TemplateArguments(has_else, *template_arg)
cond_conf = next(el for el in config if el in (CONF_ANY, CONF_ALL, CONF_CONDITION))
condition = await build_condition(config[cond_conf], template_arg, args)
var = cg.new_Pvariable(action_id, template_arg, condition)
var = cg.new_Pvariable(action_id, if_template_arg, condition)
if CONF_THEN in config:
actions = await build_action_list(config[CONF_THEN], template_arg, args)
cg.add(var.add_then(actions))
if CONF_ELSE in config:
if has_else:
actions = await build_action_list(config[CONF_ELSE], template_arg, args)
cg.add(var.add_else(actions))
return var
@@ -394,6 +450,7 @@ async def if_action_to_code(
cv.Required(CONF_THEN): validate_action_list,
}
),
synchronous=True,
)
async def while_action_to_code(
config: ConfigType,
@@ -417,6 +474,7 @@ async def while_action_to_code(
cv.Required(CONF_THEN): validate_action_list,
}
),
synchronous=True,
)
async def repeat_action_to_code(
config: ConfigType,
@@ -445,7 +503,7 @@ _validate_wait_until = cv.maybe_simple_value(
)
@register_action("wait_until", WaitUntilAction, _validate_wait_until)
@register_action("wait_until", WaitUntilAction, _validate_wait_until, synchronous=False)
async def wait_until_action_to_code(
config: ConfigType,
action_id: ID,
@@ -461,7 +519,12 @@ async def wait_until_action_to_code(
return var
@register_action("lambda", LambdaAction, cv.lambda_)
# Lambda executes user C++ inline and returns — synchronous by execution model.
# User code could theoretically store the StringRef for deferred use, but StringRef
# is a view type and storing views beyond their scope is always unsafe regardless
# of this optimization. Marking non-synchronous would disable StringRef for nearly
# all user services since most use lambda.
@register_action("lambda", LambdaAction, cv.lambda_, synchronous=True)
async def lambda_action_to_code(
config: ConfigType,
action_id: ID,
@@ -480,6 +543,7 @@ async def lambda_action_to_code(
cv.Required(CONF_ID): cv.use_id(cg.PollingComponent),
}
),
synchronous=True,
)
async def component_update_action_to_code(
config: ConfigType,
@@ -499,6 +563,7 @@ async def component_update_action_to_code(
cv.Required(CONF_ID): cv.use_id(cg.PollingComponent),
}
),
synchronous=True,
)
async def component_suspend_action_to_code(
config: ConfigType,
@@ -521,6 +586,7 @@ async def component_suspend_action_to_code(
),
}
),
synchronous=True,
)
async def component_resume_action_to_code(
config: ConfigType,
@@ -578,6 +644,27 @@ async def build_condition_list(
return conditions
def has_non_synchronous_actions(actions: ConfigType) -> bool:
"""Check if a validated action list contains any non-synchronous actions.
Non-synchronous actions (delay, wait_until, script.wait, etc.) store
trigger args for later execution, making non-owning types like StringRef
unsafe.
"""
if isinstance(actions, list):
return any(has_non_synchronous_actions(item) for item in actions)
if isinstance(actions, dict):
for key in actions:
if key in ACTION_REGISTRY and not ACTION_REGISTRY[key].synchronous:
return True
return any(
has_non_synchronous_actions(v)
for v in actions.values()
if isinstance(v, (list, dict))
)
return False
async def build_automation(
trigger: MockObj, args: TemplateArgsType, config: ConfigType
) -> MockObj:
@@ -587,3 +674,44 @@ async def build_automation(
actions = await build_action_list(config[CONF_THEN], templ, args)
cg.add(obj.add_actions(actions))
return obj
async def build_callback_automation(
parent: MockObj,
callback_method: str,
args: TemplateArgsType,
config: ConfigType,
forwarder: MockObj | MockObjClass | None = None,
) -> None:
"""Build an Automation and register it as a callback on the parent.
Eliminates the need for a Trigger wrapper object by registering the
automation's trigger() directly as a callback on the parent component.
Uses template forwarder structs so the compiler deduplicates the operator()
body across all call sites with the same signature. The forwarder must be
pointer-sized (single Automation* field) to fit inline in Callback::ctx_
and avoid heap allocation.
:param parent: The component object (e.g., button, sensor).
:param callback_method: Name of the callback method (e.g., "add_on_press_callback").
:param args: Automation template args as list of (type, name) tuples.
:param config: The automation config dict.
:param forwarder: Optional forwarder type to use instead of the default
TriggerForwarder<Ts...>. Pass any struct type whose aggregate init takes
a single Automation pointer (e.g., TriggerOnTrueForwarder).
"""
arg_types = [arg[0] for arg in args]
templ = cg.TemplateArguments(*arg_types)
obj = cg.new_Pvariable(config[CONF_AUTOMATION_ID], templ)
actions = await build_action_list(config[CONF_THEN], templ, args)
cg.add(obj.add_actions(actions))
# Use template forwarder structs for deduplication. The compiler generates
# one operator() per forwarder type; different automation pointers are just
# data in the struct.
if forwarder is None:
forwarder = TriggerForwarder.template(templ)
# RawExpression for aggregate init — both forwarder and obj are codegen
# MockObjs (not user input), and there's no Expression type for positional
# aggregate initialization (StructInitializer uses named fields).
cg.add(getattr(parent, callback_method)(cg.RawExpression(f"{forwarder}{{{obj}}}")))

View File

@@ -53,6 +53,13 @@ def get_project_cmakelists() -> str:
variant = get_esp32_variant()
idf_target = variant.lower().replace("-", "")
# Extract compile definitions from build flags (-DXXX -> XXX)
compile_defs = [flag for flag in CORE.build_flags if flag.startswith("-D")]
extra_compile_options = "\n".join(
f'idf_build_set_property(COMPILE_OPTIONS "{compile_def}" APPEND)'
for compile_def in compile_defs
)
return f"""\
# Auto-generated by ESPHome
cmake_minimum_required(VERSION 3.16)
@@ -61,6 +68,9 @@ set(IDF_TARGET {idf_target})
set(EXTRA_COMPONENT_DIRS ${{CMAKE_SOURCE_DIR}}/src)
include($ENV{{IDF_PATH}}/tools/cmake/project.cmake)
{extra_compile_options}
project({CORE.name})
"""
@@ -70,21 +80,17 @@ def get_component_cmakelists(minimal: bool = False) -> str:
idf_requires = [] if minimal else (get_available_components() or [])
requires_str = " ".join(idf_requires)
# Extract compile definitions from build flags (-DXXX -> XXX)
compile_defs = [flag[2:] for flag in CORE.build_flags if flag.startswith("-D")]
compile_defs_str = "\n ".join(compile_defs) if compile_defs else ""
# Extract compile options (-W flags, excluding linker flags)
compile_opts = [
flag
for flag in CORE.build_flags
if flag.startswith("-W") and not flag.startswith("-Wl,")
]
compile_opts_str = "\n ".join(compile_opts) if compile_opts else ""
compile_opts_str = "\n ".join(sorted(compile_opts)) if compile_opts else ""
# Extract linker options (-Wl, flags)
link_opts = [flag for flag in CORE.build_flags if flag.startswith("-Wl,")]
link_opts_str = "\n ".join(link_opts) if link_opts else ""
link_opts_str = "\n ".join(sorted(link_opts)) if link_opts else ""
return f"""\
# Auto-generated by ESPHome
@@ -104,11 +110,6 @@ idf_component_register(
# Apply C++ standard
target_compile_features(${{COMPONENT_LIB}} PUBLIC cxx_std_20)
# ESPHome compile definitions
target_compile_definitions(${{COMPONENT_LIB}} PUBLIC
{compile_defs_str}
)
# ESPHome compile options
target_compile_options(${{COMPONENT_LIB}} PUBLIC
{compile_opts_str}

View File

@@ -11,6 +11,7 @@
from esphome.cpp_generator import ( # noqa: F401
ArrayInitializer,
Expression,
FlashStringLiteral,
LineComment,
LogStringLiteral,
MockObj,

View File

@@ -1,22 +1,29 @@
#include "esphome/core/log.h"
#include "absolute_humidity.h"
namespace esphome {
namespace absolute_humidity {
namespace esphome::absolute_humidity {
static const char *const TAG = "absolute_humidity.sensor";
static const char *const TAG{"absolute_humidity.sensor"};
void AbsoluteHumidityComponent::setup() {
this->temperature_sensor_->add_on_state_callback([this](float state) {
this->temperature_ = state;
this->enable_loop();
});
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); });
// Get initial value
if (this->temperature_sensor_->has_state()) {
this->temperature_callback_(this->temperature_sensor_->get_state());
this->temperature_ = this->temperature_sensor_->get_state();
}
this->humidity_sensor_->add_on_state_callback([this](float state) {
this->humidity_ = state;
this->enable_loop();
});
ESP_LOGD(TAG, " Added callback for relative humidity '%s'", this->humidity_sensor_->get_name().c_str());
this->humidity_sensor_->add_on_state_callback([this](float state) { this->humidity_callback_(state); });
// Get initial value
if (this->humidity_sensor_->has_state()) {
this->humidity_callback_(this->humidity_sensor_->get_state());
this->humidity_ = this->humidity_sensor_->get_state();
}
}
@@ -46,14 +53,12 @@ void AbsoluteHumidityComponent::dump_config() {
}
void AbsoluteHumidityComponent::loop() {
if (!this->next_update_) {
return;
}
this->next_update_ = false;
// Only run once
this->disable_loop();
// Ensure we have source data
const bool no_temperature = std::isnan(this->temperature_);
const bool no_humidity = std::isnan(this->humidity_);
const bool no_temperature{std::isnan(this->temperature_)};
const bool no_humidity{std::isnan(this->humidity_)};
if (no_temperature || no_humidity) {
if (no_temperature) {
ESP_LOGW(TAG, "No valid state from temperature sensor!");
@@ -67,9 +72,9 @@ void AbsoluteHumidityComponent::loop() {
}
// Convert to desired units
const float temperature_c = this->temperature_;
const float temperature_k = temperature_c + 273.15;
const float hr = this->humidity_ / 100;
const float temperature_c{this->temperature_};
const float temperature_k{temperature_c + 273.15f};
const float hr{this->humidity_ / 100.0f};
// Calculate saturation vapor pressure
float es;
@@ -90,12 +95,9 @@ void AbsoluteHumidityComponent::loop() {
}
// Calculate absolute humidity
const float absolute_humidity = vapor_density(es, hr, temperature_k);
const float absolute_humidity{vapor_density(es, hr, temperature_k)};
ESP_LOGD(TAG,
"Saturation vapor pressure %f kPa\n"
"Publishing absolute humidity %f g/m³",
es, absolute_humidity);
ESP_LOGD(TAG, "Saturation vapor pressure %f kPa, absolute humidity %f g/m³", es, absolute_humidity);
// Publish absolute humidity
this->status_clear_warning();
@@ -106,16 +108,16 @@ void AbsoluteHumidityComponent::loop() {
// More accurate than Tetens in normal meteorologic conditions
float AbsoluteHumidityComponent::es_buck(float temperature_c) {
float a, b, c, d;
if (temperature_c >= 0) {
a = 0.61121;
b = 18.678;
c = 234.5;
d = 257.14;
if (temperature_c >= 0.0f) {
a = 0.61121f;
b = 18.678f;
c = 234.5f;
d = 257.14f;
} else {
a = 0.61115;
b = 18.678;
c = 233.7;
d = 279.82;
a = 0.61115f;
b = 18.678f;
c = 233.7f;
d = 279.82f;
}
return a * expf((b - (temperature_c / c)) * (temperature_c / (d + temperature_c)));
}
@@ -123,14 +125,14 @@ float AbsoluteHumidityComponent::es_buck(float temperature_c) {
// Tetens equation (https://en.wikipedia.org/wiki/Tetens_equation)
float AbsoluteHumidityComponent::es_tetens(float temperature_c) {
float a, b;
if (temperature_c >= 0) {
a = 17.27;
b = 237.3;
if (temperature_c >= 0.0f) {
a = 17.27f;
b = 237.3f;
} else {
a = 21.875;
b = 265.5;
a = 21.875f;
b = 265.5f;
}
return 0.61078 * expf((a * temperature_c) / (temperature_c + b));
return 0.61078f * expf((a * temperature_c) / (temperature_c + b));
}
// Wobus equation
@@ -149,18 +151,18 @@ float AbsoluteHumidityComponent::es_wobus(float t) {
//
// Baker, Schlatter 17-MAY-1982 Original version.
const float c0 = +0.99999683e00;
const float c1 = -0.90826951e-02;
const float c2 = +0.78736169e-04;
const float c3 = -0.61117958e-06;
const float c4 = +0.43884187e-08;
const float c5 = -0.29883885e-10;
const float c6 = +0.21874425e-12;
const float c7 = -0.17892321e-14;
const float c8 = +0.11112018e-16;
const float c9 = -0.30994571e-19;
const float p = c0 + t * (c1 + t * (c2 + t * (c3 + t * (c4 + t * (c5 + t * (c6 + t * (c7 + t * (c8 + t * (c9)))))))));
return 0.61078 / pow(p, 8);
constexpr float c0{+0.99999683e+00f};
constexpr float c1{-0.90826951e-02f};
constexpr float c2{+0.78736169e-04f};
constexpr float c3{-0.61117958e-06f};
constexpr float c4{+0.43884187e-08f};
constexpr float c5{-0.29883885e-10f};
constexpr float c6{+0.21874425e-12f};
constexpr float c7{-0.17892321e-14f};
constexpr float c8{+0.11112018e-16f};
constexpr float c9{-0.30994571e-19f};
const float p{c0 + t * (c1 + t * (c2 + t * (c3 + t * (c4 + t * (c5 + t * (c6 + t * (c7 + t * (c8 + t * (c9)))))))))};
return 0.61078f / powf(p, 8.0f);
}
// From https://www.environmentalbiophysics.org/chalk-talk-how-to-calculate-absolute-humidity/
@@ -171,11 +173,10 @@ float AbsoluteHumidityComponent::vapor_density(float es, float hr, float ta) {
// hr = relative humidity [0-1]
// ta = absolute temperature (K)
const float ea = hr * es * 1000; // vapor pressure of the air (Pa)
const float mw = 18.01528; // molar mass of water (g⋅mol⁻¹)
const float r = 8.31446261815324; // molar gas constant (J⋅K⁻¹)
const float ea{hr * es * 1000.0f}; // vapor pressure of the air (Pa)
const float mw{18.01528f}; // molar mass of water (g⋅mol⁻¹)
const float r{8.31446261815324f}; // molar gas constant (J⋅K⁻¹)
return (ea * mw) / (r * ta);
}
} // namespace absolute_humidity
} // namespace esphome
} // namespace esphome::absolute_humidity

View File

@@ -3,8 +3,7 @@
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
namespace esphome {
namespace absolute_humidity {
namespace esphome::absolute_humidity {
/// Enum listing all implemented saturation vapor pressure equations.
enum SaturationVaporPressureEquation {
@@ -16,8 +15,6 @@ enum SaturationVaporPressureEquation {
/// This class implements calculation of absolute humidity from temperature and relative humidity.
class AbsoluteHumidityComponent : public sensor::Sensor, public Component {
public:
AbsoluteHumidityComponent() = default;
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; }
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
void set_equation(SaturationVaporPressureEquation equation) { this->equation_ = equation; }
@@ -27,15 +24,6 @@ class AbsoluteHumidityComponent : public sensor::Sensor, public Component {
void loop() override;
protected:
void temperature_callback_(float state) {
this->next_update_ = true;
this->temperature_ = state;
}
void humidity_callback_(float state) {
this->next_update_ = true;
this->humidity_ = state;
}
/** Buck equation for saturation vapor pressure in kPa.
*
* @param temperature_c Air temperature in °C.
@@ -57,19 +45,15 @@ class AbsoluteHumidityComponent : public sensor::Sensor, public Component {
* @param es Saturation vapor pressure in kPa.
* @param hr Relative humidity 0 to 1.
* @param ta Absolute temperature in K.
* @param heater_duration The duration in ms that the heater should turn on for when measuring.
*/
static float vapor_density(float es, float hr, float ta);
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *humidity_sensor_{nullptr};
bool next_update_{false};
float temperature_{NAN};
float humidity_{NAN};
SaturationVaporPressureEquation equation_;
};
} // namespace absolute_humidity
} // namespace esphome
} // namespace esphome::absolute_humidity

View File

@@ -199,12 +199,19 @@ void AcDimmer::setup() {
setTimer1Callback(&timer_interrupt);
#endif
#ifdef USE_ESP32
dimmer_timer = timer_begin(TIMER_FREQUENCY_HZ);
timer_attach_interrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr);
// For ESP32, we can't use dynamic interval calculation because the timerX functions
// are not callable from ISR (placed in flash storage).
// Here we just use an interrupt firing every 50 µs.
timer_alarm(dimmer_timer, TIMER_INTERVAL_US, true, 0);
if (dimmer_timer == nullptr) {
dimmer_timer = timer_begin(TIMER_FREQUENCY_HZ);
if (dimmer_timer == nullptr) {
ESP_LOGE(TAG, "Failed to create GPTimer for AC dimmer");
this->mark_failed();
return;
}
timer_attach_interrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr);
// For ESP32, we can't use dynamic interval calculation because the timerX functions
// are not callable from ISR (placed in flash storage).
// Here we just use an interrupt firing every 50 µs.
timer_alarm(dimmer_timer, TIMER_INTERVAL_US, true, 0);
}
#endif
}

View File

@@ -22,7 +22,8 @@ namespace adc {
#ifdef USE_ESP32
// clang-format off
#if (ESP_IDF_VERSION_MAJOR == 5 && \
#if ESP_IDF_VERSION_MAJOR >= 6 || \
(ESP_IDF_VERSION_MAJOR == 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 >= 2)) \

View File

@@ -2,6 +2,7 @@
#include "adc_sensor.h"
#include "esphome/core/log.h"
#include <cinttypes>
namespace esphome {
namespace adc {
@@ -346,7 +347,8 @@ float ADCSensor::sample_autorange_() {
ESP_LOGVV(TAG, "Autorange summary:");
ESP_LOGVV(TAG, " Raw readings: 12db=%d, 6db=%d, 2.5db=%d, 0db=%d", raw12, raw6, raw2, raw0);
ESP_LOGVV(TAG, " Voltages: 12db=%.6f, 6db=%.6f, 2.5db=%.6f, 0db=%.6f", mv12, mv6, mv2, mv0);
ESP_LOGVV(TAG, " Coefficients: c12=%u, c6=%u, c2=%u, c0=%u, sum=%u", c12, c6, c2, c0, csum);
ESP_LOGVV(TAG, " Coefficients: c12=%" PRIu32 ", c6=%" PRIu32 ", c2=%" PRIu32 ", c0=%" PRIu32 ", sum=%" PRIu32, c12,
c6, c2, c0, csum);
if (csum == 0) {
ESP_LOGE(TAG, "Invalid weight sum in autorange calculation");
@@ -354,8 +356,10 @@ float ADCSensor::sample_autorange_() {
}
const float final_result = (mv12 * c12 + mv6 * c6 + mv2 * c2 + mv0 * c0) / csum;
ESP_LOGV(TAG, "Autorange final: (%.6f*%u + %.6f*%u + %.6f*%u + %.6f*%u)/%u = %.6fV", mv12, c12, mv6, c6, mv2, c2, mv0,
c0, csum, final_result);
ESP_LOGV(TAG,
"Autorange final: (%.6f*%" PRIu32 " + %.6f*%" PRIu32 " + %.6f*%" PRIu32 " + %.6f*%" PRIu32 ")/%" PRIu32
" = %.6fV",
mv12, c12, mv6, c6, mv2, c2, mv0, c0, csum, final_result);
return final_result;
}

View File

@@ -8,6 +8,13 @@
#endif // CYW43_USES_VSYS_PIN
#include <hardware/adc.h>
// PICO_VSYS_PIN is defined in pico-sdk board headers (e.g. boards/pico2.h),
// but the Arduino framework's config_autogen.h includes a generic board header
// that doesn't define it. Provide the standard value (pin 29) as a fallback.
#ifndef PICO_VSYS_PIN
#define PICO_VSYS_PIN 29 // NOLINT(cppcoreguidelines-macro-usage)
#endif
namespace esphome {
namespace adc {

View File

@@ -58,7 +58,10 @@ void HOT AddressableLightDisplay::draw_absolute_pixel_internal(int x, int y, Col
if (this->pixel_mapper_f_.has_value()) {
// Params are passed by reference, so they may be modified in call.
this->addressable_light_buffer_[(*this->pixel_mapper_f_)(x, y)] = color;
int index = (*this->pixel_mapper_f_)(x, y);
if (index < 0 || static_cast<size_t>(index) >= this->addressable_light_buffer_.size())
return;
this->addressable_light_buffer_[index] = color;
} else {
this->addressable_light_buffer_[y * this->get_width_internal() + x] = color;
}

View File

@@ -33,7 +33,7 @@ class AddressableLightDisplay : public display::DisplayBuffer {
// - Save the current effect index.
this->last_effect_index_ = light_state_->get_current_effect_index();
// - Disable any current effect.
light_state_->make_call().set_effect(0).perform();
light_state_->make_call().set_effect(uint32_t{0}).perform();
}
}
enabled_ = enabled;

View File

@@ -121,7 +121,7 @@ void ADE7880::update() {
this->update_sensor_from_s32_register16_(chan->forward_active_energy, AFWATTHR, [&chan](float val) {
return chan->forward_active_energy_total += val / 14400.0f;
});
this->update_sensor_from_s32_register16_(chan->reverse_active_energy, AFWATTHR, [&chan](float val) {
this->update_sensor_from_s32_register16_(chan->reverse_active_energy, ARWATTHR, [&chan](float val) {
return chan->reverse_active_energy_total += val / 14400.0f;
});
}
@@ -137,7 +137,7 @@ void ADE7880::update() {
this->update_sensor_from_s32_register16_(chan->forward_active_energy, BFWATTHR, [&chan](float val) {
return chan->forward_active_energy_total += val / 14400.0f;
});
this->update_sensor_from_s32_register16_(chan->reverse_active_energy, BFWATTHR, [&chan](float val) {
this->update_sensor_from_s32_register16_(chan->reverse_active_energy, BRWATTHR, [&chan](float val) {
return chan->reverse_active_energy_total += val / 14400.0f;
});
}
@@ -153,7 +153,7 @@ void ADE7880::update() {
this->update_sensor_from_s32_register16_(chan->forward_active_energy, CFWATTHR, [&chan](float val) {
return chan->forward_active_energy_total += val / 14400.0f;
});
this->update_sensor_from_s32_register16_(chan->reverse_active_energy, CFWATTHR, [&chan](float val) {
this->update_sensor_from_s32_register16_(chan->reverse_active_energy, CRWATTHR, [&chan](float val) {
return chan->reverse_active_energy_total += val / 14400.0f;
});
}

View File

@@ -85,6 +85,9 @@ constexpr uint16_t CWATTHR = 0xE402;
constexpr uint16_t AFWATTHR = 0xE403;
constexpr uint16_t BFWATTHR = 0xE404;
constexpr uint16_t CFWATTHR = 0xE405;
constexpr uint16_t ARWATTHR = 0xE406;
constexpr uint16_t BRWATTHR = 0xE407;
constexpr uint16_t CRWATTHR = 0xE408;
constexpr uint16_t AFVARHR = 0xE409;
constexpr uint16_t BFVARHR = 0xE40A;
constexpr uint16_t CFVARHR = 0xE40B;

View File

@@ -173,19 +173,8 @@ float ADS1115Component::request_measurement(ADS1115Multiplexer multiplexer, ADS1
}
if (resolution == ADS1015_12_BITS) {
bool negative = (raw_conversion >> 15) == 1;
// shift raw_conversion as it's only 12-bits, left justified
raw_conversion = raw_conversion >> (16 - ADS1015_12_BITS);
// check if number was negative in order to keep the sign
if (negative) {
// the number was negative
// 1) set the negative bit back
raw_conversion |= 0x8000;
// 2) reset the former (shifted) negative bit
raw_conversion &= 0xF7FF;
}
// ADS1015 returns 12-bit value left-justified in 16 bits; shift right and sign-extend
raw_conversion = static_cast<uint16_t>(static_cast<int16_t>(raw_conversion) >> (16 - ADS1015_12_BITS));
}
auto signed_conversion = static_cast<int16_t>(raw_conversion);

View File

@@ -92,6 +92,7 @@ AGS10_NEW_I2C_ADDRESS_SCHEMA = cv.maybe_simple_value(
"ags10.new_i2c_address",
AGS10NewI2cAddressAction,
AGS10_NEW_I2C_ADDRESS_SCHEMA,
synchronous=True,
)
async def ags10newi2caddress_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -121,6 +122,7 @@ AGS10_SET_ZERO_POINT_SCHEMA = cv.Schema(
"ags10.set_zero_point",
AGS10SetZeroPointAction,
AGS10_SET_ZERO_POINT_SCHEMA,
synchronous=True,
)
async def ags10setzeropoint_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)

View File

@@ -34,7 +34,10 @@ SET_AUTO_MUTE_ACTION_SCHEMA = cv.maybe_simple_value(
@automation.register_action(
"aic3204.set_auto_mute_mode", SetAutoMuteAction, SET_AUTO_MUTE_ACTION_SCHEMA
"aic3204.set_auto_mute_mode",
SetAutoMuteAction,
SET_AUTO_MUTE_ACTION_SCHEMA,
synchronous=True,
)
async def aic3204_set_volume_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])

View File

@@ -10,7 +10,6 @@ from esphome.const import (
CONF_ID,
CONF_MQTT_ID,
CONF_ON_STATE,
CONF_TRIGGER_ID,
CONF_WEB_SERVER,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
@@ -34,39 +33,9 @@ CONF_ON_READY = "on_ready"
alarm_control_panel_ns = cg.esphome_ns.namespace("alarm_control_panel")
AlarmControlPanel = alarm_control_panel_ns.class_("AlarmControlPanel", cg.EntityBase)
StateTrigger = alarm_control_panel_ns.class_(
"StateTrigger", automation.Trigger.template()
)
TriggeredTrigger = alarm_control_panel_ns.class_(
"TriggeredTrigger", automation.Trigger.template()
)
ClearedTrigger = alarm_control_panel_ns.class_(
"ClearedTrigger", automation.Trigger.template()
)
ArmingTrigger = alarm_control_panel_ns.class_(
"ArmingTrigger", automation.Trigger.template()
)
PendingTrigger = alarm_control_panel_ns.class_(
"PendingTrigger", automation.Trigger.template()
)
ArmedHomeTrigger = alarm_control_panel_ns.class_(
"ArmedHomeTrigger", automation.Trigger.template()
)
ArmedNightTrigger = alarm_control_panel_ns.class_(
"ArmedNightTrigger", automation.Trigger.template()
)
ArmedAwayTrigger = alarm_control_panel_ns.class_(
"ArmedAwayTrigger", automation.Trigger.template()
)
DisarmedTrigger = alarm_control_panel_ns.class_(
"DisarmedTrigger", automation.Trigger.template()
)
ChimeTrigger = alarm_control_panel_ns.class_(
"ChimeTrigger", automation.Trigger.template()
)
ReadyTrigger = alarm_control_panel_ns.class_(
"ReadyTrigger", automation.Trigger.template()
)
StateAnyForwarder = alarm_control_panel_ns.class_("StateAnyForwarder")
StateEnterForwarder = alarm_control_panel_ns.class_("StateEnterForwarder")
AlarmControlPanelState = alarm_control_panel_ns.enum("AlarmControlPanelState")
ArmAwayAction = alarm_control_panel_ns.class_("ArmAwayAction", automation.Action)
ArmHomeAction = alarm_control_panel_ns.class_("ArmHomeAction", automation.Action)
@@ -89,61 +58,17 @@ _ALARM_CONTROL_PANEL_SCHEMA = (
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
mqtt.MQTTAlarmControlPanelComponent
),
cv.Optional(CONF_ON_STATE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger),
}
),
cv.Optional(CONF_ON_TRIGGERED): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TriggeredTrigger),
}
),
cv.Optional(CONF_ON_ARMING): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmingTrigger),
}
),
cv.Optional(CONF_ON_PENDING): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PendingTrigger),
}
),
cv.Optional(CONF_ON_ARMED_HOME): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedHomeTrigger),
}
),
cv.Optional(CONF_ON_ARMED_NIGHT): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedNightTrigger),
}
),
cv.Optional(CONF_ON_ARMED_AWAY): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedAwayTrigger),
}
),
cv.Optional(CONF_ON_DISARMED): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DisarmedTrigger),
}
),
cv.Optional(CONF_ON_CLEARED): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClearedTrigger),
}
),
cv.Optional(CONF_ON_CHIME): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ChimeTrigger),
}
),
cv.Optional(CONF_ON_READY): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ReadyTrigger),
}
),
cv.Optional(CONF_ON_STATE): automation.validate_automation({}),
cv.Optional(CONF_ON_TRIGGERED): automation.validate_automation({}),
cv.Optional(CONF_ON_ARMING): automation.validate_automation({}),
cv.Optional(CONF_ON_PENDING): automation.validate_automation({}),
cv.Optional(CONF_ON_ARMED_HOME): automation.validate_automation({}),
cv.Optional(CONF_ON_ARMED_NIGHT): automation.validate_automation({}),
cv.Optional(CONF_ON_ARMED_AWAY): automation.validate_automation({}),
cv.Optional(CONF_ON_DISARMED): automation.validate_automation({}),
cv.Optional(CONF_ON_CLEARED): automation.validate_automation({}),
cv.Optional(CONF_ON_CHIME): automation.validate_automation({}),
cv.Optional(CONF_ON_READY): automation.validate_automation({}),
}
)
)
@@ -186,41 +111,42 @@ ALARM_CONTROL_PANEL_CONDITION_SCHEMA = maybe_simple_id(
)
@setup_entity("alarm_control_panel")
async def setup_alarm_control_panel_core_(var, config):
await setup_entity(var, config, "alarm_control_panel")
for conf in config.get(CONF_ON_STATE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_TRIGGERED, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_ARMING, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_PENDING, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_ARMED_HOME, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_ARMED_NIGHT, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_ARMED_AWAY, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_DISARMED, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
await automation.build_callback_automation(
var, "add_on_state_callback", [], conf, forwarder=StateAnyForwarder
)
_STATE_ENTER_MAP = {
CONF_ON_TRIGGERED: AlarmControlPanelState.ACP_STATE_TRIGGERED,
CONF_ON_ARMING: AlarmControlPanelState.ACP_STATE_ARMING,
CONF_ON_PENDING: AlarmControlPanelState.ACP_STATE_PENDING,
CONF_ON_ARMED_HOME: AlarmControlPanelState.ACP_STATE_ARMED_HOME,
CONF_ON_ARMED_NIGHT: AlarmControlPanelState.ACP_STATE_ARMED_NIGHT,
CONF_ON_ARMED_AWAY: AlarmControlPanelState.ACP_STATE_ARMED_AWAY,
CONF_ON_DISARMED: AlarmControlPanelState.ACP_STATE_DISARMED,
}
for conf_key, state_enum in _STATE_ENTER_MAP.items():
for conf in config.get(conf_key, []):
await automation.build_callback_automation(
var,
"add_on_state_callback",
[],
conf,
forwarder=StateEnterForwarder.template(state_enum),
)
for conf in config.get(CONF_ON_CLEARED, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
await automation.build_callback_automation(
var, "add_on_cleared_callback", [], conf
)
for conf in config.get(CONF_ON_CHIME, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
await automation.build_callback_automation(
var, "add_on_chime_callback", [], conf
)
for conf in config.get(CONF_ON_READY, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
await automation.build_callback_automation(
var, "add_on_ready_callback", [], conf
)
if web_server_config := config.get(CONF_WEB_SERVER):
await web_server.add_entity_config(var, web_server_config)
if mqtt_id := config.get(CONF_MQTT_ID):
@@ -243,7 +169,10 @@ async def new_alarm_control_panel(config, *args):
@automation.register_action(
"alarm_control_panel.arm_away", ArmAwayAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
"alarm_control_panel.arm_away",
ArmAwayAction,
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
synchronous=True,
)
async def alarm_action_arm_away_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -255,7 +184,10 @@ async def alarm_action_arm_away_to_code(config, action_id, template_arg, args):
@automation.register_action(
"alarm_control_panel.arm_home", ArmHomeAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
"alarm_control_panel.arm_home",
ArmHomeAction,
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
synchronous=True,
)
async def alarm_action_arm_home_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -267,7 +199,10 @@ async def alarm_action_arm_home_to_code(config, action_id, template_arg, args):
@automation.register_action(
"alarm_control_panel.arm_night", ArmNightAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
"alarm_control_panel.arm_night",
ArmNightAction,
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
synchronous=True,
)
async def alarm_action_arm_night_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -279,7 +214,10 @@ async def alarm_action_arm_night_to_code(config, action_id, template_arg, args):
@automation.register_action(
"alarm_control_panel.disarm", DisarmAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
"alarm_control_panel.disarm",
DisarmAction,
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
synchronous=True,
)
async def alarm_action_disarm_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -291,7 +229,10 @@ async def alarm_action_disarm_to_code(config, action_id, template_arg, args):
@automation.register_action(
"alarm_control_panel.pending", PendingAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
"alarm_control_panel.pending",
PendingAction,
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
synchronous=True,
)
async def alarm_action_pending_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -299,7 +240,10 @@ async def alarm_action_pending_to_code(config, action_id, template_arg, args):
@automation.register_action(
"alarm_control_panel.triggered", TriggeredAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
"alarm_control_panel.triggered",
TriggeredAction,
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
synchronous=True,
)
async def alarm_action_trigger_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -307,7 +251,10 @@ async def alarm_action_trigger_to_code(config, action_id, template_arg, args):
@automation.register_action(
"alarm_control_panel.chime", ChimeAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
"alarm_control_panel.chime",
ChimeAction,
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
synchronous=True,
)
async def alarm_action_chime_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
@@ -315,7 +262,10 @@ async def alarm_action_chime_to_code(config, action_id, template_arg, args):
@automation.register_action(
"alarm_control_panel.ready", ReadyAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
"alarm_control_panel.ready",
ReadyAction,
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
synchronous=True,
)
@automation.register_condition(
"alarm_control_panel.ready",

View File

@@ -31,12 +31,12 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) {
this->last_update_ = millis();
if (state != this->current_state_) {
auto prev_state = this->current_state_;
ESP_LOGD(TAG, "'%s' >> %s (was %s)", this->get_name().c_str(),
ESP_LOGV(TAG, "'%s' >> %s (was %s)", this->get_name().c_str(),
LOG_STR_ARG(alarm_control_panel_state_to_string(state)),
LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state)));
this->current_state_ = state;
// Single state callback - triggers check get_state() for specific states
this->state_callback_.call();
// Single state callback - listeners receive the new state as an argument
this->state_callback_.call(state);
#if defined(USE_ALARM_CONTROL_PANEL) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_alarm_control_panel_update(this);
#endif
@@ -51,22 +51,6 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) {
}
}
void AlarmControlPanel::add_on_state_callback(std::function<void()> &&callback) {
this->state_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_cleared_callback(std::function<void()> &&callback) {
this->cleared_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_chime_callback(std::function<void()> &&callback) {
this->chime_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_ready_callback(std::function<void()> &&callback) {
this->ready_callback_.add(std::move(callback));
}
void AlarmControlPanel::arm_with_code_(AlarmControlPanelCall &(AlarmControlPanelCall::*arm_method)(),
const char *code) {
auto call = this->make_call();

View File

@@ -37,25 +37,24 @@ class AlarmControlPanel : public EntityBase {
*
* @param callback The callback function
*/
void add_on_state_callback(std::function<void()> &&callback);
template<typename F> void add_on_state_callback(F &&callback) {
this->state_callback_.add(std::forward<F>(callback));
}
/** Add a callback for when the state of the alarm_control_panel clears from triggered
*
* @param callback The callback function
*/
void add_on_cleared_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel clears from triggered. */
template<typename F> void add_on_cleared_callback(F &&callback) {
this->cleared_callback_.add(std::forward<F>(callback));
}
/** Add a callback for when a chime zone goes from closed to open
*
* @param callback The callback function
*/
void add_on_chime_callback(std::function<void()> &&callback);
/** Add a callback for when a chime zone goes from closed to open. */
template<typename F> void add_on_chime_callback(F &&callback) {
this->chime_callback_.add(std::forward<F>(callback));
}
/** Add a callback for when a ready state changes
*
* @param callback The callback function
*/
void add_on_ready_callback(std::function<void()> &&callback);
/** Add a callback for when a ready state changes. */
template<typename F> void add_on_ready_callback(F &&callback) {
this->ready_callback_.add(std::forward<F>(callback));
}
/** A numeric representation of the supported features as per HomeAssistant
*
@@ -146,8 +145,8 @@ class AlarmControlPanel : public EntityBase {
uint32_t last_update_;
// the call control function
virtual void control(const AlarmControlPanelCall &call) = 0;
// state callback - triggers check get_state() for specific state
LazyCallbackManager<void()> state_callback_{};
// state callback - passes the new state to listeners
LazyCallbackManager<void(AlarmControlPanelState)> state_callback_{};
// clear callback - fires when leaving TRIGGERED state
LazyCallbackManager<void()> cleared_callback_{};
// chime callback

View File

@@ -12,7 +12,14 @@ AlarmControlPanelCall::AlarmControlPanelCall(AlarmControlPanel *parent) : parent
AlarmControlPanelCall &AlarmControlPanelCall::set_code(const char *code) {
if (code != nullptr) {
this->code_ = std::string(code);
return this->set_code(code, strlen(code));
}
return *this;
}
AlarmControlPanelCall &AlarmControlPanelCall::set_code(const char *code, size_t len) {
if (code != nullptr) {
this->code_ = std::string(code, len);
}
return *this;
}

View File

@@ -15,7 +15,8 @@ class AlarmControlPanelCall {
AlarmControlPanelCall(AlarmControlPanel *parent);
AlarmControlPanelCall &set_code(const char *code);
AlarmControlPanelCall &set_code(const std::string &code) { return this->set_code(code.c_str()); }
AlarmControlPanelCall &set_code(const char *code, size_t len);
AlarmControlPanelCall &set_code(const std::string &code) { return this->set_code(code.c_str(), code.size()); }
AlarmControlPanelCall &arm_away();
AlarmControlPanelCall &arm_home();
AlarmControlPanelCall &arm_night();

View File

@@ -5,60 +5,27 @@
namespace esphome::alarm_control_panel {
/// Trigger on any state change
class StateTrigger : public Trigger<> {
public:
explicit StateTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_state_callback([this]() { this->trigger(); });
/// Callback forwarder that triggers an Automation<> on any state change.
/// Pointer-sized (single Automation* field) to fit inline in Callback::ctx_.
struct StateAnyForwarder {
Automation<> *automation;
void operator()(AlarmControlPanelState /*state*/) const { this->automation->trigger(); }
};
/// Callback forwarder that triggers an Automation<> only when the alarm enters a specific state.
/// Pointer-sized (single Automation* field) to fit inline in Callback::ctx_.
template<AlarmControlPanelState State> struct StateEnterForwarder {
Automation<> *automation;
void operator()(AlarmControlPanelState state) const {
if (state == State)
this->automation->trigger();
}
};
/// Template trigger that fires when entering a specific state
template<AlarmControlPanelState State> class StateEnterTrigger : public Trigger<> {
public:
explicit StateEnterTrigger(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {
alarm_control_panel->add_on_state_callback([this]() {
if (this->alarm_control_panel_->get_state() == State)
this->trigger();
});
}
protected:
AlarmControlPanel *alarm_control_panel_;
};
// Type aliases for state-specific triggers
using TriggeredTrigger = StateEnterTrigger<ACP_STATE_TRIGGERED>;
using ArmingTrigger = StateEnterTrigger<ACP_STATE_ARMING>;
using PendingTrigger = StateEnterTrigger<ACP_STATE_PENDING>;
using ArmedHomeTrigger = StateEnterTrigger<ACP_STATE_ARMED_HOME>;
using ArmedNightTrigger = StateEnterTrigger<ACP_STATE_ARMED_NIGHT>;
using ArmedAwayTrigger = StateEnterTrigger<ACP_STATE_ARMED_AWAY>;
using DisarmedTrigger = StateEnterTrigger<ACP_STATE_DISARMED>;
/// Trigger when leaving TRIGGERED state (alarm cleared)
class ClearedTrigger : public Trigger<> {
public:
explicit ClearedTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_cleared_callback([this]() { this->trigger(); });
}
};
/// Trigger on chime event (zone opened while disarmed)
class ChimeTrigger : public Trigger<> {
public:
explicit ChimeTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_chime_callback([this]() { this->trigger(); });
}
};
/// Trigger on ready state change
class ReadyTrigger : public Trigger<> {
public:
explicit ReadyTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_ready_callback([this]() { this->trigger(); });
}
};
static_assert(sizeof(StateAnyForwarder) <= sizeof(void *));
static_assert(std::is_trivially_copyable_v<StateAnyForwarder>);
static_assert(sizeof(StateEnterForwarder<ACP_STATE_TRIGGERED>) <= sizeof(void *));
static_assert(std::is_trivially_copyable_v<StateEnterForwarder<ACP_STATE_TRIGGERED>>);
template<typename... Ts> class ArmAwayAction : public Action<Ts...> {
public:

View File

@@ -125,7 +125,7 @@ void Alpha3::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc
this->current_sensor_->publish_state(NAN);
if (this->speed_sensor_ != nullptr)
this->speed_sensor_->publish_state(NAN);
if (this->speed_sensor_ != nullptr)
if (this->voltage_sensor_ != nullptr)
this->voltage_sensor_->publish_state(NAN);
break;
}

View File

@@ -63,8 +63,9 @@ void Am43Component::control(const CoverCall &call) {
ESP_LOGW(TAG, "[%s] Error writing stop command to device, error = %d", this->get_name().c_str(), status);
}
}
if (call.get_position().has_value()) {
auto pos = *call.get_position();
auto opt_pos = call.get_position();
if (opt_pos.has_value()) {
auto pos = *opt_pos;
if (this->invert_position_)
pos = 1 - pos;

View File

@@ -35,7 +35,7 @@ class Am43 : public esphome::ble_client::BLEClientNode, public PollingComponent
uint8_t current_sensor_;
// The AM43 often gets into a state where it spams loads of battery update
// notifications. Here we will limit to no more than every 10s.
uint8_t last_battery_update_;
uint32_t last_battery_update_;
};
} // namespace am43

View File

@@ -1,5 +1,6 @@
#pragma once
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/components/sensor/sensor.h"

View File

@@ -69,9 +69,15 @@ SET_FRAME_SCHEMA = cv.Schema(
)
@automation.register_action("animation.next_frame", NextFrameAction, NEXT_FRAME_SCHEMA)
@automation.register_action("animation.prev_frame", PrevFrameAction, PREV_FRAME_SCHEMA)
@automation.register_action("animation.set_frame", SetFrameAction, SET_FRAME_SCHEMA)
@automation.register_action(
"animation.next_frame", NextFrameAction, NEXT_FRAME_SCHEMA, synchronous=True
)
@automation.register_action(
"animation.prev_frame", PrevFrameAction, PREV_FRAME_SCHEMA, synchronous=True
)
@automation.register_action(
"animation.set_frame", SetFrameAction, SET_FRAME_SCHEMA, synchronous=True
)
async def animation_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)

View File

@@ -24,8 +24,9 @@ void Anova::loop() {
}
void Anova::control(const ClimateCall &call) {
if (call.get_mode().has_value()) {
ClimateMode mode = *call.get_mode();
auto mode_val = call.get_mode();
if (mode_val.has_value()) {
ClimateMode mode = *mode_val;
AnovaPacket *pkt;
switch (mode) {
case climate::CLIMATE_MODE_OFF:
@@ -45,8 +46,9 @@ void Anova::control(const ClimateCall &call) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
}
}
if (call.get_target_temperature().has_value()) {
auto *pkt = this->codec_->get_set_target_temp_request(*call.get_target_temperature());
auto target_temp = call.get_target_temperature();
if (target_temp.has_value()) {
auto *pkt = this->codec_->get_set_target_temp_request(*target_temp);
auto status =
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_,
pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
@@ -67,10 +69,8 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_
case ESP_GATTC_SEARCH_CMPL_EVT: {
auto *chr = this->parent_->get_characteristic(ANOVA_SERVICE_UUID, ANOVA_CHARACTERISTIC_UUID);
if (chr == nullptr) {
ESP_LOGW(TAG,
"[%s] No control service found at device, not an Anova..?\n"
"[%s] Note, this component does not currently support Anova Nano.",
this->get_name().c_str(), this->get_name().c_str());
ESP_LOGW(TAG, "[%s] No control service found at device, not an Anova..?", this->get_name().c_str());
ESP_LOGW(TAG, "[%s] Note, this component does not currently support Anova Nano.", this->get_name().c_str());
break;
}
this->char_handle_ = chr->handle;
@@ -144,9 +144,12 @@ void Anova::update() {
return;
if (this->current_request_ < 2) {
auto *pkt = this->codec_->get_read_device_status_request();
if (this->current_request_ == 0)
this->codec_->get_set_unit_request(this->fahrenheit_ ? 'f' : 'c');
AnovaPacket *pkt;
if (this->current_request_ == 0) {
pkt = this->codec_->get_set_unit_request(this->fahrenheit_ ? 'f' : 'c');
} else {
pkt = this->codec_->get_read_device_status_request();
}
auto status =
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_,
pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);

View File

@@ -251,11 +251,11 @@ void APDS9960::read_gesture_data_() {
uint8_t buf[128];
for (uint8_t pos = 0; pos < fifo_level * 4; pos += 32) {
// The ESP's i2c driver has a limited buffer size.
// This way of retrieving the data should be wrong according to the datasheet
// but it seems to work.
// Read in 32-byte chunks due to ESP8266 I2C buffer limit.
// Always read from 0xFC — the FIFO auto-increments through 0xFC-0xFF
// and advances its internal pointer after every 4th byte.
uint8_t read = std::min(32, fifo_level * 4 - pos);
APDS9960_WARNING_CHECK(this->read_bytes(0xFC + pos, buf + pos, read), "Reading FIFO buffer failed.");
APDS9960_WARNING_CHECK(this->read_bytes(0xFC, buf + pos, read), "Reading FIFO buffer failed.");
}
if (millis() - this->gesture_start_ > 500) {

View File

@@ -76,7 +76,7 @@ SERVICE_ARG_NATIVE_TYPES: dict[str, MockObj] = {
"bool": cg.bool_,
"int": cg.int32,
"float": cg.float_,
"string": cg.std_string,
"string": cg.StringRef,
"bool[]": cg.FixedVector.template(cg.bool_).operator("const").operator("ref"),
"int[]": cg.FixedVector.template(cg.int32).operator("const").operator("ref"),
"float[]": cg.FixedVector.template(cg.float_).operator("const").operator("ref"),
@@ -233,8 +233,8 @@ def _consume_api_sockets(config: ConfigType) -> ConfigType:
# API needs 1 listening socket + typically 3 concurrent client connections
# (not max_connections, which is the upper limit rarely reached)
sockets_needed = 1 + 3
socket.consume_sockets(sockets_needed, "api")(config)
socket.consume_sockets(3, "api")(config)
socket.consume_sockets(1, "api", socket.SocketType.TCP_LISTEN)(config)
return config
@@ -301,11 +301,12 @@ CONFIG_SCHEMA = cv.All(
# Maximum queued send buffers per connection before dropping connection
# Each buffer uses ~8-12 bytes overhead plus actual message size
# Platform defaults based on available RAM and typical message rates:
# CONF_MAX_SEND_QUEUE defaults are power of 2 for efficient modulo
cv.SplitDefault(
CONF_MAX_SEND_QUEUE,
esp8266=5, # Limited RAM, need to fail fast
esp8266=4, # Limited RAM, need to fail fast
esp32=8, # More RAM, can buffer more
rp2040=5, # Limited RAM
rp2040=8, # Moderate RAM
bk72xx=8, # Moderate RAM
nrf52=8, # Moderate RAM
rtl87xx=8, # Moderate RAM
@@ -380,9 +381,18 @@ async def to_code(config: ConfigType) -> None:
if is_optional:
func_args.append((cg.bool_, "return_response"))
# Check if action chain has non-synchronous actions that would make
# non-owning StringRef dangle (rx_buf_ reused after delay)
has_non_synchronous = automation.has_non_synchronous_actions(
conf.get(CONF_THEN, [])
)
service_arg_names: list[str] = []
for name, var_ in conf[CONF_VARIABLES].items():
native = SERVICE_ARG_NATIVE_TYPES[var_]
# Fall back to std::string for string args if non-synchronous actions exist
if has_non_synchronous and native is cg.StringRef:
native = cg.std_string
service_template_args.append(native)
func_args.append((native, name))
service_arg_names.append(name)
@@ -444,7 +454,10 @@ async def to_code(config: ConfigType) -> None:
# and plaintext disabled. Only a factory reset can remove it.
cg.add_define("USE_API_PLAINTEXT")
cg.add_define("USE_API_NOISE")
cg.add_library("esphome/noise-c", "0.1.10")
cg.add_library("esphome/noise-c", "0.1.11")
# Enable optimized memzero/memcmp in libsodium instead of volatile byte loops
cg.add_build_flag("-DHAVE_WEAK_SYMBOLS=1")
cg.add_build_flag("-DHAVE_INLINE_ASM=1")
else:
cg.add_define("USE_API_PLAINTEXT")
@@ -509,11 +522,13 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
"homeassistant.action",
HomeAssistantServiceCallAction,
HOMEASSISTANT_ACTION_ACTION_SCHEMA,
synchronous=True,
)
@automation.register_action(
"homeassistant.service",
HomeAssistantServiceCallAction,
HOMEASSISTANT_ACTION_ACTION_SCHEMA,
synchronous=True,
)
async def homeassistant_service_to_code(
config: ConfigType,
@@ -524,24 +539,31 @@ async def homeassistant_service_to_code(
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
serv = await cg.get_variable(config[CONF_ID])
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, cg.std_string)
cg.add(var.set_service(templ))
# Initialize FixedVectors with exact sizes from config
cg.add(var.init_data(len(config[CONF_DATA])))
for key, value in config[CONF_DATA].items():
# output_type=None because lambdas can return non-string types (int,
# float, char*) that TemplatableStringValue converts via to_string.
# Static strings are manually wrapped for PROGMEM on ESP8266.
templ = await cg.templatable(value, args, None)
cg.add(var.add_data(key, templ))
if isinstance(templ, str):
templ = cg.FlashStringLiteral(templ)
cg.add(var.add_data(cg.FlashStringLiteral(key), templ))
cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE])))
for key, value in config[CONF_DATA_TEMPLATE].items():
templ = await cg.templatable(value, args, None)
cg.add(var.add_data_template(key, templ))
if isinstance(templ, str):
templ = cg.FlashStringLiteral(templ)
cg.add(var.add_data_template(cg.FlashStringLiteral(key), templ))
cg.add(var.init_variables(len(config[CONF_VARIABLES])))
for key, value in config[CONF_VARIABLES].items():
templ = await cg.templatable(value, args, None)
cg.add(var.add_variable(key, templ))
cg.add(var.add_variable(cg.FlashStringLiteral(key), templ))
if on_error := config.get(CONF_ON_ERROR):
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES")
@@ -604,29 +626,37 @@ HOMEASSISTANT_EVENT_ACTION_SCHEMA = cv.Schema(
"homeassistant.event",
HomeAssistantServiceCallAction,
HOMEASSISTANT_EVENT_ACTION_SCHEMA,
synchronous=True,
)
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])
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, cg.std_string)
cg.add(var.set_service(templ))
# Initialize FixedVectors with exact sizes from config
cg.add(var.init_data(len(config[CONF_DATA])))
for key, value in config[CONF_DATA].items():
# output_type=None because lambdas can return non-string types (int,
# float, char*) that TemplatableStringValue converts via to_string.
# Static strings are manually wrapped for PROGMEM on ESP8266.
templ = await cg.templatable(value, args, None)
cg.add(var.add_data(key, templ))
if isinstance(templ, str):
templ = cg.FlashStringLiteral(templ)
cg.add(var.add_data(cg.FlashStringLiteral(key), templ))
cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE])))
for key, value in config[CONF_DATA_TEMPLATE].items():
templ = await cg.templatable(value, args, None)
cg.add(var.add_data_template(key, templ))
if isinstance(templ, str):
templ = cg.FlashStringLiteral(templ)
cg.add(var.add_data_template(cg.FlashStringLiteral(key), templ))
cg.add(var.init_variables(len(config[CONF_VARIABLES])))
for key, value in config[CONF_VARIABLES].items():
templ = await cg.templatable(value, args, None)
cg.add(var.add_variable(key, templ))
cg.add(var.add_variable(cg.FlashStringLiteral(key), templ))
return var
@@ -644,16 +674,17 @@ HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA = cv.maybe_simple_value(
"homeassistant.tag_scanned",
HomeAssistantServiceCallAction,
HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA,
synchronous=True,
)
async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, args):
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
serv = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, serv, True)
cg.add(var.set_service("esphome.tag_scanned"))
cg.add(var.set_service(cg.FlashStringLiteral("esphome.tag_scanned")))
# Initialize FixedVector with exact size (1 data field)
cg.add(var.init_data(1))
templ = await cg.templatable(config[CONF_TAG], args, cg.std_string)
cg.add(var.add_data("tag_id", templ))
cg.add(var.add_data(cg.FlashStringLiteral("tag_id"), templ))
return var
@@ -685,6 +716,7 @@ API_RESPOND_ACTION_SCHEMA = cv.All(
"api.respond",
APIRespondAction,
API_RESPOND_ACTION_SCHEMA,
synchronous=True,
)
async def api_respond_to_code(
config: ConfigType,

View File

@@ -58,6 +58,7 @@ service APIConnection {
rpc subscribe_bluetooth_connections_free(SubscribeBluetoothConnectionsFreeRequest) returns (BluetoothConnectionsFreeResponse) {}
rpc unsubscribe_bluetooth_le_advertisements(UnsubscribeBluetoothLEAdvertisementsRequest) returns (void) {}
rpc bluetooth_scanner_set_mode(BluetoothScannerSetModeRequest) returns (void) {}
rpc bluetooth_set_connection_params(BluetoothSetConnectionParamsRequest) returns (BluetoothSetConnectionParamsResponse) {}
rpc subscribe_voice_assistant(SubscribeVoiceAssistantRequest) returns (void) {}
rpc voice_assistant_get_configuration(VoiceAssistantConfigurationRequest) returns (VoiceAssistantConfigurationResponse) {}
@@ -69,6 +70,12 @@ service APIConnection {
rpc zwave_proxy_request(ZWaveProxyRequest) returns (void) {}
rpc infrared_rf_transmit_raw_timings(InfraredRFTransmitRawTimingsRequest) returns (void) {}
rpc serial_proxy_configure(SerialProxyConfigureRequest) returns (void) {}
rpc serial_proxy_write(SerialProxyWriteRequest) returns (void) {}
rpc serial_proxy_set_modem_pins(SerialProxySetModemPinsRequest) returns (void) {}
rpc serial_proxy_get_modem_pins(SerialProxyGetModemPinsRequest) returns (void) {}
rpc serial_proxy_request(SerialProxyRequest) returns (void) {}
}
@@ -198,6 +205,17 @@ message DeviceInfo {
uint32 area_id = 3;
}
enum SerialProxyPortType {
SERIAL_PROXY_PORT_TYPE_TTL = 0;
SERIAL_PROXY_PORT_TYPE_RS232 = 1;
SERIAL_PROXY_PORT_TYPE_RS485 = 2;
}
message SerialProxyInfo {
string name = 1; // Human-readable port name
SerialProxyPortType port_type = 2; // Port type (RS232, RS485)
}
message DeviceInfoResponse {
option (id) = 10;
option (source) = SOURCE_SERVER;
@@ -260,6 +278,9 @@ message DeviceInfoResponse {
// Indicates if Z-Wave proxy support is available and features supported
uint32 zwave_proxy_feature_flags = 23 [(field_ifdef) = "USE_ZWAVE_PROXY"];
uint32 zwave_home_id = 24 [(field_ifdef) = "USE_ZWAVE_PROXY"];
// Serial proxy instance metadata
repeated SerialProxyInfo serial_proxies = 25 [(field_ifdef) = "USE_SERIAL_PROXY", (fixed_array_size_define) = "SERIAL_PROXY_COUNT"];
}
message ListEntitiesRequest {
@@ -295,7 +316,7 @@ message ListEntitiesBinarySensorResponse {
option (ifdef) = "USE_BINARY_SENSOR";
string object_id = 1;
fixed32 key = 2;
fixed32 key = 2 [(force) = true];
string name = 3;
reserved 4; // Deprecated: was string unique_id
@@ -313,7 +334,7 @@ message BinarySensorStateResponse {
option (ifdef) = "USE_BINARY_SENSOR";
option (no_delay) = true;
fixed32 key = 1;
fixed32 key = 1 [(force) = true];
bool state = 2;
// If the binary sensor does not have a valid state yet.
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
@@ -329,7 +350,7 @@ message ListEntitiesCoverResponse {
option (ifdef) = "USE_COVER";
string object_id = 1;
fixed32 key = 2;
fixed32 key = 2 [(force) = true];
string name = 3;
reserved 4; // Deprecated: was string unique_id
@@ -362,7 +383,7 @@ message CoverStateResponse {
option (ifdef) = "USE_COVER";
option (no_delay) = true;
fixed32 key = 1;
fixed32 key = 1 [(force) = true];
// legacy: state has been removed in 1.13
// clients/servers must still send/accept it until the next protocol change
// Deprecated in API version 1.1
@@ -388,7 +409,7 @@ message CoverCommandRequest {
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
fixed32 key = 1 [(force) = true];
// legacy: command has been removed in 1.13
// clients/servers must still send/accept it until the next protocol change
@@ -413,7 +434,7 @@ message ListEntitiesFanResponse {
option (ifdef) = "USE_FAN";
string object_id = 1;
fixed32 key = 2;
fixed32 key = 2 [(force) = true];
string name = 3;
reserved 4; // Deprecated: was string unique_id
@@ -445,7 +466,7 @@ message FanStateResponse {
option (ifdef) = "USE_FAN";
option (no_delay) = true;
fixed32 key = 1;
fixed32 key = 1 [(force) = true];
bool state = 2;
bool oscillating = 3;
// Deprecated in API version 1.6
@@ -462,7 +483,7 @@ message FanCommandRequest {
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
fixed32 key = 1 [(force) = true];
bool has_state = 2;
bool state = 3;
// Deprecated in API version 1.6
@@ -501,7 +522,7 @@ message ListEntitiesLightResponse {
option (ifdef) = "USE_LIGHT";
string object_id = 1;
fixed32 key = 2;
fixed32 key = 2 [(force) = true];
string name = 3;
reserved 4; // Deprecated: was string unique_id
@@ -530,7 +551,7 @@ message LightStateResponse {
option (ifdef) = "USE_LIGHT";
option (no_delay) = true;
fixed32 key = 1;
fixed32 key = 1 [(force) = true];
bool state = 2;
float brightness = 3;
ColorMode color_mode = 11;
@@ -552,7 +573,7 @@ message LightCommandRequest {
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
fixed32 key = 1 [(force) = true];
bool has_state = 2;
bool state = 3;
bool has_brightness = 4;
@@ -606,7 +627,7 @@ message ListEntitiesSensorResponse {
option (ifdef) = "USE_SENSOR";
string object_id = 1;
fixed32 key = 2;
fixed32 key = 2 [(force) = true];
string name = 3;
reserved 4; // Deprecated: was string unique_id
@@ -630,7 +651,7 @@ message SensorStateResponse {
option (ifdef) = "USE_SENSOR";
option (no_delay) = true;
fixed32 key = 1;
fixed32 key = 1 [(force) = true];
float state = 2;
// If the sensor does not have a valid state yet.
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
@@ -646,7 +667,7 @@ message ListEntitiesSwitchResponse {
option (ifdef) = "USE_SWITCH";
string object_id = 1;
fixed32 key = 2;
fixed32 key = 2 [(force) = true];
string name = 3;
reserved 4; // Deprecated: was string unique_id
@@ -664,7 +685,7 @@ message SwitchStateResponse {
option (ifdef) = "USE_SWITCH";
option (no_delay) = true;
fixed32 key = 1;
fixed32 key = 1 [(force) = true];
bool state = 2;
uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
}
@@ -675,7 +696,7 @@ message SwitchCommandRequest {
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
fixed32 key = 1 [(force) = true];
bool state = 2;
uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
}
@@ -688,7 +709,7 @@ message ListEntitiesTextSensorResponse {
option (ifdef) = "USE_TEXT_SENSOR";
string object_id = 1;
fixed32 key = 2;
fixed32 key = 2 [(force) = true];
string name = 3;
reserved 4; // Deprecated: was string unique_id
@@ -705,7 +726,7 @@ message TextSensorStateResponse {
option (ifdef) = "USE_TEXT_SENSOR";
option (no_delay) = true;
fixed32 key = 1;
fixed32 key = 1 [(force) = true];
string state = 2;
// If the text sensor does not have a valid state yet.
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
@@ -834,13 +855,41 @@ message GetTimeRequest {
option (source) = SOURCE_SERVER;
}
enum DSTRuleType {
DST_RULE_TYPE_NONE = 0;
DST_RULE_TYPE_MONTH_WEEK_DAY = 1;
DST_RULE_TYPE_JULIAN_NO_LEAP = 2;
DST_RULE_TYPE_DAY_OF_YEAR = 3;
}
message DSTRule {
option (source) = SOURCE_CLIENT;
sint32 time_seconds = 1;
uint32 day = 2;
DSTRuleType type = 3;
uint32 month = 4;
uint32 week = 5;
uint32 day_of_week = 6;
}
message ParsedTimezone {
option (source) = SOURCE_CLIENT;
sint32 std_offset_seconds = 1;
sint32 dst_offset_seconds = 2;
DSTRule dst_start = 3;
DSTRule dst_end = 4;
}
message GetTimeResponse {
option (id) = 37;
option (source) = SOURCE_CLIENT;
option (no_delay) = true;
fixed32 epoch_seconds = 1;
string timezone = 2;
string timezone = 2 [deprecated = true]; // Use parsed_timezone instead. Remove before 2026.9.0.
ParsedTimezone parsed_timezone = 3;
}
// ==================== USER-DEFINES SERVICES ====================
@@ -873,7 +922,7 @@ message ListEntitiesServicesResponse {
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
string name = 1;
fixed32 key = 2;
fixed32 key = 2 [(force) = true];
repeated ListEntitiesServicesArgument args = 3 [(fixed_vector) = true];
SupportsResponseType supports_response = 4;
}
@@ -896,7 +945,7 @@ message ExecuteServiceRequest {
option (no_delay) = true;
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
fixed32 key = 1;
fixed32 key = 1 [(force) = true];
repeated ExecuteServiceArgument args = 2 [(fixed_vector) = true];
uint32 call_id = 3 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_RESPONSES"];
bool return_response = 4 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_RESPONSES"];
@@ -923,7 +972,7 @@ message ListEntitiesCameraResponse {
option (ifdef) = "USE_CAMERA";
string object_id = 1;
fixed32 key = 2;
fixed32 key = 2 [(force) = true];
string name = 3;
reserved 4; // Deprecated: was string unique_id
bool disabled_by_default = 5;
@@ -938,7 +987,7 @@ message CameraImageResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_CAMERA";
fixed32 key = 1;
fixed32 key = 1 [(force) = true];
bytes data = 2;
bool done = 3;
uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"];
@@ -989,6 +1038,7 @@ enum ClimateAction {
CLIMATE_ACTION_IDLE = 4;
CLIMATE_ACTION_DRYING = 5;
CLIMATE_ACTION_FAN = 6;
CLIMATE_ACTION_DEFROSTING = 7;
}
enum ClimatePreset {
CLIMATE_PRESET_NONE = 0;
@@ -1007,7 +1057,7 @@ message ListEntitiesClimateResponse {
option (ifdef) = "USE_CLIMATE";
string object_id = 1;
fixed32 key = 2;
fixed32 key = 2 [(force) = true];
string name = 3;
reserved 4; // Deprecated: was string unique_id
@@ -1045,7 +1095,7 @@ message ClimateStateResponse {
option (ifdef) = "USE_CLIMATE";
option (no_delay) = true;
fixed32 key = 1;
fixed32 key = 1 [(force) = true];
ClimateMode mode = 2;
float current_temperature = 3;
float target_temperature = 4;
@@ -1071,7 +1121,7 @@ message ClimateCommandRequest {
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
fixed32 key = 1 [(force) = true];
bool has_mode = 2;
ClimateMode mode = 3;
bool has_target_temperature = 4;
@@ -1118,7 +1168,7 @@ message ListEntitiesWaterHeaterResponse {
option (ifdef) = "USE_WATER_HEATER";
string object_id = 1;
fixed32 key = 2;
fixed32 key = 2 [(force) = true];
string name = 3;
string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON"];
bool disabled_by_default = 5;
@@ -1139,7 +1189,7 @@ message WaterHeaterStateResponse {
option (ifdef) = "USE_WATER_HEATER";
option (no_delay) = true;
fixed32 key = 1;
fixed32 key = 1 [(force) = true];
float current_temperature = 2;
float target_temperature = 3;
WaterHeaterMode mode = 4;
@@ -1169,7 +1219,7 @@ message WaterHeaterCommandRequest {
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
fixed32 key = 1 [(force) = true];
// Bitmask of which fields are set (see WaterHeaterCommandHasField)
uint32 has_fields = 2;
WaterHeaterMode mode = 3;
@@ -1194,7 +1244,7 @@ message ListEntitiesNumberResponse {
option (ifdef) = "USE_NUMBER";
string object_id = 1;
fixed32 key = 2;
fixed32 key = 2 [(force) = true];
string name = 3;
reserved 4; // Deprecated: was string unique_id
@@ -1216,7 +1266,7 @@ message NumberStateResponse {
option (ifdef) = "USE_NUMBER";
option (no_delay) = true;
fixed32 key = 1;
fixed32 key = 1 [(force) = true];
float state = 2;
// If the number does not have a valid state yet.
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
@@ -1230,7 +1280,7 @@ message NumberCommandRequest {
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
fixed32 key = 1 [(force) = true];
float state = 2;
uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
}
@@ -1243,7 +1293,7 @@ message ListEntitiesSelectResponse {
option (ifdef) = "USE_SELECT";
string object_id = 1;
fixed32 key = 2;
fixed32 key = 2 [(force) = true];
string name = 3;
reserved 4; // Deprecated: was string unique_id
@@ -1260,7 +1310,7 @@ message SelectStateResponse {
option (ifdef) = "USE_SELECT";
option (no_delay) = true;
fixed32 key = 1;
fixed32 key = 1 [(force) = true];
string state = 2;
// If the select does not have a valid state yet.
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
@@ -1274,7 +1324,7 @@ message SelectCommandRequest {
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
fixed32 key = 1 [(force) = true];
string state = 2;
uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
}
@@ -1287,7 +1337,7 @@ message ListEntitiesSirenResponse {
option (ifdef) = "USE_SIREN";
string object_id = 1;
fixed32 key = 2;
fixed32 key = 2 [(force) = true];
string name = 3;
reserved 4; // Deprecated: was string unique_id
@@ -1306,7 +1356,7 @@ message SirenStateResponse {
option (ifdef) = "USE_SIREN";
option (no_delay) = true;
fixed32 key = 1;
fixed32 key = 1 [(force) = true];
bool state = 2;
uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
}
@@ -1317,7 +1367,7 @@ message SirenCommandRequest {
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
fixed32 key = 1 [(force) = true];
bool has_state = 2;
bool state = 3;
bool has_tone = 4;
@@ -1350,7 +1400,7 @@ message ListEntitiesLockResponse {
option (ifdef) = "USE_LOCK";
string object_id = 1;
fixed32 key = 2;
fixed32 key = 2 [(force) = true];
string name = 3;
reserved 4; // Deprecated: was string unique_id
@@ -1372,7 +1422,7 @@ message LockStateResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_LOCK";
option (no_delay) = true;
fixed32 key = 1;
fixed32 key = 1 [(force) = true];
LockState state = 2;
uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
}
@@ -1382,7 +1432,7 @@ message LockCommandRequest {
option (ifdef) = "USE_LOCK";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
fixed32 key = 1 [(force) = true];
LockCommand command = 2;
// Not yet implemented:
@@ -1399,7 +1449,7 @@ message ListEntitiesButtonResponse {
option (ifdef) = "USE_BUTTON";
string object_id = 1;
fixed32 key = 2;
fixed32 key = 2 [(force) = true];
string name = 3;
reserved 4; // Deprecated: was string unique_id
@@ -1416,7 +1466,7 @@ message ButtonCommandRequest {
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
fixed32 key = 1 [(force) = true];
uint32 device_id = 2 [(field_ifdef) = "USE_DEVICES"];
}
@@ -1466,7 +1516,7 @@ message ListEntitiesMediaPlayerResponse {
option (ifdef) = "USE_MEDIA_PLAYER";
string object_id = 1;
fixed32 key = 2;
fixed32 key = 2 [(force) = true];
string name = 3;
reserved 4; // Deprecated: was string unique_id
@@ -1488,7 +1538,7 @@ message MediaPlayerStateResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_MEDIA_PLAYER";
option (no_delay) = true;
fixed32 key = 1;
fixed32 key = 1 [(force) = true];
MediaPlayerState state = 2;
float volume = 3;
bool muted = 4;
@@ -1501,7 +1551,7 @@ message MediaPlayerCommandRequest {
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
fixed32 key = 1 [(force) = true];
bool has_command = 2;
MediaPlayerCommand command = 3;
@@ -1554,11 +1604,11 @@ message BluetoothLEAdvertisementResponse {
}
message BluetoothLERawAdvertisement {
uint64 address = 1;
sint32 rssi = 2;
uint64 address = 1 [(force) = true];
sint32 rssi = 2 [(force) = true];
uint32 address_type = 3;
bytes data = 4 [(fixed_array_size) = 62];
bytes data = 4 [(fixed_array_size) = 62, (force) = true];
}
message BluetoothLERawAdvertisementsResponse {
@@ -2054,7 +2104,7 @@ message ListEntitiesAlarmControlPanelResponse {
option (ifdef) = "USE_ALARM_CONTROL_PANEL";
string object_id = 1;
fixed32 key = 2;
fixed32 key = 2 [(force) = true];
string name = 3;
reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
@@ -2072,7 +2122,7 @@ message AlarmControlPanelStateResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_ALARM_CONTROL_PANEL";
option (no_delay) = true;
fixed32 key = 1;
fixed32 key = 1 [(force) = true];
AlarmControlPanelState state = 2;
uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
}
@@ -2083,7 +2133,7 @@ message AlarmControlPanelCommandRequest {
option (ifdef) = "USE_ALARM_CONTROL_PANEL";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
fixed32 key = 1 [(force) = true];
AlarmControlPanelStateCommand command = 2;
string code = 3;
uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"];
@@ -2101,7 +2151,7 @@ message ListEntitiesTextResponse {
option (ifdef) = "USE_TEXT";
string object_id = 1;
fixed32 key = 2;
fixed32 key = 2 [(force) = true];
string name = 3;
reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
@@ -2121,7 +2171,7 @@ message TextStateResponse {
option (ifdef) = "USE_TEXT";
option (no_delay) = true;
fixed32 key = 1;
fixed32 key = 1 [(force) = true];
string state = 2;
// If the Text does not have a valid state yet.
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
@@ -2135,7 +2185,7 @@ message TextCommandRequest {
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
fixed32 key = 1 [(force) = true];
string state = 2;
uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
}
@@ -2149,7 +2199,7 @@ message ListEntitiesDateResponse {
option (ifdef) = "USE_DATETIME_DATE";
string object_id = 1;
fixed32 key = 2;
fixed32 key = 2 [(force) = true];
string name = 3;
reserved 4; // Deprecated: was string unique_id
@@ -2165,7 +2215,7 @@ message DateStateResponse {
option (ifdef) = "USE_DATETIME_DATE";
option (no_delay) = true;
fixed32 key = 1;
fixed32 key = 1 [(force) = true];
// If the date does not have a valid state yet.
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
bool missing_state = 2;
@@ -2181,7 +2231,7 @@ message DateCommandRequest {
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
fixed32 key = 1 [(force) = true];
uint32 year = 2;
uint32 month = 3;
uint32 day = 4;
@@ -2196,7 +2246,7 @@ message ListEntitiesTimeResponse {
option (ifdef) = "USE_DATETIME_TIME";
string object_id = 1;
fixed32 key = 2;
fixed32 key = 2 [(force) = true];
string name = 3;
reserved 4; // Deprecated: was string unique_id
@@ -2212,7 +2262,7 @@ message TimeStateResponse {
option (ifdef) = "USE_DATETIME_TIME";
option (no_delay) = true;
fixed32 key = 1;
fixed32 key = 1 [(force) = true];
// If the time does not have a valid state yet.
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
bool missing_state = 2;
@@ -2228,7 +2278,7 @@ message TimeCommandRequest {
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
fixed32 key = 1 [(force) = true];
uint32 hour = 2;
uint32 minute = 3;
uint32 second = 4;
@@ -2243,7 +2293,7 @@ message ListEntitiesEventResponse {
option (ifdef) = "USE_EVENT";
string object_id = 1;
fixed32 key = 2;
fixed32 key = 2 [(force) = true];
string name = 3;
reserved 4; // Deprecated: was string unique_id
@@ -2261,7 +2311,7 @@ message EventResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_EVENT";
fixed32 key = 1;
fixed32 key = 1 [(force) = true];
string event_type = 2;
uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
}
@@ -2274,7 +2324,7 @@ message ListEntitiesValveResponse {
option (ifdef) = "USE_VALVE";
string object_id = 1;
fixed32 key = 2;
fixed32 key = 2 [(force) = true];
string name = 3;
reserved 4; // Deprecated: was string unique_id
@@ -2301,7 +2351,7 @@ message ValveStateResponse {
option (ifdef) = "USE_VALVE";
option (no_delay) = true;
fixed32 key = 1;
fixed32 key = 1 [(force) = true];
float position = 2;
ValveOperation current_operation = 3;
uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"];
@@ -2314,7 +2364,7 @@ message ValveCommandRequest {
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
fixed32 key = 1 [(force) = true];
bool has_position = 2;
float position = 3;
bool stop = 4;
@@ -2329,7 +2379,7 @@ message ListEntitiesDateTimeResponse {
option (ifdef) = "USE_DATETIME_DATETIME";
string object_id = 1;
fixed32 key = 2;
fixed32 key = 2 [(force) = true];
string name = 3;
reserved 4; // Deprecated: was string unique_id
@@ -2345,7 +2395,7 @@ message DateTimeStateResponse {
option (ifdef) = "USE_DATETIME_DATETIME";
option (no_delay) = true;
fixed32 key = 1;
fixed32 key = 1 [(force) = true];
// If the datetime does not have a valid state yet.
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
bool missing_state = 2;
@@ -2359,7 +2409,7 @@ message DateTimeCommandRequest {
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
fixed32 key = 1 [(force) = true];
fixed32 epoch_seconds = 2;
uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
}
@@ -2372,7 +2422,7 @@ message ListEntitiesUpdateResponse {
option (ifdef) = "USE_UPDATE";
string object_id = 1;
fixed32 key = 2;
fixed32 key = 2 [(force) = true];
string name = 3;
reserved 4; // Deprecated: was string unique_id
@@ -2389,7 +2439,7 @@ message UpdateStateResponse {
option (ifdef) = "USE_UPDATE";
option (no_delay) = true;
fixed32 key = 1;
fixed32 key = 1 [(force) = true];
bool missing_state = 2;
bool in_progress = 3;
bool has_progress = 4;
@@ -2413,7 +2463,7 @@ message UpdateCommandRequest {
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
fixed32 key = 1 [(force) = true];
UpdateCommand command = 2;
uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
}
@@ -2455,13 +2505,14 @@ message ListEntitiesInfraredResponse {
option (ifdef) = "USE_INFRARED";
string object_id = 1;
fixed32 key = 2;
fixed32 key = 2 [(force) = true];
string name = 3;
string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON"];
bool disabled_by_default = 5;
EntityCategory entity_category = 6;
uint32 device_id = 7 [(field_ifdef) = "USE_DEVICES"];
uint32 capabilities = 8; // Bitfield of InfraredCapabilityFlags
uint32 receiver_frequency = 9; // Demodulation frequency of the IR receiver in Hz (0 = unspecified)
}
// Command to transmit infrared/RF data using raw timings
@@ -2471,7 +2522,7 @@ message InfraredRFTransmitRawTimingsRequest {
option (ifdef) = "USE_IR_RF";
uint32 device_id = 1 [(field_ifdef) = "USE_DEVICES"];
fixed32 key = 2; // Key identifying the transmitter instance
fixed32 key = 2 [(force) = true]; // Key identifying the transmitter instance
uint32 carrier_frequency = 3; // Carrier frequency in Hz
uint32 repeat_count = 4; // Number of times to transmit (1 = once, 2 = twice, etc.)
repeated sint32 timings = 5 [packed = true, (packed_buffer) = true]; // Raw timings in microseconds (zigzag-encoded): positive = mark (LED/TX on), negative = space (LED/TX off)
@@ -2485,6 +2536,137 @@ message InfraredRFReceiveEvent {
option (no_delay) = true;
uint32 device_id = 1 [(field_ifdef) = "USE_DEVICES"];
fixed32 key = 2; // Key identifying the receiver instance
fixed32 key = 2 [(force) = true]; // Key identifying the receiver instance
repeated sint32 timings = 3 [packed = true, (container_pointer_no_template) = "std::vector<int32_t>"]; // Raw timings in microseconds (zigzag-encoded): alternating mark/space periods
}
// ==================== SERIAL PROXY ====================
enum SerialProxyParity {
SERIAL_PROXY_PARITY_NONE = 0;
SERIAL_PROXY_PARITY_EVEN = 1;
SERIAL_PROXY_PARITY_ODD = 2;
}
// Configure UART parameters for a serial proxy instance
message SerialProxyConfigureRequest {
option (id) = 138;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_SERIAL_PROXY";
uint32 instance = 1; // Instance index (0-based)
uint32 baudrate = 2; // Baud rate in bits per second
bool flow_control = 3; // Enable hardware flow control
SerialProxyParity parity = 4; // Parity setting
uint32 stop_bits = 5; // Number of stop bits (1 or 2)
uint32 data_size = 6; // Number of data bits (5-8)
}
// Data received from a serial device, forwarded to clients
message SerialProxyDataReceived {
option (id) = 139;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SERIAL_PROXY";
option (no_delay) = true;
uint32 instance = 1; // Instance index (0-based)
bytes data = 2; // Raw data received from the serial device
}
// Write data to a serial device
message SerialProxyWriteRequest {
option (id) = 140;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_SERIAL_PROXY";
option (no_delay) = true;
uint32 instance = 1; // Instance index (0-based)
bytes data = 2; // Raw data to write to the serial device
}
// Set modem control pin states (RTS and DTR)
message SerialProxySetModemPinsRequest {
option (id) = 141;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_SERIAL_PROXY";
uint32 instance = 1; // Instance index (0-based)
uint32 line_states = 2; // Bitmask of SerialProxyLineStateFlags
}
// Request current modem control pin states
message SerialProxyGetModemPinsRequest {
option (id) = 142;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_SERIAL_PROXY";
uint32 instance = 1; // Instance index (0-based)
}
// Response with current modem control pin states
message SerialProxyGetModemPinsResponse {
option (id) = 143;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SERIAL_PROXY";
uint32 instance = 1; // Instance index (0-based)
uint32 line_states = 2; // Bitmask of SerialProxyLineStateFlags
}
enum SerialProxyRequestType {
SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE = 0; // Subscribe to receive data from this serial proxy instance
SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1; // Unsubscribe from this serial proxy instance
SERIAL_PROXY_REQUEST_TYPE_FLUSH = 2; // Flush the serial port (block until all TX data is sent)
}
enum SerialProxyStatus {
SERIAL_PROXY_STATUS_OK = 0; // Completed successfully; TX drain confirmed
SERIAL_PROXY_STATUS_ASSUMED_SUCCESS = 1; // Platform cannot confirm TX drain; success assumed
SERIAL_PROXY_STATUS_ERROR = 2; // Driver or hardware error
SERIAL_PROXY_STATUS_TIMEOUT = 3; // Timed out before TX completed
SERIAL_PROXY_STATUS_NOT_SUPPORTED = 4; // Request type not supported by this instance
}
// Generic request message for simple serial proxy operations
message SerialProxyRequest {
option (id) = 144;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_SERIAL_PROXY";
uint32 instance = 1; // Instance index (0-based)
SerialProxyRequestType type = 2; // Request type
}
// Response to a SerialProxyRequest (e.g. flush completion or failure)
message SerialProxyRequestResponse {
option (id) = 147;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SERIAL_PROXY";
uint32 instance = 1; // Instance index (0-based)
SerialProxyRequestType type = 2; // Which request type this responds to
SerialProxyStatus status = 3; // Result status
string error_message = 4; // Additional detail on failure (optional)
}
// ==================== BLUETOOTH CONNECTION PARAMS ====================
message BluetoothSetConnectionParamsRequest {
option (id) = 145;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_BLUETOOTH_PROXY";
uint64 address = 1;
uint32 min_interval = 2; // units of 1.25ms
uint32 max_interval = 3; // units of 1.25ms
uint32 latency = 4;
uint32 timeout = 5; // units of 10ms
}
message BluetoothSetConnectionParamsResponse {
option (id) = 146;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_BLUETOOTH_PROXY";
uint64 address = 1;
int32 error = 2;
}

View File

@@ -0,0 +1,13 @@
#include "api_buffer.h"
namespace esphome::api {
void APIBuffer::grow_(size_t n) {
auto new_data = make_buffer(n);
if (this->size_)
std::memcpy(new_data.get(), this->data_.get(), this->size_);
this->data_ = std::move(new_data);
this->capacity_ = n;
}
} // namespace esphome::api

View File

@@ -0,0 +1,73 @@
#pragma once
#include <cstdint>
#include <cstring>
#include <memory>
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
namespace esphome::api {
/// Helper to use make_unique_for_overwrite where available (skips zero-fill),
/// falling back to make_unique on older GCC (ESP8266, LibreTiny).
inline std::unique_ptr<uint8_t[]> make_buffer(size_t n) {
#if defined(USE_ESP8266) || defined(USE_LIBRETINY)
return std::make_unique<uint8_t[]>(n);
#else
return std::make_unique_for_overwrite<uint8_t[]>(n);
#endif
}
/// Byte buffer that skips zero-initialization on resize().
///
/// std::vector<uint8_t>::resize() zero-fills new bytes via memset. For the
/// shared protobuf write buffer, every byte is overwritten by the encoder,
/// making the zero-fill pure waste. For the receive buffer, bytes are
/// overwritten by socket reads.
///
/// Designed for bulk clear/resize/overwrite patterns. grow_() allocates
/// exactly the requested size (no growth factor) since callers resize to
/// known sizes rather than appending incrementally.
///
/// Safe because: callers always write exactly the number of bytes they
/// resize for. In the protobuf write path, debug_check_bounds_ validates
/// writes in debug builds.
class APIBuffer {
public:
void clear() { this->size_ = 0; }
inline void reserve(size_t n) ESPHOME_ALWAYS_INLINE {
if (n > this->capacity_)
this->grow_(n);
}
inline void resize(size_t n) ESPHOME_ALWAYS_INLINE {
this->reserve(n);
this->size_ = n; // no zero-fill
}
/// Reserve capacity for max(reserve_size, new_size) bytes, then set size to new_size.
/// Single grow_ check regardless of argument order.
inline void reserve_and_resize(size_t reserve_size, size_t new_size) ESPHOME_ALWAYS_INLINE {
this->reserve(std::max(reserve_size, new_size));
this->size_ = new_size;
}
uint8_t *data() { return this->data_.get(); }
const uint8_t *data() const { return this->data_.get(); }
size_t size() const { return this->size_; }
bool empty() const { return this->size_ == 0; }
uint8_t &operator[](size_t i) { return this->data_[i]; }
const uint8_t &operator[](size_t i) const { return this->data_[i]; }
/// Release all memory (equivalent to std::vector swap trick).
void release() {
this->data_.reset();
this->size_ = 0;
this->capacity_ = 0;
}
protected:
void grow_(size_t n);
std::unique_ptr<uint8_t[]> data_;
size_t size_{0};
size_t capacity_{0};
};
} // namespace esphome::api

File diff suppressed because it is too large Load Diff

View File

@@ -3,11 +3,23 @@
#include "esphome/core/defines.h"
#ifdef USE_API
#include "api_frame_helper.h"
#ifdef USE_API_NOISE
#include "api_frame_helper_noise.h"
#endif
#ifdef USE_API_PLAINTEXT
#include "api_frame_helper_plaintext.h"
#endif
#include "api_pb2.h"
#include "api_pb2_service.h"
#include "api_server.h"
#include "esphome/core/application.h"
#include "esphome/core/component.h"
#ifdef USE_ESP32_CRASH_HANDLER
#include "esphome/components/esp32/crash_handler.h"
#endif
#ifdef USE_RP2040_CRASH_HANDLER
#include "esphome/components/rp2040/crash_handler.h"
#endif
#include "esphome/core/entity_base.h"
#include "esphome/core/string_ref.h"
@@ -32,16 +44,46 @@ static constexpr size_t MAX_INITIAL_PER_BATCH = 34; // For clients >= AP
static_assert(MAX_MESSAGES_PER_BATCH >= MAX_INITIAL_PER_BATCH,
"MAX_MESSAGES_PER_BATCH must be >= MAX_INITIAL_PER_BATCH");
#ifdef USE_BENCHMARK
class APIConnection;
void bench_enable_immediate_send(APIConnection *conn);
void bench_clear_batch(APIConnection *conn);
void bench_process_batch(APIConnection *conn);
#endif
class APIConnection final : public APIServerConnectionBase {
public:
friend class APIServer;
friend class ListEntitiesIterator;
#ifdef USE_BENCHMARK
friend void bench_enable_immediate_send(APIConnection *conn);
friend void bench_clear_batch(APIConnection *conn);
friend void bench_process_batch(APIConnection *conn);
#endif
APIConnection(std::unique_ptr<socket::Socket> socket, APIServer *parent);
virtual ~APIConnection();
~APIConnection();
void start();
void loop();
protected:
// read_message_ is defined here (instead of in APIServerConnectionBase) so the
// compiler can devirtualize and inline on_* handler calls within this final class.
void read_message_(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data);
// Auth helpers defined here (not in ProtoService) so the compiler can
// devirtualize is_connection_setup()/on_no_setup_connection() calls
// within this final class.
inline bool check_connection_setup_() {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return false;
}
return true;
}
inline bool check_authenticated_() { return this->check_connection_setup_(); }
public:
bool send_list_info_done() {
return this->schedule_message_(nullptr, ListEntitiesDoneResponse::MESSAGE_TYPE,
ListEntitiesDoneResponse::ESTIMATED_SIZE);
@@ -51,163 +93,173 @@ class APIConnection final : public APIServerConnectionBase {
#endif
#ifdef USE_COVER
bool send_cover_state(cover::Cover *cover);
void on_cover_command_request(const CoverCommandRequest &msg) override;
void on_cover_command_request(const CoverCommandRequest &msg);
#endif
#ifdef USE_FAN
bool send_fan_state(fan::Fan *fan);
void on_fan_command_request(const FanCommandRequest &msg) override;
void on_fan_command_request(const FanCommandRequest &msg);
#endif
#ifdef USE_LIGHT
bool send_light_state(light::LightState *light);
void on_light_command_request(const LightCommandRequest &msg) override;
void on_light_command_request(const LightCommandRequest &msg);
#endif
#ifdef USE_SENSOR
bool send_sensor_state(sensor::Sensor *sensor);
#endif
#ifdef USE_SWITCH
bool send_switch_state(switch_::Switch *a_switch);
void on_switch_command_request(const SwitchCommandRequest &msg) override;
void on_switch_command_request(const SwitchCommandRequest &msg);
#endif
#ifdef USE_TEXT_SENSOR
bool send_text_sensor_state(text_sensor::TextSensor *text_sensor);
#endif
#ifdef USE_CAMERA
void set_camera_state(std::shared_ptr<camera::CameraImage> image);
void on_camera_image_request(const CameraImageRequest &msg) override;
void on_camera_image_request(const CameraImageRequest &msg);
#endif
#ifdef USE_CLIMATE
bool send_climate_state(climate::Climate *climate);
void on_climate_command_request(const ClimateCommandRequest &msg) override;
void on_climate_command_request(const ClimateCommandRequest &msg);
#endif
#ifdef USE_NUMBER
bool send_number_state(number::Number *number);
void on_number_command_request(const NumberCommandRequest &msg) override;
void on_number_command_request(const NumberCommandRequest &msg);
#endif
#ifdef USE_DATETIME_DATE
bool send_date_state(datetime::DateEntity *date);
void on_date_command_request(const DateCommandRequest &msg) override;
void on_date_command_request(const DateCommandRequest &msg);
#endif
#ifdef USE_DATETIME_TIME
bool send_time_state(datetime::TimeEntity *time);
void on_time_command_request(const TimeCommandRequest &msg) override;
void on_time_command_request(const TimeCommandRequest &msg);
#endif
#ifdef USE_DATETIME_DATETIME
bool send_datetime_state(datetime::DateTimeEntity *datetime);
void on_date_time_command_request(const DateTimeCommandRequest &msg) override;
void on_date_time_command_request(const DateTimeCommandRequest &msg);
#endif
#ifdef USE_TEXT
bool send_text_state(text::Text *text);
void on_text_command_request(const TextCommandRequest &msg) override;
void on_text_command_request(const TextCommandRequest &msg);
#endif
#ifdef USE_SELECT
bool send_select_state(select::Select *select);
void on_select_command_request(const SelectCommandRequest &msg) override;
void on_select_command_request(const SelectCommandRequest &msg);
#endif
#ifdef USE_BUTTON
void on_button_command_request(const ButtonCommandRequest &msg) override;
void on_button_command_request(const ButtonCommandRequest &msg);
#endif
#ifdef USE_LOCK
bool send_lock_state(lock::Lock *a_lock);
void on_lock_command_request(const LockCommandRequest &msg) override;
void on_lock_command_request(const LockCommandRequest &msg);
#endif
#ifdef USE_VALVE
bool send_valve_state(valve::Valve *valve);
void on_valve_command_request(const ValveCommandRequest &msg) override;
void on_valve_command_request(const ValveCommandRequest &msg);
#endif
#ifdef USE_MEDIA_PLAYER
bool send_media_player_state(media_player::MediaPlayer *media_player);
void on_media_player_command_request(const MediaPlayerCommandRequest &msg) override;
void on_media_player_command_request(const MediaPlayerCommandRequest &msg);
#endif
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_action(const HomeassistantActionRequest &call) {
if (!this->flags_.service_call_subscription)
return;
this->send_message(call, HomeassistantActionRequest::MESSAGE_TYPE);
this->send_message(call);
}
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
void on_homeassistant_action_response(const HomeassistantActionResponse &msg) override;
void on_homeassistant_action_response(const HomeassistantActionResponse &msg);
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
#endif // USE_API_HOMEASSISTANT_SERVICES
#ifdef USE_BLUETOOTH_PROXY
void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
void on_unsubscribe_bluetooth_le_advertisements_request() override;
void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg);
void on_unsubscribe_bluetooth_le_advertisements_request();
void on_bluetooth_device_request(const BluetoothDeviceRequest &msg) override;
void on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg) override;
void on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg) override;
void on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg) override;
void on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg) override;
void on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg) override;
void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) override;
void on_subscribe_bluetooth_connections_free_request() override;
void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) override;
void on_bluetooth_device_request(const BluetoothDeviceRequest &msg);
void on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg);
void on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg);
void on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg);
void on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg);
void on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg);
void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg);
void on_subscribe_bluetooth_connections_free_request();
void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg);
void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &msg);
#endif
#ifdef USE_HOMEASSISTANT_TIME
void send_time_request() {
GetTimeRequest req;
this->send_message(req, GetTimeRequest::MESSAGE_TYPE);
this->send_message(req);
}
#endif
#ifdef USE_VOICE_ASSISTANT
void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) override;
void on_voice_assistant_response(const VoiceAssistantResponse &msg) override;
void on_voice_assistant_event_response(const VoiceAssistantEventResponse &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_announce_request(const VoiceAssistantAnnounceRequest &msg) override;
void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) override;
void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override;
void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg);
void on_voice_assistant_response(const VoiceAssistantResponse &msg);
void on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg);
void on_voice_assistant_audio(const VoiceAssistantAudio &msg);
void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg);
void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg);
void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg);
void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg);
#endif
#ifdef USE_ZWAVE_PROXY
void on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) override;
void on_z_wave_proxy_request(const ZWaveProxyRequest &msg) override;
void on_z_wave_proxy_frame(const ZWaveProxyFrame &msg);
void on_z_wave_proxy_request(const ZWaveProxyRequest &msg);
#endif
#ifdef USE_ALARM_CONTROL_PANEL
bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) override;
void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg);
#endif
#ifdef USE_WATER_HEATER
bool send_water_heater_state(water_heater::WaterHeater *water_heater);
void on_water_heater_command_request(const WaterHeaterCommandRequest &msg) override;
void on_water_heater_command_request(const WaterHeaterCommandRequest &msg);
#endif
#ifdef USE_IR_RF
void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) override;
void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg);
void send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg);
#endif
#ifdef USE_SERIAL_PROXY
void on_serial_proxy_configure_request(const SerialProxyConfigureRequest &msg);
void on_serial_proxy_write_request(const SerialProxyWriteRequest &msg);
void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &msg);
void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg);
void on_serial_proxy_request(const SerialProxyRequest &msg);
void send_serial_proxy_data(const SerialProxyDataReceived &msg);
#endif
#ifdef USE_EVENT
void send_event(event::Event *event);
#endif
#ifdef USE_UPDATE
bool send_update_state(update::UpdateEntity *update);
void on_update_command_request(const UpdateCommandRequest &msg) override;
void on_update_command_request(const UpdateCommandRequest &msg);
#endif
void on_disconnect_response() override;
void on_ping_response() override {
void on_disconnect_response();
void on_ping_response() {
// we initiated ping
this->flags_.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);
#endif
#ifdef USE_HOMEASSISTANT_TIME
void on_get_time_response(const GetTimeResponse &value) override;
void on_get_time_response(const GetTimeResponse &value);
#endif
void on_hello_request(const HelloRequest &msg) override;
void on_disconnect_request() override;
void on_ping_request() override;
void on_device_info_request() override;
void on_list_entities_request() override { this->begin_iterator_(ActiveIterator::LIST_ENTITIES); }
void on_subscribe_states_request() override {
void on_hello_request(const HelloRequest &msg);
void on_disconnect_request();
void on_ping_request();
void on_device_info_request();
void on_list_entities_request() { this->begin_iterator_(ActiveIterator::LIST_ENTITIES); }
void on_subscribe_states_request() {
this->flags_.state_subscription = true;
// Start initial state iterator only if no iterator is active
// If list_entities is running, we'll start initial_state when it completes
@@ -215,19 +267,25 @@ class APIConnection final : public APIServerConnectionBase {
this->begin_iterator_(ActiveIterator::INITIAL_STATE);
}
}
void on_subscribe_logs_request(const SubscribeLogsRequest &msg) override {
void on_subscribe_logs_request(const SubscribeLogsRequest &msg) {
this->flags_.log_subscription = msg.level;
if (msg.dump_config)
App.schedule_dump_config();
#ifdef USE_ESP32_CRASH_HANDLER
esp32::crash_handler_log();
#endif
#ifdef USE_RP2040_CRASH_HANDLER
rp2040::crash_handler_log();
#endif
}
#ifdef USE_API_HOMEASSISTANT_SERVICES
void on_subscribe_homeassistant_services_request() override { this->flags_.service_call_subscription = true; }
void on_subscribe_homeassistant_services_request() { this->flags_.service_call_subscription = true; }
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
void on_subscribe_home_assistant_states_request() override;
void on_subscribe_home_assistant_states_request();
#endif
#ifdef USE_API_USER_DEFINED_ACTIONS
void on_execute_service_request(const ExecuteServiceRequest &msg) override;
void on_execute_service_request(const ExecuteServiceRequest &msg);
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
void send_execute_service_response(uint32_t call_id, bool success, StringRef error_message);
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
@@ -237,16 +295,17 @@ class APIConnection final : public APIServerConnectionBase {
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
#endif
#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);
#endif
bool is_authenticated() override {
bool is_authenticated() {
return static_cast<ConnectionState>(this->flags_.connection_state) == ConnectionState::AUTHENTICATED;
}
bool is_connection_setup() override {
bool is_connection_setup() {
return static_cast<ConnectionState>(this->flags_.connection_state) == ConnectionState::CONNECTED ||
this->is_authenticated();
}
bool is_marked_for_removal() const { return this->flags_.remove; }
uint8_t get_log_subscription_level() const { return this->flags_.log_subscription; }
// Get client API version for feature detection
@@ -255,29 +314,47 @@ class APIConnection final : public APIServerConnectionBase {
(this->client_api_version_major_ == major && this->client_api_version_minor_ >= minor);
}
void on_fatal_error() override;
void on_no_setup_connection() override;
bool send_message_impl(const ProtoMessage &msg, uint8_t message_type) override;
void on_fatal_error();
void on_no_setup_connection();
void prepare_first_message_buffer(std::vector<uint8_t> &shared_buf, size_t header_padding, size_t total_size) {
// Function pointer type for type-erased message encoding
using MessageEncodeFn = void (*)(const void *, ProtoWriteBuffer &);
// Function pointer type for type-erased size calculation
using CalculateSizeFn = uint32_t (*)(const void *);
template<typename T> bool send_message(const T &msg) {
if constexpr (T::ESTIMATED_SIZE == 0) {
return this->send_message_(0, T::MESSAGE_TYPE, &encode_msg_noop, &msg);
} else {
return this->send_message_(msg.calculate_size(), T::MESSAGE_TYPE, &proto_encode_msg<T>, &msg);
}
}
void prepare_first_message_buffer(APIBuffer &shared_buf, size_t header_padding, size_t total_size) {
shared_buf.clear();
// Reserve space for header padding + message + footer
// - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext)
// - Footer: space for MAC (16 bytes for Noise, 0 for Plaintext)
shared_buf.reserve(total_size);
// Resize to add header padding so message encoding starts at the correct position
shared_buf.resize(header_padding);
// Reserve full size but only set initial size to header padding
// so message encoding starts at the correct position
shared_buf.reserve_and_resize(total_size, header_padding);
}
// Convenience overload - computes frame overhead internally
void prepare_first_message_buffer(std::vector<uint8_t> &shared_buf, size_t payload_size) {
void prepare_first_message_buffer(APIBuffer &shared_buf, size_t payload_size) {
const uint8_t header_padding = this->helper_->frame_header_padding();
const uint8_t footer_size = this->helper_->frame_footer_size();
this->prepare_first_message_buffer(shared_buf, header_padding, payload_size + header_padding + footer_size);
}
bool try_to_clear_buffer(bool log_out_of_space);
bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override;
bool try_to_clear_buffer(bool log_out_of_space) {
if (this->flags_.remove)
return false;
if (this->helper_->can_write_without_blocking())
return true;
return this->try_to_clear_buffer_slow_(log_out_of_space);
}
bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type);
const char *get_name() const { return this->helper_->get_client_name(); }
/// Get peer name (IP address) into caller-provided buffer, returns buf for convenience
@@ -286,6 +363,8 @@ class APIConnection final : public APIServerConnectionBase {
}
protected:
bool try_to_clear_buffer_slow_(bool log_out_of_space);
// Helper function to handle authentication completion
void complete_authentication_();
@@ -312,50 +391,67 @@ class APIConnection final : public APIServerConnectionBase {
void process_state_subscriptions_();
#endif
// Non-template helper to encode any ProtoMessage
static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn,
uint32_t remaining_size);
// 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) {
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);
// Size thunk — converts void* back to concrete type for direct calculate_size() call
template<typename T> static uint32_t calc_size(const void *msg) {
return static_cast<const T *>(msg)->calculate_size();
}
// 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) {
// Set common fields that are shared by all entity types
msg.key = entity->get_object_id_hash();
// Shared no-op encode thunk for empty messages (ESTIMATED_SIZE == 0)
static void encode_msg_noop(const void *, ProtoWriteBuffer &) {}
// API 1.14+ clients compute object_id client-side from the entity name
// For older clients, we must send object_id for backward compatibility
// See: https://github.com/esphome/backlog/issues/76
// TODO: Remove this backward compat code before 2026.7.0 - all clients should support API 1.14 by then
// Buffer must remain in scope until encode_message_to_buffer is called
char object_id_buf[OBJECT_ID_MAX_LEN];
if (!conn->client_supports_api_version(1, 14)) {
msg.object_id = entity->get_object_id_to(object_id_buf);
// Non-template buffer management for send_message
bool send_message_(uint32_t payload_size, uint8_t message_type, MessageEncodeFn encode_fn, const void *msg);
// Non-template buffer management for batch encoding
static uint16_t encode_to_buffer(uint32_t calculated_size, MessageEncodeFn encode_fn, const void *msg,
APIConnection *conn, uint32_t remaining_size);
// Thin template wrapper — computes size, delegates buffer work to non-template helper
template<typename T> static uint16_t encode_message_to_buffer(T &msg, APIConnection *conn, uint32_t remaining_size) {
if constexpr (T::ESTIMATED_SIZE == 0) {
return encode_to_buffer(0, &encode_msg_noop, &msg, conn, remaining_size);
} else {
return encode_to_buffer(msg.calculate_size(), &proto_encode_msg<T>, &msg, conn, remaining_size);
}
}
if (entity->has_own_name()) {
msg.name = entity->get_name();
}
// Non-template core — fills state fields and encodes
static uint16_t fill_and_encode_entity_state(EntityBase *entity, StateResponseProtoMessage &msg,
CalculateSizeFn size_fn, MessageEncodeFn encode_fn, APIConnection *conn,
uint32_t remaining_size);
// Set common EntityBase properties
#ifdef USE_ENTITY_ICON
msg.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);
// Thin template wrapper
template<typename T>
static uint16_t fill_and_encode_entity_state(EntityBase *entity, T &msg, APIConnection *conn,
uint32_t remaining_size) {
return fill_and_encode_entity_state(entity, msg, &calc_size<T>, &proto_encode_msg<T>, conn, remaining_size);
}
// Non-template core — fills info fields, allocates buffers, and encodes
static uint16_t fill_and_encode_entity_info(EntityBase *entity, InfoResponseProtoMessage &msg,
CalculateSizeFn size_fn, MessageEncodeFn encode_fn, APIConnection *conn,
uint32_t remaining_size);
// Thin template wrapper
template<typename T>
static uint16_t fill_and_encode_entity_info(EntityBase *entity, T &msg, APIConnection *conn,
uint32_t remaining_size) {
return fill_and_encode_entity_info(entity, msg, &calc_size<T>, &proto_encode_msg<T>, conn, remaining_size);
}
// Non-template core — fills device_class, then delegates to fill_and_encode_entity_info
static uint16_t fill_and_encode_entity_info_with_device_class(EntityBase *entity, InfoResponseProtoMessage &msg,
StringRef &device_class_field, CalculateSizeFn size_fn,
MessageEncodeFn encode_fn, APIConnection *conn,
uint32_t remaining_size);
// Thin template wrapper
template<typename T>
static uint16_t fill_and_encode_entity_info_with_device_class(EntityBase *entity, T &msg,
StringRef &device_class_field, APIConnection *conn,
uint32_t remaining_size) {
return fill_and_encode_entity_info_with_device_class(entity, msg, device_class_field, &calc_size<T>,
&proto_encode_msg<T>, conn, remaining_size);
}
#ifdef USE_VOICE_ASSISTANT
@@ -370,6 +466,10 @@ class APIConnection final : public APIServerConnectionBase {
return this->client_supports_api_version(1, 14) ? MAX_INITIAL_PER_BATCH : MAX_INITIAL_PER_BATCH_LEGACY;
}
// Send keepalive ping or disconnect unresponsive client.
// Cold path — extracted from loop() to reduce instruction cache pressure.
void __attribute__((noinline)) check_keepalive_(uint32_t now);
// Process active iterator (list_entities/initial_state) during connection setup.
// Extracted from loop() — only runs during initial handshake, NONE in steady state.
void __attribute__((noinline)) process_active_iterator_();
@@ -485,7 +585,13 @@ class APIConnection final : public APIServerConnectionBase {
// === Optimal member ordering for 32-bit systems ===
// Group 1: Pointers (4 bytes each on 32-bit)
#if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT)
std::unique_ptr<APIFrameHelper> helper_;
#elif defined(USE_API_NOISE)
std::unique_ptr<APINoiseFrameHelper> helper_;
#elif defined(USE_API_PLAINTEXT)
std::unique_ptr<APIPlaintextFrameHelper> helper_;
#endif
APIServer *parent_;
// Group 2: Iterator union (saves ~16 bytes vs separate iterators)
@@ -538,9 +644,28 @@ class APIConnection final : public APIServerConnectionBase {
// Add item to the batch (with deduplication)
void add_item(EntityBase *entity, uint8_t message_type, uint8_t estimated_size,
uint8_t aux_data_index = AUX_DATA_UNUSED);
uint8_t aux_data_index = AUX_DATA_UNUSED) {
// Dedup: O(n) scan but optimized for RAM over performance
// Skip deduplication for events - they are edge-triggered, every occurrence matters
#ifdef USE_EVENT
if (message_type != EventResponse::MESSAGE_TYPE)
#endif
{
for (const auto &item : this->items) {
if (item.entity == entity && item.message_type == message_type)
return; // Already queued
}
}
this->items.push_back({entity, message_type, estimated_size, aux_data_index});
}
// Add item to the front of the batch (for high priority messages like ping)
void add_item_front(EntityBase *entity, uint8_t message_type, uint8_t estimated_size);
void add_item_front(EntityBase *entity, uint8_t message_type, uint8_t estimated_size) {
// Swap to front avoids expensive vector::insert which shifts all elements
this->items.push_back({entity, message_type, estimated_size, AUX_DATA_UNUSED});
if (this->items.size() > 1) {
std::swap(this->items.front(), this->items.back());
}
}
// Clear all items
void clear() {
@@ -605,7 +730,7 @@ class APIConnection final : public APIServerConnectionBase {
ActiveIterator active_iterator_{ActiveIterator::NONE};
// Total: 2 (flags) + 2 + 2 + 1 = 7 bytes, then 1 byte padding to next 4-byte boundary
uint32_t get_batch_delay_ms_() const;
uint32_t get_batch_delay_ms_() const { return this->parent_->get_batch_delay(); }
// Message will use 8 more bytes than the minimum size, and typical
// MTU is 1500. Sometimes users will see as low as 1460 MTU.
// If its IPv6 the header is 40 bytes, and if its IPv4
@@ -621,8 +746,8 @@ class APIConnection final : public APIServerConnectionBase {
bool schedule_batch_();
void process_batch_();
void process_batch_multi_(std::vector<uint8_t> &shared_buf, size_t num_items, uint8_t header_padding,
uint8_t footer_size) __attribute__((noinline));
void process_batch_multi_(APIBuffer &shared_buf, size_t num_items, uint8_t header_padding, uint8_t footer_size)
__attribute__((noinline));
void clear_batch_() {
this->deferred_batch_.clear();
this->flags_.batch_scheduled = false;
@@ -672,10 +797,8 @@ class APIConnection final : public APIServerConnectionBase {
}
// Helper function to schedule a high priority message at the front of the batch
bool schedule_message_front_(EntityBase *entity, uint8_t message_type, uint8_t estimated_size) {
this->deferred_batch_.add_item_front(entity, message_type, estimated_size);
return this->schedule_batch_();
}
// Out-of-line: callers (on_shutdown, check_keepalive_) are cold paths
bool schedule_message_front_(EntityBase *entity, uint8_t message_type, uint8_t estimated_size);
// Helper function to log client messages with name and peername
void log_client_(int level, const LogString *message);

View File

@@ -100,149 +100,61 @@ const LogString *api_error_to_logstr(APIError err) {
return LOG_STR("UNKNOWN");
}
// Default implementation for loop - handles sending buffered data
APIError APIFrameHelper::loop() {
if (this->tx_buf_count_ > 0) {
APIError err = try_send_tx_buf_();
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
return err;
APIError APIFrameHelper::drain_overflow_and_handle_errors_() {
if (this->overflow_buf_.try_drain(this->socket_.get()) == -1) {
int err = errno;
if (this->check_socket_write_err_(err) != APIError::WOULD_BLOCK) {
HELPER_LOG("Socket write failed with errno %d", err);
return APIError::SOCKET_WRITE_FAILED;
}
}
return APIError::OK; // Convert WOULD_BLOCK to OK to avoid connection termination
return APIError::OK;
}
// Common socket write error handling
APIError APIFrameHelper::handle_socket_write_error_() {
if (errno == EWOULDBLOCK || errno == EAGAIN) {
return APIError::WOULD_BLOCK;
}
HELPER_LOG("Socket write failed with errno %d", errno);
this->state_ = State::FAILED;
return APIError::SOCKET_WRITE_FAILED;
}
// Helper method to buffer data from IOVs
void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len,
uint16_t offset) {
// Check if queue is full
if (this->tx_buf_count_ >= API_MAX_SEND_QUEUE) {
HELPER_LOG("Send queue full (%u buffers), dropping connection", this->tx_buf_count_);
this->state_ = State::FAILED;
return;
}
uint16_t buffer_size = total_write_len - offset;
auto &buffer = this->tx_buf_[this->tx_buf_tail_];
buffer = std::make_unique<SendBuffer>(SendBuffer{
.data = std::make_unique<uint8_t[]>(buffer_size),
.size = buffer_size,
.offset = 0,
});
uint16_t to_skip = offset;
uint16_t write_pos = 0;
for (int i = 0; i < iovcnt; i++) {
if (to_skip >= iov[i].iov_len) {
// Skip this entire segment
to_skip -= static_cast<uint16_t>(iov[i].iov_len);
} else {
// Include this segment (partially or fully)
const uint8_t *src = reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_skip;
uint16_t len = static_cast<uint16_t>(iov[i].iov_len) - to_skip;
std::memcpy(buffer->data.get() + write_pos, src, len);
write_pos += len;
to_skip = 0;
}
}
// Update circular buffer tracking
this->tx_buf_tail_ = (this->tx_buf_tail_ + 1) % API_MAX_SEND_QUEUE;
this->tx_buf_count_++;
}
// This method writes data to socket or buffers it
// Write data to socket, overflow to backlog buffer if LWIP TCP send buffer is full.
// Returns OK if all data was sent or successfully queued.
// Returns SOCKET_WRITE_FAILED on hard error (sets state to FAILED).
APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len) {
// Returns APIError::OK if successful (or would block, but data has been buffered)
// Returns APIError::SOCKET_WRITE_FAILED if socket write failed, and sets state to FAILED
if (iovcnt == 0)
return APIError::OK; // Nothing to do, success
#ifdef HELPER_LOG_PACKETS
for (int i = 0; i < iovcnt; i++) {
LOG_PACKET_SENDING(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len);
}
#endif
// Try to send any existing buffered data first if there is any
if (this->tx_buf_count_ > 0) {
APIError send_result = try_send_tx_buf_();
// If real error occurred (not just WOULD_BLOCK), return it
if (send_result != APIError::OK && send_result != APIError::WOULD_BLOCK) {
return send_result;
}
uint16_t skip = 0;
// If there is still data in the buffer, we can't send, buffer
// the new data and return
if (this->tx_buf_count_ > 0) {
this->buffer_data_from_iov_(iov, iovcnt, total_write_len, 0);
return APIError::OK; // Success, data buffered
}
// Drain any existing backlog first
if (!this->overflow_buf_.empty()) [[unlikely]] {
APIError err = this->drain_overflow_and_handle_errors_();
if (err != APIError::OK)
return err;
}
// Try to send directly if no buffered data
// Optimize for single iovec case (common for plaintext API)
ssize_t sent =
(iovcnt == 1) ? this->socket_->write(iov[0].iov_base, iov[0].iov_len) : this->socket_->writev(iov, iovcnt);
// If backlog is clear, try direct send
if (this->overflow_buf_.empty()) [[likely]] {
ssize_t sent =
(iovcnt == 1) ? this->socket_->write(iov[0].iov_base, iov[0].iov_len) : this->socket_->writev(iov, iovcnt);
if (sent == -1) {
APIError err = this->handle_socket_write_error_();
if (err == APIError::WOULD_BLOCK) {
// Socket would block, buffer the data
this->buffer_data_from_iov_(iov, iovcnt, total_write_len, 0);
return APIError::OK; // Success, data buffered
}
return err; // Socket write failed
} else if (static_cast<uint16_t>(sent) < total_write_len) {
// Partially sent, buffer the remaining data
this->buffer_data_from_iov_(iov, iovcnt, total_write_len, static_cast<uint16_t>(sent));
}
return APIError::OK; // Success, all data sent or buffered
}
// Common implementation for trying to send buffered data
// IMPORTANT: Caller MUST ensure tx_buf_count_ > 0 before calling this method
APIError APIFrameHelper::try_send_tx_buf_() {
// Try to send from tx_buf - we assume it's not empty as it's the caller's responsibility to check
while (this->tx_buf_count_ > 0) {
// Get the first buffer in the queue
SendBuffer *front_buffer = this->tx_buf_[this->tx_buf_head_].get();
// Try to send the remaining data in this buffer
ssize_t sent = this->socket_->write(front_buffer->current_data(), front_buffer->remaining());
if (sent == -1) {
return this->handle_socket_write_error_();
} else if (sent == 0) {
// Nothing sent but not an error
return APIError::WOULD_BLOCK;
} else if (static_cast<uint16_t>(sent) < front_buffer->remaining()) {
// Partially sent, update offset
// Cast to ensure no overflow issues with uint16_t
front_buffer->offset += static_cast<uint16_t>(sent);
return APIError::WOULD_BLOCK; // Stop processing more buffers if we couldn't send a complete buffer
if (sent == -1) [[unlikely]] {
int err = errno;
if (this->check_socket_write_err_(err) != APIError::WOULD_BLOCK) {
HELPER_LOG("Socket write failed with errno %d", err);
return APIError::SOCKET_WRITE_FAILED;
}
} else if (static_cast<uint16_t>(sent) >= total_write_len) [[likely]] {
return APIError::OK;
} else {
// Buffer completely sent, remove it from the queue
this->tx_buf_[this->tx_buf_head_].reset();
this->tx_buf_head_ = (this->tx_buf_head_ + 1) % API_MAX_SEND_QUEUE;
this->tx_buf_count_--;
// Continue loop to try sending the next buffer
skip = static_cast<uint16_t>(sent);
}
}
return APIError::OK; // All buffers sent successfully
// Queue unsent data into overflow buffer
if (!this->overflow_buf_.enqueue_iov(iov, iovcnt, total_write_len, skip)) {
HELPER_LOG("Overflow buffer full, dropping connection");
this->state_ = State::FAILED;
return APIError::SOCKET_WRITE_FAILED;
}
return APIError::OK;
}
const char *APIFrameHelper::get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const {
@@ -278,11 +190,12 @@ APIError APIFrameHelper::init_common_() {
APIError APIFrameHelper::handle_socket_read_result_(ssize_t received) {
if (received == -1) {
if (errno == EWOULDBLOCK || errno == EAGAIN) {
const int err = errno;
if (err == EWOULDBLOCK || err == EAGAIN) {
return APIError::WOULD_BLOCK;
}
state_ = State::FAILED;
HELPER_LOG("Socket read failed with errno %d", errno);
HELPER_LOG("Socket read failed with errno %d", err);
return APIError::SOCKET_READ_FAILED;
} else if (received == 0) {
state_ = State::FAILED;

View File

@@ -5,13 +5,15 @@
#include <memory>
#include <span>
#include <utility>
#include <vector>
#include "esphome/core/defines.h"
#ifdef USE_API
#include "esphome/components/api/api_buffer.h"
#include "esphome/components/api/api_overflow_buffer.h"
#include "esphome/components/socket/socket.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
#include "proto.h"
namespace esphome::api {
@@ -29,12 +31,14 @@ static constexpr uint16_t MAX_MESSAGE_SIZE = 8192; // 8 KiB for ESP8266
static constexpr uint16_t MAX_MESSAGE_SIZE = 32768; // 32 KiB for ESP32 and other platforms
#endif
// Extra byte reserved in rx_buf_ beyond the message size so protobuf
// StringRef fields can be null-terminated in-place after decode.
static constexpr uint16_t RX_BUF_NULL_TERMINATOR = 1;
// Maximum number of messages to batch in a single write operation
// Must be >= MAX_INITIAL_PER_BATCH in api_connection.h (enforced by static_assert there)
static constexpr size_t MAX_MESSAGES_PER_BATCH = 34;
class ProtoWriteBuffer;
// Max client name length (e.g., "Home Assistant 2026.1.0.dev0" = 28 chars)
static constexpr size_t CLIENT_INFO_NAME_MAX_LEN = 32;
@@ -101,9 +105,9 @@ class APIFrameHelper {
}
virtual ~APIFrameHelper() = default;
virtual APIError init() = 0;
virtual APIError loop();
virtual APIError loop() = 0;
virtual APIError read_packet(ReadPacketBuffer *buffer) = 0;
bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 0; }
bool can_write_without_blocking() { return this->state_ == State::DATA && this->overflow_buf_.empty(); }
int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); }
APIError close() {
if (state_ == State::CLOSED)
@@ -130,34 +134,41 @@ class APIFrameHelper {
//
// For log messages: Use Nagle to coalesce multiple small log packets into
// fewer larger packets, reducing WiFi overhead. However, we limit batching
// to 3 messages to avoid excessive LWIP buffer pressure on memory-constrained
// devices like ESP8266. LWIP's TCP_OVERSIZE option coalesces the data into
// shared pbufs, but holding data too long waiting for Nagle's timer causes
// buffer exhaustion and dropped messages.
// to avoid excessive LWIP buffer pressure on memory-constrained devices.
// LWIP's TCP_OVERSIZE option coalesces the data into shared pbufs, but
// holding data too long waiting for Nagle's timer causes buffer exhaustion
// and dropped messages.
//
// Flow: Log 1 (Nagle on) -> Log 2 (Nagle on) -> Log 3 (NODELAY, flush all)
// ESP32 (TCP_SND_BUF=4×MSS+) / RP2040 (8×MSS) / LibreTiny (4×MSS): 4 logs per cycle
// ESP8266 (2×MSS): 3 logs per cycle (tightest buffers)
//
// Flow (ESP32/RP2040/LT): Log 1 (Nagle on) -> Log 2 -> Log 3 -> Log 4 (NODELAY, flush)
// Flow (ESP8266): Log 1 (Nagle on) -> Log 2 -> Log 3 (NODELAY, flush all)
//
void set_nodelay_for_message(bool is_log_message) {
if (!is_log_message) {
if (this->nodelay_state_ != NODELAY_ON) {
if (this->nodelay_counter_) {
this->set_nodelay_raw_(true);
this->nodelay_state_ = NODELAY_ON;
this->nodelay_counter_ = 0;
}
return;
}
// Log messages 1-3: state transitions -1 -> 1 -> 2 -> -1 (flush on 3rd)
if (this->nodelay_state_ == NODELAY_ON) {
// Log message: enable Nagle on first, flush after LOG_NAGLE_COUNT
if (!this->nodelay_counter_)
this->set_nodelay_raw_(false);
this->nodelay_state_ = 1;
} else if (this->nodelay_state_ >= LOG_NAGLE_COUNT) {
if (++this->nodelay_counter_ > LOG_NAGLE_COUNT) {
this->set_nodelay_raw_(true);
this->nodelay_state_ = NODELAY_ON;
} else {
this->nodelay_state_++;
this->nodelay_counter_ = 0;
}
}
virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0;
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
// Resize buffer to include footer space if needed (e.g. Noise MAC)
if (frame_footer_size_)
buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_);
MessageInfo msg{type, 0,
static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)};
return write_protobuf_messages(buffer, std::span<const MessageInfo>(&msg, 1));
}
// Write multiple protobuf messages in a single operation
// messages contains (message_type, offset, length) for each message in the buffer
// The buffer contains all messages with appropriate padding before each
@@ -174,37 +185,28 @@ class APIFrameHelper {
// rx_buf_len_ tracks bytes read so far; if non-zero, we're mid-frame
// and clearing would lose partially received data.
if (this->rx_buf_len_ == 0) {
// Use swap trick since shrink_to_fit() is non-binding and may be ignored
std::vector<uint8_t>().swap(this->rx_buf_);
this->rx_buf_.release();
}
}
protected:
// Buffer containing data to be sent
struct SendBuffer {
std::unique_ptr<uint8_t[]> data;
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
uint16_t remaining() const { return size - offset; }
const uint8_t *current_data() const { return data.get() + offset; }
};
// Drain backlogged overflow data to the socket and handle errors.
// Called when overflow_buf_.empty() is false. Out-of-line to keep the
// fast path (empty check) inline at call sites.
// Returns OK for transient errors (WOULD_BLOCK), SOCKET_WRITE_FAILED for hard errors.
APIError drain_overflow_and_handle_errors_();
// Common implementation for writing raw data to socket
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);
// Check if a socket write errno is a hard error (not WOULD_BLOCK/EAGAIN).
// Returns WOULD_BLOCK for transient errors, SOCKET_WRITE_FAILED for hard errors.
APIError check_socket_write_err_(int err) {
if (err == EWOULDBLOCK || err == EAGAIN)
return APIError::WOULD_BLOCK;
this->state_ = State::FAILED;
return APIError::SOCKET_WRITE_FAILED;
}
// Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit)
std::unique_ptr<socket::Socket> socket_;
@@ -228,9 +230,20 @@ class APIFrameHelper {
EXPLICIT_REJECT = 8, // Noise only
};
// Containers (size varies, but typically 12+ bytes on 32-bit)
std::array<std::unique_ptr<SendBuffer>, API_MAX_SEND_QUEUE> tx_buf_;
std::vector<uint8_t> rx_buf_;
// Fast inline state check for read_packet/write_protobuf_messages hot path.
// Returns OK only in DATA state; maps CLOSED/FAILED to BAD_STATE and any
// other intermediate state to WOULD_BLOCK.
inline APIError ESPHOME_ALWAYS_INLINE check_data_state_() const {
if (this->state_ == State::DATA)
return APIError::OK;
if (this->state_ == State::CLOSED || this->state_ == State::FAILED)
return APIError::BAD_STATE;
return APIError::WOULD_BLOCK;
}
// Backlog for unsent data when TCP send buffer is full (rarely used in production)
APIOverflowBuffer overflow_buf_;
APIBuffer rx_buf_;
// Client name buffer - stores name from Hello message or initial peername
char client_name_[CLIENT_INFO_NAME_MAX_LEN]{};
@@ -240,15 +253,17 @@ class APIFrameHelper {
State state_{State::INITIALIZE};
uint8_t frame_header_padding_{0};
uint8_t frame_footer_size_{0};
uint8_t tx_buf_head_{0};
uint8_t tx_buf_tail_{0};
uint8_t tx_buf_count_{0};
// Nagle batching state for log messages. NODELAY_ON (-1) means NODELAY is enabled
// (immediate send). Values 1-2 count log messages in the current Nagle batch.
// After LOG_NAGLE_COUNT logs, we switch to NODELAY to flush and reset.
static constexpr int8_t NODELAY_ON = -1;
static constexpr int8_t LOG_NAGLE_COUNT = 2;
int8_t nodelay_state_{NODELAY_ON};
// Nagle batching counter for log messages. 0 means NODELAY is enabled (immediate send).
// Values 1..LOG_NAGLE_COUNT count log messages in the current Nagle batch.
// After LOG_NAGLE_COUNT logs, we flush by re-enabling NODELAY and resetting to 0.
// ESP8266 has the tightest TCP send buffer (2×MSS) and needs conservative batching.
// ESP32 (4×MSS+), RP2040 (8×MSS), and LibreTiny (4×MSS) can coalesce more.
#ifdef USE_ESP8266
static constexpr uint8_t LOG_NAGLE_COUNT = 2;
#else
static constexpr uint8_t LOG_NAGLE_COUNT = 3;
#endif
uint8_t nodelay_counter_{0};
// Internal helper to set TCP_NODELAY socket option
void set_nodelay_raw_(bool enable) {

View File

@@ -19,7 +19,7 @@ namespace esphome::api {
static const char *const TAG = "api.noise";
#ifdef USE_ESP8266
static const char PROLOGUE_INIT[] PROGMEM = "NoiseAPIInit";
static constexpr char PROLOGUE_INIT[] PROGMEM = "NoiseAPIInit";
#else
static const char *const PROLOGUE_INIT = "NoiseAPIInit";
#endif
@@ -153,8 +153,10 @@ APIError APINoiseFrameHelper::loop() {
}
}
// Use base class implementation for buffer sending
return APIFrameHelper::loop();
if (!this->overflow_buf_.empty()) [[unlikely]] {
return this->drain_overflow_and_handle_errors_();
}
return APIError::OK;
}
/** Read a packet into the rx_buf_.
@@ -194,17 +196,20 @@ APIError APINoiseFrameHelper::try_read_frame_() {
uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2];
// Check against size limits to prevent OOM: MAX_HANDSHAKE_SIZE for handshake, MAX_MESSAGE_SIZE for data
uint16_t limit = (state_ == State::DATA) ? MAX_MESSAGE_SIZE : MAX_HANDSHAKE_SIZE;
bool is_data = (state_ == State::DATA);
uint16_t limit = is_data ? MAX_MESSAGE_SIZE : MAX_HANDSHAKE_SIZE;
if (msg_size > limit) {
state_ = State::FAILED;
HELPER_LOG("Bad packet: message size %u exceeds maximum %u", msg_size, limit);
return (state_ == State::DATA) ? APIError::BAD_DATA_PACKET : APIError::BAD_HANDSHAKE_PACKET_LEN;
return is_data ? APIError::BAD_DATA_PACKET : APIError::BAD_HANDSHAKE_PACKET_LEN;
}
// Reserve space for body
if (this->rx_buf_.size() != msg_size) {
this->rx_buf_.resize(msg_size);
}
// Reserve space for body (+ null terminator in DATA state so protobuf
// StringRef fields can be safely null-terminated in-place after decode.
// During handshake, rx_buf_.size() is used in prologue construction, so
// the buffer must be exactly msg_size to avoid prologue mismatch.)
uint16_t alloc_size = msg_size + (is_data ? RX_BUF_NULL_TERMINATOR : 0);
this->rx_buf_.resize(alloc_size);
if (rx_buf_len_ < msg_size) {
// more data to read
@@ -255,16 +260,19 @@ APIError APINoiseFrameHelper::state_action_() {
// ignore contents, may be used in future for flags
// Resize for: existing prologue + 2 size bytes + frame data
size_t old_size = this->prologue_.size();
this->prologue_.resize(old_size + 2 + this->rx_buf_.size());
this->prologue_[old_size] = (uint8_t) (this->rx_buf_.size() >> 8);
this->prologue_[old_size + 1] = (uint8_t) this->rx_buf_.size();
std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), this->rx_buf_.size());
size_t rx_size = this->rx_buf_.size();
this->prologue_.resize(old_size + 2 + rx_size);
this->prologue_[old_size] = (uint8_t) (rx_size >> 8);
this->prologue_[old_size + 1] = (uint8_t) rx_size;
if (rx_size > 0) {
std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), rx_size);
}
state_ = State::SERVER_HELLO;
}
if (state_ == State::SERVER_HELLO) {
// send server hello
const std::string &name = App.get_name();
const auto &name = App.get_name();
char mac[MAC_ADDRESS_BUFFER_SIZE];
get_mac_address_into_buffer(mac);
@@ -370,6 +378,7 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reaso
#ifdef USE_STORE_LOG_STR_IN_FLASH
// On ESP8266 with flash strings, we need to use PROGMEM-aware functions
size_t reason_len = strlen_P(reinterpret_cast<PGM_P>(reason));
reason_len = std::min(reason_len, sizeof(data) - 1);
if (reason_len > 0) {
memcpy_P(data + 1, reinterpret_cast<PGM_P>(reason), reason_len);
}
@@ -377,6 +386,7 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reaso
// Normal memory access
const char *reason_str = LOG_STR_ARG(reason);
size_t reason_len = strlen(reason_str);
reason_len = std::min(reason_len, sizeof(data) - 1);
if (reason_len > 0) {
// NOLINTNEXTLINE(bugprone-not-null-terminated-result) - binary protocol, not a C string
std::memcpy(data + 1, reason_str, reason_len);
@@ -392,14 +402,9 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reaso
state_ = orig_state;
}
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
APIError aerr = this->state_action_();
if (aerr != APIError::OK) {
APIError aerr = this->check_data_state_();
if (aerr != APIError::OK)
return aerr;
}
if (this->state_ != State::DATA) {
return APIError::WOULD_BLOCK;
}
aerr = this->try_read_frame_();
if (aerr != APIError::OK)
@@ -407,7 +412,18 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_inout(mbuf, this->rx_buf_.data(), this->rx_buf_.size(), this->rx_buf_.size());
// read_packet() must only be called in DATA state; the extra
// RX_BUF_NULL_TERMINATOR byte is only allocated in DATA state
// (see try_read_frame_), so calling this during handshake would
// underflow the size calculation below.
#ifdef ESPHOME_DEBUG_API
assert(this->state_ == State::DATA);
#endif
// rx_buf_ has RX_BUF_NULL_TERMINATOR extra byte for null termination
// (only added in DATA state — see try_read_frame_), so subtract it
// to get the actual encrypted data size for decryption.
size_t encrypted_size = this->rx_buf_.size() - RX_BUF_NULL_TERMINATOR;
noise_buffer_set_inout(mbuf, this->rx_buf_.data(), encrypted_size, encrypted_size);
int err = noise_cipherstate_decrypt(this->recv_cipher_, &mbuf);
APIError decrypt_err =
handle_noise_error_(err, LOG_STR("noise_cipherstate_decrypt"), APIError::CIPHERSTATE_DECRYPT_FAILED);
@@ -436,23 +452,10 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
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_);
MessageInfo msg{type, 0,
static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)};
return write_protobuf_messages(buffer, std::span<const MessageInfo>(&msg, 1));
}
APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) {
APIError aerr = state_action_();
if (aerr != APIError::OK) {
APIError aerr = this->check_data_state_();
if (aerr != APIError::OK)
return aerr;
}
if (state_ != State::DATA) {
return APIError::WOULD_BLOCK;
}
if (messages.empty()) {
return APIError::OK;
@@ -474,7 +477,7 @@ APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, s
// buf_start[1], buf_start[2] to be set after encryption
// Write message header (to be encrypted)
const uint8_t msg_offset = 3;
constexpr uint8_t msg_offset = 3;
buf_start[msg_offset] = static_cast<uint8_t>(msg.message_type >> 8); // type high byte
buf_start[msg_offset + 1] = static_cast<uint8_t>(msg.message_type); // type low byte
buf_start[msg_offset + 2] = static_cast<uint8_t>(msg.payload_size >> 8); // data_len high byte
@@ -563,8 +566,7 @@ APIError APINoiseFrameHelper::init_handshake_() {
if (aerr != APIError::OK)
return aerr;
// set_prologue copies it into handshakestate, so we can get rid of it now
// Use swap idiom to actually release memory (= {} only clears size, not capacity)
std::vector<uint8_t>().swap(prologue_);
prologue_.release();
err = noise_handshakestate_start(handshake_);
aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_start"), APIError::HANDSHAKESTATE_SETUP_FAILED);
@@ -574,7 +576,9 @@ APIError APINoiseFrameHelper::init_handshake_() {
}
APIError APINoiseFrameHelper::check_handshake_finished_() {
#ifdef ESPHOME_DEBUG_API
assert(state_ == State::HANDSHAKE);
#endif
int action = noise_handshakestate_get_action(handshake_);
if (action == NOISE_ACTION_READ_MESSAGE || action == NOISE_ACTION_WRITE_MESSAGE)

View File

@@ -22,7 +22,6 @@ class APINoiseFrameHelper final : public APIFrameHelper {
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_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override;
protected:
@@ -43,8 +42,8 @@ class APINoiseFrameHelper final : public APIFrameHelper {
// Reference to noise context (4 bytes on 32-bit)
APINoiseContext &ctx_;
// Vector (12 bytes on 32-bit)
std::vector<uint8_t> prologue_;
// Buffer for noise handshake prologue (released after handshake)
APIBuffer prologue_;
// NoiseProtocolId (size depends on implementation)
NoiseProtocolId nid_;

View File

@@ -64,8 +64,10 @@ APIError APIPlaintextFrameHelper::loop() {
if (state_ != State::DATA) {
return APIError::BAD_STATE;
}
// Use base class implementation for buffer sending
return APIFrameHelper::loop();
if (!this->overflow_buf_.empty()) [[unlikely]] {
return this->drain_overflow_and_handle_errors_();
}
return APIError::OK;
}
/** Read a packet into the rx_buf_.
@@ -128,45 +130,44 @@ APIError APIPlaintextFrameHelper::try_read_frame_() {
// 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);
// rx_header_buf_pos_ >= 3 and varint_pos == 1, so len >= 2
auto msg_size_varint = ProtoVarInt::parse_non_empty(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos);
if (!msg_size_varint.has_value()) {
// not enough data there yet
continue;
}
if (msg_size_varint->as_uint32() > MAX_MESSAGE_SIZE) {
if (msg_size_varint.value > MAX_MESSAGE_SIZE) {
state_ = State::FAILED;
HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum %u", msg_size_varint->as_uint32(),
MAX_MESSAGE_SIZE);
HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum %u",
static_cast<uint32_t>(msg_size_varint.value), MAX_MESSAGE_SIZE);
return APIError::BAD_DATA_PACKET;
}
rx_header_parsed_len_ = msg_size_varint->as_uint16();
rx_header_parsed_len_ = static_cast<uint16_t>(msg_size_varint.value);
// Move to next varint position
varint_pos += consumed;
varint_pos += msg_size_varint.consumed;
auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos, &consumed);
auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos);
if (!msg_type_varint.has_value()) {
// not enough data there yet
continue;
}
if (msg_type_varint->as_uint32() > std::numeric_limits<uint16_t>::max()) {
if (msg_type_varint.value > 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());
HELPER_LOG("Bad packet: message type %" PRIu32 " exceeds maximum %u",
static_cast<uint32_t>(msg_type_varint.value), std::numeric_limits<uint16_t>::max());
return APIError::BAD_DATA_PACKET;
}
rx_header_parsed_type_ = msg_type_varint->as_uint16();
rx_header_parsed_type_ = static_cast<uint16_t>(msg_type_varint.value);
rx_header_parsed_ = true;
}
// header reading done
// Reserve space for body
if (this->rx_buf_.size() != this->rx_header_parsed_len_) {
this->rx_buf_.resize(this->rx_header_parsed_len_);
}
// Reserve space for body (+ null terminator so protobuf StringRef fields
// can be safely null-terminated in-place after decode)
this->rx_buf_.resize(this->rx_header_parsed_len_ + RX_BUF_NULL_TERMINATOR);
if (rx_buf_len_ < rx_header_parsed_len_) {
// more data to read
@@ -194,11 +195,11 @@ APIError APIPlaintextFrameHelper::try_read_frame_() {
}
APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
if (this->state_ != State::DATA) {
return APIError::WOULD_BLOCK;
}
APIError aerr = this->check_data_state_();
if (aerr != APIError::OK)
return aerr;
APIError aerr = this->try_read_frame_();
aerr = this->try_read_frame_();
if (aerr != APIError::OK) {
if (aerr == APIError::BAD_INDICATOR) {
// Make sure to tell the remote that we don't
@@ -236,16 +237,11 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
buffer->type = this->rx_header_parsed_type_;
return APIError::OK;
}
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
MessageInfo msg{type, 0, static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_)};
return write_protobuf_messages(buffer, std::span<const MessageInfo>(&msg, 1));
}
APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer,
std::span<const MessageInfo> messages) {
if (state_ != State::DATA) {
return APIError::BAD_STATE;
}
APIError aerr = this->check_data_state_();
if (aerr != APIError::OK)
return aerr;
if (messages.empty()) {
return APIError::OK;
@@ -258,9 +254,11 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe
uint16_t total_write_len = 0;
for (const auto &msg : messages) {
// Calculate varint sizes for header layout
uint8_t size_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(msg.payload_size));
uint8_t type_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(msg.message_type));
// Calculate varint sizes for header layout using inline ternary to avoid varint_slow call overhead
uint8_t size_varint_len = msg.payload_size < ProtoSize::VARINT_THRESHOLD_1_BYTE
? 1
: (msg.payload_size < ProtoSize::VARINT_THRESHOLD_2_BYTE ? 2 : 3);
uint8_t type_varint_len = msg.message_type < ProtoSize::VARINT_THRESHOLD_1_BYTE ? 1 : 2;
uint8_t total_header_len = 1 + size_varint_len + type_varint_len;
// Calculate where to start writing the header
@@ -282,8 +280,8 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe
//
// 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)
// [1-3] - Payload size varint (3 bytes, for sizes 16384-65535)
// [4-5] - Message type varint (2 bytes, for types 128-16383)
// [6...] - Actual payload data
//
// The message starts at offset + frame_header_padding_

View File

@@ -19,7 +19,6 @@ class APIPlaintextFrameHelper final : public APIFrameHelper {
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_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override;
protected:

View File

@@ -90,4 +90,10 @@ extend google.protobuf.FieldOptions {
// - uint16_t <field>_length_{0};
// - uint16_t <field>_count_{0};
optional bool packed_buffer = 50015 [default=false];
// force: Always encode this field, even when its value equals the proto3 default.
// Skips the zero/empty check in calculate_size() and encode(), using the _force
// variant of the calc_ method. Use on fields that are almost always non-default
// to eliminate dead branches on hot paths.
optional bool force = 50016 [default=false];
}

View File

@@ -0,0 +1,73 @@
#include "api_overflow_buffer.h"
#ifdef USE_API
#include <cstring>
namespace esphome::api {
APIOverflowBuffer::~APIOverflowBuffer() {
for (auto *entry : this->queue_) {
if (entry != nullptr)
Entry::destroy(entry);
}
}
ssize_t APIOverflowBuffer::try_drain(socket::Socket *socket) {
while (this->count_ > 0) {
Entry *front = this->queue_[this->head_];
ssize_t sent = socket->write(front->current_data(), front->remaining());
if (sent <= 0) {
// -1 = error (caller checks errno for EWOULDBLOCK vs hard error)
// 0 = nothing sent (treat as no progress)
return sent;
}
if (static_cast<uint16_t>(sent) < front->remaining()) {
// Partially sent, update offset and stop
front->offset += static_cast<uint16_t>(sent);
return sent;
}
// Entry fully sent — free it and advance
Entry::destroy(front);
this->queue_[this->head_] = nullptr;
this->head_ = (this->head_ + 1) % API_MAX_SEND_QUEUE;
this->count_--;
}
return 0; // All drained
}
bool APIOverflowBuffer::enqueue_iov(const struct iovec *iov, int iovcnt, uint16_t total_len, uint16_t skip) {
if (this->count_ >= API_MAX_SEND_QUEUE)
return false;
uint16_t buffer_size = total_len - skip;
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
auto *entry = new Entry{new uint8_t[buffer_size], buffer_size, 0};
this->queue_[this->tail_] = entry;
uint16_t to_skip = skip;
uint16_t write_pos = 0;
for (int i = 0; i < iovcnt; i++) {
if (to_skip >= iov[i].iov_len) {
to_skip -= static_cast<uint16_t>(iov[i].iov_len);
} else {
const uint8_t *src = reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_skip;
uint16_t len = static_cast<uint16_t>(iov[i].iov_len) - to_skip;
std::memcpy(entry->data + write_pos, src, len);
write_pos += len;
to_skip = 0;
}
}
this->tail_ = (this->tail_ + 1) % API_MAX_SEND_QUEUE;
this->count_++;
return true;
}
} // namespace esphome::api
#endif // USE_API

View File

@@ -0,0 +1,76 @@
#pragma once
#include <array>
#include <cstdint>
#include <sys/types.h>
#include "esphome/core/defines.h"
#ifdef USE_API
#include "esphome/components/socket/headers.h"
#include "esphome/components/socket/socket.h"
#include "esphome/core/helpers.h"
namespace esphome::api {
/// Circular queue of heap-allocated byte buffers used as a TCP send backlog.
///
/// Under normal operation this buffer is **never used** — data goes straight
/// from the frame helper to the socket. It only fills when the LWIP TCP
/// send buffer is full (slow client, congested network, heavy logging).
/// The queue drains automatically on subsequent write/loop calls once the
/// socket becomes writable again.
///
/// Capacity is compile-time-fixed via API_MAX_SEND_QUEUE (set from Python
/// config). If the queue fills completely the connection is marked failed.
class APIOverflowBuffer {
public:
/// A single heap-allocated send-backlog entry.
/// Lifetime is manually managed — see destroy().
struct Entry {
uint8_t *data;
uint16_t size; // Total size of the buffer
uint16_t offset; // Current send offset within the buffer
uint16_t remaining() const { return this->size - this->offset; }
const uint8_t *current_data() const { return this->data + this->offset; }
/// Free this entry and its data buffer.
static ESPHOME_ALWAYS_INLINE void destroy(Entry *entry) {
delete[] entry->data;
delete entry; // NOLINT(cppcoreguidelines-owning-memory)
}
};
~APIOverflowBuffer();
/// True when no backlogged data is waiting.
bool empty() const { return this->count_ == 0; }
/// True when the queue has no room for another entry.
bool full() const { return this->count_ >= API_MAX_SEND_QUEUE; }
/// Number of entries currently queued.
uint8_t count() const { return this->count_; }
/// Try to drain queued data to the socket.
/// Returns bytes-written > 0 on success/partial, 0 if all drained or no progress,
/// -1 on error (caller must check errno to distinguish EWOULDBLOCK from hard errors).
/// Callers only need to act on -1; 0 and positive values both mean "no error".
/// Frees entries as they are fully sent.
ssize_t try_drain(socket::Socket *socket);
/// Enqueue unsent IOV data into the backlog.
/// Copies iov data starting at byte offset `skip` into a new entry.
/// Returns false if the queue is full (caller should fail the connection).
bool enqueue_iov(const struct iovec *iov, int iovcnt, uint16_t total_len, uint16_t skip);
protected:
std::array<Entry *, API_MAX_SEND_QUEUE> queue_{};
uint8_t head_{0};
uint8_t tail_{0};
uint8_t count_{0};
};
} // namespace esphome::api
#endif // USE_API

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,12 @@
// This file was automatically generated with a tool.
// See script/api_protobuf/api_protobuf.py
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_BLUETOOTH_PROXY
#ifndef USE_API_VARINT64
#define USE_API_VARINT64
#endif
#endif
namespace esphome::api {} // namespace esphome::api

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
// This file was automatically generated with a tool.
// See script/api_protobuf/api_protobuf.py
#include "api_pb2_service.h"
#include "api_connection.h"
#include "esphome/core/log.h"
namespace esphome::api {
@@ -8,8 +9,8 @@ namespace esphome::api {
static const char *const TAG = "api.service";
#ifdef HAS_PROTO_MESSAGE_DUMP
void APIServerConnectionBase::log_send_message_(const char *name, const char *dump) {
ESP_LOGVV(TAG, "send_message %s: %s", name, dump);
void APIServerConnectionBase::log_send_message_(const LogString *name, const char *dump) {
ESP_LOGVV(TAG, "send_message %s: %s", LOG_STR_ARG(name), dump);
}
void APIServerConnectionBase::log_receive_message_(const LogString *name, const ProtoMessage &msg) {
DumpBuffer dump_buf;
@@ -20,7 +21,7 @@ void APIServerConnectionBase::log_receive_message_(const LogString *name) {
}
#endif
void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {
void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {
// Check authentication/connection requirements
switch (msg_type) {
case HelloRequest::MESSAGE_TYPE: // No setup required
@@ -634,6 +635,72 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
this->on_infrared_rf_transmit_raw_timings_request(msg);
break;
}
#endif
#ifdef USE_SERIAL_PROXY
case SerialProxyConfigureRequest::MESSAGE_TYPE: {
SerialProxyConfigureRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_serial_proxy_configure_request"), msg);
#endif
this->on_serial_proxy_configure_request(msg);
break;
}
#endif
#ifdef USE_SERIAL_PROXY
case SerialProxyWriteRequest::MESSAGE_TYPE: {
SerialProxyWriteRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_serial_proxy_write_request"), msg);
#endif
this->on_serial_proxy_write_request(msg);
break;
}
#endif
#ifdef USE_SERIAL_PROXY
case SerialProxySetModemPinsRequest::MESSAGE_TYPE: {
SerialProxySetModemPinsRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_serial_proxy_set_modem_pins_request"), msg);
#endif
this->on_serial_proxy_set_modem_pins_request(msg);
break;
}
#endif
#ifdef USE_SERIAL_PROXY
case SerialProxyGetModemPinsRequest::MESSAGE_TYPE: {
SerialProxyGetModemPinsRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_serial_proxy_get_modem_pins_request"), msg);
#endif
this->on_serial_proxy_get_modem_pins_request(msg);
break;
}
#endif
#ifdef USE_SERIAL_PROXY
case SerialProxyRequest::MESSAGE_TYPE: {
SerialProxyRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_serial_proxy_request"), msg);
#endif
this->on_serial_proxy_request(msg);
break;
}
#endif
#ifdef USE_BLUETOOTH_PROXY
case BluetoothSetConnectionParamsRequest::MESSAGE_TYPE: {
BluetoothSetConnectionParamsRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_bluetooth_set_connection_params_request"), msg);
#endif
this->on_bluetooth_set_connection_params_request(msg);
break;
}
#endif
default:
break;

View File

@@ -8,224 +8,234 @@
namespace esphome::api {
class APIServerConnectionBase : public ProtoService {
class APIServerConnectionBase {
public:
#ifdef HAS_PROTO_MESSAGE_DUMP
protected:
void log_send_message_(const char *name, const char *dump);
void log_send_message_(const LogString *name, const char *dump);
void log_receive_message_(const LogString *name, const ProtoMessage &msg);
void log_receive_message_(const LogString *name);
public:
#endif
bool send_message(const ProtoMessage &msg, uint8_t message_type) {
#ifdef HAS_PROTO_MESSAGE_DUMP
DumpBuffer dump_buf;
this->log_send_message_(msg.message_name(), msg.dump_to(dump_buf));
#endif
return this->send_message_impl(msg, message_type);
}
void on_hello_request(const HelloRequest &value){};
virtual void on_hello_request(const HelloRequest &value){};
void on_disconnect_request(){};
void on_disconnect_response(){};
void on_ping_request(){};
void on_ping_response(){};
void on_device_info_request(){};
virtual void on_disconnect_request(){};
virtual void on_disconnect_response(){};
virtual void on_ping_request(){};
virtual void on_ping_response(){};
virtual void on_device_info_request(){};
void on_list_entities_request(){};
virtual void on_list_entities_request(){};
virtual void on_subscribe_states_request(){};
void on_subscribe_states_request(){};
#ifdef USE_COVER
virtual void on_cover_command_request(const CoverCommandRequest &value){};
void on_cover_command_request(const CoverCommandRequest &value){};
#endif
#ifdef USE_FAN
virtual void on_fan_command_request(const FanCommandRequest &value){};
void on_fan_command_request(const FanCommandRequest &value){};
#endif
#ifdef USE_LIGHT
virtual void on_light_command_request(const LightCommandRequest &value){};
void on_light_command_request(const LightCommandRequest &value){};
#endif
#ifdef USE_SWITCH
virtual void on_switch_command_request(const SwitchCommandRequest &value){};
void on_switch_command_request(const SwitchCommandRequest &value){};
#endif
virtual void on_subscribe_logs_request(const SubscribeLogsRequest &value){};
void on_subscribe_logs_request(const SubscribeLogsRequest &value){};
#ifdef USE_API_NOISE
virtual void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &value){};
void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &value){};
#endif
#ifdef USE_API_HOMEASSISTANT_SERVICES
virtual void on_subscribe_homeassistant_services_request(){};
void on_subscribe_homeassistant_services_request(){};
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
virtual void on_homeassistant_action_response(const HomeassistantActionResponse &value){};
void on_homeassistant_action_response(const HomeassistantActionResponse &value){};
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
virtual void on_subscribe_home_assistant_states_request(){};
void on_subscribe_home_assistant_states_request(){};
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
virtual void on_home_assistant_state_response(const HomeAssistantStateResponse &value){};
void on_home_assistant_state_response(const HomeAssistantStateResponse &value){};
#endif
virtual void on_get_time_response(const GetTimeResponse &value){};
void on_get_time_response(const GetTimeResponse &value){};
#ifdef USE_API_USER_DEFINED_ACTIONS
virtual void on_execute_service_request(const ExecuteServiceRequest &value){};
void on_execute_service_request(const ExecuteServiceRequest &value){};
#endif
#ifdef USE_CAMERA
virtual void on_camera_image_request(const CameraImageRequest &value){};
void on_camera_image_request(const CameraImageRequest &value){};
#endif
#ifdef USE_CLIMATE
virtual void on_climate_command_request(const ClimateCommandRequest &value){};
void on_climate_command_request(const ClimateCommandRequest &value){};
#endif
#ifdef USE_WATER_HEATER
virtual void on_water_heater_command_request(const WaterHeaterCommandRequest &value){};
void on_water_heater_command_request(const WaterHeaterCommandRequest &value){};
#endif
#ifdef USE_NUMBER
virtual void on_number_command_request(const NumberCommandRequest &value){};
void on_number_command_request(const NumberCommandRequest &value){};
#endif
#ifdef USE_SELECT
virtual void on_select_command_request(const SelectCommandRequest &value){};
void on_select_command_request(const SelectCommandRequest &value){};
#endif
#ifdef USE_SIREN
virtual void on_siren_command_request(const SirenCommandRequest &value){};
void on_siren_command_request(const SirenCommandRequest &value){};
#endif
#ifdef USE_LOCK
virtual void on_lock_command_request(const LockCommandRequest &value){};
void on_lock_command_request(const LockCommandRequest &value){};
#endif
#ifdef USE_BUTTON
virtual void on_button_command_request(const ButtonCommandRequest &value){};
void on_button_command_request(const ButtonCommandRequest &value){};
#endif
#ifdef USE_MEDIA_PLAYER
virtual void on_media_player_command_request(const MediaPlayerCommandRequest &value){};
void on_media_player_command_request(const MediaPlayerCommandRequest &value){};
#endif
#ifdef USE_BLUETOOTH_PROXY
virtual void on_subscribe_bluetooth_le_advertisements_request(
const SubscribeBluetoothLEAdvertisementsRequest &value){};
void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &value){};
#endif
#ifdef USE_BLUETOOTH_PROXY
virtual void on_bluetooth_device_request(const BluetoothDeviceRequest &value){};
void on_bluetooth_device_request(const BluetoothDeviceRequest &value){};
#endif
#ifdef USE_BLUETOOTH_PROXY
virtual void on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &value){};
void on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &value){};
#endif
#ifdef USE_BLUETOOTH_PROXY
virtual void on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &value){};
void on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &value){};
#endif
#ifdef USE_BLUETOOTH_PROXY
virtual void on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &value){};
void on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &value){};
#endif
#ifdef USE_BLUETOOTH_PROXY
virtual void on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &value){};
void on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &value){};
#endif
#ifdef USE_BLUETOOTH_PROXY
virtual void on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &value){};
void on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &value){};
#endif
#ifdef USE_BLUETOOTH_PROXY
virtual void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &value){};
void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &value){};
#endif
#ifdef USE_BLUETOOTH_PROXY
virtual void on_subscribe_bluetooth_connections_free_request(){};
void on_subscribe_bluetooth_connections_free_request(){};
#endif
#ifdef USE_BLUETOOTH_PROXY
virtual void on_unsubscribe_bluetooth_le_advertisements_request(){};
void on_unsubscribe_bluetooth_le_advertisements_request(){};
#endif
#ifdef USE_BLUETOOTH_PROXY
virtual void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &value){};
void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &value){};
#endif
#ifdef USE_VOICE_ASSISTANT
virtual void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &value){};
void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &value){};
#endif
#ifdef USE_VOICE_ASSISTANT
virtual void on_voice_assistant_response(const VoiceAssistantResponse &value){};
void on_voice_assistant_response(const VoiceAssistantResponse &value){};
#endif
#ifdef USE_VOICE_ASSISTANT
virtual void on_voice_assistant_event_response(const VoiceAssistantEventResponse &value){};
void on_voice_assistant_event_response(const VoiceAssistantEventResponse &value){};
#endif
#ifdef USE_VOICE_ASSISTANT
virtual void on_voice_assistant_audio(const VoiceAssistantAudio &value){};
void on_voice_assistant_audio(const VoiceAssistantAudio &value){};
#endif
#ifdef USE_VOICE_ASSISTANT
virtual void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &value){};
void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &value){};
#endif
#ifdef USE_VOICE_ASSISTANT
virtual void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &value){};
void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &value){};
#endif
#ifdef USE_VOICE_ASSISTANT
virtual void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &value){};
void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &value){};
#endif
#ifdef USE_VOICE_ASSISTANT
virtual void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &value){};
void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &value){};
#endif
#ifdef USE_ALARM_CONTROL_PANEL
virtual void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &value){};
void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &value){};
#endif
#ifdef USE_TEXT
virtual void on_text_command_request(const TextCommandRequest &value){};
void on_text_command_request(const TextCommandRequest &value){};
#endif
#ifdef USE_DATETIME_DATE
virtual void on_date_command_request(const DateCommandRequest &value){};
void on_date_command_request(const DateCommandRequest &value){};
#endif
#ifdef USE_DATETIME_TIME
virtual void on_time_command_request(const TimeCommandRequest &value){};
void on_time_command_request(const TimeCommandRequest &value){};
#endif
#ifdef USE_VALVE
virtual void on_valve_command_request(const ValveCommandRequest &value){};
void on_valve_command_request(const ValveCommandRequest &value){};
#endif
#ifdef USE_DATETIME_DATETIME
virtual void on_date_time_command_request(const DateTimeCommandRequest &value){};
void on_date_time_command_request(const DateTimeCommandRequest &value){};
#endif
#ifdef USE_UPDATE
virtual void on_update_command_request(const UpdateCommandRequest &value){};
void on_update_command_request(const UpdateCommandRequest &value){};
#endif
#ifdef USE_ZWAVE_PROXY
virtual void on_z_wave_proxy_frame(const ZWaveProxyFrame &value){};
void on_z_wave_proxy_frame(const ZWaveProxyFrame &value){};
#endif
#ifdef USE_ZWAVE_PROXY
virtual void on_z_wave_proxy_request(const ZWaveProxyRequest &value){};
void on_z_wave_proxy_request(const ZWaveProxyRequest &value){};
#endif
#ifdef USE_IR_RF
virtual void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &value){};
void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &value){};
#endif
protected:
void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;
#ifdef USE_SERIAL_PROXY
void on_serial_proxy_configure_request(const SerialProxyConfigureRequest &value){};
#endif
#ifdef USE_SERIAL_PROXY
void on_serial_proxy_write_request(const SerialProxyWriteRequest &value){};
#endif
#ifdef USE_SERIAL_PROXY
void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &value){};
#endif
#ifdef USE_SERIAL_PROXY
void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &value){};
#endif
#ifdef USE_SERIAL_PROXY
void on_serial_proxy_request(const SerialProxyRequest &value){};
#endif
#ifdef USE_BLUETOOTH_PROXY
void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value){};
#endif
};
} // namespace esphome::api

View File

@@ -28,10 +28,12 @@ static const char *const TAG = "api";
// APIServer
APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
APIServer::APIServer() {
global_api_server = this;
// Pre-allocate shared write buffer
shared_write_buffer_.reserve(64);
APIServer::APIServer() { global_api_server = this; }
void APIServer::socket_failed_(const LogString *msg) {
ESP_LOGW(TAG, "Socket %s: errno %d", LOG_STR_ARG(msg), errno);
this->destroy_socket_();
this->mark_failed();
}
void APIServer::setup() {
@@ -44,30 +46,26 @@ void APIServer::setup() {
#ifndef USE_API_NOISE_PSK_FROM_YAML
// Only load saved PSK if not set from YAML
SavedNoisePsk noise_pref_saved{};
if (this->noise_pref_.load(&noise_pref_saved)) {
if (this->load_and_apply_noise_psk_()) {
ESP_LOGD(TAG, "Loaded saved Noise PSK");
this->set_noise_psk(noise_pref_saved.psk);
}
#endif
#endif
this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections
this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0).release(); // monitored for incoming connections
if (this->socket_ == nullptr) {
ESP_LOGW(TAG, "Could not create socket");
this->mark_failed();
this->socket_failed_(LOG_STR("creation"));
return;
}
int enable = 1;
int err = this->socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
if (err != 0) {
ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err);
ESP_LOGW(TAG, "Socket reuseaddr: errno %d", errno);
// we can still continue
}
err = this->socket_->setblocking(false);
if (err != 0) {
ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err);
this->mark_failed();
this->socket_failed_(LOG_STR("nonblocking"));
return;
}
@@ -75,28 +73,28 @@ void APIServer::setup() {
socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), this->port_);
if (sl == 0) {
ESP_LOGW(TAG, "Socket unable to set sockaddr: errno %d", errno);
this->mark_failed();
this->socket_failed_(LOG_STR("set sockaddr"));
return;
}
err = this->socket_->bind((struct sockaddr *) &server, sl);
if (err != 0) {
ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno);
this->mark_failed();
this->socket_failed_(LOG_STR("bind"));
return;
}
err = this->socket_->listen(this->listen_backlog_);
if (err != 0) {
ESP_LOGW(TAG, "Socket unable to listen: errno %d", errno);
this->mark_failed();
this->socket_failed_(LOG_STR("listen"));
return;
}
#ifdef USE_LOGGER
if (logger::global_logger != nullptr) {
logger::global_logger->add_log_listener(this);
logger::global_logger->add_log_callback(
this, [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) {
static_cast<APIServer *>(self)->on_log(level, tag, message, message_len);
});
}
#endif
@@ -110,7 +108,7 @@ void APIServer::setup() {
this->last_connected_ = App.get_loop_component_start_time();
// Set warning status if reboot timeout is enabled
if (this->reboot_timeout_ != 0) {
this->status_set_warning();
this->status_set_warning(LOG_STR("waiting for client connection"));
}
}
@@ -189,7 +187,7 @@ void APIServer::remove_client_(size_t client_index) {
// Last client disconnected - set warning and start tracking for reboot timeout
if (this->clients_.empty() && this->reboot_timeout_ != 0) {
this->status_set_warning();
this->status_set_warning(LOG_STR("waiting for client connection"));
this->last_connected_ = App.get_loop_component_start_time();
}
@@ -199,7 +197,7 @@ void APIServer::remove_client_(size_t client_index) {
#endif
}
void APIServer::accept_new_connections_() {
void __attribute__((flatten)) APIServer::accept_new_connections_() {
while (true) {
struct sockaddr_storage source_addr;
socklen_t addr_len = sizeof(source_addr);
@@ -359,11 +357,11 @@ void APIServer::on_update(update::UpdateEntity *obj) {
#endif
#ifdef USE_ZWAVE_PROXY
void APIServer::on_zwave_proxy_request(const esphome::api::ProtoMessage &msg) {
void APIServer::on_zwave_proxy_request(const ZWaveProxyRequest &msg) {
// We could add code to manage a second subscription type, but, since this message type is
// very infrequent and small, we simply send it to all clients
for (auto &c : this->clients_)
c->send_message(msg, api::ZWaveProxyRequest::MESSAGE_TYPE);
c->send_message(msg);
}
#endif
@@ -433,8 +431,8 @@ void APIServer::handle_action_response(uint32_t call_id, bool success, StringRef
#ifdef USE_API_HOMEASSISTANT_STATES
// Helper to add subscription (reduces duplication)
void APIServer::add_state_subscription_(const char *entity_id, const char *attribute, std::function<void(StringRef)> f,
bool once) {
void APIServer::add_state_subscription_(const char *entity_id, const char *attribute,
std::function<void(StringRef)> &&f, bool once) {
this->state_subs_.push_back(HomeAssistantStateSubscription{
.entity_id = entity_id, .attribute = attribute, .callback = std::move(f), .once = once,
// entity_id_dynamic_storage and attribute_dynamic_storage remain nullptr (no heap allocation)
@@ -443,7 +441,7 @@ void APIServer::add_state_subscription_(const char *entity_id, const char *attri
// Helper to add subscription with heap-allocated strings (reduces duplication)
void APIServer::add_state_subscription_(std::string entity_id, optional<std::string> attribute,
std::function<void(StringRef)> f, bool once) {
std::function<void(StringRef)> &&f, bool once) {
HomeAssistantStateSubscription sub;
// Allocate heap storage for the strings
sub.entity_id_dynamic_storage = std::make_unique<std::string>(std::move(entity_id));
@@ -463,29 +461,29 @@ void APIServer::add_state_subscription_(std::string entity_id, optional<std::str
// New const char* overload (for internal components - zero allocation)
void APIServer::subscribe_home_assistant_state(const char *entity_id, const char *attribute,
std::function<void(StringRef)> f) {
std::function<void(StringRef)> &&f) {
this->add_state_subscription_(entity_id, attribute, std::move(f), false);
}
void APIServer::get_home_assistant_state(const char *entity_id, const char *attribute,
std::function<void(StringRef)> f) {
std::function<void(StringRef)> &&f) {
this->add_state_subscription_(entity_id, attribute, std::move(f), true);
}
// std::string overload with StringRef callback (zero-allocation callback)
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(StringRef)> f) {
std::function<void(StringRef)> &&f) {
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), false);
}
void APIServer::get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(StringRef)> f) {
std::function<void(StringRef)> &&f) {
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), true);
}
// Legacy helper: wraps std::string callback and delegates to StringRef version
void APIServer::add_state_subscription_(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f, bool once) {
std::function<void(const std::string &)> &&f, bool once) {
// Wrap callback to convert StringRef -> std::string, then delegate
this->add_state_subscription_(std::move(entity_id), std::move(attribute),
std::function<void(StringRef)>([f = std::move(f)](StringRef state) { f(state.str()); }),
@@ -494,12 +492,12 @@ void APIServer::add_state_subscription_(std::string entity_id, optional<std::str
// Legacy std::string overload (for custom_api_device.h - converts StringRef to std::string)
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f) {
std::function<void(const std::string &)> &&f) {
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), false);
}
void APIServer::get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f) {
std::function<void(const std::string &)> &&f) {
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), true);
}
@@ -514,7 +512,7 @@ void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeo
#ifdef USE_API_NOISE
bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg,
const LogString *fail_log_msg, const psk_t &active_psk, bool make_active) {
const LogString *fail_log_msg, bool make_active) {
if (!this->noise_pref_.save(&new_psk)) {
ESP_LOGW(TAG, "%s", LOG_STR_ARG(fail_log_msg));
return false;
@@ -526,18 +524,31 @@ bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString
}
ESP_LOGD(TAG, "%s", LOG_STR_ARG(save_log_msg));
if (make_active) {
this->set_timeout(100, [this, active_psk]() {
this->set_timeout(100, [this]() {
// Re-read the PSK from preferences rather than capturing the 32-byte array
// in the lambda (which would exceed std::function SBO and heap-allocate).
if (!this->load_and_apply_noise_psk_()) {
ESP_LOGW(TAG, "Failed to load saved PSK for activation");
return;
}
ESP_LOGW(TAG, "Disconnecting all clients to reset PSK");
this->set_noise_psk(active_psk);
for (auto &c : this->clients_) {
DisconnectRequest req;
c->send_message(req, DisconnectRequest::MESSAGE_TYPE);
c->send_message(req);
}
});
}
return true;
}
bool APIServer::load_and_apply_noise_psk_() {
SavedNoisePsk saved{};
if (!this->noise_pref_.load(&saved))
return false;
this->set_noise_psk(saved.psk);
return true;
}
bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
#ifdef USE_API_NOISE_PSK_FROM_YAML
// When PSK is set from YAML, this function should never be called
@@ -552,7 +563,7 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
}
SavedNoisePsk new_saved_psk{psk};
return this->update_noise_psk_(new_saved_psk, LOG_STR("Noise PSK saved"), LOG_STR("Failed to save Noise PSK"), psk,
return this->update_noise_psk_(new_saved_psk, LOG_STR("Noise PSK saved"), LOG_STR("Failed to save Noise PSK"),
make_active);
#endif
}
@@ -564,8 +575,7 @@ bool APIServer::clear_noise_psk(bool make_active) {
return false;
#else
SavedNoisePsk empty_psk{};
psk_t empty{};
return this->update_noise_psk_(empty_psk, LOG_STR("Noise PSK cleared"), LOG_STR("Failed to clear Noise PSK"), empty,
return this->update_noise_psk_(empty_psk, LOG_STR("Noise PSK cleared"), LOG_STR("Failed to clear Noise PSK"),
make_active);
#endif
}
@@ -582,11 +592,7 @@ void APIServer::request_time() {
}
#endif
bool APIServer::is_connected(bool state_subscription_only) const {
if (!state_subscription_only) {
return !this->clients_.empty();
}
bool APIServer::is_connected_with_state_subscription() const {
for (const auto &client : this->clients_) {
if (client->flags_.state_subscription) {
return true;
@@ -623,10 +629,7 @@ void APIServer::on_shutdown() {
this->shutting_down_ = true;
// Close the listening socket to prevent new connections
if (this->socket_) {
this->socket_->close();
this->socket_ = nullptr;
}
this->destroy_socket_();
// Change batch delay to 5ms for quick flushing during shutdown
this->batch_delay_ = 5;
@@ -634,7 +637,7 @@ void APIServer::on_shutdown() {
// Send disconnect requests to all connected clients
for (auto &c : this->clients_) {
DisconnectRequest req;
if (!c->send_message(req, DisconnectRequest::MESSAGE_TYPE)) {
if (!c->send_message(req)) {
// If we can't send the disconnect request directly (tx_buffer full),
// schedule it at the front of the batch so it will be sent with priority
c->schedule_message_front_(nullptr, DisconnectRequest::MESSAGE_TYPE, DisconnectRequest::ESTIMATED_SIZE);

View File

@@ -2,6 +2,7 @@
#include "esphome/core/defines.h"
#ifdef USE_API
#include "api_buffer.h"
#include "api_noise_context.h"
#include "api_pb2.h"
#include "api_pb2_service.h"
@@ -35,15 +36,11 @@ struct SavedNoisePsk {
} PACKED; // NOLINT
#endif
class APIServer : public Component,
public Controller
#ifdef USE_LOGGER
,
public logger::LogListener
#endif
class APIServer final : public Component,
public Controller
#ifdef USE_CAMERA
,
public camera::CameraListener
public camera::CameraListener
#endif
{
public:
@@ -56,7 +53,7 @@ class APIServer : public Component,
void on_shutdown() override;
bool teardown() override;
#ifdef USE_LOGGER
void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override;
void on_log(uint8_t level, const char *tag, const char *message, size_t message_len);
#endif
#ifdef USE_CAMERA
void on_camera_image(const std::shared_ptr<camera::CameraImage> &image) override;
@@ -69,7 +66,7 @@ class APIServer : public Component,
void set_max_connections(uint8_t max_connections) { this->max_connections_ = max_connections; }
// Get reference to shared buffer for API connections
std::vector<uint8_t> &get_shared_buffer_ref() { return shared_write_buffer_; }
APIBuffer &get_shared_buffer_ref() { return shared_write_buffer_; }
#ifdef USE_API_NOISE
bool save_noise_psk(psk_t psk, bool make_active = true);
@@ -183,13 +180,14 @@ class APIServer : public Component,
void on_update(update::UpdateEntity *obj) override;
#endif
#ifdef USE_ZWAVE_PROXY
void on_zwave_proxy_request(const esphome::api::ProtoMessage &msg);
void on_zwave_proxy_request(const ZWaveProxyRequest &msg);
#endif
#ifdef USE_IR_RF
void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, const std::vector<int32_t> *timings);
#endif
bool is_connected(bool state_subscription_only = false) const;
bool is_connected() const { return !this->clients_.empty(); }
bool is_connected_with_state_subscription() const;
#ifdef USE_API_HOMEASSISTANT_STATES
struct HomeAssistantStateSubscription {
@@ -205,20 +203,20 @@ class APIServer : public Component,
};
// New const char* overload (for internal components - zero allocation)
void subscribe_home_assistant_state(const char *entity_id, const char *attribute, std::function<void(StringRef)> f);
void get_home_assistant_state(const char *entity_id, const char *attribute, std::function<void(StringRef)> f);
void subscribe_home_assistant_state(const char *entity_id, const char *attribute, std::function<void(StringRef)> &&f);
void get_home_assistant_state(const char *entity_id, const char *attribute, std::function<void(StringRef)> &&f);
// std::string overload with StringRef callback (for custom_api_device.h with zero-allocation callback)
void subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(StringRef)> f);
std::function<void(StringRef)> &&f);
void get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(StringRef)> f);
std::function<void(StringRef)> &&f);
// Legacy std::string overload (for custom_api_device.h - converts StringRef to std::string for callback)
void subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f);
std::function<void(const std::string &)> &&f);
void get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f);
std::function<void(const std::string &)> &&f);
const std::vector<HomeAssistantStateSubscription> &get_state_subs() const;
#endif
@@ -241,20 +239,29 @@ class APIServer : public Component,
#ifdef USE_API_NOISE
bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg,
const psk_t &active_psk, bool make_active);
bool make_active);
// Load saved PSK from preferences and apply it. Returns true on success.
bool load_and_apply_noise_psk_();
#endif // USE_API_NOISE
#ifdef USE_API_HOMEASSISTANT_STATES
// Helper methods to reduce code duplication
void add_state_subscription_(const char *entity_id, const char *attribute, std::function<void(StringRef)> f,
bool once);
void add_state_subscription_(std::string entity_id, optional<std::string> attribute, std::function<void(StringRef)> f,
void add_state_subscription_(const char *entity_id, const char *attribute, std::function<void(StringRef)> &&f,
bool once);
void add_state_subscription_(std::string entity_id, optional<std::string> attribute,
std::function<void(StringRef)> &&f, bool once);
// Legacy helper: wraps std::string callback and delegates to StringRef version
void add_state_subscription_(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f, bool once);
std::function<void(const std::string &)> &&f, bool once);
#endif // USE_API_HOMEASSISTANT_STATES
// No explicit close() needed — listen sockets have no active connections on
// failure/shutdown. Destructor handles fd cleanup (close or abort per platform).
inline void destroy_socket_() {
delete this->socket_;
this->socket_ = nullptr;
}
void socket_failed_(const LogString *msg);
// Pointers and pointer-like types first (4 bytes each)
std::unique_ptr<socket::Socket> socket_ = nullptr;
socket::ListenSocket *socket_{nullptr};
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
Trigger<std::string, std::string> client_connected_trigger_;
#endif
@@ -268,7 +275,11 @@ class APIServer : public Component,
// Vectors and strings (12 bytes each on 32-bit)
std::vector<std::unique_ptr<APIConnection>> clients_;
std::vector<uint8_t> shared_write_buffer_; // Shared proto write buffer for all connections
// Shared proto write buffer for all connections.
// Not pre-allocated: all send paths call prepare_first_message_buffer() which
// reserves the exact needed size. Pre-allocating here would cause heap fragmentation
// since the buffer would almost always reallocate on first use.
APIBuffer shared_write_buffer_;
#ifdef USE_API_HOMEASSISTANT_STATES
std::vector<HomeAssistantStateSubscription> state_subs_;
#endif
@@ -316,7 +327,10 @@ template<typename... Ts> class APIConnectedCondition : public Condition<Ts...> {
TEMPLATABLE_VALUE(bool, state_subscription_only)
public:
bool check(const Ts &...x) override {
return global_api_server->is_connected(this->state_subscription_only_.value(x...));
if (this->state_subscription_only_.value(x...)) {
return global_api_server->is_connected_with_state_subscription();
}
return global_api_server->is_connected();
}
};

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
import asyncio
from datetime import datetime
import importlib
import logging
from typing import TYPE_CHECKING, Any
import warnings
@@ -18,6 +19,7 @@ import contextlib
from esphome.const import CONF_KEY, CONF_PORT, __version__
from esphome.core import CORE
from esphome.platformio_api import process_stacktrace
from . import CONF_ENCRYPTION
@@ -30,7 +32,11 @@ if TYPE_CHECKING:
_LOGGER = logging.getLogger(__name__)
async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None:
async def async_run_logs(
config: dict[str, Any],
addresses: list[str],
subscribe_states: bool = True,
) -> None:
"""Run the logs command in the event loop."""
conf = config["api"]
name = config["esphome"]["name"]
@@ -55,9 +61,19 @@ async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None:
addresses=addresses, # Pass all addresses for automatic retry
)
dashboard = CORE.dashboard
backtrace_state = False
# Try platform-specific stacktrace handler first, fall back to generic
platform_process_stacktrace = None
try:
module = importlib.import_module("esphome.components." + CORE.target_platform)
platform_process_stacktrace = getattr(module, "process_stacktrace")
except (AttributeError, ImportError):
pass
def on_log(msg: SubscribeLogsResponse) -> None:
"""Handle a new log message."""
nonlocal backtrace_state
time_ = datetime.now()
message: bytes = msg.message
text = message.decode("utf8", "backslashreplace")
@@ -67,15 +83,30 @@ async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None:
)
for parsed_msg in parse_log_message(text, timestamp):
print(parsed_msg.replace("\033", "\\033") if dashboard else parsed_msg)
for raw_line in text.splitlines():
if platform_process_stacktrace:
backtrace_state = platform_process_stacktrace(
config, raw_line, backtrace_state
)
else:
backtrace_state = process_stacktrace(
config, raw_line, backtrace_state=backtrace_state
)
stop = await async_run(cli, on_log, name=name)
stop = await async_run(cli, on_log, name=name, subscribe_states=subscribe_states)
try:
await asyncio.Event().wait()
finally:
await stop()
def run_logs(config: dict[str, Any], addresses: list[str]) -> None:
def run_logs(
config: dict[str, Any],
addresses: list[str],
subscribe_states: bool = True,
) -> None:
"""Run the logs command."""
with contextlib.suppress(KeyboardInterrupt):
asyncio.run(async_run_logs(config, addresses))
asyncio.run(
async_run_logs(config, addresses, subscribe_states=subscribe_states)
)

View File

@@ -136,8 +136,9 @@ class CustomAPIDevice {
template<typename T>
void subscribe_homeassistant_state(void (T::*callback)(StringRef), const std::string &entity_id,
const std::string &attribute = "") {
auto f = std::bind(callback, (T *) this, std::placeholders::_1);
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), std::move(f));
auto *obj = static_cast<T *>(this);
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute),
[obj, callback](StringRef state) { (obj->*callback)(state); });
}
/** Subscribe to the state (or attribute state) of an entity from Home Assistant (legacy std::string version).
@@ -148,10 +149,12 @@ class CustomAPIDevice {
ESPDEPRECATED("Use void callback(StringRef) instead. Will be removed in 2027.1.0.", "2026.1.0")
void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id,
const std::string &attribute = "") {
auto f = std::bind(callback, (T *) this, std::placeholders::_1);
auto *obj = static_cast<T *>(this);
// Explicit type to disambiguate overload resolution
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute),
std::function<void(const std::string &)>(f));
global_api_server->subscribe_home_assistant_state(
entity_id, optional<std::string>(attribute),
std::function<void(const std::string &)>(
[obj, callback](const std::string &state) { (obj->*callback)(state); }));
}
/** Subscribe to the state (or attribute state) of an entity from Home Assistant.
@@ -176,8 +179,10 @@ class CustomAPIDevice {
template<typename T>
void subscribe_homeassistant_state(void (T::*callback)(const std::string &, StringRef), const std::string &entity_id,
const std::string &attribute = "") {
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), std::move(f));
auto *obj = static_cast<T *>(this);
global_api_server->subscribe_home_assistant_state(
entity_id, optional<std::string>(attribute),
[obj, callback, entity_id](StringRef state) { (obj->*callback)(entity_id, state); });
}
/** Subscribe to the state (or attribute state) of an entity from Home Assistant (legacy std::string version).
@@ -188,10 +193,12 @@ class CustomAPIDevice {
ESPDEPRECATED("Use void callback(const std::string &, StringRef) instead. Will be removed in 2027.1.0.", "2026.1.0")
void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id,
const std::string &attribute = "") {
auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1);
auto *obj = static_cast<T *>(this);
// Explicit type to disambiguate overload resolution
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute),
std::function<void(const std::string &)>(f));
global_api_server->subscribe_home_assistant_state(
entity_id, optional<std::string>(attribute),
std::function<void(const std::string &)>(
[obj, callback, entity_id](const std::string &state) { (obj->*callback)(entity_id, state); }));
}
#else
template<typename T>

View File

@@ -130,6 +130,20 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
this->add_kv_(this->variables_, key, std::forward<V>(value));
}
#ifdef USE_ESP8266
// On ESP8266, ESPHOME_F() returns __FlashStringHelper* (PROGMEM pointer).
// Store as const char* — populate_service_map copies from PROGMEM at play() time.
template<typename V> void add_data(const __FlashStringHelper *key, V &&value) {
this->add_kv_(this->data_, reinterpret_cast<const char *>(key), std::forward<V>(value));
}
template<typename V> void add_data_template(const __FlashStringHelper *key, V &&value) {
this->add_kv_(this->data_template_, reinterpret_cast<const char *>(key), std::forward<V>(value));
}
template<typename V> void add_variable(const __FlashStringHelper *key, V &&value) {
this->add_kv_(this->variables_, reinterpret_cast<const char *>(key), std::forward<V>(value));
}
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
template<typename T> void set_response_template(T response_template) {
this->response_template_ = response_template;
@@ -221,7 +235,32 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
Ts... x) {
dest.init(source.size());
// Count non-static strings to allocate exact storage needed
#ifdef USE_ESP8266
// On ESP8266, all static strings from codegen are FLASH_STRING (PROGMEM),
// so is_static_string() is always false — the zero-copy STATIC_STRING fast
// path from the non-ESP8266 branch cannot trigger. We copy all keys and
// values unconditionally: keys via _P functions (may be in PROGMEM), values
// via value() which handles FLASH_STRING internally.
value_storage.init(source.size() * 2);
for (auto &it : source) {
auto &kv = dest.emplace_back();
// Key: copy from possible PROGMEM
{
size_t key_len = strlen_P(it.key);
value_storage.push_back(std::string(key_len, '\0'));
memcpy_P(value_storage.back().data(), it.key, key_len);
kv.key = StringRef(value_storage.back());
}
// Value: value() handles FLASH_STRING via _P functions internally
value_storage.push_back(it.value.value(x...));
kv.value = StringRef(value_storage.back());
}
#else
// On non-ESP8266, strings are directly readable from flash-mapped memory.
// Count non-static strings to allocate exact storage needed.
size_t lambda_count = 0;
for (const auto &it : source) {
if (!it.value.is_static_string()) {
@@ -235,14 +274,15 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
kv.key = StringRef(it.key);
if (it.value.is_static_string()) {
// Static string from YAML - zero allocation
// Static string — pointer directly readable, zero allocation
kv.value = StringRef(it.value.get_static_string());
} else {
// Lambda evaluation - store result, reference it
// Lambda evaluate and store result
value_storage.push_back(it.value.value(x...));
kv.value = StringRef(value_storage.back());
}
}
#endif
}
APIServer *parent_;

View File

@@ -94,7 +94,7 @@ ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(clie
#ifdef USE_API_USER_DEFINED_ACTIONS
bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) {
auto resp = service->encode_list_service_response();
return this->client_->send_message(resp, ListEntitiesServicesResponse::MESSAGE_TYPE);
return this->client_->send_message(resp);
}
#endif

View File

@@ -15,7 +15,7 @@ class APIConnection;
return this->client_->schedule_message_(entity, ResponseType::MESSAGE_TYPE, ResponseType::ESTIMATED_SIZE); \
}
class ListEntitiesIterator : public ComponentIterator {
class ListEntitiesIterator final : public ComponentIterator {
public:
ListEntitiesIterator(APIConnection *client);
#ifdef USE_BINARY_SENSOR

View File

@@ -1,5 +1,6 @@
#include "proto.h"
#include <cinttypes>
#include <cstring>
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -7,24 +8,71 @@ namespace esphome::api {
static const char *const TAG = "api.proto";
uint32_t ProtoSize::varint_slow(uint32_t value) { return varint_wide(value); }
void ProtoWriteBuffer::encode_varint_raw_slow_(uint32_t value) {
do {
this->debug_check_bounds_(1);
*this->pos_++ = static_cast<uint8_t>(value | 0x80);
value >>= 7;
} while (value > 0x7F);
this->debug_check_bounds_(1);
*this->pos_++ = static_cast<uint8_t>(value);
}
ProtoVarIntResult ProtoVarInt::parse_slow(const uint8_t *buffer, uint32_t len) {
// Multi-byte varint: first byte already checked to have high bit set
uint32_t result32 = buffer[0] & 0x7F;
#ifdef USE_API_VARINT64
uint32_t limit = std::min(len, uint32_t(4));
#else
uint32_t limit = std::min(len, uint32_t(5));
#endif
for (uint32_t i = 1; i < limit; i++) {
uint8_t val = buffer[i];
result32 |= uint32_t(val & 0x7F) << (i * 7);
if ((val & 0x80) == 0) {
return {result32, i + 1};
}
}
#ifdef USE_API_VARINT64
return parse_wide(buffer, len, result32);
#else
return {0, PROTO_VARINT_PARSE_FAILED};
#endif
}
#ifdef USE_API_VARINT64
ProtoVarIntResult ProtoVarInt::parse_wide(const uint8_t *buffer, uint32_t len, uint32_t result32) {
uint64_t result64 = result32;
uint32_t limit = std::min(len, uint32_t(10));
for (uint32_t i = 4; i < limit; i++) {
uint8_t val = buffer[i];
result64 |= uint64_t(val & 0x7F) << (i * 7);
if ((val & 0x80) == 0) {
return {result64, i + 1};
}
}
return {0, PROTO_VARINT_PARSE_FAILED};
}
#endif
uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size_t length, uint32_t target_field_id) {
uint32_t count = 0;
const uint8_t *ptr = buffer;
const uint8_t *end = buffer + length;
while (ptr < end) {
uint32_t consumed;
// Parse field header (tag)
auto res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
// Parse field header (tag) - ptr < end guarantees len >= 1
auto res = ProtoVarInt::parse_non_empty(ptr, end - ptr);
if (!res.has_value()) {
break; // Invalid data, stop counting
}
uint32_t tag = res->as_uint32();
uint32_t tag = static_cast<uint32_t>(res.value);
uint32_t field_type = tag & WIRE_TYPE_MASK;
uint32_t field_id = tag >> 3;
ptr += consumed;
ptr += res.consumed;
// Count if this is the target field
if (field_id == target_field_id) {
@@ -34,20 +82,20 @@ uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size
// Skip field data based on wire type
switch (field_type) {
case WIRE_TYPE_VARINT: { // VarInt - parse and skip
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
res = ProtoVarInt::parse(ptr, end - ptr);
if (!res.has_value()) {
return count; // Invalid data, return what we have
}
ptr += consumed;
ptr += res.consumed;
break;
}
case WIRE_TYPE_LENGTH_DELIMITED: { // Length-delimited - parse length and skip data
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
res = ProtoVarInt::parse(ptr, end - ptr);
if (!res.has_value()) {
return count;
}
uint32_t field_length = res->as_uint32();
ptr += consumed;
uint32_t field_length = static_cast<uint32_t>(res.value);
ptr += res.consumed;
if (field_length > static_cast<size_t>(end - ptr)) {
return count; // Out of bounds
}
@@ -70,46 +118,130 @@ uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size
return count;
}
// Single-pass encode for repeated submessage elements (non-template core).
// Writes field tag, reserves 1 byte for length varint, encodes the submessage body,
// then backpatches the actual length. For the common case (body < 128 bytes), this is
// just a single byte write with no memmove — all current repeated submessage types
// (BLE advertisements at ~47B, GATT descriptors at ~24B, service args, etc.) take
// this fast path.
//
// The memmove fallback for body >= 128 bytes exists only for correctness (e.g., a GATT
// characteristic with many descriptors). It is safe because calculate_size() already
// reserved space for the full multi-byte varint — the shift fills that reserved space:
//
// calculate_size() allocates per element: tag + varint_size(body) + body_size
//
// After encode, before memmove (1 byte reserved, body written):
// [tag][__][body ..... body][??]
// ^ ^-- unused byte (v2 space from calculate_size)
// len_pos
//
// After memmove(body_start+1, body_start, body_size):
// [tag][__][__][body ..... body]
// ^ ^-- body shifted forward, fills v2 space exactly
// len_pos
//
// After writing 2-byte varint at len_pos:
// [tag][v1][v2][body ..... body]
// ^-- pos_ = element end, within buffer
void ProtoWriteBuffer::encode_sub_message(uint32_t field_id, const void *value,
void (*encode_fn)(const void *, ProtoWriteBuffer &)) {
this->encode_field_raw(field_id, 2);
// Reserve 1 byte for length varint (optimistic: submessage < 128 bytes)
uint8_t *len_pos = this->pos_;
this->debug_check_bounds_(1);
this->pos_++;
uint8_t *body_start = this->pos_;
encode_fn(value, *this);
uint32_t body_size = static_cast<uint32_t>(this->pos_ - body_start);
if (body_size < 128) [[likely]] {
// Common case: 1-byte varint, just backpatch
*len_pos = static_cast<uint8_t>(body_size);
return;
}
// Compute extra bytes needed for varint beyond the 1 already reserved
uint8_t extra = ProtoSize::varint(body_size) - 1;
// Shift body forward to make room for the extra varint bytes
this->debug_check_bounds_(extra);
std::memmove(body_start + extra, body_start, body_size);
uint8_t *end = this->pos_ + extra;
// Write the full varint at len_pos
this->pos_ = len_pos;
this->encode_varint_raw(body_size);
this->pos_ = end;
}
// Non-template core for encode_optional_sub_message.
void ProtoWriteBuffer::encode_optional_sub_message(uint32_t field_id, uint32_t nested_size, const void *value,
void (*encode_fn)(const void *, ProtoWriteBuffer &)) {
if (nested_size == 0)
return;
this->encode_field_raw(field_id, 2);
this->encode_varint_raw(nested_size);
#ifdef ESPHOME_DEBUG_API
uint8_t *start = this->pos_;
encode_fn(value, *this);
if (static_cast<uint32_t>(this->pos_ - start) != nested_size)
this->debug_check_encode_size_(field_id, nested_size, this->pos_ - start);
#else
encode_fn(value, *this);
#endif
}
#ifdef ESPHOME_DEBUG_API
void ProtoWriteBuffer::debug_check_bounds_(size_t bytes, const char *caller) {
if (this->pos_ + bytes > this->buffer_->data() + this->buffer_->size()) {
ESP_LOGE(TAG, "ProtoWriteBuffer bounds check failed in %s: bytes=%zu offset=%td buf_size=%zu", caller, bytes,
this->pos_ - this->buffer_->data(), this->buffer_->size());
abort();
}
}
void ProtoWriteBuffer::debug_check_encode_size_(uint32_t field_id, uint32_t expected, ptrdiff_t actual) {
ESP_LOGE(TAG, "encode_message: size mismatch for field %" PRIu32 ": calculated=%" PRIu32 " actual=%td", field_id,
expected, actual);
abort();
}
#endif
void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
const uint8_t *ptr = buffer;
const uint8_t *end = buffer + length;
while (ptr < end) {
uint32_t consumed;
// Parse field header
auto res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
// Parse field header - ptr < end guarantees len >= 1
auto res = ProtoVarInt::parse_non_empty(ptr, end - ptr);
if (!res.has_value()) {
ESP_LOGV(TAG, "Invalid field start at offset %ld", (long) (ptr - buffer));
return;
}
uint32_t tag = res->as_uint32();
uint32_t tag = static_cast<uint32_t>(res.value);
uint32_t field_type = tag & WIRE_TYPE_MASK;
uint32_t field_id = tag >> 3;
ptr += consumed;
ptr += res.consumed;
switch (field_type) {
case WIRE_TYPE_VARINT: { // VarInt
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
res = ProtoVarInt::parse(ptr, end - ptr);
if (!res.has_value()) {
ESP_LOGV(TAG, "Invalid VarInt at offset %ld", (long) (ptr - buffer));
return;
}
if (!this->decode_varint(field_id, *res)) {
ESP_LOGV(TAG, "Cannot decode VarInt field %" PRIu32 " with value %" PRIu32 "!", field_id, res->as_uint32());
if (!this->decode_varint(field_id, res.value)) {
ESP_LOGV(TAG, "Cannot decode VarInt field %" PRIu32 " with value %" PRIu64 "!", field_id,
static_cast<uint64_t>(res.value));
}
ptr += consumed;
ptr += res.consumed;
break;
}
case WIRE_TYPE_LENGTH_DELIMITED: { // Length-delimited
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
res = ProtoVarInt::parse(ptr, end - ptr);
if (!res.has_value()) {
ESP_LOGV(TAG, "Invalid Length Delimited at offset %ld", (long) (ptr - buffer));
return;
}
uint32_t field_length = res->as_uint32();
ptr += consumed;
uint32_t field_length = static_cast<uint32_t>(res.value);
ptr += res.consumed;
if (field_length > static_cast<size_t>(end - ptr)) {
ESP_LOGV(TAG, "Out-of-bounds Length Delimited at offset %ld", (long) (ptr - buffer));
return;
@@ -125,7 +257,13 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer));
return;
}
uint32_t val = encode_uint32(ptr[3], ptr[2], ptr[1], ptr[0]);
uint32_t val;
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
// Protobuf fixed32 is little-endian — direct load on LE platforms
memcpy(&val, ptr, 4);
#else
val = encode_uint32(ptr[3], ptr[2], ptr[1], ptr[0]);
#endif
if (!this->decode_32bit(field_id, Proto32Bit(val))) {
ESP_LOGV(TAG, "Cannot decode 32-bit field %" PRIu32 " with value %" PRIu32 "!", field_id, val);
}

View File

@@ -1,8 +1,11 @@
#pragma once
#include "api_pb2_defines.h"
#include "api_buffer.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include "esphome/core/string_ref.h"
#include <cassert>
@@ -97,76 +100,60 @@ inline void encode_varint_to_buffer(uint32_t val, uint8_t *buffer) {
* within the same function scope where temporaries are created.
*/
/// Representation of a VarInt - in ProtoBuf should be 64bit but we only use 32bit
/// Type used for decoded varint values - uint64_t when BLE needs 64-bit addresses, uint32_t otherwise
#ifdef USE_API_VARINT64
using proto_varint_value_t = uint64_t;
#else
using proto_varint_value_t = uint32_t;
#endif
/// Sentinel value for consumed field indicating parse failure
inline constexpr uint32_t PROTO_VARINT_PARSE_FAILED = 0;
/// Result of parsing a varint: value + number of bytes consumed.
/// consumed == PROTO_VARINT_PARSE_FAILED indicates parse failure (not enough data or invalid).
struct ProtoVarIntResult {
proto_varint_value_t value;
uint32_t consumed; // PROTO_VARINT_PARSE_FAILED = parse failed
constexpr bool has_value() const { return this->consumed != PROTO_VARINT_PARSE_FAILED; }
};
/// Static varint parsing methods for the protobuf wire format.
class ProtoVarInt {
public:
ProtoVarInt() : value_(0) {}
explicit ProtoVarInt(uint64_t value) : value_(value) {}
/// Parse a varint from buffer. consumed must be a valid pointer (not null).
static optional<ProtoVarInt> parse(const uint8_t *buffer, uint32_t len, uint32_t *consumed) {
/// Parse a varint from buffer. Caller must ensure len >= 1.
/// Returns result with consumed=0 on failure (truncated multi-byte varint).
static inline ProtoVarIntResult ESPHOME_ALWAYS_INLINE parse_non_empty(const uint8_t *buffer, uint32_t len) {
#ifdef ESPHOME_DEBUG_API
assert(consumed != nullptr);
assert(len > 0);
#endif
// Fast path: single-byte varints (0-127) are the most common case
// (booleans, small enums, field tags, small message sizes/types).
if ((buffer[0] & 0x80) == 0) [[likely]]
return {buffer[0], 1};
return parse_slow(buffer, len);
}
/// Parse a varint from buffer (safe for empty buffers).
/// Returns result with consumed=0 on failure (empty buffer or truncated varint).
static inline ProtoVarIntResult ESPHOME_ALWAYS_INLINE parse(const uint8_t *buffer, uint32_t len) {
if (len == 0)
return {};
// Most common case: single-byte varint (values 0-127)
if ((buffer[0] & 0x80) == 0) {
*consumed = 1;
return ProtoVarInt(buffer[0]);
}
// General case for multi-byte varints
// Since we know buffer[0]'s high bit is set, initialize with its value
uint64_t result = buffer[0] & 0x7F;
uint8_t bitpos = 7;
// A 64-bit varint is at most 10 bytes (ceil(64/7)). Reject overlong encodings
// to avoid undefined behavior from shifting uint64_t by >= 64 bits.
uint32_t max_len = std::min(len, uint32_t(10));
// Start from the second byte since we've already processed the first
for (uint32_t i = 1; i < max_len; i++) {
uint8_t val = buffer[i];
result |= uint64_t(val & 0x7F) << uint64_t(bitpos);
bitpos += 7;
if ((val & 0x80) == 0) {
*consumed = i + 1;
return ProtoVarInt(result);
}
}
return {}; // Incomplete or invalid varint
}
constexpr uint16_t as_uint16() const { return this->value_; }
constexpr uint32_t as_uint32() const { return this->value_; }
constexpr uint64_t as_uint64() const { return this->value_; }
constexpr bool as_bool() const { return this->value_; }
constexpr int32_t as_int32() const {
// Not ZigZag encoded
return static_cast<int32_t>(this->as_int64());
}
constexpr int64_t as_int64() const {
// Not ZigZag encoded
return static_cast<int64_t>(this->value_);
}
constexpr int32_t as_sint32() const {
// with ZigZag encoding
return decode_zigzag32(static_cast<uint32_t>(this->value_));
}
constexpr int64_t as_sint64() const {
// with ZigZag encoding
return decode_zigzag64(this->value_);
return {0, PROTO_VARINT_PARSE_FAILED};
return parse_non_empty(buffer, len);
}
protected:
uint64_t value_;
// Slow path for multi-byte varints (>= 128), outlined to keep fast path small
static ProtoVarIntResult parse_slow(const uint8_t *buffer, uint32_t len) __attribute__((noinline));
#ifdef USE_API_VARINT64
/// Continue parsing varint bytes 4-9 with 64-bit arithmetic.
static ProtoVarIntResult parse_wide(const uint8_t *buffer, uint32_t len, uint32_t result32) __attribute__((noinline));
#endif
};
// Forward declarations for decode_to_message, encode_message and encode_packed_sint32
class ProtoDecodableMessage;
// Forward declarations for encoding helpers
class ProtoMessage;
class ProtoSize;
@@ -179,16 +166,9 @@ class ProtoLengthDelimited {
const uint8_t *data() const { return this->value_; }
size_t size() const { return this->length_; }
/**
* Decode the length-delimited data into an existing ProtoDecodableMessage instance.
*
* This method allows decoding without templates, enabling use in contexts
* where the message type is not known at compile time. The ProtoDecodableMessage's
* decode() method will be called with the raw data and length.
*
* @param msg The ProtoDecodableMessage instance to decode into
*/
void decode_to_message(ProtoDecodableMessage &msg) const;
/// Decode the length-delimited data into a message instance.
/// Template preserves concrete type so decode() resolves statically.
template<typename T> void decode_to_message(T &msg) const;
protected:
const uint8_t *const value_;
@@ -217,21 +197,24 @@ class Proto32Bit {
class ProtoWriteBuffer {
public:
ProtoWriteBuffer(std::vector<uint8_t> *buffer) : buffer_(buffer) {}
void write(uint8_t value) { this->buffer_->push_back(value); }
void encode_varint_raw(uint32_t value) {
while (value > 0x7F) {
this->buffer_->push_back(static_cast<uint8_t>(value | 0x80));
value >>= 7;
ProtoWriteBuffer(APIBuffer *buffer) : buffer_(buffer), pos_(buffer->data() + buffer->size()) {}
ProtoWriteBuffer(APIBuffer *buffer, size_t write_pos) : buffer_(buffer), pos_(buffer->data() + write_pos) {}
inline void ESPHOME_ALWAYS_INLINE encode_varint_raw(uint32_t value) {
if (value < 128) [[likely]] {
this->debug_check_bounds_(1);
*this->pos_++ = static_cast<uint8_t>(value);
return;
}
this->buffer_->push_back(static_cast<uint8_t>(value));
this->encode_varint_raw_slow_(value);
}
void encode_varint_raw_64(uint64_t value) {
while (value > 0x7F) {
this->buffer_->push_back(static_cast<uint8_t>(value | 0x80));
this->debug_check_bounds_(1);
*this->pos_++ = static_cast<uint8_t>(value | 0x80);
value >>= 7;
}
this->buffer_->push_back(static_cast<uint8_t>(value));
this->debug_check_bounds_(1);
*this->pos_++ = static_cast<uint8_t>(value);
}
/**
* Encode a field key (tag/wire type combination).
@@ -245,9 +228,32 @@ class ProtoWriteBuffer {
*
* Following https://protobuf.dev/programming-guides/encoding/#structure
*/
void encode_field_raw(uint32_t field_id, uint32_t type) {
uint32_t val = (field_id << 3) | (type & WIRE_TYPE_MASK);
this->encode_varint_raw(val);
void encode_field_raw(uint32_t field_id, uint32_t type) { this->encode_varint_raw((field_id << 3) | type); }
/// Write a single precomputed tag byte. Tag must be < 128.
inline void write_raw_byte(uint8_t b) ESPHOME_ALWAYS_INLINE {
this->debug_check_bounds_(1);
*this->pos_++ = b;
}
/// Write raw bytes to the buffer (no tag, no length prefix).
inline void encode_raw(const void *data, size_t len) ESPHOME_ALWAYS_INLINE {
this->debug_check_bounds_(len);
std::memcpy(this->pos_, data, len);
this->pos_ += len;
}
/// Write a precomputed tag byte + 32-bit value in one operation.
/// Tag must be a single-byte varint (< 128). No zero check.
inline void write_tag_and_fixed32(uint8_t tag, uint32_t value) ESPHOME_ALWAYS_INLINE {
this->debug_check_bounds_(5);
this->pos_[0] = tag;
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
std::memcpy(this->pos_ + 1, &value, 4);
#else
this->pos_[1] = static_cast<uint8_t>(value & 0xFF);
this->pos_[2] = static_cast<uint8_t>((value >> 8) & 0xFF);
this->pos_[3] = static_cast<uint8_t>((value >> 16) & 0xFF);
this->pos_[4] = static_cast<uint8_t>((value >> 24) & 0xFF);
#endif
this->pos_ += 5;
}
void encode_string(uint32_t field_id, const char *string, size_t len, bool force = false) {
if (len == 0 && !force)
@@ -255,13 +261,11 @@ class ProtoWriteBuffer {
this->encode_field_raw(field_id, 2); // type 2: Length-delimited string
this->encode_varint_raw(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);
// Direct memcpy into pre-sized buffer — avoids push_back() per-byte capacity checks
// and vector::insert() iterator overhead. ~10-11x faster for 16-32 byte strings.
this->debug_check_bounds_(len);
std::memcpy(this->pos_, string, len);
this->pos_ += len;
}
void encode_string(uint32_t field_id, const std::string &value, bool force = false) {
this->encode_string(field_id, value.data(), value.size(), force);
@@ -288,17 +292,25 @@ class ProtoWriteBuffer {
if (!value && !force)
return;
this->encode_field_raw(field_id, 0); // type 0: Varint - bool
this->buffer_->push_back(value ? 0x01 : 0x00);
this->debug_check_bounds_(1);
*this->pos_++ = value ? 0x01 : 0x00;
}
void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) {
if (value == 0 && !force)
return;
this->encode_field_raw(field_id, 5); // type 5: 32-bit fixed32
this->write((value >> 0) & 0xFF);
this->write((value >> 8) & 0xFF);
this->write((value >> 16) & 0xFF);
this->write((value >> 24) & 0xFF);
this->debug_check_bounds_(4);
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
// Protobuf fixed32 is little-endian, so direct copy works
std::memcpy(this->pos_, &value, 4);
this->pos_ += 4;
#else
*this->pos_++ = (value >> 0) & 0xFF;
*this->pos_++ = (value >> 8) & 0xFF;
*this->pos_++ = (value >> 16) & 0xFF;
*this->pos_++ = (value >> 24) & 0xFF;
#endif
}
// NOTE: Wire type 1 (64-bit fixed: double, fixed64, sfixed64) is intentionally
// not supported to reduce overhead on embedded systems. All ESPHome devices are
@@ -334,11 +346,33 @@ class ProtoWriteBuffer {
}
/// Encode a packed repeated sint32 field (zero-copy from vector)
void encode_packed_sint32(uint32_t field_id, const std::vector<int32_t> &values);
void encode_message(uint32_t field_id, const ProtoMessage &value);
std::vector<uint8_t> *get_buffer() const { return buffer_; }
/// Single-pass encode for repeated submessage elements.
/// Thin template wrapper; all buffer work is in the non-template core.
template<typename T> void encode_sub_message(uint32_t field_id, const T &value);
/// Encode an optional singular submessage field — skips if empty.
/// Thin template wrapper; all buffer work is in the non-template core.
template<typename T> void encode_optional_sub_message(uint32_t field_id, const T &value);
// Non-template core for encode_sub_message — backpatch approach.
void encode_sub_message(uint32_t field_id, const void *value, void (*encode_fn)(const void *, ProtoWriteBuffer &));
// Non-template core for encode_optional_sub_message.
void encode_optional_sub_message(uint32_t field_id, uint32_t nested_size, const void *value,
void (*encode_fn)(const void *, ProtoWriteBuffer &));
APIBuffer *get_buffer() const { return buffer_; }
protected:
std::vector<uint8_t> *buffer_;
// Slow path for encode_varint_raw values >= 128, outlined to keep fast path small
void encode_varint_raw_slow_(uint32_t value) __attribute__((noinline));
#ifdef ESPHOME_DEBUG_API
void debug_check_bounds_(size_t bytes, const char *caller = __builtin_FUNCTION());
void debug_check_encode_size_(uint32_t field_id, uint32_t expected, ptrdiff_t actual);
#else
void debug_check_bounds_([[maybe_unused]] size_t bytes) {}
#endif
APIBuffer *buffer_;
uint8_t *pos_;
};
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -378,6 +412,23 @@ class DumpBuffer {
return *this;
}
/// Append a PROGMEM string (flash-safe on ESP8266, regular append on other platforms)
DumpBuffer &append_p(const char *str) {
if (str) {
#ifdef USE_ESP8266
append_p_esp8266(str);
#else
append_impl_(str, strlen(str));
#endif
}
return *this;
}
#ifdef USE_ESP8266
/// Out-of-line ESP8266 PROGMEM append to avoid inlining strlen_P/memcpy_P at every call site
void append_p_esp8266(const char *str);
#endif
const char *c_str() const { return buf_; }
size_t size() const { return pos_; }
@@ -414,21 +465,31 @@ class DumpBuffer {
class ProtoMessage {
public:
virtual ~ProtoMessage() = default;
// Default implementation for messages with no fields
virtual void encode(ProtoWriteBuffer buffer) const {}
// Default implementation for messages with no fields
virtual void calculate_size(ProtoSize &size) const {}
// Non-virtual defaults for messages with no fields.
// Concrete message classes hide these with their own implementations.
// All call sites use templates to preserve the concrete type, so virtual
// dispatch is not needed. This eliminates per-message vtable entries for
// encode/calculate_size, saving ~1.3 KB of flash across all message types.
void encode(ProtoWriteBuffer &buffer) const {}
uint32_t calculate_size() const { return 0; }
#ifdef HAS_PROTO_MESSAGE_DUMP
virtual const char *dump_to(DumpBuffer &out) const = 0;
virtual const char *message_name() const { return "unknown"; }
virtual const LogString *message_name() const { return LOG_STR("unknown"); }
#endif
#ifndef USE_HOST
protected:
#endif
// Non-virtual destructor is protected to prevent polymorphic deletion.
// On host platform, made public to allow value-initialization of std::array
// members (e.g. DeviceInfoResponse::devices) without clang errors.
~ProtoMessage() = default;
};
// Base class for messages that support decoding
class ProtoDecodableMessage : public ProtoMessage {
public:
virtual void decode(const uint8_t *buffer, size_t length);
void decode(const uint8_t *buffer, size_t length);
/**
* Count occurrences of a repeated field in a protobuf buffer.
@@ -442,38 +503,20 @@ class ProtoDecodableMessage : public ProtoMessage {
static uint32_t count_repeated_field(const uint8_t *buffer, size_t length, uint32_t target_field_id);
protected:
virtual bool decode_varint(uint32_t field_id, ProtoVarInt value) { return false; }
~ProtoDecodableMessage() = default;
virtual bool decode_varint(uint32_t field_id, proto_varint_value_t 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; }
// 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_; }
// Varint encoding thresholds: values below each threshold fit in N bytes
static constexpr uint32_t VARINT_THRESHOLD_1_BYTE = 1 << 7; // 128
static constexpr uint32_t VARINT_THRESHOLD_2_BYTE = 1 << 14; // 16384
static constexpr uint32_t VARINT_THRESHOLD_3_BYTE = 1 << 21; // 2097152
static constexpr uint32_t VARINT_THRESHOLD_4_BYTE = 1 << 28; // 268435456
/**
* @brief Calculates the size in bytes needed to encode a uint32_t value as a varint
@@ -481,24 +524,29 @@ class ProtoSize {
* @param value The uint32_t value to calculate size for
* @return The number of bytes needed to encode the value
*/
static constexpr 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)
}
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE varint(uint32_t value) {
if (value < VARINT_THRESHOLD_1_BYTE) [[likely]]
return 1; // Fast path: 7 bits, most common case
if (__builtin_is_constant_evaluated())
return varint_wide(value);
return varint_slow(value);
}
private:
// Slow path for varint >= 128, outlined to keep fast path small
static uint32_t varint_slow(uint32_t value) __attribute__((noinline));
// Shared cascade for values >= 128 (used by both constexpr and noinline paths)
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE varint_wide(uint32_t value) {
if (value < VARINT_THRESHOLD_2_BYTE)
return 2;
if (value < VARINT_THRESHOLD_3_BYTE)
return 3;
if (value < VARINT_THRESHOLD_4_BYTE)
return 4;
return 5;
}
public:
/**
* @brief Calculates the size in bytes needed to encode a uint64_t value as a varint
*
@@ -571,312 +619,78 @@ class ProtoSize {
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);
}
// Static methods that RETURN size contribution (no ProtoSize object needed).
// Used by generated calculate_size() methods to accumulate into a plain uint32_t register.
static constexpr uint32_t calc_int32(uint32_t field_id_size, int32_t value) {
return value ? field_id_size + (value < 0 ? 10 : varint(static_cast<uint32_t>(value))) : 0;
}
/**
* @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)));
static constexpr uint32_t calc_int32_force(uint32_t field_id_size, int32_t value) {
return 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);
}
static constexpr uint32_t calc_uint32(uint32_t field_id_size, uint32_t value) {
return value ? field_id_size + varint(value) : 0;
}
/**
* @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);
static constexpr uint32_t calc_uint32_force(uint32_t field_id_size, uint32_t value) {
return 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;
}
static constexpr uint32_t calc_bool(uint32_t field_id_size, bool value) { return value ? field_id_size + 1 : 0; }
static constexpr uint32_t calc_bool_force(uint32_t field_id_size) { return field_id_size + 1; }
static constexpr uint32_t calc_float(uint32_t field_id_size, float value) {
return value != 0.0f ? field_id_size + 4 : 0;
}
/**
* @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;
static constexpr uint32_t calc_fixed32(uint32_t field_id_size, uint32_t value) {
return value ? field_id_size + 4 : 0;
}
/**
* @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;
}
static constexpr uint32_t calc_sfixed32(uint32_t field_id_size, int32_t value) {
return value ? field_id_size + 4 : 0;
}
// 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;
}
static constexpr uint32_t calc_sint32(uint32_t field_id_size, int32_t value) {
return value ? field_id_size + varint(encode_zigzag32(value)) : 0;
}
// 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;
}
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_sint32_force(uint32_t field_id_size, int32_t value) {
return field_id_size + varint(encode_zigzag32(value));
}
// 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);
}
static constexpr uint32_t calc_int64(uint32_t field_id_size, int64_t value) {
return value ? field_id_size + varint(value) : 0;
}
/**
* @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
total_size_ += field_id_size + varint(encode_zigzag32(value));
static constexpr uint32_t calc_int64_force(uint32_t field_id_size, int64_t value) {
return field_id_size + varint(value);
}
/**
* @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);
}
static constexpr uint32_t calc_uint64(uint32_t field_id_size, uint64_t value) {
return value ? field_id_size + varint(value) : 0;
}
/**
* @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);
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_uint64_force(uint32_t field_id_size, uint64_t value) {
return 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);
}
static constexpr uint32_t calc_length(uint32_t field_id_size, size_t len) {
return len ? field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len) : 0;
}
/**
* @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);
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_length_force(uint32_t field_id_size, size_t len) {
return field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len);
}
// 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);
}
static constexpr uint32_t calc_sint64(uint32_t field_id_size, int64_t value) {
return value ? field_id_size + varint(encode_zigzag64(value)) : 0;
}
/**
* @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);
static constexpr uint32_t calc_sint64_force(uint32_t field_id_size, int64_t value) {
return field_id_size + varint(encode_zigzag64(value));
}
/**
* @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);
}
static constexpr uint32_t calc_fixed64(uint32_t field_id_size, uint64_t value) {
return value ? field_id_size + 8 : 0;
}
/**
* @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;
static constexpr uint32_t calc_sfixed64(uint32_t field_id_size, int64_t value) {
return value ? field_id_size + 8 : 0;
}
/**
* @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);
static constexpr uint32_t calc_message(uint32_t field_id_size, uint32_t nested_size) {
return nested_size ? field_id_size + varint(nested_size) + nested_size : 0;
}
/**
* @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()) {
// Use the force version for all messages in the repeated field
for (const auto &message : messages) {
add_message_object_force(field_id_size, message);
}
}
}
/**
* @brief Calculates and adds the sizes of all messages in a repeated field to the total message size (FixedVector
* version)
*
* @tparam MessageType The type of the nested messages in the FixedVector
* @param messages FixedVector of message objects
*/
template<typename MessageType>
inline void add_repeated_message(uint32_t field_id_size, const FixedVector<MessageType> &messages) {
// Skip if the fixed vector is empty
if (!messages.empty()) {
// Use the force version for all messages in the repeated field
for (const auto &message : messages) {
add_message_object_force(field_id_size, message);
}
}
}
/**
* @brief Calculate size of a packed repeated sint32 field
*/
inline void add_packed_sint32(uint32_t field_id_size, const std::vector<int32_t> &values) {
if (values.empty())
return;
size_t packed_size = 0;
for (int value : values) {
packed_size += varint(encode_zigzag32(value));
}
// field_id + length varint + packed data
total_size_ += field_id_size + varint(static_cast<uint32_t>(packed_size)) + static_cast<uint32_t>(packed_size);
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_message_force(uint32_t field_id_size,
uint32_t nested_size) {
return field_id_size + varint(nested_size) + nested_size;
}
};
// Implementation of methods that depend on ProtoSize being fully defined
// Implementation of encode_packed_sint32 - must be after ProtoSize is defined
inline void ProtoWriteBuffer::encode_packed_sint32(uint32_t field_id, const std::vector<int32_t> &values) {
if (values.empty())
@@ -896,69 +710,29 @@ inline void ProtoWriteBuffer::encode_packed_sint32(uint32_t field_id, const std:
}
}
// Implementation of encode_message - must be after ProtoMessage is defined
inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value) {
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
encode_varint_to_buffer(msg_length_bytes, this->buffer_->data() + begin);
// Now encode the message content - it will append to the buffer
value.encode(*this);
#ifdef ESPHOME_DEBUG_API
// Verify that the encoded size matches what we calculated
assert(this->buffer_->size() == begin + varint_length_bytes + msg_length_bytes);
#endif
// Encode thunk — converts void* back to concrete type for direct encode() call
template<typename T> void proto_encode_msg(const void *msg, ProtoWriteBuffer &buf) {
static_cast<const T *>(msg)->encode(buf);
}
// Implementation of decode_to_message - must be after ProtoDecodableMessage is defined
inline void ProtoLengthDelimited::decode_to_message(ProtoDecodableMessage &msg) const {
// Thin template wrapper; delegates to non-template core in proto.cpp.
template<typename T> inline void ProtoWriteBuffer::encode_sub_message(uint32_t field_id, const T &value) {
this->encode_sub_message(field_id, &value, &proto_encode_msg<T>);
}
// Thin template wrapper; delegates to non-template core.
template<typename T> inline void ProtoWriteBuffer::encode_optional_sub_message(uint32_t field_id, const T &value) {
this->encode_optional_sub_message(field_id, value.calculate_size(), &value, &proto_encode_msg<T>);
}
// Template decode_to_message - preserves concrete type so decode() resolves statically
template<typename T> void ProtoLengthDelimited::decode_to_message(T &msg) const {
msg.decode(this->value_, this->length_);
}
template<typename T> const char *proto_enum_to_string(T value);
class ProtoService {
public:
protected:
virtual bool is_authenticated() = 0;
virtual bool is_connection_setup() = 0;
virtual void on_fatal_error() = 0;
virtual void on_no_setup_connection() = 0;
virtual bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) = 0;
virtual void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) = 0;
/**
* Send a protobuf message by calculating its size, allocating a buffer, encoding, and sending.
* This is the implementation method - callers should use send_message() which adds logging.
* @param msg The protobuf message to send.
* @param message_type The message type identifier.
* @return True if the message was sent successfully, false otherwise.
*/
virtual bool send_message_impl(const ProtoMessage &msg, uint8_t message_type) = 0;
// Authentication helper methods
inline bool check_connection_setup_() {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return false;
}
return true;
}
inline bool check_authenticated_() { return this->check_connection_setup_(); }
};
// ProtoService removed — its methods were inlined into APIConnection.
// APIConnection is the concrete server-side implementation; the extra virtual layer was unnecessary.
} // namespace esphome::api

View File

@@ -16,7 +16,7 @@ class APIConnection;
return this->client_->send_##entity_type##_state(entity); \
}
class InitialStateIterator : public ComponentIterator {
class InitialStateIterator final : public ComponentIterator {
public:
InitialStateIterator(APIConnection *client);
#ifdef USE_BINARY_SENSOR

View File

@@ -1,5 +1,6 @@
#include "user_services.h"
#include "esphome/core/log.h"
#include "esphome/core/string_ref.h"
namespace esphome::api {
@@ -11,6 +12,8 @@ template<> int32_t get_execute_arg_value<int32_t>(const ExecuteServiceArgument &
}
template<> float get_execute_arg_value<float>(const ExecuteServiceArgument &arg) { return arg.float_; }
template<> std::string get_execute_arg_value<std::string>(const ExecuteServiceArgument &arg) { return arg.string_; }
// Zero-copy StringRef version for YAML-generated services (string_ is null-terminated after decode)
template<> StringRef get_execute_arg_value<StringRef>(const ExecuteServiceArgument &arg) { return arg.string_; }
// Legacy std::vector versions for external components using custom_api_device.h - optimized with reserve
template<> std::vector<bool> get_execute_arg_value<std::vector<bool>>(const ExecuteServiceArgument &arg) {
@@ -61,6 +64,8 @@ template<> enums::ServiceArgType to_service_arg_type<bool>() { return enums::SER
template<> enums::ServiceArgType to_service_arg_type<int32_t>() { return enums::SERVICE_ARG_TYPE_INT; }
template<> enums::ServiceArgType to_service_arg_type<float>() { return enums::SERVICE_ARG_TYPE_FLOAT; }
template<> enums::ServiceArgType to_service_arg_type<std::string>() { return enums::SERVICE_ARG_TYPE_STRING; }
// Zero-copy StringRef version for YAML-generated services
template<> enums::ServiceArgType to_service_arg_type<StringRef>() { return enums::SERVICE_ARG_TYPE_STRING; }
// Legacy std::vector versions for external components using custom_api_device.h
template<> enums::ServiceArgType to_service_arg_type<std::vector<bool>>() { return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; }

View File

@@ -230,7 +230,7 @@ template<typename... Ts> class APIRespondAction : public Action<Ts...> {
void set_is_optional_mode(bool is_optional) { this->is_optional_mode_ = is_optional; }
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
void set_data(std::function<void(Ts..., JsonObject)> func) {
void set_data(std::function<void(Ts..., JsonObject)> &&func) {
this->json_builder_ = std::move(func);
this->has_data_ = true;
}
@@ -264,9 +264,9 @@ template<typename... Ts> class APIRespondAction : public Action<Ts...> {
// Build and send JSON response
json::JsonBuilder builder;
this->json_builder_(x..., builder.root());
std::string json_str = builder.serialize();
auto json_buf = builder.serialize();
this->parent_->send_action_response(call_id, success, StringRef(error_message),
reinterpret_cast<const uint8_t *>(json_str.data()), json_str.size());
reinterpret_cast<const uint8_t *>(json_buf.data()), json_buf.size());
return;
}
#endif

View File

@@ -307,9 +307,9 @@ void AS3935Component::tune_antenna() {
uint8_t tune_val = this->read_capacitance();
ESP_LOGI(TAG,
"Starting antenna tuning\n"
"Division Ratio is set to: %d\n"
"Internal Capacitor is set to: %d\n"
"Displaying oscillator on INT pin. Measure its frequency - multiply value by Division Ratio",
" Division Ratio is set to: %d\n"
" Internal Capacitor is set to: %d\n"
" Displaying oscillator on INT pin. Measure its frequency - multiply value by Division Ratio",
div_ratio, tune_val);
this->display_oscillator(true, ANTFREQ);
}

View File

@@ -41,7 +41,7 @@ enum AS3935RegisterMasks {
INT_MASK = 0xF0,
THRESH_MASK = 0x0F,
R_SPIKE_MASK = 0xF0,
ENERGY_MASK = 0xF0,
ENERGY_MASK = 0xE0,
CAP_MASK = 0xF0,
LIGHT_MASK = 0xCF,
DISTURB_MASK = 0xDF,

View File

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

View File

@@ -2,11 +2,9 @@ import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ANGLE,
CONF_GAIN,
CONF_ID,
CONF_MAGNITUDE,
CONF_POSITION,
CONF_STATUS,
ENTITY_CATEGORY_DIAGNOSTIC,
ICON_MAGNET,
@@ -21,9 +19,7 @@ DEPENDENCIES = ["as5600"]
AS5600Sensor = as5600_ns.class_("AS5600Sensor", sensor.Sensor, cg.PollingComponent)
CONF_RAW_ANGLE = "raw_angle"
CONF_RAW_POSITION = "raw_position"
CONF_WATCHDOG = "watchdog"
CONF_SLOW_FILTER = "slow_filter"
CONF_FAST_FILTER = "fast_filter"
CONF_PWM_FREQUENCY = "pwm_frequency"
@@ -90,18 +86,6 @@ async def to_code(config):
if out_of_range_mode_config := config.get(CONF_OUT_OF_RANGE_MODE):
cg.add(var.set_out_of_range_mode(out_of_range_mode_config))
if angle_config := config.get(CONF_ANGLE):
sens = await sensor.new_sensor(angle_config)
cg.add(var.set_angle_sensor(sens))
if raw_angle_config := config.get(CONF_RAW_ANGLE):
sens = await sensor.new_sensor(raw_angle_config)
cg.add(var.set_raw_angle_sensor(sens))
if position_config := config.get(CONF_POSITION):
sens = await sensor.new_sensor(position_config)
cg.add(var.set_position_sensor(sens))
if raw_position_config := config.get(CONF_RAW_POSITION):
sens = await sensor.new_sensor(raw_position_config)
cg.add(var.set_raw_position_sensor(sens))

View File

@@ -25,27 +25,10 @@ static const uint8_t REGISTER_MAGNITUDE = 0x1B; // 16 bytes / R
void AS5600Sensor::dump_config() {
LOG_SENSOR("", "AS5600 Sensor", this);
ESP_LOGCONFIG(TAG, " Out of Range Mode: %u", this->out_of_range_mode_);
if (this->angle_sensor_ != nullptr) {
LOG_SENSOR(" ", "Angle Sensor", this->angle_sensor_);
}
if (this->raw_angle_sensor_ != nullptr) {
LOG_SENSOR(" ", "Raw Angle Sensor", this->raw_angle_sensor_);
}
if (this->position_sensor_ != nullptr) {
LOG_SENSOR(" ", "Position Sensor", this->position_sensor_);
}
if (this->raw_position_sensor_ != nullptr) {
LOG_SENSOR(" ", "Raw Position Sensor", this->raw_position_sensor_);
}
if (this->gain_sensor_ != nullptr) {
LOG_SENSOR(" ", "Gain Sensor", this->gain_sensor_);
}
if (this->magnitude_sensor_ != nullptr) {
LOG_SENSOR(" ", "Magnitude Sensor", this->magnitude_sensor_);
}
if (this->status_sensor_ != nullptr) {
LOG_SENSOR(" ", "Status Sensor", this->status_sensor_);
}
LOG_SENSOR(" ", "Raw Position Sensor", this->raw_position_sensor_);
LOG_SENSOR(" ", "Gain Sensor", this->gain_sensor_);
LOG_SENSOR(" ", "Magnitude Sensor", this->magnitude_sensor_);
LOG_SENSOR(" ", "Status Sensor", this->status_sensor_);
LOG_UPDATE_INTERVAL(this);
}

View File

@@ -15,9 +15,6 @@ class AS5600Sensor : public PollingComponent, public Parented<AS5600Component>,
void update() override;
void dump_config() override;
void set_angle_sensor(sensor::Sensor *angle_sensor) { this->angle_sensor_ = angle_sensor; }
void set_raw_angle_sensor(sensor::Sensor *raw_angle_sensor) { this->raw_angle_sensor_ = raw_angle_sensor; }
void set_position_sensor(sensor::Sensor *position_sensor) { this->position_sensor_ = position_sensor; }
void set_raw_position_sensor(sensor::Sensor *raw_position_sensor) {
this->raw_position_sensor_ = raw_position_sensor;
}
@@ -28,9 +25,6 @@ class AS5600Sensor : public PollingComponent, public Parented<AS5600Component>,
OutRangeMode get_out_of_range_mode() { return this->out_of_range_mode_; }
protected:
sensor::Sensor *angle_sensor_{nullptr};
sensor::Sensor *raw_angle_sensor_{nullptr};
sensor::Sensor *position_sensor_{nullptr};
sensor::Sensor *raw_position_sensor_{nullptr};
sensor::Sensor *gain_sensor_{nullptr};
sensor::Sensor *magnitude_sensor_{nullptr};

View File

@@ -52,11 +52,12 @@ bool AsyncClient::connect(const char *host, uint16_t port) {
connect_cb_(connect_arg_, this);
return true;
}
if (errno != EINPROGRESS) {
ESP_LOGE(TAG, "Connect failed: %d", errno);
const int saved_errno = errno;
if (saved_errno != EINPROGRESS) {
ESP_LOGE(TAG, "Connect failed: %d", saved_errno);
close();
if (error_cb_)
error_cb_(error_arg_, this, errno);
error_cb_(error_arg_, this, saved_errno);
return false;
}
@@ -79,11 +80,12 @@ size_t AsyncClient::write(const char *data, size_t len) {
ssize_t sent = socket_->write(data, len);
if (sent < 0) {
if (errno != EAGAIN && errno != EWOULDBLOCK) {
ESP_LOGE(TAG, "Write error: %d", errno);
const int err = errno;
if (err != EAGAIN && err != EWOULDBLOCK) {
ESP_LOGE(TAG, "Write error: %d", err);
close();
if (error_cb_)
error_cb_(error_arg_, this, errno);
error_cb_(error_arg_, this, err);
}
return 0;
}
@@ -129,10 +131,11 @@ void AsyncClient::loop() {
error_cb_(error_arg_, this, error);
}
} else if (ret < 0) {
ESP_LOGE(TAG, "Select error: %d", errno);
const int err = errno;
ESP_LOGE(TAG, "Select error: %d", err);
close();
if (error_cb_)
error_cb_(error_arg_, this, errno);
error_cb_(error_arg_, this, err);
}
} else if (connected_) {
// For connected sockets, use the Application's select() results
@@ -148,11 +151,14 @@ void AsyncClient::loop() {
} else if (len > 0) {
if (data_cb_)
data_cb_(data_arg_, this, buf, len);
} else if (errno != EAGAIN && errno != EWOULDBLOCK) {
ESP_LOGW(TAG, "Read error: %d", errno);
close();
if (error_cb_)
error_cb_(error_arg_, this, errno);
} else {
const int err = errno;
if (err != EAGAIN && err != EWOULDBLOCK) {
ESP_LOGW(TAG, "Read error: %d", err);
close();
if (error_cb_)
error_cb_(error_arg_, this, err);
}
}
}
}

View File

@@ -89,6 +89,7 @@ AT581XSettingsAction = at581x_ns.class_("AT581XSettingsAction", automation.Actio
cv.Required(CONF_ID): cv.use_id(AT581XComponent),
}
),
synchronous=True,
)
async def at581x_reset_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
@@ -160,6 +161,7 @@ RADAR_SETTINGS_SCHEMA = cv.Schema(
"at581x.settings",
AT581XSettingsAction,
RADAR_SETTINGS_SCHEMA,
synchronous=True,
)
async def at581x_settings_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)

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