Compare commits

...

447 Commits

Author SHA1 Message Date
Jonathan Swoboda
5b5cede5f9 Merge pull request #12752 from esphome/bump-2025.12.3
2025.12.3
2025-12-30 09:31:31 -05:00
Jonathan Swoboda
c737033cc4 Bump version to 2025.12.3 2025-12-30 09:22:03 -05:00
J. Nick Koston
0194bfd9ea [core] Fix incremental build failures when adding components on ESP32-Arduino (#12745) 2025-12-30 09:22:03 -05:00
J. Nick Koston
339399eb70 [lvgl] Fix lambdas in canvas actions called from outside LVGL context (#12671) 2025-12-30 09:22:03 -05:00
Jonathan Swoboda
99f7e9aeb7 Merge pull request #12632 from esphome/bump-2025.12.2
2025.12.2
2025-12-23 11:17:01 -05:00
Jonathan Swoboda
ebb6babb3d Fix hash 2025-12-23 09:26:38 -05:00
Jonathan Swoboda
0922f240e0 Bump version to 2025.12.2 2025-12-23 09:23:04 -05:00
Jonathan Swoboda
c8fb694dcb [cc1101] Fix packet mode RSSI/LQI (#12630)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-23 09:23:04 -05:00
J. Nick Koston
6054685dae [esp32_camera] Throttle frame logging to reduce overhead and improve throughput (#12586) 2025-12-23 09:23:04 -05:00
Anna Oake
61ec3508ed [cc1101] Fix option defaults and move them to YAML (#12608) 2025-12-23 09:23:04 -05:00
Leo Bergolth
086ec770ea send NIL ("-") as timestamp if time source is not valid (#12588) 2025-12-23 09:23:04 -05:00
Stuart Parmenter
b055f5b4bf [hub75] Bump esp-hub75 version to 0.1.7 (#12564) 2025-12-23 09:23:00 -05:00
Eduard Llull
726db746c8 [display_menu_base] Call on_value_ after updating the select (#12584) 2025-12-23 09:21:54 -05:00
Keith Burzinski
1922455fa7 [wifi] Fix for wifi_info when static IP is configured (#12576) 2025-12-23 09:21:54 -05:00
Thomas Rupprecht
dc943d7e7a [pca9685,sx126x,sx127x] Use frequency/float_range check (#12490)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-12-23 09:21:54 -05:00
Jonathan Swoboda
93e38f2608 Merge pull request #12569 from esphome/bump-2025.12.1
2025.12.1
2025-12-19 10:53:05 -05:00
Jonathan Swoboda
3a888326d8 Bump version to 2025.12.1 2025-12-19 10:13:35 -05:00
Keith Burzinski
f0d0ea60a7 [esp32_ble, esp32_ble_tracker] Fix crash, error messages when ble.disable called during boot (#12560) 2025-12-19 10:13:35 -05:00
Jonathan Swoboda
7ca11764ab [template.alarm_control_panel] Fix compile without binary_sensor (#12548)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-19 10:13:35 -05:00
Jonathan Swoboda
3e38a5e630 [esp32_camera] Fix I2C driver conflict with other components (#12533)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-12-19 10:13:35 -05:00
Jonathan Swoboda
636be92c97 [bme68x_bsec2_i2c] Add MULTI_CONF support for multiple sensors (#12535)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-19 10:13:35 -05:00
Jack Wilsdon
195b1c6323 [pm1006] Fix "never" update interval detection (#12529) 2025-12-19 10:13:35 -05:00
Anna Oake
7e08092012 [cc1101] Fix default frequencies (#12539) 2025-12-19 10:13:35 -05:00
Jonathan Swoboda
0ea5f2fd81 Merge pull request #12525 from esphome/bump-2025.12.0
2025.12.0
2025-12-16 18:57:20 -05:00
Jonathan Swoboda
fa3d998c3d Bump version to 2025.12.0 2025-12-16 17:15:50 -05:00
Jonathan Swoboda
864aaeec01 Merge pull request #12520 from esphome/bump-2025.12.0b5
2025.12.0b5
2025-12-16 11:25:57 -05:00
Jonathan Swoboda
9c88e44300 Bump version to 2025.12.0b5 2025-12-16 10:35:31 -05:00
Jonathan Swoboda
4d6a93f92d [uart] Fix UART on default UART0 pins for ESP-IDF (#12519)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-16 10:35:31 -05:00
J. Nick Koston
7216120bfd [socket] Fix getpeername() returning local address instead of remote in LWIP raw TCP (#12475) 2025-12-16 10:35:31 -05:00
Jonathan Swoboda
8cf0ee38a3 Merge pull request #12513 from esphome/bump-2025.12.0b4
2025.12.0b4
2025-12-15 19:01:02 -05:00
Jonathan Swoboda
4c926cca60 Bump version to 2025.12.0b4 2025-12-15 18:09:42 -05:00
Pascal Vizeli
57634b612a [http_request] Fix infinite loop when server doesn't send Content-Length header (#12480)
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-15 18:09:42 -05:00
Jonathan Swoboda
8dff7ee746 [esp32] Support all IDF component version operators in shorthand syntax (#12499)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-15 18:09:42 -05:00
Jonathan Swoboda
803bb742c9 [remote_base] Fix crash when ABBWelcome action has no data field (#12493)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-15 18:09:42 -05:00
Jonathan Swoboda
3e6a65e7dc Merge pull request #12488 from esphome/bump-2025.12.0b3
2025.12.0b3
2025-12-14 19:17:58 -05:00
Jonathan Swoboda
3a101d8886 Bump version to 2025.12.0b3 2025-12-14 18:17:00 -05:00
J. Nick Koston
fa0f07bfe9 [wifi] Fix WiFi recovery after failed connection attempts (#12483) 2025-12-14 18:17:00 -05:00
mbohdal
fffa16e4d8 [ethernet] fix used pins validation in configuration of RMII pins (#12486) 2025-12-14 18:17:00 -05:00
guillempages
734710d22a [core] Use Arduino string macros only on ESP8266 (#12471) 2025-12-14 18:17:00 -05:00
J. Nick Koston
3a1be6822e [ota] Match client timeout to device timeout to prevent premature failures (#12484) 2025-12-14 18:17:00 -05:00
J. Nick Koston
c85b1b8609 [web_server_idf] Always enable LRU purge to prevent socket exhaustion (#12481) 2025-12-14 18:17:00 -05:00
J. Nick Koston
2e9ddd967c [wifi_signal] Skip publishing disconnected RSSI value (#12482) 2025-12-14 18:17:00 -05:00
J. Nick Koston
078afe9656 [dashboard] Add ESPHOME_TRUSTED_DOMAINS support to events WebSocket (#12479) 2025-12-14 18:17:00 -05:00
Jonathan Swoboda
46574fcbec [cc1101] Add packet mode support (#12474)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-14 18:17:00 -05:00
Jonathan Swoboda
359f45400f [core] Fix polling_component_schema and type consistency (#12478)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-14 18:16:59 -05:00
Clyde Stubbs
4da95ccd7e [packet_transport] Ensure retransmission at update intervals (#12472)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-14 18:16:59 -05:00
J. Nick Koston
c69d58273a [core] Fix CORE.raw_config not updated after package merge (#12456) 2025-12-14 18:16:59 -05:00
Jonathan Swoboda
375e53105f Merge pull request #12444 from esphome/bump-2025.12.0b2
2025.12.0b2
2025-12-12 12:15:41 -05:00
Jonathan Swoboda
c9506b056d Bump version to 2025.12.0b2 2025-12-12 11:12:58 -05:00
Jonathan Swoboda
2c77668a05 [http_request] Skip update check when network not connected (#12418)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-12 11:12:58 -05:00
J. Nick Koston
5567d96dd9 [esp8266] Eliminate up to 16ms socket latency (#12397) 2025-12-12 11:12:58 -05:00
J. Nick Koston
78b76045ce [api] Fix potential buffer overflow in noise PSK base64 decode (#12395) 2025-12-12 11:12:58 -05:00
J. Nick Koston
1d13d18a16 [light] Add zero-copy support for API effect commands (#12384) 2025-12-12 11:12:58 -05:00
Jonathan Swoboda
a3a2a6d965 Merge pull request #12396 from esphome/bump-2025.12.0b1
2025.12.0b1
2025-12-09 21:33:58 -05:00
Jonathan Swoboda
26770e09dc Bump version to 2025.12.0b1 2025-12-09 20:08:35 -05:00
Javier Peletier
9f2693ead5 [core] Packages refactor and conditional package inclusion (package refactor part 1) (#11605)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-12-10 00:59:58 +01:00
J. Nick Koston
3642399460 [tests] Fix clang-tidy warnings in custom_api_device_component fixture (#12390) 2025-12-10 00:50:26 +01:00
J. Nick Koston
3a6edbc2c7 [micronova] Fix test UART package key to match directory name (#12391) 2025-12-10 00:49:44 +01:00
J. Nick Koston
608f834eaa [ci] Isolate usb_cdc_acm in component tests due to tinyusb/usb_host conflict (#12392) 2025-12-10 00:49:29 +01:00
J. Nick Koston
5919355d18 [ci] Allow memory impact target branch build to fail without blocking CI (#12381) 2025-12-10 00:26:24 +01:00
dependabot[bot]
1e23b10eed Bump aioesphomeapi from 43.1.0 to 43.2.1 (#12385)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-09 22:02:42 +00:00
Clyde Stubbs
ad0218fd40 [mipi_rgb] Add Waveshare 3.16 (#12309) 2025-12-10 08:17:59 +11:00
Clyde Stubbs
87142efbb4 [epaper_spi] Set reasonable default update interval (#12331) 2025-12-10 06:42:11 +11:00
Robert Resch
329b38fa29 [micronova] Require memory location and address for custom entities (#12371) 2025-12-09 14:30:55 -05:00
Jonathan Swoboda
4b44c7384b Merge branch 'release' into dev 2025-12-09 12:54:45 -05:00
Jonathan Swoboda
a593965372 Merge pull request #12380 from esphome/bump-2025.11.5
2025.11.5
2025-12-09 12:54:30 -05:00
Jonathan Swoboda
4743e5592a Bump version to 2025.11.5 2025-12-09 12:02:53 -05:00
Jonathan Swoboda
464607011c [mqtt] Fix logger method case sensitivity error (#12379)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-09 12:02:53 -05:00
J. Nick Koston
16fe8f9e9e [libretiny] Fix WiFi scan timeout loop when scan fails (#12356) 2025-12-09 12:02:46 -05:00
J. Nick Koston
436d2c44e8 [wifi] Fix scan timeout loop when scan returns zero networks (#12354) 2025-12-09 12:01:51 -05:00
J. Nick Koston
b213555dd2 [scheduler] Fix missing lock when recycling items in defer queue processing (#12343) 2025-12-09 12:01:51 -05:00
Clyde Stubbs
b6336f9e63 [lvgl] Number saves value on interactive change (#12315) 2025-12-09 12:01:51 -05:00
Clyde Stubbs
fb7800a22f [binary_sensor] Fix reporting of 'unknown' (#12296)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-12-09 12:01:51 -05:00
J. Nick Koston
2c0f4d8f80 [api] Reduce heap usage for Home Assistant service call string storage (#12151) 2025-12-09 16:35:14 +01:00
J. Nick Koston
e96c37965c [wifi] Fix LibreTiny spurious disconnect events aborting connections (#12357) 2025-12-09 16:26:27 +01:00
J. Nick Koston
72c74bc0b3 [api] Store Home Assistant state subscriptions in flash instead of heap (#12008) 2025-12-09 16:26:11 +01:00
J. Nick Koston
443f9c3f57 [api] Use StringRef for ActionResponse error message to avoid copy (#12240) 2025-12-09 16:10:43 +01:00
Javier Peletier
88a2e75989 [packages] Add more information and deprecation deadline for "single package" includes (#12280) 2025-12-09 16:04:10 +01:00
J. Nick Koston
e1afd65fae [api] Store device info strings in flash on ESP8266 (#12173) 2025-12-09 15:59:27 +01:00
Jonathan Swoboda
27e031c257 [mqtt] Fix logger method case sensitivity error (#12379)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-09 09:43:47 -05:00
Jonathan Swoboda
74f509c754 [core] Add PR template instruction to AI instructions (#12375)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-09 15:42:06 +01:00
J. Nick Koston
f9aa48295c [mdns] Reduce RAM usage by eliminating MAC address heap allocation (#12073) 2025-12-09 09:33:23 -05:00
J. Nick Koston
861ed8dd41 [scheduler] Avoid std::string allocation in RetryArgs (#12311) 2025-12-09 09:27:12 -05:00
Clyde Stubbs
750f4ea797 [pio] Rationalise library definitions in platformio.ini (#12374) 2025-12-09 08:40:58 -05:00
Clyde Stubbs
6945b44af5 [psram] Fix boot failure with 120MHz Octal flash (#12377) 2025-12-09 08:38:16 -05:00
Mirko Vogt
fcae13836c [sx1509] Change setup priority from HARDWARE to IO (#12373)
Co-authored-by: Your Name <you@example.com>
2025-12-08 22:50:07 -05:00
Robert Resch
3eaa9f164b [micronova] Remove MicroNovaFunctions (#12363)
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>
2025-12-08 14:38:13 -05:00
smarthome-10
4c31961ae9 Update URLs (#12369)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-12-08 14:37:45 -05:00
Sébastien Blanchet
7a20c85eec [i2c] Fix port logic with ESP-IDF (#12063)
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>
2025-12-08 14:12:15 -05:00
Robert Resch
9f60aed9b0 [micronova] Make stove switch entity independent (#12355)
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>
2025-12-08 11:18:44 -05:00
J. Nick Koston
801d1135ab [select] Add zero-copy support for API select commands (#12329) 2025-12-08 10:37:51 -05:00
J. Nick Koston
d635892ecf [core] Use StringRef for get_comment and get_compilation_time to avoid allocations (#12219)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-08 10:36:13 -05:00
Johannes Nau
7e486b1c25 [pca9685] Allow to disable the phase balancer for PCA9685 (#9792) 2025-12-08 10:34:26 -05:00
Keith Burzinski
eda743ee48 [usb_cdc_acm] New component (#11687)
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>
2025-12-08 09:50:23 -05:00
Sébastien Blanchet
5144154f91 [hub75] fix id conflict (#12365) 2025-12-08 14:31:05 +00:00
J. Nick Koston
4466c4c69f [libretiny] Fix WiFi scan timeout loop when scan fails (#12356) 2025-12-08 09:09:04 -05:00
Richard Kubíček
c7382fc494 [hlw8032] Single-phase metering IC (#7241)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-12-08 09:07:10 -05:00
Robert Resch
95efb37045 [micronova] Set the write bit automatically (#12318)
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>
2025-12-08 08:39:43 -05:00
Berik Visschers
2515f1c080 Add seeed_xiao_esp32c6 board definition (#12307)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-12-08 08:37:59 -05:00
J. Nick Koston
53ddd1a1cd [wifi_signal] Add ifdef guards for clang-tidy compatibility (#12362) 2025-12-08 07:43:48 -05:00
J. Nick Koston
93a85d7979 [wifi_signal] Update signal strength immediately on WiFi connect/disconnect (#12347) 2025-12-07 22:08:46 -06:00
J. Nick Koston
159194587b [core] Move Color::gradient to cpp to avoid duplicate code (#12348) 2025-12-07 22:08:21 -06:00
J. Nick Koston
ffb3e2eb0a [wifi] Fix scan timeout loop when scan returns zero networks (#12354) 2025-12-07 22:00:04 -06:00
Robert Resch
c5cc91f6f0 [micronova] Add FINAL_VALIDATE_SCHEMA to validate uart (#12350)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-12-07 21:02:05 -05:00
Robert Resch
e36e6fbc3f [micronova] Move STOVE_STATES to text sensor file as it's used only there (#12349) 2025-12-07 19:08:41 -05:00
Robert Resch
1134251c32 [micronova] Set update_interval on entities instead on hub (#12226)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-12-07 23:55:36 +00:00
J. Nick Koston
68a7634228 [text] Store pattern as const char* to reduce memory usage (#12335) 2025-12-07 15:33:15 -06:00
J. Nick Koston
3d5d89ff00 [template] Use C++17 nested namespace syntax (#12346) 2025-12-07 15:09:25 -06:00
Joakim Plate
f015130f2e [esp8266] Allow use of recvfrom for esphome sockets (#12342) 2025-12-07 14:59:59 -06:00
J. Nick Koston
acda5bcd5a [text] Add component tests with pattern coverage (#12345) 2025-12-07 14:34:12 -06:00
Edward Firmo
4b5435fd93 [nextion] Use 16-bit id for pics (#12330)
Co-authored-by: Szczepan <szczepan.staszak@gmail.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-12-07 15:16:49 -05:00
J. Nick Koston
05826d5ead [scheduler] Fix missing lock when recycling items in defer queue processing (#12343) 2025-12-07 13:30:22 -06:00
J. Nick Koston
e7a3cccb4d [text_sensor] Reduce filter memory usage using const char* (#12334) 2025-12-07 13:30:06 -06:00
dependabot[bot]
1f271e7c10 Bump pytest from 9.0.1 to 9.0.2 (#12332)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-06 21:32:08 -06:00
dependabot[bot]
aeedfdcaf3 Bump aioesphomeapi from 43.0.0 to 43.1.0 (#12333)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-06 21:31:56 -06:00
Jesse Hills
f20aaf3981 [api] Device defined action responses (#12136)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-12-06 09:47:57 -06:00
Clyde Stubbs
75c41b11d1 [lvgl] Number saves value on interactive change (#12315) 2025-12-06 08:49:15 -06:00
Clyde Stubbs
3c7d6b7fc6 [ci-custom] Fix after switch from string to path (#12314) 2025-12-06 07:49:23 -06:00
Clyde Stubbs
7eae0a4972 [image] Add USE_IMAGE in defines.h (#12317) 2025-12-06 07:46:39 -06:00
Jonathan Swoboda
6220427524 [cc1101] Use Hz and cv.frequency instead of kHz (#12313) 2025-12-05 22:32:20 -05:00
Clyde Stubbs
6716194e47 [binary_sensor] Fix reporting of 'unknown' (#12296)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-12-05 16:59:29 -06:00
Jonathan Swoboda
a517e0ec80 [esp32] Add missing variant support (#12305)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-05 16:28:24 -05:00
dependabot[bot]
10b54df771 Bump github/codeql-action from 4.31.6 to 4.31.7 (#12304)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-05 15:17:10 -06:00
dependabot[bot]
bbb71b5359 Bump peter-evans/create-pull-request from 7.0.9 to 7.0.11 (#12303)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-05 15:16:55 -06:00
Ludovic BOUÉ
1fa7adbe8d [mipi_spi] Add M5CORE2 model (#12301) 2025-12-06 07:24:57 +11:00
Stuart Parmenter
7421f31160 [hub75] HUB75 display component (#11153)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-12-05 18:51:32 +00:00
c0mputerguru
78bef42473 [sps30] Add idle mode functionality (#12255)
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>
2025-12-05 13:33:00 -05:00
J. Nick Koston
7f7c913a85 [light] Fix schedule_show not enabling loop for idle addressable lights (#12302) 2025-12-05 11:47:54 -06:00
Jonathan Swoboda
1a308583b3 [esp32] Add support for ESP32-C61 variant (#12285)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-12-05 12:16:19 -05:00
J. Nick Koston
27fcff2092 [api] Simplify MessageCreator to trivially copyable type (#12295)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-05 10:27:41 -06:00
Jonathan Swoboda
f4d1c9df71 [remote_receiver] Fix Zephyr clang tidy (#12299)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-05 09:56:11 -06:00
Jesse Hills
7fd79fdded [esp32] Change imports to use esp32 only, not .const (#12243) 2025-12-05 09:53:08 -05:00
Jesse Hills
19fa768730 Update readme logo (#12294)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-12-05 08:48:04 -05:00
Jonathan Swoboda
ca1d17562a Merge branch 'release' into dev 2025-12-04 22:55:08 -05:00
Jonathan Swoboda
42811edeb4 Merge pull request #12293 from esphome/bump-2025.11.4
2025.11.4
2025-12-04 22:54:55 -05:00
Citizen07
22481d9c0e [remote_receiver] buffer usage fix and idle optimizations (#9999)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-12-04 22:50:23 -05:00
Jonathan Swoboda
8f20abebf6 Bump version to 2025.11.4 2025-12-04 21:52:48 -05:00
J. Nick Koston
7077488dc7 [scheduler] Fix use-after-free when cancelling timeouts from non-main-loop threads (#12288) 2025-12-04 21:52:48 -05:00
Jesse Hills
ef34239064 [CI] Trigger generic version notifier job on release (#12292) 2025-12-04 21:52:48 -05:00
Jonathan Swoboda
44148c0c6b [esp32_hosted] Fix build and bump IDF component version to 2.7.0 (#12282)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-04 21:52:48 -05:00
Jonathan Swoboda
1b53fcf634 [es8311] Remove MIN and MAX from mic_gain enum options (#12281)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-04 21:52:48 -05:00
Clyde Stubbs
b18e3d943a [config] Provide path for has_at_most_one_of messages (#12277) 2025-12-04 21:52:48 -05:00
Jonathan Swoboda
f0673f6304 [ld2420] Add missing USE_SELECT ifdefs (#12275)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-04 21:52:48 -05:00
Clyde Stubbs
320ba30d50 [esp32] Add build flag to suppress noexecstack message (#12272) 2025-12-04 21:52:48 -05:00
J. Nick Koston
637cb3f04a [api] Use loop-based reboot timeout check to avoid scheduler heap churn (#12291)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-04 19:14:35 -06:00
J. Nick Koston
80e881655f [scheduler] Fix use-after-free when cancelling timeouts from non-main-loop threads (#12288) 2025-12-04 19:14:22 -06:00
Jesse Hills
78b2ae8a35 [CI] Trigger generic version notifier job on release (#12292) 2025-12-05 14:00:08 +13:00
Jesse Hills
8caaf53ef0 [CI] Update renamed action repo (#12290) 2025-12-05 12:53:13 +13:00
dependabot[bot]
4db7748815 Bump ruff from 0.14.7 to 0.14.8 (#12286)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-12-04 21:53:36 +00:00
Jonathan Swoboda
0da157ab98 [tests] Bump esp32_hosted in the test code (#12289)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-04 21:14:30 +00:00
Jonathan Swoboda
cafa275579 [esp32_hosted] Fix build and bump IDF component version to 2.7.0 (#12282)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-04 14:47:21 -05:00
Jonathan Swoboda
a31fb223f3 [es8311] Remove MIN and MAX from mic_gain enum options (#12281)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-04 10:00:45 -05:00
Javier Peletier
37019231de [lvgl] refactor hello world to yaml file (#12274) 2025-12-04 20:18:27 +11:00
Clyde Stubbs
2af66bd6fc [config] Provide path for has_at_most_one_of messages (#12277) 2025-12-04 21:20:55 +13:00
Jonathan Swoboda
951c5377c5 [ld2420] Add missing USE_SELECT ifdefs (#12275)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-04 20:25:13 +13:00
Thomas Rupprecht
22803ef54b [esp32] Sort variants in situ (#10410)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-12-03 20:48:11 -05:00
Clyde Stubbs
20f82a3820 [esp32] Add build flag to suppress noexecstack message (#12272) 2025-12-03 23:49:57 +00:00
dependabot[bot]
fb331e1c5a Bump actions/stale from 10.1.0 to 10.1.1 (#12270)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-03 21:04:09 +00:00
Kevin Ahrendt
a8518d3cea [wifi, wifi_info] Add a WiFi power mode text sensor (#11480)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-12-04 09:18:59 +13:00
jsmarion
03aaa66f8e [cst816] Fix CST826 & CST836 (#12260)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-12-03 14:35:14 -05:00
J. Nick Koston
a24ba26068 [core] Improve CORE.data documentation with dataclass pattern (#12170)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-04 07:33:57 +13:00
Javier Peletier
623cdac689 [tests] Add testing of command line substitutions (#12210)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-12-03 12:36:35 -05:00
Jonathan Swoboda
1fbd91dc71 Merge branch 'release' into dev 2025-12-03 11:37:13 -05:00
Jonathan Swoboda
cfd88376b9 Merge pull request #12266 from esphome/bump-2025.11.3
2025.11.3
2025-12-03 11:36:57 -05:00
J. Nick Koston
b3812b5811 [text_sensor] Fix spurious raw_state deprecation warnings (#12262) 2025-12-03 16:22:06 +00:00
Jonathan Swoboda
577a6b2941 Bump version to 2025.11.3 2025-12-03 10:50:28 -05:00
Jonathan Swoboda
de68b56c4a [rtl87xx] Fix FreeRTOS version for RTL8720C boards (#12261)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-03 10:50:28 -05:00
Jonathan Swoboda
ccd23e692b [analog_threshold] Fix oscillation when using invert filter (#12251)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-03 10:50:28 -05:00
Jonathan Swoboda
1f5a44be3d [rtl87xx] Fix AsyncTCP compilation by upgrading FreeRTOS to 8.2.3 (#12230)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-03 10:50:28 -05:00
Jonathan Swoboda
1d1e47c757 [core] Fix clean all windows (#12217)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-12-03 10:50:28 -05:00
3fbed1fa79 [ade7953] Apply voltage_gain setting to both channels (#12180) 2025-12-03 10:50:28 -05:00
Jonathan Swoboda
5c71520635 [mopeka_pro_check] Fix negative temperatures (#12198)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-03 10:50:28 -05:00
J. Nick Koston
9d6c81ec23 [hlk_fm22x] Fix Action::play method signatures (#12192) 2025-12-03 10:50:28 -05:00
Clyde Stubbs
73fa9230e6 [helpers] Add conversion from FixedVector to std::vector (#12179) 2025-12-03 10:50:28 -05:00
J. Nick Koston
48caff13c9 [espnow] Initialize LwIP stack when running without WiFi component (#12169) 2025-12-03 10:50:28 -05:00
J. Nick Koston
71bb94524e [usb_uart] Wake main loop immediately when USB data arrives (#12148) 2025-12-03 10:50:28 -05:00
Clyde Stubbs
a3199792c6 [build] Don't clear pio cache unless requested (#11966) 2025-12-03 10:50:28 -05:00
lygris
87ac4baf3a [cc1101] Add new cc1101 component (#11849)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-12-03 10:42:04 -05:00
Jonathan Swoboda
669bcad458 [rtl87xx] Fix FreeRTOS version for RTL8720C boards (#12261)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-03 15:31:12 +00:00
H. Árkosi Róbert
6f91c75f86 [gree] turbo, light, health, xfan switches (#12160)
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2025-12-03 09:20:17 +00:00
Javier Peletier
ab60ae092d [tests] Allow substitution tests to run independently for debugging (#12224)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2025-12-02 16:17:24 -06:00
dependabot[bot]
708496c101 Bump actions/checkout from 6.0.0 to 6.0.1 (#12259)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-02 13:45:38 -06:00
Jonathan Swoboda
2f75962b19 [analog_threshold] Fix oscillation when using invert filter (#12251)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 13:40:46 -05:00
J. Nick Koston
a6a6f482e6 [core] Add PROGMEM macros and move web_server JSON keys to flash (#12214) 2025-12-02 16:51:05 +00:00
dependabot[bot]
638c59e162 Bump pylint from 4.0.3 to 4.0.4 (#12239)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-02 10:13:20 -06:00
Flo
8f97f3b81f [wifi] Fix ap_active condition (#12227) 2025-12-02 10:12:27 -06:00
J. Nick Koston
6ce2a45691 [text_sensor] Add deprecation warning for raw_state member access (#12246) 2025-12-02 10:03:58 -06:00
J. Nick Koston
77477bd330 [web_server_idf] Fix SSE multi-line message formatting (#12247) 2025-12-02 10:03:29 -06:00
J. Nick Koston
3f08cacf71 [valve] Store valve state strings in flash on ESP8266 (#12202) 2025-12-02 10:02:51 -06:00
J. Nick Koston
d1583456e9 [web_server] Store update state strings in flash on ESP8266 (#12204) 2025-12-02 10:02:29 -06:00
J. Nick Koston
101103c666 [core] Add RAM strings and symbols analysis to analyze-memory command (#12161) 2025-12-02 10:02:09 -06:00
J. Nick Koston
5142ff372b [light] Use listener pattern for state callbacks with lazy allocation (#12166) 2025-12-02 10:01:54 -06:00
J. Nick Koston
f9ad832e7b [esp32_camera] Replace std::function callbacks with CameraListener interface (#12165)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-02 09:59:32 -06:00
J. Nick Koston
deda7a1bf3 [lock] Store lock state strings in flash on ESP8266 (#12163) 2025-12-02 09:59:05 -06:00
Jonathan Swoboda
29be1423f5 [core] Filter noisy platformio log messages (#12218)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-12-02 08:59:50 -05:00
J. Nick Koston
10ddebc737 [text_sensor] Avoid duplicate string storage when no filters configured (#12205) 2025-12-01 22:17:31 -06:00
dependabot[bot]
9a0731437a Bump aioesphomeapi from 42.9.0 to 42.10.0 (#12245)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-01 22:11:33 -06:00
J. Nick Koston
82a06c697e [esp32] Place ring buffer functions in flash by default (prep for IDF 6.0) (#12184) 2025-12-02 03:57:41 +00:00
dependabot[bot]
c45cd44bb8 Bump github/codeql-action from 4.31.5 to 4.31.6 (#12234)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-01 21:49:25 -06:00
Jonathan Swoboda
2903a4aa92 [ota] Use ESP-IDF OTA backend for all ESP32 builds (#12244)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-12-02 03:41:34 +00:00
J. Nick Koston
6943803176 [cover] Store cover state strings in flash on ESP8266 (#12196) 2025-12-01 21:26:13 -06:00
J. Nick Koston
6dafc5137e [esp32] Place FreeRTOS functions in flash by default (prep for IDF 6.0) (#12182) 2025-12-01 21:24:08 -06:00
Djordje Mandic
df58e832e5 [esp8266] Allow IN&OUT pin config for ESP8266 (#12238) 2025-12-01 15:44:33 -08:00
Peter Popovec
e42cf9a4f4 [mqtt] Enable support for the RTL87XX platform (#7697)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
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>
2025-12-01 23:06:47 +00:00
J. Nick Koston
96f28f0ab4 [button] Convert to C++17 nested namespace style (#12233)
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
2025-12-01 17:50:29 -05:00
J. Nick Koston
d332edfaca [datetime] Convert to C++17 nested namespace style (#12235) 2025-12-01 17:50:03 -05:00
Keith Burzinski
d4bd282bb4 [helpers] Fix unit tests following #12135 (#12237) 2025-12-01 22:08:49 +00:00
Jonathan Swoboda
78df884bb5 [rtl87xx] Fix AsyncTCP compilation by upgrading FreeRTOS to 8.2.3 (#12230)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-01 16:03:00 -05:00
Keith Burzinski
52fe3de78f [zwave_proxy] Use new socket wake infrastructure to reduce latency, convert to C++17 namespace style (#12135)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-12-01 14:27:20 -06:00
Keith Burzinski
6a79ce8eff [uart] Automatically enable the socket wake infrastructure when RX wake requested (#12221) 2025-12-01 14:16:39 -06:00
Jonathan Swoboda
2b7695ba3f [core] Fix clean all windows (#12217)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-12-01 12:40:56 -05:00
Juri Berlanda
6d336676a2 [remote_transmitter, remote_receiver] Add RP2040 support (#12048)
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>
2025-12-01 12:09:58 -05:00
Robert Resch
b322622ef1 [micronova] Convert to C++17 namespace style (#12229) 2025-12-01 10:47:00 -05:00
J. Nick Koston
065c1bfc6a [core] Fix status_momentary API misuse and optimize parameter type (#12216) 2025-12-01 08:34:07 -06:00
Keith Burzinski
664881bc13 [uart] Convert to C++17 namespace style (#12220) 2025-12-01 07:57:18 -05:00
Keith Burzinski
dbc16ce468 [wifi_info] Fix compilation error when using only mac_address sensor, add tests (#12222) 2025-12-01 02:48:47 -06:00
Keith Burzinski
161a18b326 [uart] Add wake_loop_on_rx flag for low latency processing (#12172)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-12-01 00:33:23 -06:00
Jonathan Swoboda
4335fcdb72 [psram] Add C5 support (#12215)
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-30 23:27:10 -05:00
bf4ef36c3a [ade7953] Apply voltage_gain setting to both channels (#12180) 2025-11-30 19:17:50 -05:00
J. Nick Koston
2ca118f371 [web_server] Replace routing table with if-else chain to save 116 bytes RAM (#12139) 2025-12-01 12:25:46 +13:00
J. Nick Koston
82e1238330 [lock] Refactor trigger classes to template and add integration tests (#12193) 2025-11-30 17:09:02 -06:00
Jimmy Hedman
8308bc2911 [mdns] Bump mDNS component to 1.9.1 (#12207) 2025-11-30 08:06:06 -05:00
Jonathan Swoboda
47c767fa5e [openthread] Add C5 support (#12200) 2025-11-30 08:04:45 -05:00
Jonathan Swoboda
e95ceafc17 [mopeka_pro_check] Fix negative temperatures (#12198)
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-30 08:04:33 -05:00
Jonathan Swoboda
7317bf4a5d [esp32_can] Add P4 support (#12201) 2025-11-30 08:04:19 -05:00
J. Nick Koston
042a08887f [climate] Use C++17 nested namespace syntax (#12194) 2025-11-30 00:54:49 +00:00
J. Nick Koston
77f5f2326f [hlk_fm22x] Fix Action::play method signatures (#12192) 2025-11-29 19:36:12 -05:00
d82a92b406 [ade7953_base] Add missing CODEOWNERS (#12181) 2025-11-29 18:41:47 -05:00
dependabot[bot]
ec88bf0cb1 Bump ruff from 0.14.5 to 0.14.7 (#12190)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-11-29 22:56:26 +00:00
dependabot[bot]
46567c4716 Bump aioesphomeapi from 42.8.0 to 42.9.0 (#12189)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-29 22:55:27 +00:00
Jakub Čermák
1f47797007 Add MEASUREMENT_ANGLE to SensorStateClass (#12085) 2025-11-29 16:26:25 -06:00
Javier Peletier
cf444fc3b8 [mipi_spi] add guition JC4827W543 C/R (#12034) 2025-11-29 19:40:13 +11:00
Clyde Stubbs
c40e8e7f5c [helpers] Add conversion from FixedVector to std::vector (#12179) 2025-11-29 19:38:29 +11:00
J. Nick Koston
b71d8010d2 [light] Store log_percent parameter strings in flash on ESP8266 (#12174) 2025-11-28 22:59:31 -05:00
J. Nick Koston
2174795b27 [number] Reduce NumberCall size by 4 bytes on 32-bit platforms (#12178)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-28 22:57:36 -05:00
J. Nick Koston
5fa4ff754c [ble_client] Convert to C++17 namespace style (#12176) 2025-11-28 22:57:01 -05:00
J. Nick Koston
bc50be6053 [logger] Conditionally compile log level change listener (#12168)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-28 22:14:00 +00:00
J. Nick Koston
ca599b25c2 [espnow] Initialize LwIP stack when running without WiFi component (#12169) 2025-11-28 16:33:28 -05:00
J. Nick Koston
2e55296640 [sensor] Replace timeout filter scheduler with loop-based implementation (#11922) 2025-11-28 20:43:11 +00:00
Javier Peletier
d6ca01775e [packages] Restore remote shorthand vars and !remove in early package contents validation (#12158)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-11-28 18:24:09 +00:00
Javier Peletier
e15f3a08ae [tests] Remote packages with substitutions (#12145) 2025-11-28 12:15:55 -06:00
J. Nick Koston
fb82362e9c [api] Eliminate rx_buf heap churn and release buffers after initial sync (#12133) 2025-11-28 12:13:29 -06:00
J. Nick Koston
26e979d3d5 [wifi] Replace std::function callbacks with listener interfaces (#12155) 2025-11-28 11:27:17 -06:00
J. Nick Koston
60ffa0e52e [esp32_ble_tracker] Replace scanner state callback with listener interface (#12156) 2025-11-28 11:27:08 -06:00
J. Nick Koston
e1ec6146c0 [wifi] Save 112 bytes BSS on ESP8266 by calling SDK directly for BSSID (#12137)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-11-27 22:09:41 -06:00
J. Nick Koston
450065fdae [light] Replace sparse enum switch with linear search to save 156 bytes RAM (#12140) 2025-11-27 22:09:27 -06:00
J. Nick Koston
71dc402a30 [logger] Replace std::function callbacks with LogListener interface (#12153) 2025-11-28 04:00:33 +00:00
Jonathan Swoboda
9bd148dfd1 Merge branch 'release' into dev 2025-11-27 18:19:20 -05:00
Jonathan Swoboda
50c1720c16 Merge pull request #12149 from esphome/bump-2025.11.2
2025.11.2
2025-11-27 18:19:05 -05:00
J. Nick Koston
4c549798bc [usb_uart] Wake main loop immediately when USB data arrives (#12148) 2025-11-27 16:33:08 -06:00
Jonathan Swoboda
4115dd7222 Bump version to 2025.11.2 2025-11-27 17:23:28 -05:00
J. Nick Koston
d5e2543751 [scheduler] Fix use-after-move crash in heap operations (#12124) 2025-11-27 17:23:28 -05:00
Clyde Stubbs
b4b34aee13 [wifi] Restore blocking setup until connected for RP2040 (#12142) 2025-11-27 17:23:28 -05:00
Jonathan Swoboda
6645994700 [esp32] Fix hosted update when there is no wifi (#12123) 2025-11-27 17:23:28 -05:00
Clyde Stubbs
ae140f52e3 [lvgl] Fix position of errors in widget config (#12111)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-11-27 17:23:28 -05:00
Clyde Stubbs
46ae6d35a2 [lvgl] Allow multiple widgets per grid cell (#12091) 2025-11-27 17:23:27 -05:00
J. Nick Koston
278f12fb99 [script] Fix script.wait hanging when triggered from on_boot (#12102) 2025-11-27 17:23:27 -05:00
Jonathan Swoboda
acdcd56395 [esp32] Fix platformio flash size print (#12099)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2025-11-27 17:23:27 -05:00
Edward Firmo
9289fc36f7 [nextion] Do not set alternative baud rate when not specified or <= 0 (#12097) 2025-11-27 17:23:27 -05:00
J. Nick Koston
1fadd1227d [scheduler] Fix use-after-move crash in heap operations (#12124) 2025-11-27 10:50:21 -06:00
Clyde Stubbs
91df0548ef [wifi] Restore blocking setup until connected for RP2040 (#12142) 2025-11-27 10:30:03 -05:00
Jonathan Swoboda
a7a5a0b9a2 [esp32] Improve IDF component support (#12127) 2025-11-26 22:46:17 -05:00
Jonathan Swoboda
9c85ec9182 [esp32] Fix hosted update when there is no wifi (#12123) 2025-11-26 20:01:35 -05:00
Jesse Hills
23e58c1c7b [inkplate] Ignore strapping pin warnings on default pins (#12110) 2025-11-26 17:08:40 -06:00
Clyde Stubbs
b3955cd151 [epaper_spi] Add SSD1677 and Waveshare 4.26 (#11887)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-11-26 17:07:51 -06:00
Clyde Stubbs
927d3715c1 [lvgl] Allow setting text directly on a button (#11964)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-11-26 17:06:40 -06:00
Clyde Stubbs
a2d9941c62 [lvgl] Add option to sync updates with display (#11896)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-11-26 17:06:32 -06:00
Clyde Stubbs
caaa08d678 [core] Fix for missing arguments to shared_lambda (#12115) 2025-11-26 17:05:45 -06:00
Jon Oberheide
eb970cf44e make thermostat humidification_action public (#12132) 2025-11-26 16:56:22 -06:00
Pawelo
083886c4b0 [prometheus] Avoid generating unused light color metrics to reduce memory usage on ESP8266 (#9530)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-11-26 18:06:51 +00:00
Javier Peletier
12a51ff047 [packages] Fix package schema validation (#12116)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-11-26 11:00:44 -06:00
J. Nick Koston
b328758634 Revert "[core] Deduplicate identical stateless lambdas to reduce flash usage" (#12117) 2025-11-26 10:53:44 -06:00
Clyde Stubbs
1207b9e995 [lvgl] Automatically pad rows and columns (#11879)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-11-26 01:53:51 +00:00
Clyde Stubbs
e071380532 [lvgl] Add missing obj scroll properties (#11901)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-11-26 01:49:47 +00:00
Clyde Stubbs
f071b6232a [lvgl] Fix position of errors in widget config (#12111)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-11-26 01:47:27 +00:00
J. Nick Koston
d443dbbf34 [lvgl] Fix lambda return types for coord and font validators (#12113) 2025-11-25 19:42:09 -06:00
J. Nick Koston
03a8ef71ff [esp32_ble_client] Replace std::string with char[18] for BLE address storage (#12070) 2025-11-25 18:37:49 -06:00
J. Nick Koston
bda17180df [core] Deduplicate identical stateless lambdas to reduce flash usage (#11918) 2025-11-26 12:48:08 +13:00
J. Nick Koston
ffae3501ab [core] Replace seq<>/gens<> with std::index_sequence for code clarity (#11921) 2025-11-26 12:44:50 +13:00
Jesse Hills
50bdcdee0c Add developer-breaking-change labelling (#12095) 2025-11-26 12:39:41 +13:00
dependabot[bot]
ae60b5e6a1 Bump actions/setup-python from 6.0.0 to 6.1.0 in /.github/actions/restore-python (#12108)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-25 14:27:49 -06:00
dependabot[bot]
70df4ecaa9 Bump actions/setup-python from 6.0.0 to 6.1.0 (#12106)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-25 13:35:40 -06:00
Clyde Stubbs
b6be5e3eda [lvgl] Allow multiple widgets per grid cell (#12091) 2025-11-26 06:06:42 +11:00
Nikolai Ryzhkov
dec323e786 [sht4x] Read and store a serial number of SHT4x sensors (#12089)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2025-11-25 13:27:35 -05:00
J. Nick Koston
6ca0cd1e8b [ltr390] Simplify mode tracking with bitmask instead of vector/function (#12093) 2025-11-25 12:16:48 -06:00
J. Nick Koston
3106934678 [esp32_ble] Optimize name storage to reduce RAM and eliminate heap allocations (#12071) 2025-11-25 12:16:27 -06:00
J. Nick Koston
8c5985f68a [web_server] Consolidate turn_on/turn_off handlers to eliminate duplicate lambdas (#12094) 2025-11-25 12:16:02 -06:00
J. Nick Koston
cf8c205644 [core] Reduce flash size by combining set_name() and set_object_id() calls (#11941) 2025-11-25 12:15:45 -06:00
J. Nick Koston
a571033b43 [script] Fix script.wait hanging when triggered from on_boot (#12102) 2025-11-25 10:30:01 -06:00
Jonathan Swoboda
cdf27f1447 [esp32] Fix platformio flash size print (#12099)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2025-11-25 11:14:53 -05:00
Edward Firmo
c30b920193 [nextion] Do not set alternative baud rate when not specified or <= 0 (#12097) 2025-11-25 07:48:32 -05:00
J. Nick Koston
697c5f424e [api] Use const char* pointers for light effects to eliminate heap allocations (#12090) 2025-11-25 08:17:53 +00:00
J. Nick Koston
18c97a08c3 [esp8266] Use C++17 nested namespaces and constexpr (#12096) 2025-11-25 01:47:06 -06:00
bdm310
66a871840e Add more lvgl arc update parameters (#12066) 2025-11-25 17:14:23 +11:00
J. Nick Koston
46a26560fd [template.alarm_control_panel] Replace std::map with FixedVector for heap and flash savings (#11893) 2025-11-25 16:21:56 +13:00
J. Nick Koston
1c808a3375 [ble_client] Write static BLE data directly from flash without allocation (#11826) 2025-11-25 16:19:18 +13:00
Keith Burzinski
2bc8a4a779 [wifi_info] Use callbacks instead of polling (#10748)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2025-11-24 20:23:10 -06:00
dependabot[bot]
7f1a9a611f Bump aioesphomeapi from 42.7.0 to 42.8.0 (#12092)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-25 02:09:02 +00:00
Jonathan Swoboda
b51409ed5e Merge branch 'release' into dev 2025-11-24 17:30:08 -05:00
Jonathan Swoboda
3775b54554 Merge pull request #12086 from esphome/bump-2025.11.1
2025.11.1
2025-11-24 17:29:53 -05:00
Keith Burzinski
88b898458b [bluetooth_proxy] Fix crash due to null pointer (#12084)
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>
2025-11-24 21:25:49 +00:00
Jonathan Swoboda
9186144dcd Bump version to 2025.11.1 2025-11-24 16:24:38 -05:00
Jesse Hills
25bcd0ea25 [online_image] Fix some large PNGs causing watchdog timeout (#12025)
Co-authored-by: guillempages <guillempages@users.noreply.github.com>
2025-11-24 16:24:38 -05:00
J. Nick Koston
50d08a2eba [esp_ldo,mipi_dsi,mipi_rgb] Fix dangling pointer bugs in mark_failed() (#12077) 2025-11-24 16:24:38 -05:00
J. Nick Koston
3a7a0c66ab [script][wait_until] Fix FIFO ordering and reentrancy bugs (#12049)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-24 16:24:38 -05:00
Jonathan Swoboda
83525b7a92 [core] Add support for passing yaml files to clean-all (#12039) 2025-11-24 16:24:38 -05:00
Jonathan Swoboda
f31f023c89 [esp32] Fix C2 builds (#12050) 2025-11-24 16:24:37 -05:00
J. Nick Koston
f8efefffaa [cst816][http_request] Fix status_set_error() dangling pointer bugs (#12033) 2025-11-24 16:24:37 -05:00
Jonathan Swoboda
d698083ede [jsn_sr04t] Fix model AJ_SR04M (#11992) 2025-11-24 16:24:37 -05:00
Jonathan Swoboda
11ba6440d7 [cst816][packet_transport][udp][wake_on_lan] Fix error messages (#12019) 2025-11-24 16:24:37 -05:00
Jonathan Swoboda
89ee37a2d5 [ltr501][ltr_als_ps] Rename enum to avoid collision with lwip defines (#12017) 2025-11-24 16:24:37 -05:00
J. Nick Koston
45b8c1e267 [network] Fix IPAddress constructor causing comparison failures and garbage output (#12005) 2025-11-24 16:24:37 -05:00
Jonathan Swoboda
fbe091f167 [graph] Fix legend border (#12000) 2025-11-24 16:24:37 -05:00
dependabot[bot]
e09656f20e Bump bleak from 1.1.1 to 2.0.0 (#12083)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-24 15:21:03 -06:00
Jesse Hills
eeb373fca9 [online_image] Fix some large PNGs causing watchdog timeout (#12025)
Co-authored-by: guillempages <guillempages@users.noreply.github.com>
2025-11-25 09:15:30 +13:00
J. Nick Koston
97ba67f4ee [core] Deprecate unsafe const char* APIs in mark_failed() and status_set_error(), add LogString* overloads (#12021) 2025-11-24 13:45:56 -06:00
J. Nick Koston
909baf5e7a [prometheus] Use current_option() instead of deprecated .state for select entities (#12079) 2025-11-24 13:45:29 -06:00
J. Nick Koston
a0440603b7 [wifi] Use ESP-IDF IP formatting macros directly to eliminate heap allocations (#12078) 2025-11-24 13:45:06 -06:00
dependabot[bot]
e2cd0ccd0e Bump actions/create-github-app-token from 2.1.4 to 2.2.0 (#12081)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-24 13:44:43 -06:00
dependabot[bot]
378fc4120a Bump peter-evans/create-pull-request from 7.0.8 to 7.0.9 (#12082)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-24 13:44:27 -06:00
dependabot[bot]
0dd842744a Bump github/codeql-action from 4.31.4 to 4.31.5 (#12080)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-24 13:44:09 -06:00
J. Nick Koston
7a73a524b9 [logger] Eliminate strlen overhead on LibreTiny (#11938) 2025-11-24 12:21:09 -06:00
Kevin Ahrendt
d1a1bb446b [wifi] Add runtime power saving mode control (#11478)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-11-24 17:55:04 +00:00
J. Nick Koston
c146d92425 [api] Remove redundant socket pointer from APIFrameHelper (#11985) 2025-11-25 06:53:42 +13:00
J. Nick Koston
c888becfa7 [api] Optimize APINoiseContext memory usage by removing shared_ptr overhead (#11981) 2025-11-25 06:52:15 +13:00
Flo
09f3f62194 [api] Connected Condition - state_subscription_only flag (#11906)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-11-24 11:49:16 -06:00
Jordan Zucker
b820e67616 [prometheus] Add event and text base components metrics (#10240)
Co-authored-by: Jordan Zucker <jordan@Jordans-MacBook-Pro.local>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-11-24 11:42:07 -06:00
Sascha Ittner
d7da559885 [thermopro_ble] Add thermopro ble support (#11835)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-11-24 11:31:26 -06:00
Jonathan Swoboda
d7a197b3a3 [esp32] Use the IDF I2C implementation on Arduino (#12076) 2025-11-24 12:27:09 -05:00
Flo
66cda04664 [wifi] ap_active condition (#11852)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-11-24 11:19:38 -06:00
J. Nick Koston
0764f4da86 [esp_ldo,mipi_dsi,mipi_rgb] Fix dangling pointer bugs in mark_failed() (#12077) 2025-11-24 11:02:24 -06:00
J. Nick Koston
06815fe177 [script][wait_until] Fix FIFO ordering and reentrancy bugs (#12049)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-24 10:41:24 -06:00
J. Nick Koston
04ec6a6999 [api] Use stack buffer for MAC address in Noise handshake (#12072) 2025-11-24 10:23:31 -06:00
J. Nick Koston
737f23a0bd [light] Dynamically disable loop when idle to reduce CPU overhead (#11881) 2025-11-24 10:23:11 -06:00
J. Nick Koston
3c48e13c9f [ethernet] Conditionally compile manual_ip to save 24 bytes RAM (#11832) 2025-11-24 10:22:13 -06:00
J. Nick Koston
426734beef [web_server_base] Replace shared_ptr with unique_ptr for AsyncWebServer (#11984) 2025-11-24 10:22:01 -06:00
J. Nick Koston
056b4375eb [api] Reduce heap allocations in DeviceInfoResponse (#11952) 2025-11-24 10:21:47 -06:00
J. Nick Koston
1f0a5e1eea [logger] Reduce UART overhead on ESP32/ESP8266 and fix buffer truncation (#11927) 2025-11-24 10:21:32 -06:00
Jonathan Swoboda
8607a0881d [core] Add support for passing yaml files to clean-all (#12039) 2025-11-24 10:10:24 -05:00
James
b4b98505ba [mipi_dsi] add guition JC4880P443 display (#12068) 2025-11-24 21:05:02 +11:00
Jonathan Swoboda
60d687c2c6 [esp32] Fix C2 builds (#12050) 2025-11-23 23:31:14 -05:00
Jonathan Swoboda
5750f7fccb [ci] Fix test grouping (#12067) 2025-11-23 21:25:24 -06:00
Jonathan Swoboda
c91a9495e6 [ci] Fix filename (#12065) 2025-11-23 16:19:26 -05:00
Javier Peletier
f42b806889 [core] Fix error on invalid id extend/remove (#12064) 2025-11-24 08:03:13 +11:00
Jesse Hills
a5751b294f [api] Rename USE_API_SERVICES to USE_API_USER_DEFINED_ACTIONS (#12029) 2025-11-24 08:13:23 +13:00
Abílio Costa
3f6f2d7d65 [bm8563] Add bm8563 component (#11616)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-11-21 15:28:42 -05:00
Marko Draca
782aee92a7 [mcp3204] differential mode support (#7436)
Co-authored-by: marko <marko@>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-11-21 14:50:07 -05:00
Thomas Rupprecht
972b7e84fe [tests] Fix mipi_spi test board (#12031)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-11-21 08:38:44 -05:00
J. Nick Koston
150e26dc2b [cst816][http_request] Fix status_set_error() dangling pointer bugs (#12033) 2025-11-21 06:41:48 -06:00
Jonathan Swoboda
0dea7a23e3 [jsn_sr04t] Fix model AJ_SR04M (#11992) 2025-11-21 07:39:59 -05:00
dependabot[bot]
01addeae08 Bump actions/checkout from 5.0.1 to 6.0.0 (#12022)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-20 13:11:41 -06:00
Jonathan Swoboda
a1e507baf8 [cst816][packet_transport][udp][wake_on_lan] Fix error messages (#12019) 2025-11-20 12:10:28 -05:00
Jonathan Swoboda
1accb4ff34 [ltr501][ltr_als_ps] Rename enum to avoid collision with lwip defines (#12017) 2025-11-20 10:58:21 -05:00
damib
59cd6dbf70 [climate_ir] Add optional humidity sensor (#9805)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Djordje Mandic <6750655+DjordjeMandic@users.noreply.github.com>
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>
2025-11-20 09:28:14 -05:00
omartijn
3c86f3894b [hc8] Add support for HC8 CO2 sensor (#11872)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-11-20 09:24:45 -05:00
J. Nick Koston
06bef148f4 [core] Optimize DelayAction for no-argument case using if constexpr (#11913) 2025-11-20 09:06:52 -05:00
tomaszduda23
5d883c6e06 [nrf52,i2c] fix review comment (#11931) 2025-11-20 09:06:40 -05:00
J. Nick Koston
b62053812b [core] Document threading model rationale in ThreadModel enum (#11979) 2025-11-20 09:06:28 -05:00
J. Nick Koston
a2321edf3c [network] Fix IPAddress constructor causing comparison failures and garbage output (#12005) 2025-11-20 08:59:16 -05:00
J. Nick Koston
24a6ad148c [lock] Modernize to C++17 nested namespaces (#11982) 2025-11-20 08:57:49 -05:00
J. Nick Koston
5071473767 [mdns] Modernize to C++17 nested namespace syntax (#11983) 2025-11-20 08:57:33 -05:00
J. Nick Koston
4825da8e9c [select] Modernize namespace declarations to C++17 syntax (#12007) 2025-11-20 08:57:04 -05:00
Javier Peletier
b346666a52 [st7701s] Add explanatory comment (#12014) 2025-11-20 20:05:22 +11:00
B48D81EFCC
83307684a3 [stts22h] Add support for STTS22H temperature sensor (#11778)
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>
2025-11-20 03:58:39 +00:00
David Woodhouse
da25951f6e [socket] Fix IPv6 address parsing for BSD sockets (#11996) 2025-11-19 21:01:32 -06:00
Jonathan Swoboda
4398fd84d2 [graph] Fix legend border (#12000) 2025-11-20 13:09:22 +13:00
Jonathan Swoboda
bbd6d019e5 Merge branch 'release' into dev 2025-11-19 17:37:58 -05:00
Jonathan Swoboda
625172e07d Merge pull request #12004 from esphome/bump-2025.11.0
2025.11.0
2025-11-19 17:37:42 -05:00
Jonathan Swoboda
1e9c7d3c6d Bump version to 2025.11.0 2025-11-19 16:02:52 -05:00
Jonathan Swoboda
4cdab4e2d8 Merge branch 'beta' into dev 2025-11-19 15:06:55 -05:00
Jonathan Swoboda
c2bc7b3cdc Merge pull request #12003 from esphome/bump-2025.11.0b5
2025.11.0b5
2025-11-19 15:06:44 -05:00
dependabot[bot]
2c3417062a Bump pyupgrade from 3.21.1 to 3.21.2 (#12002)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-19 13:47:40 -06:00
Jonathan Swoboda
c75abfb894 Bump version to 2025.11.0b5 2025-11-19 14:17:03 -05:00
Jesse Hills
1157b4aee8 [epaper_spi] Add basic 7.3in-Spectra-E6 model (#12001) 2025-11-19 14:17:03 -05:00
J. Nick Koston
71dc2d374d [web_server_idf] Fix pbuf_free crash by moving shutdown before close (#11995) 2025-11-19 14:17:03 -05:00
Jonathan Swoboda
0a224f919b [wifi] Fix positive RSSI values on 8266 (#11994) 2025-11-19 14:17:03 -05:00
Jonathan Swoboda
7ef4b4f3d9 [text_sensor] Fix infinite loop in substitute filter (#11989)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-11-19 14:17:03 -05:00
J. Nick Koston
13b875c763 [tests] Fix SNTP time ID conflicts in component tests for grouped testing (#11990) 2025-11-19 14:17:03 -05:00
Jesse Hills
b02b07ffaf [epaper_spi] Add basic 7.3in-Spectra-E6 model (#12001) 2025-11-19 14:11:45 -05:00
J. Nick Koston
8804bc2815 [web_server_idf] Fix pbuf_free crash by moving shutdown before close (#11995) 2025-11-20 07:58:33 +13:00
Jonathan Swoboda
61cef0a75c [api] Fix format warnings in dump (#11999) 2025-11-19 12:58:47 -05:00
Jonathan Swoboda
73bc5252a1 [wifi] Fix positive RSSI values on 8266 (#11994) 2025-11-19 10:12:57 -05:00
Jonathan Swoboda
f2b10ad132 [text_sensor] Fix infinite loop in substitute filter (#11989)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-11-19 10:12:34 -05:00
J. Nick Koston
100ea46f03 [tests] Fix SNTP time ID conflicts in component tests for grouped testing (#11990) 2025-11-18 23:19:54 -06:00
J. Nick Koston
b3ef05e5e1 [ld24xx] Modernize namespace declarations to C++17 syntax (#11988) 2025-11-19 04:00:39 +00:00
J. Nick Koston
45c994e4de [light] Modernize namespace declarations to C++17 syntax (#11986) 2025-11-18 21:56:23 -06:00
Jesse Hills
a72545639d Merge branch 'beta' into dev 2025-11-19 13:43:25 +13:00
J. Nick Koston
29374837c6 [wifi, captive_portal, web_server, wifi_info] Use stack allocation for MAC address formatting (#11963) 2025-11-18 17:06:34 -06:00
J. Nick Koston
70ed9c7c4d [wifi] Fix captive portal unusable when WiFi credentials are wrong (#11965) 2025-11-19 08:17:21 +13:00
dependabot[bot]
81fe5deaa9 Bump github/codeql-action from 4.31.3 to 4.31.4 (#11977)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-19 08:12:42 +13:00
Jonathan Swoboda
72e4b16a5b [sfa30] Fix negative temperature values (#11973) 2025-11-18 13:29:40 -05:00
Jonathan Swoboda
fe2befcec2 [bme68x] Print error when no sensors are configured (#11976) 2025-11-18 13:18:09 -05:00
J. Nick Koston
1888f5ffd5 [scheduler] Add defensive nullptr checks and explicit locking requirements (#11974) 2025-11-18 18:16:18 +00:00
Jonathan Swoboda
c59af22217 [esp32] Fix Arduino build on some ESP32 S2 boards (#11972) 2025-11-18 12:40:31 -05:00
J. Nick Koston
33983b051b [ld24xx] Use stack allocation for MAC and version formatting (#11961) 2025-11-18 10:51:47 -06:00
Clyde Stubbs
11d0d4d128 [lvgl] Apply scale to spinbox value (#11946) 2025-11-18 17:27:50 +13:00
Clyde Stubbs
a4242dee64 [build] Don't clear pio cache unless requested (#11966) 2025-11-18 15:11:49 +11:00
J. Nick Koston
0d6c9623ce [dashboard_import] Store package import URL in .rodata instead of RAM (#11951) 2025-11-17 20:02:16 -06:00
strange_v
0923bcd2ca [mipi_rgb] Fix GUITION-4848S040 colors (#11709) 2025-11-18 01:32:17 +00:00
J. Nick Koston
fdc7ae7760 [wifi] Skip redundant setter calls for default values (#11943) 2025-11-17 17:20:32 -06:00
J. Nick Koston
1a73f49cd2 [number] Modernize to C++17 nested namespaces (#11945) 2025-11-17 17:20:18 -06:00
dependabot[bot]
23f85162d0 Bump actions/checkout from 5.0.0 to 5.0.1 (#11957)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-17 15:39:01 -06:00
dependabot[bot]
7a238028a7 Bump ruamel-yaml-clib from 0.2.14 to 0.2.15 (#11956)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-17 15:38:44 -06:00
Jonathan Swoboda
3d6c361037 [core] Add support for setting environment variables (#11953) 2025-11-17 12:32:08 -05:00
Javier Peletier
9e1f8d83f8 [config] Support !remove and !extend with LVGL-style configs (#11534) 2025-11-17 18:03:11 +11:00
Jesse Hills
fa0aa6defc Merge branch 'beta' into dev 2025-11-17 17:41:46 +13:00
J. Nick Koston
10bdb47eae [cover] Modernize to C++17 nested namespaces (#11935) 2025-11-16 20:37:06 -06:00
Anton Sergunov
aa097a2fe6 [uart] Setup uart pins only if flags are set (#11914)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2025-11-17 14:25:00 +13:00
J. Nick Koston
3b860e784c [web_server_idf] Fix lwIP assertion crash by shutting down sockets on connection close (#11937) 2025-11-17 13:39:01 +13:00
J. Nick Koston
96ee38759d [web_server.ota] Merge multiple instances to prevent undefined behavior (#11905) 2025-11-17 13:38:52 +13:00
J. Nick Koston
986d3c8f13 [sntp] Merge multiple instances to fix crash and undefined behavior (#11904) 2025-11-17 13:38:38 +13:00
Clyde Stubbs
320120883c [lvgl] Migrate lv_font creation into Font class and optimise (#11915) 2025-11-17 08:47:54 +11:00
J. Nick Koston
4fc4da6ed2 [analyze-memory] Show all core symbols > 100 B instead of top 15 (#11909) 2025-11-16 07:35:31 -06:00
J. Nick Koston
6f4042f401 Add tests for sensor timeout filters (#11923) 2025-11-15 22:21:38 -06:00
J. Nick Koston
ea2b4c3e25 [binary_sensor] Modernize to C++17 nested namespaces and remove redundant qualifications (#11929) 2025-11-16 04:21:06 +00:00
J. Nick Koston
fc546ca3f6 [scheduler] Fix timing breakage after 49 days of uptime on ESP8266/RP2040 (#11924) 2025-11-15 22:20:57 -06:00
J. Nick Koston
6b158e760d [ld2410] Add timeout filter to prevent stuck targets (#11920) 2025-11-15 22:04:25 -06:00
J. Nick Koston
5710cab972 [ld2412] Fix stuck targets by adding timeout filter (#11919) 2025-11-15 22:03:43 -06:00
Clyde Stubbs
eb759efb3d [font] Store glyph data in flash only (#11926) 2025-11-16 12:48:02 +11:00
dependabot[bot]
1df996601d Bump ruff from 0.14.4 to 0.14.5 (#11910)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-11-14 19:14:07 +00:00
dependabot[bot]
c32891ec02 Bump github/codeql-action from 4.31.2 to 4.31.3 (#11911)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-14 13:09:59 -06:00
Jonathan Swoboda
2bf6d48fcf [uart] Improve error handling and validate buffer size (#11895)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2025-11-14 14:06:08 -05:00
Edward Firmo
e49a943cf7 [wifi] Allow use_psram with Arduino (#11902) 2025-11-14 09:13:48 -05:00
dependabot[bot]
67524e14ee Bump pylint from 4.0.2 to 4.0.3 (#11894)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-13 19:05:02 +00:00
Edward Firmo
2290eb0dd2 [light] Fix missing ColorMode::BRIGHTNESS case in logging (#11836) 2025-11-13 12:08:06 -06:00
Clyde Stubbs
0afcf67c32 [esp32] Add sdkconfig flag to make OTA work for 32MB flash (#11883)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-11-13 10:52:08 -05:00
Clyde Stubbs
952bdfaac2 [esp32] Make esp-idf default framework for P4 (#11884) 2025-11-13 09:55:48 -05:00
Jesse Hills
ed7e5cd325 Bump version to 2025.12.0-dev 2025-11-13 17:00:47 +13:00
Jonathan Swoboda
a15f46e741 Merge branch 'beta' into dev 2025-11-12 22:46:34 -05:00
tomaszduda23
d869108416 [nrf52] add settings for dcdc converter (#11841) 2025-11-12 20:06:20 -06:00
J. Nick Koston
2d6618da3c [wifi] Fix slow reconnection after connection loss for all network types (#11873)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-13 13:44:22 +13:00
J. Nick Koston
47fe84e922 [wifi][ethernet] Fix spurious warnings and unclear status after PR #9823 (#11871) 2025-11-13 13:43:51 +13:00
J. Nick Koston
735bf9930a [light] Fix dangling reference in compute_color_mode causing memory corruption (#11868) 2025-11-13 13:41:28 +13:00
J. Nick Koston
769137fc09 [mqtt] Fix crash with empty broker during upload/logs (#11866)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-13 13:40:26 +13:00
J. Nick Koston
3a5b3ad77d [thermostat] Replace std::map with FixedVector, reduce flash usage (#11875) 2025-11-12 17:55:06 -06:00
J. Nick Koston
859101ddc9 [api][event] Send events immediately to prevent loss during rapid triggers (#11777) 2025-11-13 12:42:50 +13:00
J. Nick Koston
29a50da635 [wifi] Use stack allocation for BSSID formatting in logging (#11859) 2025-11-12 14:27:06 -06:00
J. Nick Koston
5f0fa68d73 [esp32_ble] Use stack allocation for MAC formatting in dump_config (#11860) 2025-11-12 14:26:57 -06:00
J. Nick Koston
2f39b10baa [esp32_ble_tracker] Use initializer_list to eliminate compiler warning and reduce flash usage (#11861) 2025-11-12 14:26:46 -06:00
J. Nick Koston
5a550cc579 [api] Eliminate heap allocations when transmitting Event types (#11773) 2025-11-12 14:26:36 -06:00
J. Nick Koston
4b58cb4ce6 [wifi] Pass ManualIP by const reference to reduce stack usage (#11858) 2025-11-12 14:01:19 -06:00
J. Nick Koston
3872a2fd91 [captive_portal] Warn when enabled without WiFi AP configured (#11856) 2025-11-12 14:01:07 -06:00
dependabot[bot]
5d613ada83 Bump pytest from 9.0.0 to 9.0.1 (#11874)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-12 14:00:50 -06:00
J. Nick Koston
9de80b635a [core] Fix wait_until hanging when used in on_boot automations (#11869) 2025-11-12 17:56:19 +00:00
Jonathan Swoboda
748aee584a [esp32] Update the recommended platform to 55.03.31-2 (#11865) 2025-11-12 10:41:22 -05:00
Jonathan Swoboda
3cbfddcc83 Merge branch 'beta' into dev 2025-11-11 23:27:24 -05:00
J. Nick Koston
398dba4fc8 [ci] Reduce release time by removing 21 redundant ESP32-S3 IDF tests (#11850) 2025-11-12 16:44:19 +13:00
878 changed files with 19754 additions and 5369 deletions

View File

@@ -276,12 +276,12 @@ This document provides essential context for AI models interacting with this pro
## 7. Specific Instructions for AI Collaboration
* **Contribution Workflow (Pull Request Process):**
1. **Fork & Branch:** Create a new branch in your fork.
1. **Fork & Branch:** Create a new branch based on the `dev` branch (always use `git checkout -b <branch-name> dev` to ensure you're branching from `dev`, not the currently checked out branch).
2. **Make Changes:** Adhere to all coding conventions and patterns.
3. **Test:** Create component tests for all supported platforms and run the full test suite locally.
4. **Lint:** Run `pre-commit` to ensure code is compliant.
5. **Commit:** Commit your changes. There is no strict format for commit messages.
6. **Pull Request:** Submit a PR against the `dev` branch. The Pull Request title should have a prefix of the component being worked on (e.g., `[display] Fix bug`, `[abc123] Add new component`). Update documentation, examples, and add `CODEOWNERS` entries as needed. Pull requests should always be made with the PULL_REQUEST_TEMPLATE.md template filled out correctly.
6. **Pull Request:** Submit a PR against the `dev` branch. The Pull Request title should have a prefix of the component being worked on (e.g., `[display] Fix bug`, `[abc123] Add new component`). Update documentation, examples, and add `CODEOWNERS` entries as needed. Pull requests should always be made using the `.github/PULL_REQUEST_TEMPLATE.md` template - fill out all sections completely without removing any parts of the template.
* **Documentation Contributions:**
* Documentation is hosted in the separate `esphome/esphome-docs` repository.
@@ -402,35 +402,45 @@ This document provides essential context for AI models interacting with this pro
_use_feature = True
```
**Good Pattern (CORE.data with Helpers):**
**Bad Pattern (Flat Keys):**
```python
# Don't do this - keys should be namespaced under component domain
MY_FEATURE_KEY = "my_component_feature"
CORE.data[MY_FEATURE_KEY] = True
```
**Good Pattern (dataclass):**
```python
from dataclasses import dataclass, field
from esphome.core import CORE
# Keys for CORE.data storage
COMPONENT_STATE_KEY = "my_component_state"
USE_FEATURE_KEY = "my_component_use_feature"
DOMAIN = "my_component"
def _get_component_state() -> list:
"""Get component state from CORE.data."""
return CORE.data.setdefault(COMPONENT_STATE_KEY, [])
@dataclass
class MyComponentData:
feature_enabled: bool = False
item_count: int = 0
items: list[str] = field(default_factory=list)
def _get_use_feature() -> bool | None:
"""Get feature flag from CORE.data."""
return CORE.data.get(USE_FEATURE_KEY)
def _get_data() -> MyComponentData:
if DOMAIN not in CORE.data:
CORE.data[DOMAIN] = MyComponentData()
return CORE.data[DOMAIN]
def _set_use_feature(value: bool) -> None:
"""Set feature flag in CORE.data."""
CORE.data[USE_FEATURE_KEY] = value
def request_feature() -> None:
_get_data().feature_enabled = True
def enable_feature():
_set_use_feature(True)
def add_item(item: str) -> None:
_get_data().items.append(item)
```
If you need a real-world example, search for components that use `@dataclass` with `CORE.data` in the codebase. Note: Some components may use `TypedDict` for dictionary-based storage; both patterns are acceptable depending on your needs.
**Why this matters:**
- Module-level globals persist between compilation runs if the dashboard doesn't fork/exec
- `CORE.data` automatically clears between runs
- Typed helper functions provide better IDE support and maintainability
- Encapsulation makes state management explicit and testable
- Namespacing under `DOMAIN` prevents key collisions between components
- `@dataclass` provides type safety and cleaner attribute access
* **Security:** Be mindful of security when making changes to the API, web server, or any other network-related code. Do not hardcode secrets or keys.

View File

@@ -1 +1 @@
3d46b63015d761c85ca9cb77ab79a389509e5776701fb22aed16e7b79d432c0c
5969e705693278d984c5292e998df0cbaf34f7e1f04dfc7f7b7ad7168527bfa7

View File

@@ -7,6 +7,7 @@
- [ ] 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)
- [ ] Code quality improvements to existing code or addition of tests
- [ ] Other

View File

@@ -17,7 +17,7 @@ runs:
steps:
- name: Set up Python ${{ inputs.python-version }}
id: python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: ${{ inputs.python-version }}
- name: Restore Python virtual environment

View File

@@ -22,11 +22,11 @@ jobs:
if: github.event.action != 'labeled' || github.event.sender.type != 'Bot'
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
@@ -68,6 +68,7 @@ jobs:
'bugfix',
'new-feature',
'breaking-change',
'developer-breaking-change',
'code-quality'
];
@@ -367,6 +368,7 @@ jobs:
{ pattern: /- \[x\] Bugfix \(non-breaking change which fixes an issue\)/i, label: 'bugfix' },
{ pattern: /- \[x\] New feature \(non-breaking change which adds functionality\)/i, label: 'new-feature' },
{ pattern: /- \[x\] Breaking change \(fix or feature that would cause existing functionality to not work as expected\)/i, label: 'breaking-change' },
{ pattern: /- \[x\] Developer breaking change \(an API change that could break external components\)/i, label: 'developer-breaking-change' },
{ pattern: /- \[x\] Code quality improvements to existing code or addition of tests/i, label: 'code-quality' }
];

View File

@@ -21,9 +21,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: "3.11"

View File

@@ -21,10 +21,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: "3.11"

View File

@@ -43,9 +43,9 @@ jobs:
- "docker"
# - "lint"
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: "3.11"
- name: Set up Docker Buildx

View File

@@ -49,7 +49,7 @@ jobs:
- name: Check out code from base repository
if: steps.pr.outputs.skip != 'true'
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
# Always check out from the base repository (esphome/esphome), never from forks
# Use the PR's target branch to ensure we run trusted code from the main repo

View File

@@ -36,13 +36,13 @@ jobs:
cache-key: ${{ steps.cache-key.outputs.key }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Generate cache-key
id: cache-key
run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment
@@ -70,7 +70,7 @@ jobs:
if: needs.determine-jobs.outputs.python-linters == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -91,7 +91,7 @@ jobs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -132,7 +132,7 @@ jobs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Restore Python
id: restore-python
uses: ./.github/actions/restore-python
@@ -183,7 +183,7 @@ jobs:
component-test-batches: ${{ steps.determine.outputs.component-test-batches }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
# Fetch enough history to find the merge base
fetch-depth: 2
@@ -237,10 +237,10 @@ jobs:
if: needs.determine-jobs.outputs.integration-tests == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Python 3.13
id: python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: "3.13"
- name: Restore Python virtual environment
@@ -273,7 +273,7 @@ jobs:
if: github.event_name == 'pull_request' && (needs.determine-jobs.outputs.cpp-unit-tests-run-all == 'true' || needs.determine-jobs.outputs.cpp-unit-tests-components != '[]')
steps:
- name: Check out code from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Restore Python
uses: ./.github/actions/restore-python
@@ -321,7 +321,7 @@ jobs:
steps:
- name: Check out code from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
@@ -400,7 +400,7 @@ jobs:
GH_TOKEN: ${{ github.token }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
@@ -489,7 +489,7 @@ jobs:
steps:
- name: Check out code from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
@@ -577,7 +577,7 @@ jobs:
version: 1.0
- name: Check out code from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -662,13 +662,13 @@ jobs:
if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release')
steps:
- name: Check out code from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- uses: esphome/action@43cd1109c09c544d97196f7730ee5b2e0cc6d81e # v3.0.1 fork with pinned actions/cache
- uses: esphome/pre-commit-action@43cd1109c09c544d97196f7730ee5b2e0cc6d81e # v3.0.1 fork with pinned actions/cache
env:
SKIP: pylint,clang-tidy-hash
- uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0
@@ -688,7 +688,7 @@ jobs:
skip: ${{ steps.check-script.outputs.skip }}
steps:
- name: Check out target branch
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ github.base_ref }}
@@ -840,7 +840,7 @@ jobs:
flash_usage: ${{ steps.extract.outputs.flash_usage }}
steps:
- name: Check out PR branch
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -908,7 +908,7 @@ jobs:
GH_TOKEN: ${{ github.token }}
steps:
- name: Check out code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -959,13 +959,13 @@ jobs:
- memory-impact-comment
if: always()
steps:
- name: Success
if: ${{ !(contains(needs.*.result, 'failure')) }}
run: exit 0
- name: Failure
if: ${{ contains(needs.*.result, 'failure') }}
- name: Check job results
env:
JSON_DOC: ${{ toJSON(needs) }}
NEEDS_JSON: ${{ toJSON(needs) }}
run: |
echo $JSON_DOC | jq
exit 1
# memory-impact-target-branch is allowed to fail without blocking CI.
# This job builds the target branch (dev/beta/release) which may fail because:
# 1. The target branch has a build issue independent of this PR
# 2. This PR fixes a build issue on the target branch
# In either case, we only care that the PR branch builds successfully.
echo "$NEEDS_JSON" | jq -e 'del(.["memory-impact-target-branch"]) | all(.result != "failure")'

View File

@@ -54,11 +54,11 @@ jobs:
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
uses: github/codeql-action/init@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
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@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
uses: github/codeql-action/analyze@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
with:
category: "/language:${{matrix.language}}"

View File

@@ -20,7 +20,7 @@ jobs:
branch_build: ${{ steps.tag.outputs.branch_build }}
deploy_env: ${{ steps.tag.outputs.deploy_env }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Get tag
id: tag
# yamllint disable rule:line-length
@@ -60,9 +60,9 @@ jobs:
contents: read
id-token: write
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: "3.x"
- name: Build
@@ -92,9 +92,9 @@ jobs:
os: "ubuntu-24.04-arm"
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: "3.11"
@@ -168,7 +168,7 @@ jobs:
- ghcr
- dockerhub
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Download digests
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
@@ -219,10 +219,19 @@ jobs:
- init
- deploy-manifest
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
owner: esphome
repositories: home-assistant-addon
- name: Trigger Workflow
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ secrets.DEPLOY_HA_ADDON_REPO_TOKEN }}
github-token: ${{ steps.generate-token.outputs.token }}
script: |
let description = "ESPHome";
if (context.eventName == "release") {
@@ -245,10 +254,19 @@ jobs:
needs: [init]
environment: ${{ needs.init.outputs.deploy_env }}
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
owner: esphome
repositories: esphome-schema
- name: Trigger Workflow
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ secrets.DEPLOY_ESPHOME_SCHEMA_REPO_TOKEN }}
github-token: ${{ steps.generate-token.outputs.token }}
script: |
github.rest.actions.createWorkflowDispatch({
owner: "esphome",
@@ -259,3 +277,34 @@ jobs:
version: "${{ needs.init.outputs.tag }}",
}
})
version-notifier:
if: github.repository == 'esphome/esphome' && needs.init.outputs.branch_build == 'false'
runs-on: ubuntu-latest
needs:
- init
- deploy-manifest
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
owner: esphome
repositories: version-notifier
- name: Trigger Workflow
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
github.rest.actions.createWorkflowDispatch({
owner: "esphome",
repo: "version-notifier",
workflow_id: "notify.yml",
ref: "main",
inputs: {
version: "${{ needs.init.outputs.tag }}",
}
})

View File

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

View File

@@ -13,16 +13,16 @@ jobs:
if: github.repository == 'esphome/esphome'
steps:
- name: Checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Checkout Home Assistant
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
repository: home-assistant/core
path: lib/home-assistant
- name: Setup Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: 3.13
@@ -41,7 +41,7 @@ jobs:
python script/run-in-env.py pre-commit run --all-files
- name: Commit changes
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.11
with:
commit-message: "Synchronise Device Classes from Home Assistant"
committer: esphomebot <esphome@openhomefoundation.org>

View File

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

View File

@@ -21,6 +21,7 @@ esphome/components/adc128s102/* @DeerMaximum
esphome/components/addressable_light/* @justfalter
esphome/components/ade7880/* @kpfleming
esphome/components/ade7953/* @angelnu
esphome/components/ade7953_base/* @angelnu
esphome/components/ade7953_i2c/* @angelnu
esphome/components/ade7953_spi/* @angelnu
esphome/components/ads1118/* @solomondg1
@@ -72,6 +73,7 @@ esphome/components/bl0942/* @dbuezas @dwmw2
esphome/components/ble_client/* @buxtronix @clydebarrow
esphome/components/ble_nus/* @tomaszduda23
esphome/components/bluetooth_proxy/* @bdraco @jesserockz
esphome/components/bm8563/* @abmantis
esphome/components/bme280_base/* @esphome/core
esphome/components/bme280_spi/* @apbodrov
esphome/components/bme680_bsec/* @trvrnrth
@@ -95,6 +97,7 @@ esphome/components/camera_encoder/* @DT-art1
esphome/components/canbus/* @danielschramm @mvturnho
esphome/components/cap1188/* @mreditor97
esphome/components/captive_portal/* @esphome/core
esphome/components/cc1101/* @gabest11 @lygris
esphome/components/ccs811/* @habbie
esphome/components/cd74hc4067/* @asoehlke
esphome/components/ch422g/* @clydebarrow @jesterret
@@ -188,6 +191,7 @@ esphome/components/gps/* @coogle @ximex
esphome/components/graph/* @synco
esphome/components/graphical_display_menu/* @MrMDavidson
esphome/components/gree/* @orestismers
esphome/components/gree/switch/* @nagyrobi
esphome/components/grove_gas_mc_v2/* @YorkshireIoT
esphome/components/grove_tb6612fng/* @max246
esphome/components/growatt_solar/* @leeuwte
@@ -202,11 +206,13 @@ esphome/components/havells_solar/* @sourabhjaiswal
esphome/components/hbridge/fan/* @WeekendWarrior
esphome/components/hbridge/light/* @DotNetDann
esphome/components/hbridge/switch/* @dwmw2
esphome/components/hc8/* @omartijn
esphome/components/hdc2010/* @optimusprimespace @ssieb
esphome/components/he60r/* @clydebarrow
esphome/components/heatpumpir/* @rob-deutsch
esphome/components/hitachi_ac424/* @sourabhjaiswal
esphome/components/hlk_fm22x/* @OnFreund
esphome/components/hlw8032/* @rici4kubicek
esphome/components/hm3301/* @freekode
esphome/components/hmac_md5/* @dwmw2
esphome/components/homeassistant/* @esphome/core @OttoWinter
@@ -222,6 +228,7 @@ esphome/components/hte501/* @Stock-M
esphome/components/http_request/ota/* @oarcher
esphome/components/http_request/update/* @jesserockz
esphome/components/htu31d/* @betterengineering
esphome/components/hub75/* @stuartparmenter
esphome/components/hydreon_rgxx/* @functionpointer
esphome/components/hyt271/* @Philippe12
esphome/components/i2c/* @esphome/core
@@ -301,7 +308,7 @@ esphome/components/md5/* @esphome/core
esphome/components/mdns/* @esphome/core
esphome/components/media_player/* @jesserockz
esphome/components/micro_wake_word/* @jesserockz @kahrendt
esphome/components/micronova/* @jorre05
esphome/components/micronova/* @edenhaus @jorre05
esphome/components/microphone/* @jesserockz @kahrendt
esphome/components/mics_4514/* @jesserockz
esphome/components/midea/* @dudanov
@@ -460,6 +467,7 @@ esphome/components/st7735/* @SenexCrenshaw
esphome/components/st7789v/* @kbx81
esphome/components/st7920/* @marsjan155
esphome/components/statsd/* @Links2004
esphome/components/stts22h/* @B48D81EFCC
esphome/components/substitutions/* @esphome/core
esphome/components/sun/* @OttoWinter
esphome/components/sun_gtil2/* @Mat931
@@ -481,6 +489,7 @@ esphome/components/template/datetime/* @rfdarter
esphome/components/template/event/* @nohat
esphome/components/template/fan/* @ssieb
esphome/components/text/* @mauritskorse
esphome/components/thermopro_ble/* @sittner
esphome/components/thermostat/* @kbx81
esphome/components/time/* @esphome/core
esphome/components/tinyusb/* @kbx81
@@ -515,6 +524,7 @@ esphome/components/ufire_ise/* @pvizeli
esphome/components/ultrasonic/* @OttoWinter
esphome/components/update/* @jesserockz
esphome/components/uponor_smatrix/* @kroimon
esphome/components/usb_cdc_acm/* @kbx81
esphome/components/usb_host/* @clydebarrow
esphome/components/usb_uart/* @clydebarrow
esphome/components/valve/* @esphome/core

View File

@@ -2,7 +2,7 @@
We welcome contributions to the ESPHome suite of code and documentation!
Please read our [contributing guide](https://esphome.io/guides/contributing.html) if you wish to contribute to the
Please read our [contributing guide](https://developers.esphome.io/contributing/code/) if you wish to contribute to the
project and be sure to join us on [Discord](https://discord.gg/KhAMKrd).
**See also:**

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 = 2025.11.0b4
PROJECT_NUMBER = 2025.12.3
# 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

@@ -2,8 +2,8 @@
<a href="https://esphome.io/">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://esphome.io/_static/logo-text-on-dark.svg", alt="ESPHome Logo">
<img src="https://esphome.io/_static/logo-text-on-light.svg" alt="ESPHome Logo">
<source media="(prefers-color-scheme: dark)" srcset="https://media.esphome.io/logo/logo-text-on-dark.svg">
<img src="https://media.esphome.io/logo/logo-text-on-light.svg" alt="ESPHome Logo">
</picture>
</a>

View File

@@ -944,6 +944,7 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
"""
from esphome import platformio_api
from esphome.analyze_memory.cli import MemoryAnalyzerCLI
from esphome.analyze_memory.ram_strings import RamStringsAnalyzer
# Always compile to ensure fresh data (fast if no changes - just relinks)
exit_code = write_cpp(config)
@@ -966,7 +967,7 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
external_components = detect_external_components(config)
_LOGGER.debug("Detected external components: %s", external_components)
# Perform memory analysis
# Perform component memory analysis
_LOGGER.info("Analyzing memory usage...")
analyzer = MemoryAnalyzerCLI(
str(firmware_elf),
@@ -976,11 +977,28 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
)
analyzer.analyze()
# Generate and display report
# Generate and display component report
report = analyzer.generate_report()
print()
print(report)
# Perform RAM strings analysis
_LOGGER.info("Analyzing RAM strings...")
try:
ram_analyzer = RamStringsAnalyzer(
str(firmware_elf),
objdump_path=idedata.objdump_path,
platform=CORE.target_platform,
)
ram_analyzer.analyze()
# Generate and display RAM strings report
ram_report = ram_analyzer.generate_report()
print()
print(ram_report)
except Exception as e: # pylint: disable=broad-except
_LOGGER.warning("RAM strings analysis failed: %s", e)
return 0
@@ -1319,7 +1337,7 @@ def parse_args(argv):
"clean-all", help="Clean all build and platform files."
)
parser_clean_all.add_argument(
"configuration", help="Your YAML configuration directory.", nargs="*"
"configuration", help="Your YAML file or configuration directory.", nargs="*"
)
parser_dashboard = subparsers.add_parser(

View File

@@ -15,6 +15,7 @@ from .const import (
SECTION_TO_ATTR,
SYMBOL_PATTERNS,
)
from .demangle import batch_demangle
from .helpers import (
get_component_class_patterns,
get_esphome_components,
@@ -27,15 +28,6 @@ if TYPE_CHECKING:
_LOGGER = logging.getLogger(__name__)
# GCC global constructor/destructor prefix annotations
_GCC_PREFIX_ANNOTATIONS = {
"_GLOBAL__sub_I_": "global constructor for",
"_GLOBAL__sub_D_": "global destructor for",
}
# GCC optimization suffix pattern (e.g., $isra$0, $part$1, $constprop$2)
_GCC_OPTIMIZATION_SUFFIX_PATTERN = re.compile(r"(\$(?:isra|part|constprop)\$\d+)")
# C++ runtime patterns for categorization
_CPP_RUNTIME_PATTERNS = frozenset(["vtable", "typeinfo", "thunk"])
@@ -312,168 +304,9 @@ class MemoryAnalyzer:
if not symbols:
return
# Try to find the appropriate c++filt for the platform
cppfilt_cmd = "c++filt"
_LOGGER.info("Demangling %d symbols", len(symbols))
_LOGGER.debug("objdump_path = %s", self.objdump_path)
# Check if we have a toolchain-specific c++filt
if self.objdump_path and self.objdump_path != "objdump":
# Replace objdump with c++filt in the path
potential_cppfilt = self.objdump_path.replace("objdump", "c++filt")
_LOGGER.info("Checking for toolchain c++filt at: %s", potential_cppfilt)
if Path(potential_cppfilt).exists():
cppfilt_cmd = potential_cppfilt
_LOGGER.info("✓ Using toolchain c++filt: %s", cppfilt_cmd)
else:
_LOGGER.info(
"✗ Toolchain c++filt not found at %s, using system c++filt",
potential_cppfilt,
)
else:
_LOGGER.info("✗ Using system c++filt (objdump_path=%s)", self.objdump_path)
# Strip GCC optimization suffixes and prefixes before demangling
# Suffixes like $isra$0, $part$0, $constprop$0 confuse c++filt
# Prefixes like _GLOBAL__sub_I_ need to be removed and tracked
symbols_stripped: list[str] = []
symbols_prefixes: list[str] = [] # Track removed prefixes
for symbol in symbols:
# Remove GCC optimization markers
stripped = _GCC_OPTIMIZATION_SUFFIX_PATTERN.sub("", symbol)
# Handle GCC global constructor/initializer prefixes
# _GLOBAL__sub_I_<mangled> -> extract <mangled> for demangling
prefix = ""
for gcc_prefix in _GCC_PREFIX_ANNOTATIONS:
if stripped.startswith(gcc_prefix):
prefix = gcc_prefix
stripped = stripped[len(prefix) :]
break
symbols_stripped.append(stripped)
symbols_prefixes.append(prefix)
try:
# Send all symbols to c++filt at once
result = subprocess.run(
[cppfilt_cmd],
input="\n".join(symbols_stripped),
capture_output=True,
text=True,
check=False,
)
except (subprocess.SubprocessError, OSError, UnicodeDecodeError) as e:
# On error, cache originals
_LOGGER.warning("Failed to batch demangle symbols: %s", e)
for symbol in symbols:
self._demangle_cache[symbol] = symbol
return
if result.returncode != 0:
_LOGGER.warning(
"c++filt exited with code %d: %s",
result.returncode,
result.stderr[:200] if result.stderr else "(no error output)",
)
# Cache originals on failure
for symbol in symbols:
self._demangle_cache[symbol] = symbol
return
# Process demangled output
self._process_demangled_output(
symbols, symbols_stripped, symbols_prefixes, result.stdout, cppfilt_cmd
)
def _process_demangled_output(
self,
symbols: list[str],
symbols_stripped: list[str],
symbols_prefixes: list[str],
demangled_output: str,
cppfilt_cmd: str,
) -> None:
"""Process demangled symbol output and populate cache.
Args:
symbols: Original symbol names
symbols_stripped: Stripped symbol names sent to c++filt
symbols_prefixes: Removed prefixes to restore
demangled_output: Output from c++filt
cppfilt_cmd: Path to c++filt command (for logging)
"""
demangled_lines = demangled_output.strip().split("\n")
failed_count = 0
for original, stripped, prefix, demangled in zip(
symbols, symbols_stripped, symbols_prefixes, demangled_lines
):
# Add back any prefix that was removed
demangled = self._restore_symbol_prefix(prefix, stripped, demangled)
# If we stripped a suffix, add it back to the demangled name for clarity
if original != stripped and not prefix:
demangled = self._restore_symbol_suffix(original, demangled)
self._demangle_cache[original] = demangled
# Log symbols that failed to demangle (stayed the same as stripped version)
if stripped == demangled and stripped.startswith("_Z"):
failed_count += 1
if failed_count <= 5: # Only log first 5 failures
_LOGGER.warning("Failed to demangle: %s", original)
if failed_count == 0:
_LOGGER.info("Successfully demangled all %d symbols", len(symbols))
return
_LOGGER.warning(
"Failed to demangle %d/%d symbols using %s",
failed_count,
len(symbols),
cppfilt_cmd,
)
@staticmethod
def _restore_symbol_prefix(prefix: str, stripped: str, demangled: str) -> str:
"""Restore prefix that was removed before demangling.
Args:
prefix: Prefix that was removed (e.g., "_GLOBAL__sub_I_")
stripped: Stripped symbol name
demangled: Demangled symbol name
Returns:
Demangled name with prefix restored/annotated
"""
if not prefix:
return demangled
# Successfully demangled - add descriptive prefix
if demangled != stripped and (
annotation := _GCC_PREFIX_ANNOTATIONS.get(prefix)
):
return f"[{annotation}: {demangled}]"
# Failed to demangle - restore original prefix
return prefix + demangled
@staticmethod
def _restore_symbol_suffix(original: str, demangled: str) -> str:
"""Restore GCC optimization suffix that was removed before demangling.
Args:
original: Original symbol name with suffix
demangled: Demangled symbol name without suffix
Returns:
Demangled name with suffix annotation
"""
if suffix_match := _GCC_OPTIMIZATION_SUFFIX_PATTERN.search(original):
return f"{demangled} [{suffix_match.group(1)}]"
return demangled
self._demangle_cache = batch_demangle(symbols, objdump_path=self.objdump_path)
_LOGGER.info("Successfully demangled %d symbols", len(self._demangle_cache))
def _demangle_symbol(self, symbol: str) -> str:
"""Get demangled C++ symbol name from cache."""

View File

@@ -0,0 +1,182 @@
"""Symbol demangling utilities for memory analysis.
This module provides functions for demangling C++ symbol names using c++filt.
"""
from __future__ import annotations
import logging
import re
import subprocess
from .toolchain import find_tool
_LOGGER = logging.getLogger(__name__)
# GCC global constructor/destructor prefix annotations
GCC_PREFIX_ANNOTATIONS = {
"_GLOBAL__sub_I_": "global constructor for",
"_GLOBAL__sub_D_": "global destructor for",
}
# GCC optimization suffix pattern (e.g., $isra$0, $part$1, $constprop$2)
GCC_OPTIMIZATION_SUFFIX_PATTERN = re.compile(r"(\$(?:isra|part|constprop)\$\d+)")
def _strip_gcc_annotations(symbol: str) -> tuple[str, str]:
"""Strip GCC optimization suffixes and prefixes from a symbol.
Args:
symbol: The mangled symbol name
Returns:
Tuple of (stripped_symbol, removed_prefix)
"""
# Remove GCC optimization markers
stripped = GCC_OPTIMIZATION_SUFFIX_PATTERN.sub("", symbol)
# Handle GCC global constructor/initializer prefixes
prefix = ""
for gcc_prefix in GCC_PREFIX_ANNOTATIONS:
if stripped.startswith(gcc_prefix):
prefix = gcc_prefix
stripped = stripped[len(prefix) :]
break
return stripped, prefix
def _restore_symbol_prefix(prefix: str, stripped: str, demangled: str) -> str:
"""Restore prefix that was removed before demangling.
Args:
prefix: Prefix that was removed (e.g., "_GLOBAL__sub_I_")
stripped: Stripped symbol name
demangled: Demangled symbol name
Returns:
Demangled name with prefix restored/annotated
"""
if not prefix:
return demangled
# Successfully demangled - add descriptive prefix
if demangled != stripped and (annotation := GCC_PREFIX_ANNOTATIONS.get(prefix)):
return f"[{annotation}: {demangled}]"
# Failed to demangle - restore original prefix
return prefix + demangled
def _restore_symbol_suffix(original: str, demangled: str) -> str:
"""Restore GCC optimization suffix that was removed before demangling.
Args:
original: Original symbol name with suffix
demangled: Demangled symbol name without suffix
Returns:
Demangled name with suffix annotation
"""
if suffix_match := GCC_OPTIMIZATION_SUFFIX_PATTERN.search(original):
return f"{demangled} [{suffix_match.group(1)}]"
return demangled
def batch_demangle(
symbols: list[str],
cppfilt_path: str | None = None,
objdump_path: str | None = None,
) -> dict[str, str]:
"""Batch demangle C++ symbol names.
Args:
symbols: List of symbol names to demangle
cppfilt_path: Path to c++filt binary (auto-detected if not provided)
objdump_path: Path to objdump binary to derive c++filt path from
Returns:
Dictionary mapping original symbol names to demangled names
"""
cache: dict[str, str] = {}
if not symbols:
return cache
# Find c++filt tool
cppfilt_cmd = cppfilt_path or find_tool("c++filt", objdump_path)
if not cppfilt_cmd:
_LOGGER.warning("Could not find c++filt, symbols will not be demangled")
return {s: s for s in symbols}
_LOGGER.debug("Demangling %d symbols using %s", len(symbols), cppfilt_cmd)
# Strip GCC optimization suffixes and prefixes before demangling
symbols_stripped: list[str] = []
symbols_prefixes: list[str] = []
for symbol in symbols:
stripped, prefix = _strip_gcc_annotations(symbol)
symbols_stripped.append(stripped)
symbols_prefixes.append(prefix)
try:
result = subprocess.run(
[cppfilt_cmd],
input="\n".join(symbols_stripped),
capture_output=True,
text=True,
check=False,
)
except (subprocess.SubprocessError, OSError, UnicodeDecodeError) as e:
_LOGGER.warning("Failed to batch demangle symbols: %s", e)
return {s: s for s in symbols}
if result.returncode != 0:
_LOGGER.warning(
"c++filt exited with code %d: %s",
result.returncode,
result.stderr[:200] if result.stderr else "(no error output)",
)
return {s: s for s in symbols}
# Process demangled output
demangled_lines = result.stdout.strip().split("\n")
# Check for output length mismatch
if len(demangled_lines) != len(symbols):
_LOGGER.warning(
"c++filt output mismatch: expected %d lines, got %d",
len(symbols),
len(demangled_lines),
)
return {s: s for s in symbols}
failed_count = 0
for original, stripped, prefix, demangled in zip(
symbols, symbols_stripped, symbols_prefixes, demangled_lines
):
# Add back any prefix that was removed
demangled = _restore_symbol_prefix(prefix, stripped, demangled)
# If we stripped a suffix, add it back to the demangled name for clarity
if original != stripped and not prefix:
demangled = _restore_symbol_suffix(original, demangled)
cache[original] = demangled
# Count symbols that failed to demangle
if stripped == demangled and stripped.startswith("_Z"):
failed_count += 1
if failed_count <= 5:
_LOGGER.debug("Failed to demangle: %s", original)
if failed_count > 0:
_LOGGER.debug(
"Failed to demangle %d/%d symbols using %s",
failed_count,
len(symbols),
cppfilt_cmd,
)
return cache

View File

@@ -0,0 +1,493 @@
"""Analyzer for RAM-stored strings in ESP8266/ESP32 firmware ELF files.
This module identifies strings that are stored in RAM sections (.data, .bss, .rodata)
rather than in flash sections (.irom0.text, .irom.text), which is important for
memory-constrained platforms like ESP8266.
"""
from __future__ import annotations
from collections import defaultdict
from dataclasses import dataclass
import logging
from pathlib import Path
import re
import subprocess
from .demangle import batch_demangle
from .toolchain import find_tool
_LOGGER = logging.getLogger(__name__)
# ESP8266: .rodata is in RAM (DRAM), not flash
# ESP32: .rodata is in flash, mapped to data bus
ESP8266_RAM_SECTIONS = frozenset([".data", ".rodata", ".bss"])
ESP8266_FLASH_SECTIONS = frozenset([".irom0.text", ".irom.text", ".text"])
# ESP32: .rodata is memory-mapped from flash
ESP32_RAM_SECTIONS = frozenset([".data", ".bss", ".dram0.data", ".dram0.bss"])
ESP32_FLASH_SECTIONS = frozenset([".text", ".rodata", ".flash.text", ".flash.rodata"])
# nm symbol types for data symbols (D=global data, d=local data, R=rodata, B=bss)
DATA_SYMBOL_TYPES = frozenset(["D", "d", "R", "r", "B", "b"])
@dataclass
class SectionInfo:
"""Information about an ELF section."""
name: str
address: int
size: int
@dataclass
class RamString:
"""A string found in RAM."""
section: str
address: int
content: str
@property
def size(self) -> int:
"""Size in bytes including null terminator."""
return len(self.content) + 1
@dataclass
class RamSymbol:
"""A symbol found in RAM."""
name: str
sym_type: str
address: int
size: int
section: str
demangled: str = "" # Demangled name, set after batch demangling
class RamStringsAnalyzer:
"""Analyzes ELF files to find strings stored in RAM."""
def __init__(
self,
elf_path: str,
objdump_path: str | None = None,
min_length: int = 8,
platform: str = "esp32",
) -> None:
"""Initialize the RAM strings analyzer.
Args:
elf_path: Path to the ELF file to analyze
objdump_path: Path to objdump binary (used to find other tools)
min_length: Minimum string length to report (default: 8)
platform: Platform name ("esp8266", "esp32", etc.) for section mapping
"""
self.elf_path = Path(elf_path)
if not self.elf_path.exists():
raise FileNotFoundError(f"ELF file not found: {elf_path}")
self.objdump_path = objdump_path
self.min_length = min_length
self.platform = platform
# Set RAM/flash sections based on platform
if self.platform == "esp8266":
self.ram_sections = ESP8266_RAM_SECTIONS
self.flash_sections = ESP8266_FLASH_SECTIONS
else:
# ESP32 and other platforms
self.ram_sections = ESP32_RAM_SECTIONS
self.flash_sections = ESP32_FLASH_SECTIONS
self.sections: dict[str, SectionInfo] = {}
self.ram_strings: list[RamString] = []
self.ram_symbols: list[RamSymbol] = []
def _run_command(self, cmd: list[str]) -> str:
"""Run a command and return its output."""
try:
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
return result.stdout
except subprocess.CalledProcessError as e:
_LOGGER.debug("Command failed: %s - %s", " ".join(cmd), e.stderr)
raise
except FileNotFoundError:
_LOGGER.warning("Command not found: %s", cmd[0])
raise
def analyze(self) -> None:
"""Perform the full RAM analysis."""
self._parse_sections()
self._extract_strings()
self._analyze_symbols()
self._demangle_symbols()
def _parse_sections(self) -> None:
"""Parse section headers from ELF file."""
objdump = find_tool("objdump", self.objdump_path)
if not objdump:
_LOGGER.error("Could not find objdump command")
return
try:
output = self._run_command([objdump, "-h", str(self.elf_path)])
except (subprocess.CalledProcessError, FileNotFoundError):
return
# Parse section headers
# Format: Idx Name Size VMA LMA File off Algn
section_pattern = re.compile(
r"^\s*\d+\s+(\S+)\s+([0-9a-fA-F]+)\s+([0-9a-fA-F]+)"
)
for line in output.split("\n"):
if match := section_pattern.match(line):
name = match.group(1)
size = int(match.group(2), 16)
vma = int(match.group(3), 16)
self.sections[name] = SectionInfo(name, vma, size)
def _extract_strings(self) -> None:
"""Extract strings from RAM sections."""
objdump = find_tool("objdump", self.objdump_path)
if not objdump:
return
for section_name in self.ram_sections:
if section_name not in self.sections:
continue
try:
output = self._run_command(
[objdump, "-s", "-j", section_name, str(self.elf_path)]
)
except subprocess.CalledProcessError:
# Section may exist but have no content (e.g., .bss)
continue
except FileNotFoundError:
continue
strings = self._parse_hex_dump(output, section_name)
self.ram_strings.extend(strings)
def _parse_hex_dump(self, output: str, section_name: str) -> list[RamString]:
"""Parse hex dump output to extract strings.
Args:
output: Output from objdump -s
section_name: Name of the section being parsed
Returns:
List of RamString objects
"""
strings: list[RamString] = []
current_string = bytearray()
string_start_addr = 0
for line in output.split("\n"):
# Lines look like: " 3ffef8a0 00000000 00000000 00000000 00000000 ................"
match = re.match(r"^\s+([0-9a-fA-F]+)\s+((?:[0-9a-fA-F]{2,8}\s*)+)", line)
if not match:
continue
addr = int(match.group(1), 16)
hex_data = match.group(2).strip()
# Convert hex to bytes
hex_bytes = hex_data.split()
byte_offset = 0
for hex_chunk in hex_bytes:
# Handle both byte-by-byte and word formats
for i in range(0, len(hex_chunk), 2):
byte_val = int(hex_chunk[i : i + 2], 16)
if 0x20 <= byte_val <= 0x7E: # Printable ASCII
if not current_string:
string_start_addr = addr + byte_offset
current_string.append(byte_val)
else:
if byte_val == 0 and len(current_string) >= self.min_length:
# Found null terminator
strings.append(
RamString(
section=section_name,
address=string_start_addr,
content=current_string.decode(
"ascii", errors="ignore"
),
)
)
current_string = bytearray()
byte_offset += 1
return strings
def _analyze_symbols(self) -> None:
"""Analyze symbols in RAM sections."""
nm = find_tool("nm", self.objdump_path)
if not nm:
return
try:
output = self._run_command([nm, "-S", "--size-sort", str(self.elf_path)])
except (subprocess.CalledProcessError, FileNotFoundError):
return
for line in output.split("\n"):
parts = line.split()
if len(parts) < 4:
continue
try:
addr = int(parts[0], 16)
size = int(parts[1], 16) if parts[1] != "?" else 0
except ValueError:
continue
sym_type = parts[2]
name = " ".join(parts[3:])
# Filter for data symbols
if sym_type not in DATA_SYMBOL_TYPES:
continue
# Check if symbol is in a RAM section
for section_name in self.ram_sections:
if section_name not in self.sections:
continue
section = self.sections[section_name]
if section.address <= addr < section.address + section.size:
self.ram_symbols.append(
RamSymbol(
name=name,
sym_type=sym_type,
address=addr,
size=size,
section=section_name,
)
)
break
def _demangle_symbols(self) -> None:
"""Batch demangle all RAM symbol names."""
if not self.ram_symbols:
return
# Collect all symbol names and demangle them
symbol_names = [s.name for s in self.ram_symbols]
demangle_cache = batch_demangle(symbol_names, objdump_path=self.objdump_path)
# Assign demangled names to symbols
for symbol in self.ram_symbols:
symbol.demangled = demangle_cache.get(symbol.name, symbol.name)
def _get_sections_size(self, section_names: frozenset[str]) -> int:
"""Get total size of specified sections."""
return sum(
section.size
for name, section in self.sections.items()
if name in section_names
)
def get_total_ram_usage(self) -> int:
"""Get total RAM usage from RAM sections."""
return self._get_sections_size(self.ram_sections)
def get_total_flash_usage(self) -> int:
"""Get total flash usage from flash sections."""
return self._get_sections_size(self.flash_sections)
def get_total_string_bytes(self) -> int:
"""Get total bytes used by strings in RAM."""
return sum(s.size for s in self.ram_strings)
def get_repeated_strings(self) -> list[tuple[str, int]]:
"""Find strings that appear multiple times.
Returns:
List of (string, count) tuples sorted by potential savings
"""
string_counts: dict[str, int] = defaultdict(int)
for ram_string in self.ram_strings:
string_counts[ram_string.content] += 1
return sorted(
[(s, c) for s, c in string_counts.items() if c > 1],
key=lambda x: x[1] * (len(x[0]) + 1),
reverse=True,
)
def get_long_strings(self, min_len: int = 20) -> list[RamString]:
"""Get strings longer than the specified length.
Args:
min_len: Minimum string length
Returns:
List of RamString objects sorted by length
"""
return sorted(
[s for s in self.ram_strings if len(s.content) >= min_len],
key=lambda x: len(x.content),
reverse=True,
)
def get_largest_symbols(self, min_size: int = 100) -> list[RamSymbol]:
"""Get RAM symbols larger than the specified size.
Args:
min_size: Minimum symbol size in bytes
Returns:
List of RamSymbol objects sorted by size
"""
return sorted(
[s for s in self.ram_symbols if s.size >= min_size],
key=lambda x: x.size,
reverse=True,
)
def generate_report(self, show_all_sections: bool = False) -> str:
"""Generate a formatted RAM strings analysis report.
Args:
show_all_sections: If True, show all sections, not just RAM
Returns:
Formatted report string
"""
lines: list[str] = []
table_width = 80
lines.append("=" * table_width)
lines.append(
f"RAM Strings Analysis ({self.platform.upper()})".center(table_width)
)
lines.append("=" * table_width)
lines.append("")
# Section Analysis
lines.append("SECTION ANALYSIS")
lines.append("-" * table_width)
lines.append(f"{'Section':<20} {'Address':<12} {'Size':<12} {'Location'}")
lines.append("-" * table_width)
total_ram_usage = 0
total_flash_usage = 0
for name, section in sorted(self.sections.items(), key=lambda x: x[1].address):
if name in self.ram_sections:
location = "RAM"
total_ram_usage += section.size
elif name in self.flash_sections:
location = "FLASH"
total_flash_usage += section.size
else:
location = "OTHER"
if show_all_sections or name in self.ram_sections:
lines.append(
f"{name:<20} 0x{section.address:08x} {section.size:>8} B {location}"
)
lines.append("-" * table_width)
lines.append(f"Total RAM sections size: {total_ram_usage:,} bytes")
lines.append(f"Total Flash sections size: {total_flash_usage:,} bytes")
# Strings in RAM
lines.append("")
lines.append("=" * table_width)
lines.append("STRINGS IN RAM SECTIONS")
lines.append("=" * table_width)
lines.append(
"Note: .bss sections contain uninitialized data (no strings to extract)"
)
# Group strings by section
strings_by_section: dict[str, list[RamString]] = defaultdict(list)
for ram_string in self.ram_strings:
strings_by_section[ram_string.section].append(ram_string)
for section_name in sorted(strings_by_section.keys()):
section_strings = strings_by_section[section_name]
lines.append(f"\nSection: {section_name}")
lines.append("-" * 40)
for ram_string in sorted(section_strings, key=lambda x: x.address):
clean_string = ram_string.content[:100] + (
"..." if len(ram_string.content) > 100 else ""
)
lines.append(
f' 0x{ram_string.address:08x}: "{clean_string}" (len={len(ram_string.content)})'
)
# Large RAM symbols
lines.append("")
lines.append("=" * table_width)
lines.append("LARGE DATA SYMBOLS IN RAM (>= 50 bytes)")
lines.append("=" * table_width)
largest_symbols = self.get_largest_symbols(50)
lines.append(f"\n{'Symbol':<50} {'Type':<6} {'Size':<10} {'Section'}")
lines.append("-" * table_width)
for symbol in largest_symbols:
# Use demangled name if available, otherwise raw name
display_name = symbol.demangled or symbol.name
name_display = display_name[:49] if len(display_name) > 49 else display_name
lines.append(
f"{name_display:<50} {symbol.sym_type:<6} {symbol.size:>8} B {symbol.section}"
)
# Summary
lines.append("")
lines.append("=" * table_width)
lines.append("SUMMARY")
lines.append("=" * table_width)
lines.append(f"Total strings found in RAM: {len(self.ram_strings)}")
total_string_bytes = self.get_total_string_bytes()
lines.append(f"Total bytes used by strings: {total_string_bytes:,}")
# Optimization targets
lines.append("")
lines.append("=" * table_width)
lines.append("POTENTIAL OPTIMIZATION TARGETS")
lines.append("=" * table_width)
# Repeated strings
repeated = self.get_repeated_strings()[:10]
if repeated:
lines.append("\nRepeated strings (could be deduplicated):")
for string, count in repeated:
savings = (count - 1) * (len(string) + 1)
clean_string = string[:50] + ("..." if len(string) > 50 else "")
lines.append(
f' "{clean_string}" - appears {count} times (potential savings: {savings} bytes)'
)
# Long strings - platform-specific advice
long_strings = self.get_long_strings(20)[:10]
if long_strings:
if self.platform == "esp8266":
lines.append(
"\nLong strings that could be moved to PROGMEM (>= 20 chars):"
)
else:
# ESP32: strings in DRAM are typically there for a reason
# (interrupt handlers, pre-flash-init code, etc.)
lines.append("\nLong strings in DRAM (>= 20 chars):")
lines.append(
"Note: ESP32 DRAM strings may be required for interrupt/early-boot contexts"
)
for ram_string in long_strings:
clean_string = ram_string.content[:60] + (
"..." if len(ram_string.content) > 60 else ""
)
lines.append(
f' {ram_string.section} @ 0x{ram_string.address:08x}: "{clean_string}" ({len(ram_string.content)} bytes)'
)
lines.append("")
return "\n".join(lines)

View File

@@ -0,0 +1,57 @@
"""Toolchain utilities for memory analysis."""
from __future__ import annotations
import logging
from pathlib import Path
import subprocess
_LOGGER = logging.getLogger(__name__)
# Platform-specific toolchain prefixes
TOOLCHAIN_PREFIXES = [
"xtensa-lx106-elf-", # ESP8266
"xtensa-esp32-elf-", # ESP32
"xtensa-esp-elf-", # ESP32 (newer IDF)
"", # System default (no prefix)
]
def find_tool(
tool_name: str,
objdump_path: str | None = None,
) -> str | None:
"""Find a toolchain tool by name.
First tries to derive the tool path from objdump_path (if provided),
then falls back to searching for platform-specific tools.
Args:
tool_name: Name of the tool (e.g., "objdump", "nm", "c++filt")
objdump_path: Path to objdump binary to derive other tool paths from
Returns:
Path to the tool or None if not found
"""
# Try to derive from objdump path first (most reliable)
if objdump_path and objdump_path != "objdump":
objdump_file = Path(objdump_path)
# Replace just the filename portion, preserving any prefix (e.g., xtensa-esp32-elf-)
new_name = objdump_file.name.replace("objdump", tool_name)
potential_path = str(objdump_file.with_name(new_name))
if Path(potential_path).exists():
_LOGGER.debug("Found %s at: %s", tool_name, potential_path)
return potential_path
# Try platform-specific tools
for prefix in TOOLCHAIN_PREFIXES:
cmd = f"{prefix}{tool_name}"
try:
subprocess.run([cmd, "--version"], capture_output=True, check=True)
_LOGGER.debug("Found %s: %s", tool_name, cmd)
return cmd
except (subprocess.CalledProcessError, FileNotFoundError):
continue
_LOGGER.warning("Could not find %s tool", tool_name)
return None

View File

@@ -87,7 +87,7 @@ void AbsoluteHumidityComponent::loop() {
break;
default:
this->publish_state(NAN);
this->status_set_error("Invalid saturation vapor pressure equation selection!");
this->status_set_error(LOG_STR("Invalid saturation vapor pressure equation selection!"));
return;
}
ESP_LOGD(TAG, "Saturation vapor pressure %f kPa", es);
@@ -163,7 +163,7 @@ float AbsoluteHumidityComponent::es_wobus(float t) {
}
// From https://www.environmentalbiophysics.org/chalk-talk-how-to-calculate-absolute-humidity/
// H/T to https://esphome.io/cookbook/bme280_environment.html
// H/T to https://esphome.io/cookbook/bme280_environment/
// H/T to https://carnotcycle.wordpress.com/2012/08/04/how-to-convert-relative-humidity-to-absolute-humidity/
float AbsoluteHumidityComponent::vapor_density(float es, float hr, float ta) {
// es = saturated vapor pressure (kPa)

View File

@@ -1,15 +1,17 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components.esp32 import VARIANT_ESP32P4, get_esp32_variant
from esphome.components.esp32.const import (
from esphome.components.esp32 import (
VARIANT_ESP32,
VARIANT_ESP32C2,
VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6,
VARIANT_ESP32C61,
VARIANT_ESP32H2,
VARIANT_ESP32P4,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
get_esp32_variant,
)
import esphome.config_validation as cv
from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER, PLATFORM_ESP8266
@@ -99,6 +101,13 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
5: adc_channel_t.ADC_CHANNEL_5,
6: adc_channel_t.ADC_CHANNEL_6,
},
# https://docs.espressif.com/projects/esp-idf/en/latest/esp32c61/api-reference/peripherals/gpio.html
VARIANT_ESP32C61: {
1: adc_channel_t.ADC_CHANNEL_0,
3: adc_channel_t.ADC_CHANNEL_1,
4: adc_channel_t.ADC_CHANNEL_2,
5: adc_channel_t.ADC_CHANNEL_3,
},
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h
VARIANT_ESP32H2: {
1: adc_channel_t.ADC_CHANNEL_0,
@@ -107,6 +116,17 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
4: adc_channel_t.ADC_CHANNEL_3,
5: adc_channel_t.ADC_CHANNEL_4,
},
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32p4/include/soc/adc_channel.h
VARIANT_ESP32P4: {
16: adc_channel_t.ADC_CHANNEL_0,
17: adc_channel_t.ADC_CHANNEL_1,
18: adc_channel_t.ADC_CHANNEL_2,
19: adc_channel_t.ADC_CHANNEL_3,
20: adc_channel_t.ADC_CHANNEL_4,
21: adc_channel_t.ADC_CHANNEL_5,
22: adc_channel_t.ADC_CHANNEL_6,
23: adc_channel_t.ADC_CHANNEL_7,
},
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h
VARIANT_ESP32S2: {
1: adc_channel_t.ADC_CHANNEL_0,
@@ -133,16 +153,6 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
9: adc_channel_t.ADC_CHANNEL_8,
10: adc_channel_t.ADC_CHANNEL_9,
},
VARIANT_ESP32P4: {
16: adc_channel_t.ADC_CHANNEL_0,
17: adc_channel_t.ADC_CHANNEL_1,
18: adc_channel_t.ADC_CHANNEL_2,
19: adc_channel_t.ADC_CHANNEL_3,
20: adc_channel_t.ADC_CHANNEL_4,
21: adc_channel_t.ADC_CHANNEL_5,
22: adc_channel_t.ADC_CHANNEL_6,
23: adc_channel_t.ADC_CHANNEL_7,
},
}
# pin to adc2 channel mapping
@@ -173,8 +183,19 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
VARIANT_ESP32C5: {}, # no ADC2
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h
VARIANT_ESP32C6: {}, # no ADC2
# ESP32-C61 has no ADC2
VARIANT_ESP32C61: {}, # no ADC2
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h
VARIANT_ESP32H2: {}, # no ADC2
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32p4/include/soc/adc_channel.h
VARIANT_ESP32P4: {
49: adc_channel_t.ADC_CHANNEL_0,
50: adc_channel_t.ADC_CHANNEL_1,
51: adc_channel_t.ADC_CHANNEL_2,
52: adc_channel_t.ADC_CHANNEL_3,
53: adc_channel_t.ADC_CHANNEL_4,
54: adc_channel_t.ADC_CHANNEL_5,
},
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h
VARIANT_ESP32S2: {
11: adc_channel_t.ADC_CHANNEL_0,
@@ -201,14 +222,6 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
19: adc_channel_t.ADC_CHANNEL_8,
20: adc_channel_t.ADC_CHANNEL_9,
},
VARIANT_ESP32P4: {
49: adc_channel_t.ADC_CHANNEL_0,
50: adc_channel_t.ADC_CHANNEL_1,
51: adc_channel_t.ADC_CHANNEL_2,
52: adc_channel_t.ADC_CHANNEL_3,
53: adc_channel_t.ADC_CHANNEL_4,
54: adc_channel_t.ADC_CHANNEL_5,
},
}

View File

@@ -42,10 +42,11 @@ void ADCSensor::setup() {
adc_oneshot_unit_init_cfg_t init_config = {}; // Zero initialize
init_config.unit_id = this->adc_unit_;
init_config.ulp_mode = ADC_ULP_MODE_DISABLE;
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32H2
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2
init_config.clk_src = ADC_DIGI_CLK_SRC_DEFAULT;
#endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 ||
// USE_ESP32_VARIANT_ESP32H2
// USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2
esp_err_t err = adc_oneshot_new_unit(&init_config, &ADCSensor::shared_adc_handles[this->adc_unit_]);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Error initializing %s: %d", LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)), err);
@@ -74,7 +75,7 @@ void ADCSensor::setup() {
adc_cali_handle_t handle = nullptr;
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
// RISC-V variants and S3 use curve fitting calibration
adc_cali_curve_fitting_config_t cali_config = {}; // Zero initialize first
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
@@ -111,7 +112,7 @@ void ADCSensor::setup() {
ESP_LOGW(TAG, "Line fitting calibration failed with error %d, will use uncalibrated readings", err);
this->setup_flags_.calibration_complete = false;
}
#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32S3 || ESP32H2
#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3
}
this->setup_flags_.init_complete = true;
@@ -186,11 +187,11 @@ float ADCSensor::sample_fixed_attenuation_() {
ESP_LOGW(TAG, "ADC calibration conversion failed with error %d, disabling calibration", err);
if (this->calibration_handle_ != nullptr) {
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
adc_cali_delete_scheme_curve_fitting(this->calibration_handle_);
#else // Other ESP32 variants use line fitting calibration
adc_cali_delete_scheme_line_fitting(this->calibration_handle_);
#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32S3 || ESP32H2
#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3
this->calibration_handle_ = nullptr;
}
}
@@ -219,7 +220,7 @@ float ADCSensor::sample_autorange_() {
if (this->calibration_handle_ != nullptr) {
// Delete old calibration handle
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
adc_cali_delete_scheme_curve_fitting(this->calibration_handle_);
#else
adc_cali_delete_scheme_line_fitting(this->calibration_handle_);
@@ -231,7 +232,7 @@ float ADCSensor::sample_autorange_() {
adc_cali_handle_t handle = nullptr;
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
adc_cali_curve_fitting_config_t cali_config = {};
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
cali_config.chan = this->channel_;
@@ -266,7 +267,7 @@ float ADCSensor::sample_autorange_() {
ESP_LOGW(TAG, "ADC read failed in autorange with error %d", err);
if (handle != nullptr) {
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
adc_cali_delete_scheme_curve_fitting(handle);
#else
adc_cali_delete_scheme_line_fitting(handle);
@@ -288,7 +289,7 @@ float ADCSensor::sample_autorange_() {
}
// Clean up calibration handle
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
adc_cali_delete_scheme_curve_fitting(handle);
#else
adc_cali_delete_scheme_line_fitting(handle);

View File

@@ -227,7 +227,7 @@ CONFIG_SCHEMA = cv.All(
{
cv.GenerateID(): cv.declare_id(ADE7880),
cv.Optional(CONF_FREQUENCY, default="50Hz"): cv.All(
cv.frequency, cv.Range(min=45.0, max=66.0)
cv.frequency, cv.float_range(min=45.0, max=66.0)
),
cv.Optional(CONF_IRQ0_PIN): pins.internal_gpio_input_pin_schema,
cv.Required(CONF_IRQ1_PIN): pins.internal_gpio_input_pin_schema,

View File

@@ -24,6 +24,8 @@ from esphome.const import (
UNIT_WATT,
)
CODEOWNERS = ["@angelnu"]
CONF_CURRENT_A = "current_a"
CONF_CURRENT_B = "current_b"
CONF_ACTIVE_POWER_A = "active_power_a"

View File

@@ -25,7 +25,8 @@ void ADE7953::setup() {
this->ade_write_8(PGA_V_8, pga_v_);
this->ade_write_8(PGA_IA_8, pga_ia_);
this->ade_write_8(PGA_IB_8, pga_ib_);
this->ade_write_32(AVGAIN_32, vgain_);
this->ade_write_32(AVGAIN_32, avgain_);
this->ade_write_32(BVGAIN_32, bvgain_);
this->ade_write_32(AIGAIN_32, aigain_);
this->ade_write_32(BIGAIN_32, bigain_);
this->ade_write_32(AWGAIN_32, awgain_);
@@ -34,7 +35,8 @@ void ADE7953::setup() {
this->ade_read_8(PGA_V_8, &pga_v_);
this->ade_read_8(PGA_IA_8, &pga_ia_);
this->ade_read_8(PGA_IB_8, &pga_ib_);
this->ade_read_32(AVGAIN_32, &vgain_);
this->ade_read_32(AVGAIN_32, &avgain_);
this->ade_read_32(BVGAIN_32, &bvgain_);
this->ade_read_32(AIGAIN_32, &aigain_);
this->ade_read_32(BIGAIN_32, &bigain_);
this->ade_read_32(AWGAIN_32, &awgain_);
@@ -63,13 +65,14 @@ void ADE7953::dump_config() {
" PGA_V_8: 0x%X\n"
" PGA_IA_8: 0x%X\n"
" PGA_IB_8: 0x%X\n"
" VGAIN_32: 0x%08jX\n"
" AVGAIN_32: 0x%08jX\n"
" BVGAIN_32: 0x%08jX\n"
" AIGAIN_32: 0x%08jX\n"
" BIGAIN_32: 0x%08jX\n"
" AWGAIN_32: 0x%08jX\n"
" BWGAIN_32: 0x%08jX",
this->use_acc_energy_regs_, pga_v_, pga_ia_, pga_ib_, (uintmax_t) vgain_, (uintmax_t) aigain_,
(uintmax_t) bigain_, (uintmax_t) awgain_, (uintmax_t) bwgain_);
this->use_acc_energy_regs_, pga_v_, pga_ia_, pga_ib_, (uintmax_t) avgain_, (uintmax_t) bvgain_,
(uintmax_t) aigain_, (uintmax_t) bigain_, (uintmax_t) awgain_, (uintmax_t) bwgain_);
}
#define ADE_PUBLISH_(name, val, factor) \

View File

@@ -46,7 +46,12 @@ class ADE7953 : public PollingComponent, public sensor::Sensor {
void set_pga_ib(uint8_t pga_ib) { pga_ib_ = pga_ib; }
// Set input gains
void set_vgain(uint32_t vgain) { vgain_ = vgain; }
void set_vgain(uint32_t vgain) {
// Datasheet says: "to avoid discrepancies in other registers,
// if AVGAIN is set then BVGAIN should be set to the same value."
avgain_ = vgain;
bvgain_ = vgain;
}
void set_aigain(uint32_t aigain) { aigain_ = aigain; }
void set_bigain(uint32_t bigain) { bigain_ = bigain; }
void set_awgain(uint32_t awgain) { awgain_ = awgain; }
@@ -100,7 +105,8 @@ class ADE7953 : public PollingComponent, public sensor::Sensor {
uint8_t pga_v_;
uint8_t pga_ia_;
uint8_t pga_ib_;
uint32_t vgain_;
uint32_t avgain_;
uint32_t bvgain_;
uint32_t aigain_;
uint32_t bigain_;
uint32_t awgain_;

View File

@@ -83,7 +83,7 @@ void AHT10Component::setup() {
void AHT10Component::restart_read_() {
if (this->read_count_ == AHT10_ATTEMPTS) {
this->read_count_ = 0;
this->status_set_error("Reading timed out");
this->status_set_error(LOG_STR("Reading timed out"));
return;
}
this->read_count_++;

View File

@@ -56,13 +56,13 @@ bool Alpha3::is_current_response_type_(const uint8_t *response_type) {
void Alpha3::handle_geni_response_(const uint8_t *response, uint16_t length) {
if (this->response_offset_ >= this->response_length_) {
ESP_LOGD(TAG, "[%s] GENI response begin", this->parent_->address_str().c_str());
ESP_LOGD(TAG, "[%s] GENI response begin", this->parent_->address_str());
if (length < GENI_RESPONSE_HEADER_LENGTH) {
ESP_LOGW(TAG, "[%s] response to short", this->parent_->address_str().c_str());
ESP_LOGW(TAG, "[%s] response too short", this->parent_->address_str());
return;
}
if (response[0] != 36 || response[2] != 248 || response[3] != 231 || response[4] != 10) {
ESP_LOGW(TAG, "[%s] response bytes %d %d %d %d %d don't match GENI HEADER", this->parent_->address_str().c_str(),
ESP_LOGW(TAG, "[%s] response bytes %d %d %d %d %d don't match GENI HEADER", this->parent_->address_str(),
response[0], response[1], response[2], response[3], response[4]);
return;
}
@@ -77,11 +77,11 @@ void Alpha3::handle_geni_response_(const uint8_t *response, uint16_t length) {
};
if (this->is_current_response_type_(GENI_RESPONSE_TYPE_FLOW_HEAD)) {
ESP_LOGD(TAG, "[%s] FLOW HEAD Response", this->parent_->address_str().c_str());
ESP_LOGD(TAG, "[%s] FLOW HEAD Response", this->parent_->address_str());
extract_publish_sensor_value(GENI_RESPONSE_FLOW_OFFSET, this->flow_sensor_, 3600.0F);
extract_publish_sensor_value(GENI_RESPONSE_HEAD_OFFSET, this->head_sensor_, .0001F);
} else if (this->is_current_response_type_(GENI_RESPONSE_TYPE_POWER)) {
ESP_LOGD(TAG, "[%s] POWER Response", this->parent_->address_str().c_str());
ESP_LOGD(TAG, "[%s] POWER Response", this->parent_->address_str());
extract_publish_sensor_value(GENI_RESPONSE_POWER_OFFSET, this->power_sensor_, 1.0F);
extract_publish_sensor_value(GENI_RESPONSE_CURRENT_OFFSET, this->current_sensor_, 1.0F);
extract_publish_sensor_value(GENI_RESPONSE_MOTOR_SPEED_OFFSET, this->speed_sensor_, 1.0F);
@@ -100,7 +100,7 @@ void Alpha3::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc
if (param->open.status == ESP_GATT_OK) {
this->response_offset_ = 0;
this->response_length_ = 0;
ESP_LOGI(TAG, "[%s] connection open", this->parent_->address_str().c_str());
ESP_LOGI(TAG, "[%s] connection open", this->parent_->address_str());
}
break;
}
@@ -132,7 +132,7 @@ void Alpha3::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(ALPHA3_GENI_SERVICE_UUID, ALPHA3_GENI_CHARACTERISTIC_UUID);
if (chr == nullptr) {
ESP_LOGE(TAG, "[%s] No GENI service found at device, not an Alpha3..?", this->parent_->address_str().c_str());
ESP_LOGE(TAG, "[%s] No GENI service found at device, not an Alpha3..?", this->parent_->address_str());
break;
}
auto status = esp_ble_gattc_register_for_notify(this->parent_->get_gattc_if(), this->parent_->get_remote_bda(),
@@ -164,12 +164,12 @@ void Alpha3::send_request_(uint8_t *request, size_t len) {
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->geni_handle_, len,
request, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
if (status)
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
}
void Alpha3::update() {
if (this->node_state != espbt::ClientState::ESTABLISHED) {
ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str().c_str());
ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str());
return;
}

View File

@@ -44,11 +44,9 @@ void Am43::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_i
auto *chr = this->parent_->get_characteristic(AM43_SERVICE_UUID, AM43_CHARACTERISTIC_UUID);
if (chr == nullptr) {
if (this->parent_->get_characteristic(AM43_TUYA_SERVICE_UUID, AM43_TUYA_CHARACTERISTIC_UUID) != nullptr) {
ESP_LOGE(TAG, "[%s] Detected a Tuya AM43 which is not supported, sorry.",
this->parent_->address_str().c_str());
ESP_LOGE(TAG, "[%s] Detected a Tuya AM43 which is not supported, sorry.", this->parent_->address_str());
} else {
ESP_LOGE(TAG, "[%s] No control service found at device, not an AM43..?",
this->parent_->address_str().c_str());
ESP_LOGE(TAG, "[%s] No control service found at device, not an AM43..?", this->parent_->address_str());
}
break;
}
@@ -82,8 +80,7 @@ void Am43::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_i
this->char_handle_, packet->length, packet->data,
ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(),
status);
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
}
}
this->current_sensor_ = 0;
@@ -97,7 +94,7 @@ void Am43::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_i
void Am43::update() {
if (this->node_state != espbt::ClientState::ESTABLISHED) {
ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str().c_str());
ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str());
return;
}
if (this->current_sensor_ == 0) {
@@ -107,7 +104,7 @@ void Am43::update() {
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_,
packet->length, packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
}
}
this->current_sensor_++;

View File

@@ -12,10 +12,11 @@ void AnalogThresholdBinarySensor::setup() {
// TRUE state is defined to be when sensor is >= threshold
// so when undefined sensor value initialize to FALSE
if (std::isnan(sensor_value)) {
this->raw_state_ = false;
this->publish_initial_state(false);
} else {
this->publish_initial_state(sensor_value >=
(this->lower_threshold_.value() + this->upper_threshold_.value()) / 2.0f);
this->raw_state_ = sensor_value >= (this->lower_threshold_.value() + this->upper_threshold_.value()) / 2.0f;
this->publish_initial_state(this->raw_state_);
}
}
@@ -25,8 +26,10 @@ void AnalogThresholdBinarySensor::set_sensor(sensor::Sensor *analog_sensor) {
this->sensor_->add_on_state_callback([this](float sensor_value) {
// if there is an invalid sensor reading, ignore the change and keep the current state
if (!std::isnan(sensor_value)) {
this->publish_state(sensor_value >=
(this->state ? this->lower_threshold_.value() : this->upper_threshold_.value()));
// Use raw_state_ for hysteresis logic, not this->state which is post-filter
this->raw_state_ =
sensor_value >= (this->raw_state_ ? this->lower_threshold_.value() : this->upper_threshold_.value());
this->publish_state(this->raw_state_);
}
});
}

View File

@@ -20,6 +20,7 @@ class AnalogThresholdBinarySensor : public Component, public binary_sensor::Bina
sensor::Sensor *sensor_{nullptr};
TemplatableValue<float> upper_threshold_{};
TemplatableValue<float> lower_threshold_{};
bool raw_state_{false}; // Pre-filter state for hysteresis logic
};
} // namespace analog_threshold

View File

@@ -42,7 +42,7 @@ void Anova::control(const ClimateCall &call) {
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);
if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
}
}
if (call.get_target_temperature().has_value()) {
@@ -51,7 +51,7 @@ void Anova::control(const ClimateCall &call) {
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);
if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
}
}
}
@@ -124,8 +124,7 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_
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);
if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(),
status);
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
}
}
}
@@ -150,7 +149,7 @@ void Anova::update() {
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);
if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
}
this->current_request_++;
}

View File

@@ -27,12 +27,13 @@ from esphome.const import (
CONF_SERVICE,
CONF_SERVICES,
CONF_TAG,
CONF_THEN,
CONF_TRIGGER_ID,
CONF_VARIABLES,
)
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.cpp_generator import TemplateArgsType
from esphome.types import ConfigType
from esphome.core import CORE, ID, CoroPriority, EsphomeError, coroutine_with_priority
from esphome.cpp_generator import MockObj, TemplateArgsType
from esphome.types import ConfigFragmentType, ConfigType
_LOGGER = logging.getLogger(__name__)
@@ -63,17 +64,21 @@ HomeAssistantActionResponseTrigger = api_ns.class_(
"HomeAssistantActionResponseTrigger", automation.Trigger
)
APIConnectedCondition = api_ns.class_("APIConnectedCondition", Condition)
APIRespondAction = api_ns.class_("APIRespondAction", automation.Action)
APIUnregisterServiceCallAction = api_ns.class_(
"APIUnregisterServiceCallAction", automation.Action
)
UserServiceTrigger = api_ns.class_("UserServiceTrigger", automation.Trigger)
ListEntitiesServicesArgument = api_ns.class_("ListEntitiesServicesArgument")
SERVICE_ARG_NATIVE_TYPES = {
"bool": bool,
SERVICE_ARG_NATIVE_TYPES: dict[str, MockObj] = {
"bool": cg.bool_,
"int": cg.int32,
"float": float,
"float": cg.float_,
"string": cg.std_string,
"bool[]": cg.FixedVector.template(bool).operator("const").operator("ref"),
"bool[]": cg.FixedVector.template(cg.bool_).operator("const").operator("ref"),
"int[]": cg.FixedVector.template(cg.int32).operator("const").operator("ref"),
"float[]": cg.FixedVector.template(float).operator("const").operator("ref"),
"float[]": cg.FixedVector.template(cg.float_).operator("const").operator("ref"),
"string[]": cg.FixedVector.template(cg.std_string)
.operator("const")
.operator("ref"),
@@ -85,6 +90,7 @@ CONF_HOMEASSISTANT_SERVICES = "homeassistant_services"
CONF_HOMEASSISTANT_STATES = "homeassistant_states"
CONF_LISTEN_BACKLOG = "listen_backlog"
CONF_MAX_SEND_QUEUE = "max_send_queue"
CONF_STATE_SUBSCRIPTION_ONLY = "state_subscription_only"
def validate_encryption_key(value):
@@ -101,6 +107,85 @@ def validate_encryption_key(value):
return value
CONF_SUPPORTS_RESPONSE = "supports_response"
# Enum values in api::enums namespace
enums_ns = api_ns.namespace("enums")
SUPPORTS_RESPONSE_OPTIONS = {
"none": enums_ns.SUPPORTS_RESPONSE_NONE,
"optional": enums_ns.SUPPORTS_RESPONSE_OPTIONAL,
"only": enums_ns.SUPPORTS_RESPONSE_ONLY,
"status": enums_ns.SUPPORTS_RESPONSE_STATUS,
}
def _auto_detect_supports_response(config: ConfigType) -> ConfigType:
"""Auto-detect supports_response based on api.respond usage in the action's then block.
- If api.respond with data found: set to "optional" (unless user explicitly set)
- If api.respond without data found: set to "status" (unless user explicitly set)
- If no api.respond found: set to "none" (unless user explicitly set)
"""
def scan_actions(items: ConfigFragmentType) -> tuple[bool, bool]:
"""Recursively scan actions for api.respond.
Returns: (found, has_data) tuple - has_data is True if ANY api.respond has data
"""
found_any = False
has_data_any = False
if isinstance(items, list):
for item in items:
found, has_data = scan_actions(item)
if found:
found_any = True
has_data_any = has_data_any or has_data
elif isinstance(items, dict):
# Check if this is an api.respond action
if "api.respond" in items:
respond_config = items["api.respond"]
has_data = isinstance(respond_config, dict) and "data" in respond_config
return True, has_data
# Recursively check all values
for value in items.values():
found, has_data = scan_actions(value)
if found:
found_any = True
has_data_any = has_data_any or has_data
return found_any, has_data_any
then = config.get(CONF_THEN, [])
action_name = config.get(CONF_ACTION)
found, has_data = scan_actions(then)
# If user explicitly set supports_response, validate and use that
if CONF_SUPPORTS_RESPONSE in config:
user_value = config[CONF_SUPPORTS_RESPONSE]
# Validate: "only" requires api.respond with data
if user_value == "only" and not has_data:
raise cv.Invalid(
f"Action '{action_name}' has supports_response=only but no api.respond "
"action with 'data:' was found. Use 'status' for responses without data, "
"or add 'data:' to your api.respond action."
)
return config
# Auto-detect based on api.respond usage
if found:
config[CONF_SUPPORTS_RESPONSE] = "optional" if has_data else "status"
else:
config[CONF_SUPPORTS_RESPONSE] = "none"
return config
def _validate_supports_response(value):
"""Validate supports_response after auto-detection has set the value."""
return cv.enum(SUPPORTS_RESPONSE_OPTIONS, lower=True)(value)
ACTIONS_SCHEMA = automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UserServiceTrigger),
@@ -111,10 +196,20 @@ ACTIONS_SCHEMA = automation.validate_automation(
cv.validate_id_name: cv.one_of(*SERVICE_ARG_NATIVE_TYPES, lower=True),
}
),
# No default - auto-detected by _auto_detect_supports_response
cv.Optional(CONF_SUPPORTS_RESPONSE): cv.enum(
SUPPORTS_RESPONSE_OPTIONS, lower=True
),
},
cv.All(
cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION),
cv.rename_key(CONF_SERVICE, CONF_ACTION),
_auto_detect_supports_response,
# Re-validate supports_response after auto-detection sets it
cv.Schema(
{cv.Required(CONF_SUPPORTS_RESPONSE): _validate_supports_response},
extra=cv.ALLOW_EXTRA,
),
),
)
@@ -151,7 +246,7 @@ def _validate_api_config(config: ConfigType) -> ConfigType:
_LOGGER.warning(
"API 'password' authentication has been deprecated since May 2022 and will be removed in version 2026.1.0. "
"Please migrate to the 'encryption' configuration. "
"See https://esphome.io/components/api.html#configuration-variables"
"See https://esphome.io/components/api/#configuration-variables"
)
return config
@@ -241,7 +336,7 @@ CONFIG_SCHEMA = cv.All(
@coroutine_with_priority(CoroPriority.WEB)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
@@ -260,9 +355,9 @@ async def to_code(config):
cg.add(var.set_max_connections(config[CONF_MAX_CONNECTIONS]))
cg.add_define("API_MAX_SEND_QUEUE", config[CONF_MAX_SEND_QUEUE])
# Set USE_API_SERVICES if any services are enabled
# Set USE_API_USER_DEFINED_ACTIONS if any services are enabled
if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]:
cg.add_define("USE_API_SERVICES")
cg.add_define("USE_API_USER_DEFINED_ACTIONS")
# Set USE_API_CUSTOM_SERVICES if external components need dynamic service registration
if config[CONF_CUSTOM_SERVICES]:
@@ -278,20 +373,61 @@ async def to_code(config):
# Collect all triggers first, then register all at once with initializer_list
triggers: list[cg.Pvariable] = []
for conf in actions:
template_args = []
func_args = []
service_arg_names = []
func_args: list[tuple[MockObj, str]] = []
service_template_args: list[MockObj] = [] # User service argument types
# Determine supports_response mode
# cv.enum returns the key with enum_value attribute containing the MockObj
supports_response_key = conf[CONF_SUPPORTS_RESPONSE]
supports_response = supports_response_key.enum_value
is_none = supports_response_key == "none"
is_optional = supports_response_key == "optional"
# Add call_id and return_response based on supports_response mode
# These must match the C++ Trigger template arguments
# - none: no extra args
# - status: call_id only (for reporting success/error without data)
# - only: call_id only (response always expected with data)
# - optional: call_id + return_response (client decides)
if not is_none:
# call_id is present for "optional", "only", and "status"
func_args.append((cg.uint32, "call_id"))
# return_response only present for "optional"
if is_optional:
func_args.append((cg.bool_, "return_response"))
service_arg_names: list[str] = []
for name, var_ in conf[CONF_VARIABLES].items():
native = SERVICE_ARG_NATIVE_TYPES[var_]
template_args.append(native)
service_template_args.append(native)
func_args.append((native, name))
service_arg_names.append(name)
templ = cg.TemplateArguments(*template_args)
# Template args: supports_response mode, then user service arg types
templ = cg.TemplateArguments(supports_response, *service_template_args)
trigger = cg.new_Pvariable(
conf[CONF_TRIGGER_ID], templ, conf[CONF_ACTION], service_arg_names
conf[CONF_TRIGGER_ID],
templ,
conf[CONF_ACTION],
service_arg_names,
)
triggers.append(trigger)
await automation.build_automation(trigger, func_args, conf)
auto = await automation.build_automation(trigger, func_args, conf)
# For non-none response modes, automatically append unregister action
# This ensures the call is unregistered after all actions complete (including async ones)
if not is_none:
arg_types = [arg[0] for arg in func_args]
action_templ = cg.TemplateArguments(*arg_types)
unregister_id = ID(
f"{conf[CONF_TRIGGER_ID]}__unregister",
is_declaration=True,
type=APIUnregisterServiceCallAction.template(action_templ),
)
unregister_action = cg.new_Pvariable(
unregister_id,
var,
)
cg.add(auto.add_actions([unregister_action]))
# Register all services at once - single allocation, no reallocations
cg.add(var.initialize_user_services(triggers))
@@ -537,9 +673,98 @@ async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, arg
return var
@automation.register_condition("api.connected", APIConnectedCondition, {})
CONF_SUCCESS = "success"
CONF_ERROR_MESSAGE = "error_message"
def _validate_api_respond_data(config):
"""Set flag during validation so AUTO_LOAD can include json component."""
if CONF_DATA in config:
CORE.data.setdefault(DOMAIN, {})[CONF_CAPTURE_RESPONSE] = True
return config
API_RESPOND_ACTION_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.use_id(APIServer),
cv.Optional(CONF_SUCCESS, default=True): cv.templatable(cv.boolean),
cv.Optional(CONF_ERROR_MESSAGE, default=""): cv.templatable(cv.string),
cv.Optional(CONF_DATA): cv.lambda_,
}
),
_validate_api_respond_data,
)
@automation.register_action(
"api.respond",
APIRespondAction,
API_RESPOND_ACTION_SCHEMA,
)
async def api_respond_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
# Validate that api.respond is used inside an API action context.
# We can't easily validate this at config time since the schema validation
# doesn't have access to the parent action context. Validating here in to_code
# is still much better than a cryptic C++ compile error.
has_call_id = any(name == "call_id" for _, name in args)
if not has_call_id:
raise EsphomeError(
"api.respond can only be used inside an API action's 'then:' block. "
"The 'call_id' variable is required to send a response."
)
cg.add_define("USE_API_USER_DEFINED_ACTION_RESPONSES")
serv = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, serv)
# Check if we're in optional mode (has return_response arg)
is_optional = any(name == "return_response" for _, name in args)
if is_optional:
cg.add(var.set_is_optional_mode(True))
templ = await cg.templatable(config[CONF_SUCCESS], args, cg.bool_)
cg.add(var.set_success(templ))
templ = await cg.templatable(config[CONF_ERROR_MESSAGE], args, cg.std_string)
cg.add(var.set_error_message(templ))
if CONF_DATA in config:
cg.add_define("USE_API_USER_DEFINED_ACTION_RESPONSES_JSON")
# Lambda populates the JsonObject root - no return value needed
lambda_ = await cg.process_lambda(
config[CONF_DATA],
args + [(cg.JsonObject, "root")],
return_type=cg.void,
)
cg.add(var.set_data(lambda_))
return var
API_CONNECTED_CONDITION_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.use_id(APIServer),
cv.Optional(CONF_STATE_SUBSCRIPTION_ONLY, default=False): cv.templatable(
cv.boolean
),
}
)
@automation.register_condition(
"api.connected", APIConnectedCondition, API_CONNECTED_CONDITION_SCHEMA
)
async def api_connected_to_code(config, condition_id, template_arg, args):
return cg.new_Pvariable(condition_id, template_arg)
var = cg.new_Pvariable(condition_id, template_arg)
templ = await cg.templatable(config[CONF_STATE_SUBSCRIPTION_ONLY], args, cg.bool_)
cg.add(var.set_state_subscription_only(templ))
return var
def FILTER_SOURCE_FILES() -> list[str]:

View File

@@ -518,7 +518,7 @@ message ListEntitiesLightResponse {
bool legacy_supports_color_temperature = 8 [deprecated=true];
float min_mireds = 9;
float max_mireds = 10;
repeated string effects = 11;
repeated string effects = 11 [(container_pointer_no_template) = "FixedVector<const char *>"];
bool disabled_by_default = 13;
string icon = 14 [(field_ifdef) = "USE_ENTITY_ICON"];
EntityCategory entity_category = 15;
@@ -579,7 +579,7 @@ message LightCommandRequest {
bool has_flash_length = 16;
uint32 flash_length = 17;
bool has_effect = 18;
string effect = 19;
string effect = 19 [(pointer_to_buffer) = true];
uint32 device_id = 28 [(field_ifdef) = "USE_DEVICES"];
}
@@ -589,6 +589,7 @@ enum SensorStateClass {
STATE_CLASS_MEASUREMENT = 1;
STATE_CLASS_TOTAL_INCREASING = 2;
STATE_CLASS_TOTAL = 3;
STATE_CLASS_MEASUREMENT_ANGLE = 4;
}
// Deprecated in API version 1.5
@@ -854,22 +855,31 @@ enum ServiceArgType {
SERVICE_ARG_TYPE_FLOAT_ARRAY = 6;
SERVICE_ARG_TYPE_STRING_ARRAY = 7;
}
enum SupportsResponseType {
SUPPORTS_RESPONSE_NONE = 0;
SUPPORTS_RESPONSE_OPTIONAL = 1;
SUPPORTS_RESPONSE_ONLY = 2;
// Status-only response - reports success/error without data payload
// Value is higher to avoid conflicts with future Home Assistant values
SUPPORTS_RESPONSE_STATUS = 100;
}
message ListEntitiesServicesArgument {
option (ifdef) = "USE_API_SERVICES";
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
string name = 1;
ServiceArgType type = 2;
}
message ListEntitiesServicesResponse {
option (id) = 41;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_API_SERVICES";
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
string name = 1;
fixed32 key = 2;
repeated ListEntitiesServicesArgument args = 3 [(fixed_vector) = true];
SupportsResponseType supports_response = 4;
}
message ExecuteServiceArgument {
option (ifdef) = "USE_API_SERVICES";
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
bool bool_ = 1;
int32 legacy_int = 2;
float float_ = 3;
@@ -885,10 +895,25 @@ message ExecuteServiceRequest {
option (id) = 42;
option (source) = SOURCE_CLIENT;
option (no_delay) = true;
option (ifdef) = "USE_API_SERVICES";
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
fixed32 key = 1;
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"];
}
// Message sent by ESPHome to Home Assistant with service execution response data
message ExecuteServiceResponse {
option (id) = 131;
option (source) = SOURCE_SERVER;
option (no_delay) = true;
option (ifdef) = "USE_API_USER_DEFINED_ACTION_RESPONSES";
uint32 call_id = 1; // Matches the call_id from ExecuteServiceRequest
bool success = 2; // Whether the service execution succeeded
string error_message = 3; // Error message if success = false
bytes response_data = 4 [(pointer_to_buffer) = true, (field_ifdef) = "USE_API_USER_DEFINED_ACTION_RESPONSES_JSON"];
}
// ==================== CAMERA ====================
@@ -1170,7 +1195,7 @@ message SelectCommandRequest {
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
string state = 2;
string state = 2 [(pointer_to_buffer) = true];
uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
}

View File

@@ -6,11 +6,17 @@
#ifdef USE_API_PLAINTEXT
#include "api_frame_helper_plaintext.h"
#endif
#ifdef USE_API_USER_DEFINED_ACTIONS
#include "user_services.h"
#endif
#include <cerrno>
#include <cinttypes>
#include <functional>
#include <limits>
#include <utility>
#ifdef USE_ESP8266
#include <pgmspace.h>
#endif
#include "esphome/components/network/util.h"
#include "esphome/core/application.h"
#include "esphome/core/entity_base.h"
@@ -90,8 +96,8 @@ static const int CAMERA_STOP_STREAM = 5000;
APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent)
: parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) {
#if defined(USE_API_PLAINTEXT) && defined(USE_API_NOISE)
auto noise_ctx = parent->get_noise_ctx();
if (noise_ctx->has_psk()) {
auto &noise_ctx = parent->get_noise_ctx();
if (noise_ctx.has_psk()) {
this->helper_ =
std::unique_ptr<APIFrameHelper>{new APINoiseFrameHelper(std::move(sock), noise_ctx, &this->client_info_)};
} else {
@@ -169,8 +175,7 @@ void APIConnection::loop() {
} else {
this->last_traffic_ = now;
// read a packet
this->read_message(buffer.data_len, buffer.type,
buffer.data_len > 0 ? &buffer.container[buffer.data_offset] : nullptr);
this->read_message(buffer.data_len, buffer.type, buffer.data);
if (this->flags_.remove)
return;
}
@@ -195,6 +200,9 @@ void APIConnection::loop() {
}
// Now that everything is sent, enable immediate sending for future state changes
this->flags_.should_try_send_immediately = true;
// Release excess memory from buffers that grew during initial sync
this->deferred_batch_.release_buffer();
this->helper_->release_buffers();
}
}
@@ -484,12 +492,16 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c
msg.min_mireds = traits.get_min_mireds();
msg.max_mireds = traits.get_max_mireds();
}
FixedVector<const char *> effects_list;
if (light->supports_effects()) {
msg.effects.emplace_back("None");
for (auto *effect : light->get_effects()) {
msg.effects.emplace_back(effect->get_name());
auto &light_effects = light->get_effects();
effects_list.init(light_effects.size() + 1);
effects_list.push_back("None");
for (auto *effect : light_effects) {
effects_list.push_back(effect->get_name());
}
}
msg.effects = &effects_list;
return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size,
is_single);
}
@@ -521,7 +533,7 @@ void APIConnection::light_command(const LightCommandRequest &msg) {
if (msg.has_flash_length)
call.set_flash_length(msg.flash_length);
if (msg.has_effect)
call.set_effect(msg.effect);
call.set_effect(reinterpret_cast<const char *>(msg.effect), msg.effect_len);
call.perform();
}
#endif
@@ -893,7 +905,7 @@ uint16_t APIConnection::try_send_select_info(EntityBase *entity, APIConnection *
}
void APIConnection::select_command(const SelectCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(select::Select, select, select)
call.set_option(msg.state);
call.set_option(reinterpret_cast<const char *>(msg.state), msg.state_len);
call.perform();
}
#endif
@@ -1451,50 +1463,83 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
#ifdef USE_AREAS
resp.set_suggested_area(StringRef(App.get_area()));
#endif
// mac_address must store temporary string - will be valid during send_message call
std::string mac_address = get_mac_address_pretty();
// Stack buffer for MAC address (XX:XX:XX:XX:XX:XX\0 = 18 bytes)
char mac_address[18];
uint8_t mac[6];
get_mac_address_raw(mac);
format_mac_addr_upper(mac, mac_address);
resp.set_mac_address(StringRef(mac_address));
resp.set_esphome_version(ESPHOME_VERSION_REF);
resp.set_compilation_time(App.get_compilation_time_ref());
// Compile-time StringRef constants for manufacturers
// Manufacturer string - define once, handle ESP8266 PROGMEM separately
#if defined(USE_ESP8266) || defined(USE_ESP32)
static constexpr auto MANUFACTURER = StringRef::from_lit("Espressif");
#define ESPHOME_MANUFACTURER "Espressif"
#elif defined(USE_RP2040)
static constexpr auto MANUFACTURER = StringRef::from_lit("Raspberry Pi");
#define ESPHOME_MANUFACTURER "Raspberry Pi"
#elif defined(USE_BK72XX)
static constexpr auto MANUFACTURER = StringRef::from_lit("Beken");
#define ESPHOME_MANUFACTURER "Beken"
#elif defined(USE_LN882X)
static constexpr auto MANUFACTURER = StringRef::from_lit("Lightning");
#define ESPHOME_MANUFACTURER "Lightning"
#elif defined(USE_NRF52)
static constexpr auto MANUFACTURER = StringRef::from_lit("Nordic Semiconductor");
#define ESPHOME_MANUFACTURER "Nordic Semiconductor"
#elif defined(USE_RTL87XX)
static constexpr auto MANUFACTURER = StringRef::from_lit("Realtek");
#define ESPHOME_MANUFACTURER "Realtek"
#elif defined(USE_HOST)
static constexpr auto MANUFACTURER = StringRef::from_lit("Host");
#define ESPHOME_MANUFACTURER "Host"
#endif
resp.set_manufacturer(MANUFACTURER);
#ifdef USE_ESP8266
// ESP8266 requires PROGMEM for flash storage, copy to stack for memcpy compatibility
static const char MANUFACTURER_PROGMEM[] PROGMEM = ESPHOME_MANUFACTURER;
char manufacturer_buf[sizeof(MANUFACTURER_PROGMEM)];
memcpy_P(manufacturer_buf, MANUFACTURER_PROGMEM, sizeof(MANUFACTURER_PROGMEM));
resp.set_manufacturer(StringRef(manufacturer_buf, sizeof(MANUFACTURER_PROGMEM) - 1));
#else
static constexpr auto MANUFACTURER = StringRef::from_lit(ESPHOME_MANUFACTURER);
resp.set_manufacturer(MANUFACTURER);
#endif
#undef ESPHOME_MANUFACTURER
#ifdef USE_ESP8266
static const char MODEL_PROGMEM[] PROGMEM = ESPHOME_BOARD;
char model_buf[sizeof(MODEL_PROGMEM)];
memcpy_P(model_buf, MODEL_PROGMEM, sizeof(MODEL_PROGMEM));
resp.set_model(StringRef(model_buf, sizeof(MODEL_PROGMEM) - 1));
#else
static constexpr auto MODEL = StringRef::from_lit(ESPHOME_BOARD);
resp.set_model(MODEL);
#endif
#ifdef USE_DEEP_SLEEP
resp.has_deep_sleep = deep_sleep::global_has_deep_sleep;
#endif
#ifdef ESPHOME_PROJECT_NAME
#ifdef USE_ESP8266
static const char PROJECT_NAME_PROGMEM[] PROGMEM = ESPHOME_PROJECT_NAME;
static const char PROJECT_VERSION_PROGMEM[] PROGMEM = ESPHOME_PROJECT_VERSION;
char project_name_buf[sizeof(PROJECT_NAME_PROGMEM)];
char project_version_buf[sizeof(PROJECT_VERSION_PROGMEM)];
memcpy_P(project_name_buf, PROJECT_NAME_PROGMEM, sizeof(PROJECT_NAME_PROGMEM));
memcpy_P(project_version_buf, PROJECT_VERSION_PROGMEM, sizeof(PROJECT_VERSION_PROGMEM));
resp.set_project_name(StringRef(project_name_buf, sizeof(PROJECT_NAME_PROGMEM) - 1));
resp.set_project_version(StringRef(project_version_buf, sizeof(PROJECT_VERSION_PROGMEM) - 1));
#else
static constexpr auto PROJECT_NAME = StringRef::from_lit(ESPHOME_PROJECT_NAME);
static constexpr auto PROJECT_VERSION = StringRef::from_lit(ESPHOME_PROJECT_VERSION);
resp.set_project_name(PROJECT_NAME);
resp.set_project_version(PROJECT_VERSION);
#endif
#endif
#ifdef USE_WEBSERVER
resp.webserver_port = USE_WEBSERVER_PORT;
#endif
#ifdef USE_BLUETOOTH_PROXY
resp.bluetooth_proxy_feature_flags = bluetooth_proxy::global_bluetooth_proxy->get_feature_flags();
// bt_mac must store temporary string - will be valid during send_message call
std::string bluetooth_mac = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_mac_address_pretty();
// Stack buffer for Bluetooth MAC address (XX:XX:XX:XX:XX:XX\0 = 18 bytes)
char bluetooth_mac[18];
bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_mac_address_pretty(bluetooth_mac);
resp.set_bluetooth_mac_address(StringRef(bluetooth_mac));
#endif
#ifdef USE_VOICE_ASSISTANT
@@ -1535,24 +1580,68 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
#ifdef USE_API_HOMEASSISTANT_STATES
void APIConnection::on_home_assistant_state_response(const HomeAssistantStateResponse &msg) {
for (auto &it : this->parent_->get_state_subs()) {
if (it.entity_id == msg.entity_id && it.attribute.value() == msg.attribute) {
// Compare entity_id and attribute with message fields
bool entity_match = (strcmp(it.entity_id, msg.entity_id.c_str()) == 0);
bool attribute_match = (it.attribute != nullptr && strcmp(it.attribute, msg.attribute.c_str()) == 0) ||
(it.attribute == nullptr && msg.attribute.empty());
if (entity_match && attribute_match) {
it.callback(msg.state);
}
}
}
#endif
#ifdef USE_API_SERVICES
#ifdef USE_API_USER_DEFINED_ACTIONS
void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
bool found = false;
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
// Register the call and get a unique server-generated action_call_id
// This avoids collisions when multiple clients use the same call_id
uint32_t action_call_id = 0;
if (msg.call_id != 0) {
action_call_id = this->parent_->register_active_action_call(msg.call_id, this);
}
// Use the overload that passes action_call_id separately (avoids copying msg)
for (auto *service : this->parent_->get_user_services()) {
if (service->execute_service(msg, action_call_id)) {
found = true;
}
}
#else
for (auto *service : this->parent_->get_user_services()) {
if (service->execute_service(msg)) {
found = true;
}
}
#endif
if (!found) {
ESP_LOGV(TAG, "Could not find service");
}
// Note: For services with supports_response != none, the call is unregistered
// by an automatically appended APIUnregisterServiceCallAction at the end of
// the action list. This ensures async actions (delays, waits) complete first.
}
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
void APIConnection::send_execute_service_response(uint32_t call_id, bool success, const std::string &error_message) {
ExecuteServiceResponse resp;
resp.call_id = call_id;
resp.success = success;
resp.set_error_message(StringRef(error_message));
this->send_message(resp, ExecuteServiceResponse::MESSAGE_TYPE);
}
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
void APIConnection::send_execute_service_response(uint32_t call_id, bool success, const std::string &error_message,
const uint8_t *response_data, size_t response_data_len) {
ExecuteServiceResponse resp;
resp.call_id = call_id;
resp.success = success;
resp.set_error_message(StringRef(error_message));
resp.response_data = response_data;
resp.response_data_len = response_data_len;
this->send_message(resp, ExecuteServiceResponse::MESSAGE_TYPE);
}
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
@@ -1580,7 +1669,7 @@ bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryption
} else {
ESP_LOGW(TAG, "Failed to clear encryption key");
}
} else if (base64_decode(msg.key, psk.data(), msg.key.size()) != psk.size()) {
} else if (base64_decode(msg.key, psk.data(), psk.size()) != psk.size()) {
ESP_LOGW(TAG, "Invalid encryption key length");
} else if (!this->parent_->save_noise_psk(psk, true)) {
ESP_LOGW(TAG, "Failed to save encryption key");
@@ -1652,13 +1741,13 @@ void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator c
for (auto &item : items) {
if (item.entity == entity && item.message_type == message_type) {
// Replace with new creator
item.creator = std::move(creator);
item.creator = creator;
return;
}
}
// No existing item found, add new one
items.emplace_back(entity, std::move(creator), message_type, estimated_size);
items.emplace_back(entity, creator, message_type, estimated_size);
}
void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, MessageCreator creator, uint8_t message_type,
@@ -1667,7 +1756,7 @@ void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, MessageCre
// This avoids expensive vector::insert which shifts all elements
// Note: We only ever have one high-priority message at a time (ping OR disconnect)
// If we're disconnecting, pings are blocked, so this simple swap is sufficient
items.emplace_back(entity, std::move(creator), message_type, estimated_size);
items.emplace_back(entity, creator, message_type, estimated_size);
if (items.size() > 1) {
// Swap the new high-priority item to the front
std::swap(items.front(), items.back());
@@ -1875,8 +1964,8 @@ void APIConnection::process_state_subscriptions_() {
SubscribeHomeAssistantStateResponse resp;
resp.set_entity_id(StringRef(it.entity_id));
// Avoid string copy by directly using the optional's value if it exists
resp.set_attribute(it.attribute.has_value() ? StringRef(it.attribute.value()) : StringRef(""));
// Avoid string copy by using the const char* pointer if it exists
resp.set_attribute(it.attribute != nullptr ? StringRef(it.attribute) : StringRef(""));
resp.once = it.once;
if (this->send_message(resp, SubscribeHomeAssistantStateResponse::MESSAGE_TYPE)) {

View File

@@ -221,8 +221,15 @@ class APIConnection final : public APIServerConnection {
#ifdef USE_API_HOMEASSISTANT_STATES
void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override;
#endif
#ifdef USE_API_SERVICES
#ifdef USE_API_USER_DEFINED_ACTIONS
void execute_service(const ExecuteServiceRequest &msg) override;
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
void send_execute_service_response(uint32_t call_id, bool success, const std::string &error_message);
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
void send_execute_service_response(uint32_t call_id, bool success, const std::string &error_message,
const uint8_t *response_data, size_t response_data_len);
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
#endif
#ifdef USE_API_NOISE
bool send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) override;
@@ -505,28 +512,9 @@ class APIConnection final : public APIServerConnection {
class MessageCreator {
public:
// Constructor for function pointer
MessageCreator(MessageCreatorPtr ptr) { data_.function_ptr = ptr; }
// Constructor for const char * (Event types - no allocation needed)
explicit MessageCreator(const char *str_value) { data_.const_char_ptr = str_value; }
// Delete copy operations - MessageCreator should only be moved
MessageCreator(const MessageCreator &other) = delete;
MessageCreator &operator=(const MessageCreator &other) = delete;
// Move constructor
MessageCreator(MessageCreator &&other) noexcept : data_(other.data_) { other.data_.function_ptr = nullptr; }
// Move assignment
MessageCreator &operator=(MessageCreator &&other) noexcept {
if (this != &other) {
data_ = other.data_;
other.data_.function_ptr = nullptr;
}
return *this;
}
// Call operator - uses message_type to determine union type
uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single,
uint8_t message_type) const;
@@ -535,7 +523,7 @@ class APIConnection final : public APIServerConnection {
union Data {
MessageCreatorPtr function_ptr;
const char *const_char_ptr;
} data_; // 4 bytes on 32-bit, 8 bytes on 64-bit - same as before
} data_; // 4 bytes on 32-bit, 8 bytes on 64-bit
};
// Generic batching mechanism for both state updates and entity info
@@ -548,16 +536,14 @@ class APIConnection final : public APIServerConnection {
// Constructor for creating BatchItem
BatchItem(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size)
: entity(entity), creator(std::move(creator)), message_type(message_type), estimated_size(estimated_size) {}
: entity(entity), creator(creator), message_type(message_type), estimated_size(estimated_size) {}
};
std::vector<BatchItem> items;
uint32_t batch_start_time{0};
DeferredBatch() {
// Pre-allocate capacity for typical batch sizes to avoid reallocation
items.reserve(8);
}
// No pre-allocation - log connections never use batching, and for
// connections that do, buffers are released after initial sync anyway
// Add item to the batch
void add_item(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size);
@@ -576,6 +562,15 @@ class APIConnection final : public APIServerConnection {
bool empty() const { return items.empty(); }
size_t size() const { return items.size(); }
const BatchItem &operator[](size_t index) const { return items[index]; }
// Release excess capacity - only releases if items already empty
void release_buffer() {
// Safe to call: batch is processed before release_buffer is called,
// and if any items remain (partial processing), we must not clear them.
// Use swap trick since shrink_to_fit() is non-binding and may be ignored.
if (items.empty()) {
std::vector<BatchItem>().swap(items);
}
}
};
// DeferredBatch here (16 bytes, 4-byte aligned)
@@ -709,12 +704,12 @@ class APIConnection final : public APIServerConnection {
}
// Fall back to scheduled batching
return this->schedule_message_(entity, std::move(creator), message_type, estimated_size);
return this->schedule_message_(entity, creator, message_type, estimated_size);
}
// Helper function to schedule a deferred message with known message type
bool schedule_message_(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) {
this->deferred_batch_.add_item(entity, std::move(creator), message_type, estimated_size);
this->deferred_batch_.add_item(entity, creator, message_type, estimated_size);
return this->schedule_batch_();
}

View File

@@ -35,10 +35,9 @@ struct ClientInfo;
class ProtoWriteBuffer;
struct ReadPacketBuffer {
std::vector<uint8_t> container;
uint16_t type;
uint16_t data_offset;
const uint8_t *data; // Points directly into frame helper's rx_buf_ (valid until next read_packet call)
uint16_t data_len;
uint16_t type;
};
// Packed packet info structure to minimize memory usage
@@ -84,9 +83,7 @@ class APIFrameHelper {
public:
APIFrameHelper() = default;
explicit APIFrameHelper(std::unique_ptr<socket::Socket> socket, const ClientInfo *client_info)
: socket_owned_(std::move(socket)), client_info_(client_info) {
socket_ = socket_owned_.get();
}
: socket_(std::move(socket)), client_info_(client_info) {}
virtual ~APIFrameHelper() = default;
virtual APIError init() = 0;
virtual APIError loop();
@@ -121,6 +118,22 @@ class APIFrameHelper {
uint8_t frame_footer_size() const { return frame_footer_size_; }
// Check if socket has data ready to read
bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); }
// Release excess memory from internal buffers after initial sync
void release_buffers() {
// rx_buf_: Safe to clear only if no partial read in progress.
// 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_);
}
// reusable_iovs_: Safe to release unconditionally.
// Only used within write_protobuf_packets() calls - cleared at start,
// populated with pointers, used for writev(), then function returns.
// The iovecs contain stale pointers after the call (data was either sent
// or copied to tx_buf_), and are cleared on next write_protobuf_packets().
std::vector<struct iovec>().swap(this->reusable_iovs_);
}
protected:
// Buffer containing data to be sent
@@ -149,9 +162,8 @@ class APIFrameHelper {
APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf,
const std::string &info, StateEnum &state, StateEnum failed_state);
// Pointers first (4 bytes each)
socket::Socket *socket_{nullptr};
std::unique_ptr<socket::Socket> socket_owned_;
// Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit)
std::unique_ptr<socket::Socket> socket_;
// Common state enum for all frame helpers
// Note: Not all states are used by all implementations

View File

@@ -239,12 +239,13 @@ APIError APINoiseFrameHelper::state_action_() {
}
if (state_ == State::SERVER_HELLO) {
// send server hello
constexpr size_t mac_len = 13; // 12 hex chars + null terminator
const std::string &name = App.get_name();
const std::string &mac = get_mac_address();
char mac[mac_len];
get_mac_address_into_buffer(mac);
// Calculate positions and sizes
size_t name_len = name.size() + 1; // including null terminator
size_t mac_len = mac.size() + 1; // including null terminator
size_t name_offset = 1;
size_t mac_offset = name_offset + name_len;
size_t total_size = 1 + name_len + mac_len;
@@ -257,7 +258,7 @@ APIError APINoiseFrameHelper::state_action_() {
// node name, terminated by null byte
std::memcpy(msg.get() + name_offset, name.c_str(), name_len);
// node mac, terminated by null byte
std::memcpy(msg.get() + mac_offset, mac.c_str(), mac_len);
std::memcpy(msg.get() + mac_offset, mac, mac_len);
aerr = write_frame_(msg.get(), total_size);
if (aerr != APIError::OK)
@@ -406,8 +407,7 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
return APIError::BAD_DATA_PACKET;
}
buffer->container = std::move(this->rx_buf_);
buffer->data_offset = 4;
buffer->data = msg_data + 4; // Skip 4-byte header (type + length)
buffer->data_len = data_len;
buffer->type = type;
return APIError::OK;
@@ -527,7 +527,7 @@ APIError APINoiseFrameHelper::init_handshake_() {
if (aerr != APIError::OK)
return aerr;
const auto &psk = ctx_->get_psk();
const auto &psk = this->ctx_.get_psk();
err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size());
aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_set_pre_shared_key"),
APIError::HANDSHAKESTATE_SETUP_FAILED);

View File

@@ -9,9 +9,8 @@ namespace esphome::api {
class APINoiseFrameHelper final : public APIFrameHelper {
public:
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx,
const ClientInfo *client_info)
: APIFrameHelper(std::move(socket), client_info), ctx_(std::move(ctx)) {
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, APINoiseContext &ctx, const ClientInfo *client_info)
: APIFrameHelper(std::move(socket), client_info), ctx_(ctx) {
// Noise header structure:
// Pos 0: indicator (0x01)
// Pos 1-2: encrypted payload size (16-bit big-endian)
@@ -41,8 +40,8 @@ class APINoiseFrameHelper final : public APIFrameHelper {
NoiseCipherState *send_cipher_{nullptr};
NoiseCipherState *recv_cipher_{nullptr};
// Shared pointer (8 bytes on 32-bit = 4 bytes control block pointer + 4 bytes object pointer)
std::shared_ptr<APINoiseContext> ctx_;
// Reference to noise context (4 bytes on 32-bit)
APINoiseContext &ctx_;
// Vector (12 bytes on 32-bit)
std::vector<uint8_t> prologue_;

View File

@@ -210,8 +210,7 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
return aerr;
}
buffer->container = std::move(this->rx_buf_);
buffer->data_offset = 0;
buffer->data = this->rx_buf_.data();
buffer->data_len = this->rx_header_parsed_len_;
buffer->type = this->rx_header_parsed_type_;
return APIError::OK;

View File

@@ -476,8 +476,8 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const {
}
buffer.encode_float(9, this->min_mireds);
buffer.encode_float(10, this->max_mireds);
for (auto &it : this->effects) {
buffer.encode_string(11, it, true);
for (const char *it : *this->effects) {
buffer.encode_string(11, it, strlen(it), true);
}
buffer.encode_bool(13, this->disabled_by_default);
#ifdef USE_ENTITY_ICON
@@ -499,9 +499,9 @@ void ListEntitiesLightResponse::calculate_size(ProtoSize &size) const {
}
size.add_float(1, this->min_mireds);
size.add_float(1, this->max_mireds);
if (!this->effects.empty()) {
for (const auto &it : this->effects) {
size.add_length_force(1, it.size());
if (!this->effects->empty()) {
for (const char *it : *this->effects) {
size.add_length_force(1, strlen(it));
}
}
size.add_bool(1, this->disabled_by_default);
@@ -611,9 +611,12 @@ bool LightCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
}
bool LightCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 19:
this->effect = value.as_string();
case 19: {
// Use raw data directly to avoid allocation
this->effect = value.data();
this->effect_len = value.size();
break;
}
default:
return false;
}
@@ -995,7 +998,7 @@ bool GetTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
}
return true;
}
#ifdef USE_API_SERVICES
#ifdef USE_API_USER_DEFINED_ACTIONS
void ListEntitiesServicesArgument::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->name_ref_);
buffer.encode_uint32(2, static_cast<uint32_t>(this->type));
@@ -1010,11 +1013,13 @@ void ListEntitiesServicesResponse::encode(ProtoWriteBuffer buffer) const {
for (auto &it : this->args) {
buffer.encode_message(3, it, true);
}
buffer.encode_uint32(4, static_cast<uint32_t>(this->supports_response));
}
void ListEntitiesServicesResponse::calculate_size(ProtoSize &size) const {
size.add_length(1, this->name_ref_.size());
size.add_fixed32(1, this->key);
size.add_repeated_message(1, this->args);
size.add_uint32(1, static_cast<uint32_t>(this->supports_response));
}
bool ExecuteServiceArgument::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
@@ -1075,6 +1080,23 @@ void ExecuteServiceArgument::decode(const uint8_t *buffer, size_t length) {
this->string_array.init(count_string_array);
ProtoDecodableMessage::decode(buffer, length);
}
bool ExecuteServiceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
case 3:
this->call_id = value.as_uint32();
break;
#endif
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
case 4:
this->return_response = value.as_bool();
break;
#endif
default:
return false;
}
return true;
}
bool ExecuteServiceRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 2:
@@ -1102,6 +1124,24 @@ void ExecuteServiceRequest::decode(const uint8_t *buffer, size_t length) {
ProtoDecodableMessage::decode(buffer, length);
}
#endif
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
void ExecuteServiceResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_uint32(1, this->call_id);
buffer.encode_bool(2, this->success);
buffer.encode_string(3, this->error_message_ref_);
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
buffer.encode_bytes(4, this->response_data, this->response_data_len);
#endif
}
void ExecuteServiceResponse::calculate_size(ProtoSize &size) const {
size.add_uint32(1, this->call_id);
size.add_bool(1, this->success);
size.add_length(1, this->error_message_ref_.size());
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
size.add_length(4, this->response_data_len);
#endif
}
#endif
#ifdef USE_CAMERA
void ListEntitiesCameraResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->object_id_ref_);
@@ -1532,9 +1572,12 @@ bool SelectCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
}
bool SelectCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 2:
this->state = value.as_string();
case 2: {
// Use raw data directly to avoid allocation
this->state = value.data();
this->state_len = value.size();
break;
}
default:
return false;
}

View File

@@ -51,6 +51,7 @@ enum SensorStateClass : uint32_t {
STATE_CLASS_MEASUREMENT = 1,
STATE_CLASS_TOTAL_INCREASING = 2,
STATE_CLASS_TOTAL = 3,
STATE_CLASS_MEASUREMENT_ANGLE = 4,
};
#endif
enum LogLevel : uint32_t {
@@ -63,7 +64,7 @@ enum LogLevel : uint32_t {
LOG_LEVEL_VERBOSE = 6,
LOG_LEVEL_VERY_VERBOSE = 7,
};
#ifdef USE_API_SERVICES
#ifdef USE_API_USER_DEFINED_ACTIONS
enum ServiceArgType : uint32_t {
SERVICE_ARG_TYPE_BOOL = 0,
SERVICE_ARG_TYPE_INT = 1,
@@ -74,6 +75,12 @@ enum ServiceArgType : uint32_t {
SERVICE_ARG_TYPE_FLOAT_ARRAY = 6,
SERVICE_ARG_TYPE_STRING_ARRAY = 7,
};
enum SupportsResponseType : uint32_t {
SUPPORTS_RESPONSE_NONE = 0,
SUPPORTS_RESPONSE_OPTIONAL = 1,
SUPPORTS_RESPONSE_ONLY = 2,
SUPPORTS_RESPONSE_STATUS = 100,
};
#endif
#ifdef USE_CLIMATE
enum ClimateMode : uint32_t {
@@ -793,7 +800,7 @@ class ListEntitiesLightResponse final : public InfoResponseProtoMessage {
const light::ColorModeMask *supported_color_modes{};
float min_mireds{0.0f};
float max_mireds{0.0f};
std::vector<std::string> effects{};
const FixedVector<const char *> *effects{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -833,7 +840,7 @@ class LightStateResponse final : public StateResponseProtoMessage {
class LightCommandRequest final : public CommandProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 32;
static constexpr uint8_t ESTIMATED_SIZE = 112;
static constexpr uint8_t ESTIMATED_SIZE = 122;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "light_command_request"; }
#endif
@@ -862,7 +869,8 @@ class LightCommandRequest final : public CommandProtoMessage {
bool has_flash_length{false};
uint32_t flash_length{0};
bool has_effect{false};
std::string effect{};
const uint8_t *effect{nullptr};
uint16_t effect_len{0};
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1239,7 +1247,7 @@ class GetTimeResponse final : public ProtoDecodableMessage {
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
};
#ifdef USE_API_SERVICES
#ifdef USE_API_USER_DEFINED_ACTIONS
class ListEntitiesServicesArgument final : public ProtoMessage {
public:
StringRef name_ref_{};
@@ -1256,7 +1264,7 @@ class ListEntitiesServicesArgument final : public ProtoMessage {
class ListEntitiesServicesResponse final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 41;
static constexpr uint8_t ESTIMATED_SIZE = 48;
static constexpr uint8_t ESTIMATED_SIZE = 50;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_services_response"; }
#endif
@@ -1264,6 +1272,7 @@ class ListEntitiesServicesResponse final : public ProtoMessage {
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
uint32_t key{0};
FixedVector<ListEntitiesServicesArgument> args{};
enums::SupportsResponseType supports_response{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -1296,12 +1305,18 @@ class ExecuteServiceArgument final : public ProtoDecodableMessage {
class ExecuteServiceRequest final : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 42;
static constexpr uint8_t ESTIMATED_SIZE = 39;
static constexpr uint8_t ESTIMATED_SIZE = 45;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "execute_service_request"; }
#endif
uint32_t key{0};
FixedVector<ExecuteServiceArgument> args{};
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
uint32_t call_id{0};
#endif
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
bool return_response{false};
#endif
void decode(const uint8_t *buffer, size_t length) override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
@@ -1310,6 +1325,32 @@ class ExecuteServiceRequest final : public ProtoDecodableMessage {
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
#endif
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
class ExecuteServiceResponse final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 131;
static constexpr uint8_t ESTIMATED_SIZE = 34;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "execute_service_response"; }
#endif
uint32_t call_id{0};
bool success{false};
StringRef error_message_ref_{};
void set_error_message(const StringRef &ref) { this->error_message_ref_ = ref; }
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
const uint8_t *response_data{nullptr};
uint16_t response_data_len{0};
#endif
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
};
#endif
#ifdef USE_CAMERA
@@ -1564,11 +1605,12 @@ class SelectStateResponse final : public StateResponseProtoMessage {
class SelectCommandRequest final : public CommandProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 54;
static constexpr uint8_t ESTIMATED_SIZE = 18;
static constexpr uint8_t ESTIMATED_SIZE = 28;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "select_command_request"; }
#endif
std::string state{};
const uint8_t *state{nullptr};
uint16_t state_len{0};
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif

View File

@@ -66,7 +66,7 @@ static void dump_field(std::string &out, const char *field_name, float value, in
static void dump_field(std::string &out, const char *field_name, uint64_t value, int indent = 2) {
char buffer[64];
append_field_prefix(out, field_name, indent);
snprintf(buffer, 64, "%llu", value);
snprintf(buffer, 64, "%" PRIu64, value);
append_with_newline(out, buffer);
}
@@ -179,6 +179,8 @@ template<> const char *proto_enum_to_string<enums::SensorStateClass>(enums::Sens
return "STATE_CLASS_TOTAL_INCREASING";
case enums::STATE_CLASS_TOTAL:
return "STATE_CLASS_TOTAL";
case enums::STATE_CLASS_MEASUREMENT_ANGLE:
return "STATE_CLASS_MEASUREMENT_ANGLE";
default:
return "UNKNOWN";
}
@@ -206,7 +208,7 @@ template<> const char *proto_enum_to_string<enums::LogLevel>(enums::LogLevel val
return "UNKNOWN";
}
}
#ifdef USE_API_SERVICES
#ifdef USE_API_USER_DEFINED_ACTIONS
template<> const char *proto_enum_to_string<enums::ServiceArgType>(enums::ServiceArgType value) {
switch (value) {
case enums::SERVICE_ARG_TYPE_BOOL:
@@ -229,6 +231,20 @@ template<> const char *proto_enum_to_string<enums::ServiceArgType>(enums::Servic
return "UNKNOWN";
}
}
template<> const char *proto_enum_to_string<enums::SupportsResponseType>(enums::SupportsResponseType value) {
switch (value) {
case enums::SUPPORTS_RESPONSE_NONE:
return "SUPPORTS_RESPONSE_NONE";
case enums::SUPPORTS_RESPONSE_OPTIONAL:
return "SUPPORTS_RESPONSE_OPTIONAL";
case enums::SUPPORTS_RESPONSE_ONLY:
return "SUPPORTS_RESPONSE_ONLY";
case enums::SUPPORTS_RESPONSE_STATUS:
return "SUPPORTS_RESPONSE_STATUS";
default:
return "UNKNOWN";
}
}
#endif
#ifdef USE_CLIMATE
template<> const char *proto_enum_to_string<enums::ClimateMode>(enums::ClimateMode value) {
@@ -924,7 +940,7 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const {
}
dump_field(out, "min_mireds", this->min_mireds);
dump_field(out, "max_mireds", this->max_mireds);
for (const auto &it : this->effects) {
for (const auto &it : *this->effects) {
dump_field(out, "effects", it, 4);
}
dump_field(out, "disabled_by_default", this->disabled_by_default);
@@ -983,7 +999,9 @@ void LightCommandRequest::dump_to(std::string &out) const {
dump_field(out, "has_flash_length", this->has_flash_length);
dump_field(out, "flash_length", this->flash_length);
dump_field(out, "has_effect", this->has_effect);
dump_field(out, "effect", this->effect);
out.append(" effect: ");
out.append(format_hex_pretty(this->effect, this->effect_len));
out.append("\n");
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
#endif
@@ -1177,7 +1195,7 @@ void GetTimeResponse::dump_to(std::string &out) const {
out.append(format_hex_pretty(this->timezone, this->timezone_len));
out.append("\n");
}
#ifdef USE_API_SERVICES
#ifdef USE_API_USER_DEFINED_ACTIONS
void ListEntitiesServicesArgument::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ListEntitiesServicesArgument");
dump_field(out, "name", this->name_ref_);
@@ -1192,6 +1210,7 @@ void ListEntitiesServicesResponse::dump_to(std::string &out) const {
it.dump_to(out);
out.append("\n");
}
dump_field(out, "supports_response", static_cast<enums::SupportsResponseType>(this->supports_response));
}
void ExecuteServiceArgument::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ExecuteServiceArgument");
@@ -1221,6 +1240,25 @@ void ExecuteServiceRequest::dump_to(std::string &out) const {
it.dump_to(out);
out.append("\n");
}
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
dump_field(out, "call_id", this->call_id);
#endif
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
dump_field(out, "return_response", this->return_response);
#endif
}
#endif
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
void ExecuteServiceResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ExecuteServiceResponse");
dump_field(out, "call_id", this->call_id);
dump_field(out, "success", this->success);
dump_field(out, "error_message", this->error_message_ref_);
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
out.append(" response_data: ");
out.append(format_hex_pretty(this->response_data, this->response_data_len));
out.append("\n");
#endif
}
#endif
#ifdef USE_CAMERA
@@ -1417,7 +1455,9 @@ void SelectStateResponse::dump_to(std::string &out) const {
void SelectCommandRequest::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "SelectCommandRequest");
dump_field(out, "key", this->key);
dump_field(out, "state", this->state);
out.append(" state: ");
out.append(format_hex_pretty(this->state, this->state_len));
out.append("\n");
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
#endif

View File

@@ -13,7 +13,7 @@ void APIServerConnectionBase::log_send_message_(const char *name, const std::str
}
#endif
void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {
void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {
switch (msg_type) {
case HelloRequest::MESSAGE_TYPE: {
HelloRequest msg;
@@ -193,7 +193,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
break;
}
#endif
#ifdef USE_API_SERVICES
#ifdef USE_API_USER_DEFINED_ACTIONS
case ExecuteServiceRequest::MESSAGE_TYPE: {
ExecuteServiceRequest msg;
msg.decode(msg_data, msg_size);
@@ -670,7 +670,7 @@ void APIServerConnection::on_subscribe_home_assistant_states_request(const Subsc
this->subscribe_home_assistant_states(msg);
}
#endif
#ifdef USE_API_SERVICES
#ifdef USE_API_USER_DEFINED_ACTIONS
void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) { this->execute_service(msg); }
#endif
#ifdef USE_API_NOISE
@@ -827,7 +827,7 @@ void APIServerConnection::on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) { th
void APIServerConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) { this->zwave_proxy_request(msg); }
#endif
void APIServerConnection::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {
void APIServerConnection::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {
// Check authentication/connection requirements for messages
switch (msg_type) {
case HelloRequest::MESSAGE_TYPE: // No setup required

View File

@@ -79,7 +79,7 @@ class APIServerConnectionBase : public ProtoService {
virtual void on_get_time_response(const GetTimeResponse &value){};
#ifdef USE_API_SERVICES
#ifdef USE_API_USER_DEFINED_ACTIONS
virtual void on_execute_service_request(const ExecuteServiceRequest &value){};
#endif
@@ -218,7 +218,7 @@ class APIServerConnectionBase : public ProtoService {
virtual void on_z_wave_proxy_request(const ZWaveProxyRequest &value){};
#endif
protected:
void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;
void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;
};
class APIServerConnection : public APIServerConnectionBase {
@@ -239,7 +239,7 @@ class APIServerConnection : public APIServerConnectionBase {
#ifdef USE_API_HOMEASSISTANT_STATES
virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0;
#endif
#ifdef USE_API_SERVICES
#ifdef USE_API_USER_DEFINED_ACTIONS
virtual void execute_service(const ExecuteServiceRequest &msg) = 0;
#endif
#ifdef USE_API_NOISE
@@ -368,7 +368,7 @@ class APIServerConnection : public APIServerConnectionBase {
#ifdef USE_API_HOMEASSISTANT_STATES
void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override;
#endif
#ifdef USE_API_SERVICES
#ifdef USE_API_USER_DEFINED_ACTIONS
void on_execute_service_request(const ExecuteServiceRequest &msg) override;
#endif
#ifdef USE_API_NOISE
@@ -480,7 +480,7 @@ class APIServerConnection : public APIServerConnectionBase {
#ifdef USE_ZWAVE_PROXY
void on_z_wave_proxy_request(const ZWaveProxyRequest &msg) override;
#endif
void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;
void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;
};
} // namespace esphome::api

View File

@@ -4,8 +4,8 @@
#include "api_connection.h"
#include "esphome/components/network/util.h"
#include "esphome/core/application.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/util.h"
@@ -52,11 +52,6 @@ void APIServer::setup() {
#endif
#endif
// Schedule reboot if no clients connect within timeout
if (this->reboot_timeout_ != 0) {
this->schedule_reboot_timeout_();
}
this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections
if (this->socket_ == nullptr) {
ESP_LOGW(TAG, "Could not create socket");
@@ -101,42 +96,22 @@ void APIServer::setup() {
#ifdef USE_LOGGER
if (logger::global_logger != nullptr) {
logger::global_logger->add_on_log_callback(
[this](int level, const char *tag, const char *message, size_t message_len) {
if (this->shutting_down_) {
// Don't try to send logs during shutdown
// as it could result in a recursion and
// we would be filling a buffer we are trying to clear
return;
}
for (auto &c : this->clients_) {
if (!c->flags_.remove && c->get_log_subscription_level() >= level)
c->try_send_log_message(level, tag, message, message_len);
}
});
logger::global_logger->add_log_listener(this);
}
#endif
#ifdef USE_CAMERA
if (camera::Camera::instance() != nullptr && !camera::Camera::instance()->is_internal()) {
camera::Camera::instance()->add_image_callback([this](const std::shared_ptr<camera::CameraImage> &image) {
for (auto &c : this->clients_) {
if (!c->flags_.remove)
c->set_camera_state(image);
}
});
camera::Camera::instance()->add_listener(this);
}
#endif
}
void APIServer::schedule_reboot_timeout_() {
this->status_set_warning();
this->set_timeout("api_reboot", this->reboot_timeout_, []() {
if (!global_api_server->is_connected()) {
ESP_LOGE(TAG, "No clients; rebooting");
App.reboot();
}
});
// Initialize last_connected_ for reboot timeout tracking
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();
}
}
void APIServer::loop() {
@@ -164,15 +139,24 @@ void APIServer::loop() {
this->clients_.emplace_back(conn);
conn->start();
// Clear warning status and cancel reboot when first client connects
// First client connected - clear warning and update timestamp
if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) {
this->status_clear_warning();
this->cancel_timeout("api_reboot");
this->last_connected_ = App.get_loop_component_start_time();
}
}
}
if (this->clients_.empty()) {
// Check reboot timeout - done in loop to avoid scheduler heap churn
// (cancelled scheduler items sit in heap memory until their scheduled time)
if (this->reboot_timeout_ != 0) {
const uint32_t now = App.get_loop_component_start_time();
if (now - this->last_connected_ > this->reboot_timeout_) {
ESP_LOGE(TAG, "No clients; rebooting");
App.reboot();
}
}
return;
}
@@ -202,6 +186,9 @@ void APIServer::loop() {
// Rare case: handle disconnection
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
this->client_disconnected_trigger_->trigger(client->client_info_.name, client->client_info_.peername);
#endif
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
this->unregister_active_action_calls_for_connection(client.get());
#endif
ESP_LOGV(TAG, "Remove connection %s", client->client_info_.name.c_str());
@@ -211,9 +198,10 @@ void APIServer::loop() {
}
this->clients_.pop_back();
// Schedule reboot when last client disconnects
// Last client disconnected - set warning and start tracking for reboot timeout
if (this->clients_.empty() && this->reboot_timeout_ != 0) {
this->schedule_reboot_timeout_();
this->status_set_warning();
this->last_connected_ = App.get_loop_component_start_time();
}
// Don't increment client_index since we need to process the swapped element
}
@@ -227,8 +215,8 @@ void APIServer::dump_config() {
" Max connections: %u",
network::get_use_address(), this->port_, this->listen_backlog_, this->max_connections_);
#ifdef USE_API_NOISE
ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_->has_psk()));
if (!this->noise_ctx_->has_psk()) {
ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_.has_psk()));
if (!this->noise_ctx_.has_psk()) {
ESP_LOGCONFIG(TAG, " Supports encryption: YES");
}
#else
@@ -431,25 +419,56 @@ void APIServer::handle_action_response(uint32_t call_id, bool success, const std
#endif // USE_API_HOMEASSISTANT_SERVICES
#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(std::string)> 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)
});
}
// 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(std::string)> 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));
sub.entity_id = sub.entity_id_dynamic_storage->c_str();
if (attribute.has_value()) {
sub.attribute_dynamic_storage = std::make_unique<std::string>(std::move(attribute.value()));
sub.attribute = sub.attribute_dynamic_storage->c_str();
} else {
sub.attribute = nullptr;
}
sub.callback = std::move(f);
sub.once = once;
this->state_subs_.push_back(std::move(sub));
}
// 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(std::string)> 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(std::string)> f) {
this->add_state_subscription_(entity_id, attribute, std::move(f), true);
}
// Existing std::string overload (for custom_api_device.h - heap allocation)
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(std::string)> f) {
this->state_subs_.push_back(HomeAssistantStateSubscription{
.entity_id = std::move(entity_id),
.attribute = std::move(attribute),
.callback = std::move(f),
.once = false,
});
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(std::string)> f) {
this->state_subs_.push_back(HomeAssistantStateSubscription{
.entity_id = std::move(entity_id),
.attribute = std::move(attribute),
.callback = std::move(f),
.once = true,
});
};
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), true);
}
const std::vector<APIServer::HomeAssistantStateSubscription> &APIServer::get_state_subs() const {
return this->state_subs_;
@@ -493,7 +512,7 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
ESP_LOGW(TAG, "Key set in YAML");
return false;
#else
auto &old_psk = this->noise_ctx_->get_psk();
auto &old_psk = this->noise_ctx_.get_psk();
if (std::equal(old_psk.begin(), old_psk.end(), psk.begin())) {
ESP_LOGW(TAG, "New PSK matches old");
return true;
@@ -528,7 +547,42 @@ void APIServer::request_time() {
}
#endif
bool APIServer::is_connected() const { return !this->clients_.empty(); }
bool APIServer::is_connected(bool state_subscription_only) const {
if (!state_subscription_only) {
return !this->clients_.empty();
}
for (const auto &client : this->clients_) {
if (client->flags_.state_subscription) {
return true;
}
}
return false;
}
#ifdef USE_LOGGER
void APIServer::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) {
if (this->shutting_down_) {
// Don't try to send logs during shutdown
// as it could result in a recursion and
// we would be filling a buffer we are trying to clear
return;
}
for (auto &c : this->clients_) {
if (!c->flags_.remove && c->get_log_subscription_level() >= level)
c->try_send_log_message(level, tag, message, message_len);
}
}
#endif
#ifdef USE_CAMERA
void APIServer::on_camera_image(const std::shared_ptr<camera::CameraImage> &image) {
for (auto &c : this->clients_) {
if (!c->flags_.remove)
c->set_camera_state(image);
}
}
#endif
void APIServer::on_shutdown() {
this->shutting_down_ = true;
@@ -565,5 +619,84 @@ bool APIServer::teardown() {
return this->clients_.empty();
}
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
// Timeout for action calls - matches aioesphomeapi client timeout (default 30s)
// Can be overridden via USE_API_ACTION_CALL_TIMEOUT_MS define for testing
#ifndef USE_API_ACTION_CALL_TIMEOUT_MS
#define USE_API_ACTION_CALL_TIMEOUT_MS 30000 // NOLINT
#endif
uint32_t APIServer::register_active_action_call(uint32_t client_call_id, APIConnection *conn) {
uint32_t action_call_id = this->next_action_call_id_++;
// Handle wraparound (skip 0 as it means "no call")
if (this->next_action_call_id_ == 0) {
this->next_action_call_id_ = 1;
}
this->active_action_calls_.push_back({action_call_id, client_call_id, conn});
// Schedule automatic cleanup after timeout (client will have given up by then)
this->set_timeout(str_sprintf("action_call_%u", action_call_id), USE_API_ACTION_CALL_TIMEOUT_MS,
[this, action_call_id]() {
ESP_LOGD(TAG, "Action call %u timed out", action_call_id);
this->unregister_active_action_call(action_call_id);
});
return action_call_id;
}
void APIServer::unregister_active_action_call(uint32_t action_call_id) {
// Cancel the timeout for this action call
this->cancel_timeout(str_sprintf("action_call_%u", action_call_id));
// Swap-and-pop is more efficient than remove_if for unordered vectors
for (size_t i = 0; i < this->active_action_calls_.size(); i++) {
if (this->active_action_calls_[i].action_call_id == action_call_id) {
std::swap(this->active_action_calls_[i], this->active_action_calls_.back());
this->active_action_calls_.pop_back();
return;
}
}
}
void APIServer::unregister_active_action_calls_for_connection(APIConnection *conn) {
// Remove all active action calls for disconnected connection using swap-and-pop
for (size_t i = 0; i < this->active_action_calls_.size();) {
if (this->active_action_calls_[i].connection == conn) {
// Cancel the timeout for this action call
this->cancel_timeout(str_sprintf("action_call_%u", this->active_action_calls_[i].action_call_id));
std::swap(this->active_action_calls_[i], this->active_action_calls_.back());
this->active_action_calls_.pop_back();
// Don't increment i - need to check the swapped element
} else {
i++;
}
}
}
void APIServer::send_action_response(uint32_t action_call_id, bool success, const std::string &error_message) {
for (auto &call : this->active_action_calls_) {
if (call.action_call_id == action_call_id) {
call.connection->send_execute_service_response(call.client_call_id, success, error_message);
return;
}
}
ESP_LOGW(TAG, "Cannot send response: no active call found for action_call_id %u", action_call_id);
}
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
void APIServer::send_action_response(uint32_t action_call_id, bool success, const std::string &error_message,
const uint8_t *response_data, size_t response_data_len) {
for (auto &call : this->active_action_calls_) {
if (call.action_call_id == action_call_id) {
call.connection->send_execute_service_response(call.client_call_id, success, error_message, response_data,
response_data_len);
return;
}
}
ESP_LOGW(TAG, "Cannot send response: no active call found for action_call_id %u", action_call_id);
}
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
} // namespace esphome::api
#endif

View File

@@ -12,22 +12,39 @@
#include "esphome/core/log.h"
#include "list_entities.h"
#include "subscribe_state.h"
#ifdef USE_API_SERVICES
#include "user_services.h"
#ifdef USE_LOGGER
#include "esphome/components/logger/logger.h"
#endif
#ifdef USE_CAMERA
#include "esphome/components/camera/camera.h"
#endif
#include <map>
#include <vector>
namespace esphome::api {
#ifdef USE_API_USER_DEFINED_ACTIONS
// Forward declaration - full definition in user_services.h
class UserServiceDescriptor;
#endif
#ifdef USE_API_NOISE
struct SavedNoisePsk {
psk_t psk;
} PACKED; // NOLINT
#endif
class APIServer : public Component, public Controller {
class APIServer : public Component,
public Controller
#ifdef USE_LOGGER
,
public logger::LogListener
#endif
#ifdef USE_CAMERA
,
public camera::CameraListener
#endif
{
public:
APIServer();
void setup() override;
@@ -37,6 +54,12 @@ class APIServer : public Component, public Controller {
void dump_config() override;
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;
#endif
#ifdef USE_CAMERA
void on_camera_image(const std::shared_ptr<camera::CameraImage> &image) override;
#endif
#ifdef USE_API_PASSWORD
bool check_password(const uint8_t *password_data, size_t password_len) const;
void set_password(const std::string &password);
@@ -54,8 +77,8 @@ class APIServer : public Component, public Controller {
#ifdef USE_API_NOISE
bool save_noise_psk(psk_t psk, bool make_active = true);
bool clear_noise_psk(bool make_active = true);
void set_noise_psk(psk_t psk) { noise_ctx_->set_psk(psk); }
std::shared_ptr<APINoiseContext> get_noise_ctx() { return noise_ctx_; }
void set_noise_psk(psk_t psk) { this->noise_ctx_.set_psk(psk); }
APINoiseContext &get_noise_ctx() { return this->noise_ctx_; }
#endif // USE_API_NOISE
void handle_disconnect(APIConnection *conn);
@@ -124,7 +147,7 @@ class APIServer : public Component, public Controller {
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
#endif // USE_API_HOMEASSISTANT_SERVICES
#ifdef USE_API_SERVICES
#ifdef USE_API_USER_DEFINED_ACTIONS
void initialize_user_services(std::initializer_list<UserServiceDescriptor *> services) {
this->user_services_.assign(services);
}
@@ -132,6 +155,19 @@ class APIServer : public Component, public Controller {
// Only compile push_back method when custom_services: true (external components)
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
#endif
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
// Action call context management - supports concurrent calls from multiple clients
// Returns server-generated action_call_id to avoid collisions when clients use same call_id
uint32_t register_active_action_call(uint32_t client_call_id, APIConnection *conn);
void unregister_active_action_call(uint32_t action_call_id);
void unregister_active_action_calls_for_connection(APIConnection *conn);
// Send response for a specific action call (uses action_call_id, sends client_call_id in response)
void send_action_response(uint32_t action_call_id, bool success, const std::string &error_message);
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
void send_action_response(uint32_t action_call_id, bool success, const std::string &error_message,
const uint8_t *response_data, size_t response_data_len);
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
#endif
#ifdef USE_HOMEASSISTANT_TIME
void request_time();
@@ -150,23 +186,34 @@ class APIServer : public Component, public Controller {
void on_zwave_proxy_request(const esphome::api::ProtoMessage &msg);
#endif
bool is_connected() const;
bool is_connected(bool state_subscription_only = false) const;
#ifdef USE_API_HOMEASSISTANT_STATES
struct HomeAssistantStateSubscription {
std::string entity_id;
optional<std::string> attribute;
const char *entity_id; // Pointer to flash (internal) or heap (external)
const char *attribute; // Pointer to flash or nullptr (nullptr means no attribute)
std::function<void(std::string)> callback;
bool once;
// Dynamic storage for external components using std::string API (custom_api_device.h)
// These are only allocated when using the std::string overload (nullptr for const char* overload)
std::unique_ptr<std::string> entity_id_dynamic_storage;
std::unique_ptr<std::string> attribute_dynamic_storage;
};
// New const char* overload (for internal components - zero allocation)
void subscribe_home_assistant_state(const char *entity_id, const char *attribute, std::function<void(std::string)> f);
void get_home_assistant_state(const char *entity_id, const char *attribute, std::function<void(std::string)> f);
// Existing std::string overload (for custom_api_device.h - heap allocation)
void subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(std::string)> f);
void get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(std::string)> f);
const std::vector<HomeAssistantStateSubscription> &get_state_subs() const;
#endif
#ifdef USE_API_SERVICES
#ifdef USE_API_USER_DEFINED_ACTIONS
const std::vector<UserServiceDescriptor *> &get_user_services() const { return this->user_services_; }
#endif
@@ -180,11 +227,17 @@ class APIServer : public Component, public Controller {
#endif
protected:
void schedule_reboot_timeout_();
#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);
#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(std::string)> f,
bool once);
void add_state_subscription_(std::string entity_id, optional<std::string> attribute,
std::function<void(std::string)> f, bool once);
#endif // USE_API_HOMEASSISTANT_STATES
// Pointers and pointer-like types first (4 bytes each)
std::unique_ptr<socket::Socket> socket_ = nullptr;
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
@@ -196,6 +249,7 @@ class APIServer : public Component, public Controller {
// 4-byte aligned types
uint32_t reboot_timeout_{300000};
uint32_t last_connected_{0};
// Vectors and strings (12 bytes each on 32-bit)
std::vector<std::unique_ptr<APIConnection>> clients_;
@@ -206,8 +260,19 @@ class APIServer : public Component, public Controller {
#ifdef USE_API_HOMEASSISTANT_STATES
std::vector<HomeAssistantStateSubscription> state_subs_;
#endif
#ifdef USE_API_SERVICES
#ifdef USE_API_USER_DEFINED_ACTIONS
std::vector<UserServiceDescriptor *> user_services_;
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
// Active action calls - supports concurrent calls from multiple clients
// Uses server-generated action_call_id to avoid collisions when multiple clients use same call_id
struct ActiveActionCall {
uint32_t action_call_id; // Server-generated unique ID (passed to actions)
uint32_t client_call_id; // Client's original call_id (used in response)
APIConnection *connection;
};
std::vector<ActiveActionCall> active_action_calls_;
uint32_t next_action_call_id_{1}; // Counter for generating unique action_call_ids
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
struct PendingActionResponse {
@@ -228,7 +293,7 @@ class APIServer : public Component, public Controller {
// 7 bytes used, 1 byte padding
#ifdef USE_API_NOISE
std::shared_ptr<APINoiseContext> noise_ctx_ = std::make_shared<APINoiseContext>();
APINoiseContext noise_ctx_;
ESPPreferenceObject noise_pref_;
#endif // USE_API_NOISE
};
@@ -236,8 +301,11 @@ class APIServer : public Component, public Controller {
extern APIServer *global_api_server; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
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(); }
bool check(const Ts &...x) override {
return global_api_server->is_connected(this->state_subscription_only_.value(x...));
}
};
} // namespace esphome::api

View File

@@ -3,12 +3,12 @@
#include <map>
#include "api_server.h"
#ifdef USE_API
#ifdef USE_API_SERVICES
#ifdef USE_API_USER_DEFINED_ACTIONS
#include "user_services.h"
#endif
namespace esphome::api {
#ifdef USE_API_SERVICES
#ifdef USE_API_USER_DEFINED_ACTIONS
template<typename T, typename... Ts> class CustomAPIDeviceService : public UserServiceDynamic<Ts...> {
public:
CustomAPIDeviceService(const std::string &name, const std::array<std::string, sizeof...(Ts)> &arg_names, T *obj,
@@ -16,12 +16,15 @@ template<typename T, typename... Ts> class CustomAPIDeviceService : public UserS
: UserServiceDynamic<Ts...>(name, arg_names), obj_(obj), callback_(callback) {}
protected:
void execute(Ts... x) override { (this->obj_->*this->callback_)(x...); } // NOLINT
// CustomAPIDevice services don't support action responses - ignore call_id and return_response
void execute(uint32_t /*call_id*/, bool /*return_response*/, Ts... x) override {
(this->obj_->*this->callback_)(x...); // NOLINT
}
T *obj_;
void (T::*callback_)(Ts...);
};
#endif // USE_API_SERVICES
#endif // USE_API_USER_DEFINED_ACTIONS
class CustomAPIDevice {
public:
@@ -49,7 +52,7 @@ class CustomAPIDevice {
* @param name The name of the service to register.
* @param arg_names The name of the arguments for the service, must match the arguments of the function.
*/
#ifdef USE_API_SERVICES
#ifdef USE_API_USER_DEFINED_ACTIONS
template<typename T, typename... Ts>
void register_service(void (T::*callback)(Ts...), const std::string &name,
const std::array<std::string, sizeof...(Ts)> &arg_names) {
@@ -90,7 +93,7 @@ class CustomAPIDevice {
* @param callback The member function to call when the service is triggered.
* @param name The name of the arguments for the service, must match the arguments of the function.
*/
#ifdef USE_API_SERVICES
#ifdef USE_API_USER_DEFINED_ACTIONS
template<typename T> void register_service(void (T::*callback)(), const std::string &name) {
#ifdef USE_API_CUSTOM_SERVICES
auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback); // NOLINT

View File

@@ -12,10 +12,17 @@
#endif
#include "esphome/core/automation.h"
#include "esphome/core/helpers.h"
#include "esphome/core/string_ref.h"
namespace esphome::api {
template<typename... X> class TemplatableStringValue : public TemplatableValue<std::string, X...> {
// Verify that const char* uses the base class STATIC_STRING optimization (no heap allocation)
// rather than being wrapped in a lambda. The base class constructor for const char* is more
// specialized than the templated constructor here, so it should be selected.
static_assert(std::is_constructible_v<TemplatableValue<std::string, X...>, const char *>,
"Base class must have const char* constructor for STATIC_STRING optimization");
private:
// Helper to convert value to string - handles the case where value is already a string
template<typename T> static std::string value_to_string(T &&val) { return to_string(std::forward<T>(val)); }
@@ -46,23 +53,25 @@ template<typename... Ts> class TemplatableKeyValuePair {
// Keys are always string literals from YAML dictionary keys (e.g., "code", "event")
// and never templatable values or lambdas. Only the value parameter can be a lambda/template.
// Using pass-by-value with std::move allows optimal performance for both lvalues and rvalues.
template<typename T> TemplatableKeyValuePair(std::string key, T value) : key(std::move(key)), value(value) {}
// Using const char* avoids std::string heap allocation - keys remain in flash.
template<typename T> TemplatableKeyValuePair(const char *key, T value) : key(key), value(value) {}
std::string key;
const char *key{nullptr};
TemplatableStringValue<Ts...> value;
};
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
// Represents the response data from a Home Assistant action
// Note: This class holds a StringRef to the error_message from the protobuf message.
// The protobuf message must outlive the ActionResponse (which is guaranteed since
// the callback is invoked synchronously while the message is on the stack).
class ActionResponse {
public:
ActionResponse(bool success, std::string error_message = "")
: success_(success), error_message_(std::move(error_message)) {}
ActionResponse(bool success, const std::string &error_message) : success_(success), error_message_(error_message) {}
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
ActionResponse(bool success, std::string error_message, const uint8_t *data, size_t data_len)
: success_(success), error_message_(std::move(error_message)) {
ActionResponse(bool success, const std::string &error_message, const uint8_t *data, size_t data_len)
: success_(success), error_message_(error_message) {
if (data == nullptr || data_len == 0)
return;
this->json_document_ = json::parse_json(data, data_len);
@@ -70,7 +79,8 @@ class ActionResponse {
#endif
bool is_success() const { return this->success_; }
const std::string &get_error_message() const { return this->error_message_; }
// Returns reference to error message - can be implicitly converted to std::string if needed
const StringRef &get_error_message() const { return this->error_message_; }
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
// Get data as parsed JSON object (const version returns read-only view)
@@ -79,7 +89,7 @@ class ActionResponse {
protected:
bool success_;
std::string error_message_;
StringRef error_message_;
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
JsonDocument json_document_;
#endif
@@ -105,14 +115,15 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
// Keys are always string literals from the Python code generation (e.g., cg.add(var.add_data("tag_id", templ))).
// The value parameter can be a lambda/template, but keys are never templatable.
template<typename K, typename V> void add_data(K &&key, V &&value) {
this->add_kv_(this->data_, std::forward<K>(key), std::forward<V>(value));
// Using const char* for keys avoids std::string heap allocation - keys remain in flash.
template<typename V> void add_data(const char *key, V &&value) {
this->add_kv_(this->data_, key, std::forward<V>(value));
}
template<typename K, typename V> void add_data_template(K &&key, V &&value) {
this->add_kv_(this->data_template_, std::forward<K>(key), std::forward<V>(value));
template<typename V> void add_data_template(const char *key, V &&value) {
this->add_kv_(this->data_template_, key, std::forward<V>(value));
}
template<typename K, typename V> void add_variable(K &&key, V &&value) {
this->add_kv_(this->variables_, std::forward<K>(key), std::forward<V>(value));
template<typename V> void add_variable(const char *key, V &&value) {
this->add_kv_(this->variables_, key, std::forward<V>(value));
}
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
@@ -185,10 +196,11 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
}
protected:
// Helper to add key-value pairs to FixedVectors with perfect forwarding to avoid copies
template<typename K, typename V> void add_kv_(FixedVector<TemplatableKeyValuePair<Ts...>> &vec, K &&key, V &&value) {
// Helper to add key-value pairs to FixedVectors
// Keys are always string literals (const char*), values can be lambdas/templates
template<typename V> void add_kv_(FixedVector<TemplatableKeyValuePair<Ts...>> &vec, const char *key, V &&value) {
auto &kv = vec.emplace_back();
kv.key = std::forward<K>(key);
kv.key = key;
kv.value = std::forward<V>(value);
}

View File

@@ -5,6 +5,9 @@
#include "esphome/core/application.h"
#include "esphome/core/log.h"
#include "esphome/core/util.h"
#ifdef USE_API_USER_DEFINED_ACTIONS
#include "user_services.h"
#endif
namespace esphome::api {
@@ -82,7 +85,7 @@ bool ListEntitiesIterator::on_end() { return this->client_->send_list_info_done(
ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(client) {}
#ifdef USE_API_SERVICES
#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);

View File

@@ -43,7 +43,7 @@ class ListEntitiesIterator : public ComponentIterator {
#ifdef USE_TEXT_SENSOR
bool on_text_sensor(text_sensor::TextSensor *entity) override;
#endif
#ifdef USE_API_SERVICES
#ifdef USE_API_USER_DEFINED_ACTIONS
bool on_service(UserServiceDescriptor *service) override;
#endif
#ifdef USE_CAMERA

View File

@@ -846,7 +846,7 @@ class ProtoService {
*/
virtual ProtoWriteBuffer create_buffer(uint32_t reserve_size) = 0;
virtual bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) = 0;
virtual void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) = 0;
virtual void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) = 0;
// Optimized method that pre-allocates buffer based on message size
bool send_message_(const ProtoMessage &msg, uint8_t message_type) {

View File

@@ -1,20 +1,31 @@
#pragma once
#include <tuple>
#include <utility>
#include <vector>
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "api_pb2.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
#include "esphome/components/json/json_util.h"
#endif
#ifdef USE_API_SERVICES
#ifdef USE_API_USER_DEFINED_ACTIONS
namespace esphome::api {
// Forward declaration - full definition in api_server.h
class APIServer;
class UserServiceDescriptor {
public:
virtual ListEntitiesServicesResponse encode_list_service_response() = 0;
virtual bool execute_service(const ExecuteServiceRequest &req) = 0;
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
// Overload that accepts server-generated action_call_id (avoids client call_id collisions)
virtual bool execute_service(const ExecuteServiceRequest &req, uint32_t action_call_id) = 0;
#endif
bool is_internal() { return false; }
};
@@ -27,8 +38,9 @@ template<typename T> enums::ServiceArgType to_service_arg_type();
// Stores only pointers to string literals in flash - no heap allocation
template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
public:
UserServiceBase(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
: name_(name), arg_names_(arg_names) {
UserServiceBase(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names,
enums::SupportsResponseType supports_response = enums::SUPPORTS_RESPONSE_NONE)
: name_(name), arg_names_(arg_names), supports_response_(supports_response) {
this->key_ = fnv1_hash(name);
}
@@ -36,6 +48,7 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
ListEntitiesServicesResponse msg;
msg.set_name(StringRef(this->name_));
msg.key = this->key_;
msg.supports_response = this->supports_response_;
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
msg.args.init(sizeof...(Ts));
for (size_t i = 0; i < sizeof...(Ts); i++) {
@@ -51,20 +64,37 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
return false;
if (req.args.size() != sizeof...(Ts))
return false;
this->execute_(req.args, typename gens<sizeof...(Ts)>::type());
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
this->execute_(req.args, req.call_id, req.return_response, std::make_index_sequence<sizeof...(Ts)>{});
#else
this->execute_(req.args, 0, false, std::make_index_sequence<sizeof...(Ts)>{});
#endif
return true;
}
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
bool execute_service(const ExecuteServiceRequest &req, uint32_t action_call_id) override {
if (req.key != this->key_)
return false;
if (req.args.size() != sizeof...(Ts))
return false;
this->execute_(req.args, action_call_id, req.return_response, std::make_index_sequence<sizeof...(Ts)>{});
return true;
}
#endif
protected:
virtual void execute(Ts... x) = 0;
template<typename ArgsContainer, int... S> void execute_(const ArgsContainer &args, seq<S...> type) {
this->execute((get_execute_arg_value<Ts>(args[S]))...);
virtual void execute(uint32_t call_id, bool return_response, Ts... x) = 0;
template<typename ArgsContainer, size_t... S>
void execute_(const ArgsContainer &args, uint32_t call_id, bool return_response, std::index_sequence<S...> /*type*/) {
this->execute(call_id, return_response, (get_execute_arg_value<Ts>(args[S]))...);
}
// Pointers to string literals in flash - no heap allocation
const char *name_;
std::array<const char *, sizeof...(Ts)> arg_names_;
uint32_t key_{0};
enums::SupportsResponseType supports_response_{enums::SUPPORTS_RESPONSE_NONE};
};
// Separate class for custom_api_device services (rare case)
@@ -80,6 +110,7 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
ListEntitiesServicesResponse msg;
msg.set_name(StringRef(this->name_));
msg.key = this->key_;
msg.supports_response = enums::SUPPORTS_RESPONSE_NONE; // Dynamic services don't support responses yet
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
msg.args.init(sizeof...(Ts));
for (size_t i = 0; i < sizeof...(Ts); i++) {
@@ -95,14 +126,31 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
return false;
if (req.args.size() != sizeof...(Ts))
return false;
this->execute_(req.args, typename gens<sizeof...(Ts)>::type());
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
this->execute_(req.args, req.call_id, req.return_response, std::make_index_sequence<sizeof...(Ts)>{});
#else
this->execute_(req.args, 0, false, std::make_index_sequence<sizeof...(Ts)>{});
#endif
return true;
}
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
// Dynamic services don't support responses yet, but need to implement the interface
bool execute_service(const ExecuteServiceRequest &req, uint32_t action_call_id) override {
if (req.key != this->key_)
return false;
if (req.args.size() != sizeof...(Ts))
return false;
this->execute_(req.args, action_call_id, req.return_response, std::make_index_sequence<sizeof...(Ts)>{});
return true;
}
#endif
protected:
virtual void execute(Ts... x) = 0;
template<typename ArgsContainer, int... S> void execute_(const ArgsContainer &args, seq<S...> type) {
this->execute((get_execute_arg_value<Ts>(args[S]))...);
virtual void execute(uint32_t call_id, bool return_response, Ts... x) = 0;
template<typename ArgsContainer, size_t... S>
void execute_(const ArgsContainer &args, uint32_t call_id, bool return_response, std::index_sequence<S...> /*type*/) {
this->execute(call_id, return_response, (get_execute_arg_value<Ts>(args[S]))...);
}
// Heap-allocated strings for runtime-generated names
@@ -111,15 +159,149 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
uint32_t key_{0};
};
template<typename... Ts> class UserServiceTrigger : public UserServiceBase<Ts...>, public Trigger<Ts...> {
// Primary template declaration
template<enums::SupportsResponseType Mode, typename... Ts> class UserServiceTrigger;
// Specialization for NONE - no extra trigger arguments
template<typename... Ts>
class UserServiceTrigger<enums::SUPPORTS_RESPONSE_NONE, Ts...> : public UserServiceBase<Ts...>, public Trigger<Ts...> {
public:
// Constructor for static names (YAML-defined services - used by code generator)
UserServiceTrigger(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
: UserServiceBase<Ts...>(name, arg_names) {}
: UserServiceBase<Ts...>(name, arg_names, enums::SUPPORTS_RESPONSE_NONE) {}
protected:
void execute(Ts... x) override { this->trigger(x...); } // NOLINT
void execute(uint32_t /*call_id*/, bool /*return_response*/, Ts... x) override { this->trigger(x...); }
};
// Specialization for OPTIONAL - call_id and return_response trigger arguments
template<typename... Ts>
class UserServiceTrigger<enums::SUPPORTS_RESPONSE_OPTIONAL, Ts...> : public UserServiceBase<Ts...>,
public Trigger<uint32_t, bool, Ts...> {
public:
UserServiceTrigger(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
: UserServiceBase<Ts...>(name, arg_names, enums::SUPPORTS_RESPONSE_OPTIONAL) {}
protected:
void execute(uint32_t call_id, bool return_response, Ts... x) override {
this->trigger(call_id, return_response, x...);
}
};
// Specialization for ONLY - just call_id trigger argument
template<typename... Ts>
class UserServiceTrigger<enums::SUPPORTS_RESPONSE_ONLY, Ts...> : public UserServiceBase<Ts...>,
public Trigger<uint32_t, Ts...> {
public:
UserServiceTrigger(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
: UserServiceBase<Ts...>(name, arg_names, enums::SUPPORTS_RESPONSE_ONLY) {}
protected:
void execute(uint32_t call_id, bool /*return_response*/, Ts... x) override { this->trigger(call_id, x...); }
};
// Specialization for STATUS - just call_id trigger argument (reports success/error without data)
template<typename... Ts>
class UserServiceTrigger<enums::SUPPORTS_RESPONSE_STATUS, Ts...> : public UserServiceBase<Ts...>,
public Trigger<uint32_t, Ts...> {
public:
UserServiceTrigger(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
: UserServiceBase<Ts...>(name, arg_names, enums::SUPPORTS_RESPONSE_STATUS) {}
protected:
void execute(uint32_t call_id, bool /*return_response*/, Ts... x) override { this->trigger(call_id, x...); }
};
} // namespace esphome::api
#endif // USE_API_SERVICES
#endif // USE_API_USER_DEFINED_ACTIONS
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
// Include full definition of APIServer for template implementation
// Must be outside namespace to avoid including STL headers inside namespace
#include "api_server.h"
namespace esphome::api {
template<typename... Ts> class APIRespondAction : public Action<Ts...> {
public:
explicit APIRespondAction(APIServer *parent) : parent_(parent) {}
template<typename V> void set_success(V success) { this->success_ = success; }
template<typename V> void set_error_message(V error) { this->error_message_ = error; }
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) {
this->json_builder_ = std::move(func);
this->has_data_ = true;
}
#endif
void play(const Ts &...x) override {
// Extract call_id from first argument - it's always first for optional/only/status modes
auto args = std::make_tuple(x...);
uint32_t call_id = std::get<0>(args);
bool success = this->success_.value(x...);
std::string error_message = this->error_message_.value(x...);
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
if (this->has_data_) {
// For optional mode, check return_response (second arg) to decide if client wants data
// Use nested if constexpr to avoid compile error when tuple doesn't have enough elements
// (std::tuple_element_t is evaluated before the && short-circuit, so we must nest)
if constexpr (sizeof...(Ts) >= 2) {
if constexpr (std::is_same_v<std::tuple_element_t<1, std::tuple<Ts...>>, bool>) {
if (this->is_optional_mode_) {
bool return_response = std::get<1>(args);
if (!return_response) {
// Client doesn't want response data, just send success/error
this->parent_->send_action_response(call_id, success, error_message);
return;
}
}
}
}
// Build and send JSON response
json::JsonBuilder builder;
this->json_builder_(x..., builder.root());
std::string json_str = builder.serialize();
this->parent_->send_action_response(call_id, success, error_message,
reinterpret_cast<const uint8_t *>(json_str.data()), json_str.size());
return;
}
#endif
this->parent_->send_action_response(call_id, success, error_message);
}
protected:
APIServer *parent_;
TemplatableValue<bool, Ts...> success_{true};
TemplatableValue<std::string, Ts...> error_message_{""};
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
std::function<void(Ts..., JsonObject)> json_builder_;
bool has_data_{false};
#endif
bool is_optional_mode_{false};
};
// Action to unregister a service call after execution completes
// Automatically appended to the end of action lists for non-none response modes
template<typename... Ts> class APIUnregisterServiceCallAction : public Action<Ts...> {
public:
explicit APIUnregisterServiceCallAction(APIServer *parent) : parent_(parent) {}
void play(const Ts &...x) override {
// Extract call_id from first argument - same convention as APIRespondAction
auto args = std::make_tuple(x...);
uint32_t call_id = std::get<0>(args);
if (call_id != 0) {
this->parent_->unregister_active_action_call(call_id);
}
}
protected:
APIServer *parent_;
};
} // namespace esphome::api
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES

View File

@@ -44,7 +44,7 @@ CONFIG_SCHEMA = (
cv.Optional(ble_client.CONF_BLE_CLIENT_ID): cv.invalid(
"The 'ble_client_id' option has been removed. Please migrate "
"to the new `bedjet_id` option in the `bedjet` component.\n"
"See https://esphome.io/components/climate/bedjet.html"
"See https://esphome.io/components/climate/bedjet/"
),
cv.Optional(CONF_TIME_ID): cv.invalid(
"The 'time_id' option has been moved to the `bedjet` component."

View File

@@ -23,7 +23,7 @@ void BH1900NUXSensor::setup() {
i2c::ErrorCode result_code =
this->write_register(SOFT_RESET_REG, &SOFT_RESET_PAYLOAD, 1); // Software Reset to check communication
if (result_code != i2c::ERROR_OK) {
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
}

View File

@@ -1,12 +1,11 @@
#include "automation.h"
#include "esphome/core/log.h"
namespace esphome {
namespace binary_sensor {
namespace esphome::binary_sensor {
static const char *const TAG = "binary_sensor.automation";
void binary_sensor::MultiClickTrigger::on_state_(bool state) {
void MultiClickTrigger::on_state_(bool state) {
// Handle duplicate events
if (state == this->last_state_) {
return;
@@ -67,7 +66,7 @@ void binary_sensor::MultiClickTrigger::on_state_(bool state) {
*this->at_index_ = *this->at_index_ + 1;
}
void binary_sensor::MultiClickTrigger::schedule_cooldown_() {
void MultiClickTrigger::schedule_cooldown_() {
ESP_LOGV(TAG, "Multi Click: Invalid length of press, starting cooldown of %" PRIu32 " ms", this->invalid_cooldown_);
this->is_in_cooldown_ = true;
this->set_timeout("cooldown", this->invalid_cooldown_, [this]() {
@@ -79,7 +78,7 @@ void binary_sensor::MultiClickTrigger::schedule_cooldown_() {
this->cancel_timeout("is_valid");
this->cancel_timeout("is_not_valid");
}
void binary_sensor::MultiClickTrigger::schedule_is_valid_(uint32_t min_length) {
void MultiClickTrigger::schedule_is_valid_(uint32_t min_length) {
if (min_length == 0) {
this->is_valid_ = true;
return;
@@ -90,19 +89,19 @@ void binary_sensor::MultiClickTrigger::schedule_is_valid_(uint32_t min_length) {
this->is_valid_ = true;
});
}
void binary_sensor::MultiClickTrigger::schedule_is_not_valid_(uint32_t max_length) {
void MultiClickTrigger::schedule_is_not_valid_(uint32_t max_length) {
this->set_timeout("is_not_valid", max_length, [this]() {
ESP_LOGV(TAG, "Multi Click: You waited too long to %s.", this->parent_->state ? "RELEASE" : "PRESS");
this->is_valid_ = false;
this->schedule_cooldown_();
});
}
void binary_sensor::MultiClickTrigger::cancel() {
void MultiClickTrigger::cancel() {
ESP_LOGV(TAG, "Multi Click: Sequence explicitly cancelled.");
this->is_valid_ = false;
this->schedule_cooldown_();
}
void binary_sensor::MultiClickTrigger::trigger_() {
void MultiClickTrigger::trigger_() {
ESP_LOGV(TAG, "Multi Click: Hooray, multi click is valid. Triggering!");
this->at_index_.reset();
this->cancel_timeout("trigger");
@@ -118,5 +117,4 @@ bool match_interval(uint32_t min_length, uint32_t max_length, uint32_t length) {
return length >= min_length && length <= max_length;
}
}
} // namespace binary_sensor
} // namespace esphome
} // namespace esphome::binary_sensor

View File

@@ -9,8 +9,7 @@
#include "esphome/core/helpers.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
namespace esphome {
namespace binary_sensor {
namespace esphome::binary_sensor {
struct MultiClickTriggerEvent {
bool state;
@@ -172,5 +171,4 @@ template<typename... Ts> class BinarySensorInvalidateAction : public Action<Ts..
BinarySensor *sensor_;
};
} // namespace binary_sensor
} // namespace esphome
} // namespace esphome::binary_sensor

View File

@@ -3,9 +3,7 @@
#include "esphome/core/controller_registry.h"
#include "esphome/core/log.h"
namespace esphome {
namespace binary_sensor {
namespace esphome::binary_sensor {
static const char *const TAG = "binary_sensor";
@@ -36,13 +34,20 @@ void BinarySensor::publish_initial_state(bool new_state) {
void BinarySensor::send_state_internal(bool new_state) {
// copy the new state to the visible property for backwards compatibility, before any callbacks
this->state = new_state;
// Note that set_state_ de-dups and will only trigger callbacks if the state has actually changed
if (this->set_state_(new_state)) {
ESP_LOGD(TAG, "'%s': New state is %s", this->get_name().c_str(), ONOFF(new_state));
// Note that set_new_state_ de-dups and will only trigger callbacks if the state has actually changed
this->set_new_state(new_state);
}
bool BinarySensor::set_new_state(const optional<bool> &new_state) {
if (StatefulEntityBase::set_new_state(new_state)) {
// weirdly, this file could be compiled even without USE_BINARY_SENSOR defined
#if defined(USE_BINARY_SENSOR) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_binary_sensor_update(this);
#endif
ESP_LOGD(TAG, "'%s': %s", this->get_name().c_str(), ONOFFMAYBE(new_state));
return true;
}
return false;
}
void BinarySensor::add_filter(Filter *filter) {
@@ -63,6 +68,4 @@ void BinarySensor::add_filters(std::initializer_list<Filter *> filters) {
}
bool BinarySensor::is_status_binary_sensor() const { return false; }
} // namespace binary_sensor
} // namespace esphome
} // namespace esphome::binary_sensor

View File

@@ -6,9 +6,7 @@
#include <initializer_list>
namespace esphome {
namespace binary_sensor {
namespace esphome::binary_sensor {
class BinarySensor;
void log_binary_sensor(const char *tag, const char *prefix, const char *type, BinarySensor *obj);
@@ -63,6 +61,8 @@ class BinarySensor : public StatefulEntityBase<bool>, public EntityBase_DeviceCl
protected:
Filter *filter_list_{nullptr};
bool set_new_state(const optional<bool> &new_state) override;
};
class BinarySensorInitiallyOff : public BinarySensor {
@@ -70,5 +70,4 @@ class BinarySensorInitiallyOff : public BinarySensor {
bool has_state() const override { return true; }
};
} // namespace binary_sensor
} // namespace esphome
} // namespace esphome::binary_sensor

View File

@@ -2,9 +2,7 @@
#include "binary_sensor.h"
namespace esphome {
namespace binary_sensor {
namespace esphome::binary_sensor {
static const char *const TAG = "sensor.filter";
@@ -132,6 +130,4 @@ optional<bool> SettleFilter::new_value(bool value) {
float SettleFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
} // namespace binary_sensor
} // namespace esphome
} // namespace esphome::binary_sensor

View File

@@ -4,9 +4,7 @@
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace binary_sensor {
namespace esphome::binary_sensor {
class BinarySensor;
@@ -139,6 +137,4 @@ class SettleFilter : public Filter, public Component {
bool steady_{true};
};
} // namespace binary_sensor
} // namespace esphome
} // namespace esphome::binary_sensor

View File

@@ -2,12 +2,10 @@
#include "automation.h"
namespace esphome {
namespace ble_client {
namespace esphome::ble_client {
const char *const Automation::TAG = "ble_client.automation";
} // namespace ble_client
} // namespace esphome
} // namespace esphome::ble_client
#endif

View File

@@ -9,8 +9,7 @@
#include "esphome/components/ble_client/ble_client.h"
#include "esphome/core/log.h"
namespace esphome {
namespace ble_client {
namespace esphome::ble_client {
// placeholder class for static TAG .
class Automation {
@@ -122,16 +121,19 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
void play_complex(const Ts &...x) override {
this->num_running_++;
this->var_ = std::make_tuple(x...);
std::vector<uint8_t> value;
bool result;
if (this->len_ >= 0) {
// Static mode: copy from flash to vector
value.assign(this->value_.data, this->value_.data + this->len_);
// Static mode: write directly from flash pointer
result = this->write(this->value_.data, this->len_);
} else {
// Template mode: call function
value = this->value_.func(x...);
// Template mode: call function and write the vector
std::vector<uint8_t> value = this->value_.func(x...);
result = this->write(value);
}
// on write failure, continue the automation chain rather than stopping so that e.g. disconnect can work.
if (!write(value))
if (!result)
this->play_next_(x...);
}
@@ -144,15 +146,15 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
* errors.
*/
// initiate the write. Return true if all went well, will be followed by a WRITE_CHAR event.
bool write(const std::vector<uint8_t> &value) {
bool write(const uint8_t *data, size_t len) {
if (this->node_state != espbt::ClientState::ESTABLISHED) {
esph_log_w(Automation::TAG, "Cannot write to BLE characteristic - not connected");
return false;
}
esph_log_vv(Automation::TAG, "Will write %d bytes: %s", value.size(), format_hex_pretty(value).c_str());
esp_err_t err = esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(),
this->char_handle_, value.size(), const_cast<uint8_t *>(value.data()),
this->write_type_, ESP_GATT_AUTH_REQ_NONE);
esph_log_vv(Automation::TAG, "Will write %d bytes: %s", len, format_hex_pretty(data, len).c_str());
esp_err_t err =
esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->char_handle_, len,
const_cast<uint8_t *>(data), this->write_type_, ESP_GATT_AUTH_REQ_NONE);
if (err != ESP_OK) {
esph_log_e(Automation::TAG, "Error writing to characteristic: %s!", esp_err_to_name(err));
return false;
@@ -160,6 +162,8 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
return true;
}
bool write(const std::vector<uint8_t> &value) { return this->write(value.data(), value.size()); }
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override {
switch (event) {
@@ -193,7 +197,7 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
}
this->node_state = espbt::ClientState::ESTABLISHED;
esph_log_d(Automation::TAG, "Found characteristic %s on device %s", this->char_uuid_.to_string().c_str(),
ble_client_->address_str().c_str());
ble_client_->address_str());
break;
}
default:
@@ -386,7 +390,6 @@ template<typename... Ts> class BLEClientDisconnectAction : public Action<Ts...>,
BLEClient *ble_client_;
std::tuple<Ts...> var_{};
};
} // namespace ble_client
} // namespace esphome
} // namespace esphome::ble_client
#endif

View File

@@ -7,8 +7,7 @@
#ifdef USE_ESP32
namespace esphome {
namespace ble_client {
namespace esphome::ble_client {
static const char *const TAG = "ble_client";
@@ -39,7 +38,7 @@ void BLEClient::set_enabled(bool enabled) {
return;
this->enabled = enabled;
if (!enabled) {
ESP_LOGI(TAG, "[%s] Disabling BLE client.", this->address_str().c_str());
ESP_LOGI(TAG, "[%s] Disabling BLE client.", this->address_str());
this->disconnect();
}
}
@@ -82,7 +81,6 @@ bool BLEClient::all_nodes_established_() {
return true;
}
} // namespace ble_client
} // namespace esphome
} // namespace esphome::ble_client
#endif

View File

@@ -15,8 +15,7 @@
#include <string>
#include <vector>
namespace esphome {
namespace ble_client {
namespace esphome::ble_client {
namespace espbt = esphome::esp32_ble_tracker;
@@ -75,7 +74,6 @@ class BLEClient : public BLEClientBase {
std::vector<BLEClientNode *> nodes_;
};
} // namespace ble_client
} // namespace esphome
} // namespace esphome::ble_client
#endif

View File

@@ -3,8 +3,7 @@
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
#ifdef USE_ESP32
namespace esphome {
namespace ble_client {
namespace esphome::ble_client {
static const char *const TAG = "ble_binary_output";
@@ -14,7 +13,7 @@ void BLEBinaryOutput::dump_config() {
" MAC address : %s\n"
" Service UUID : %s\n"
" Characteristic UUID: %s",
this->parent_->address_str().c_str(), this->service_uuid_.to_string().c_str(),
this->parent_->address_str(), this->service_uuid_.to_string().c_str(),
this->char_uuid_.to_string().c_str());
LOG_BINARY_OUTPUT(this);
}
@@ -44,7 +43,7 @@ void BLEBinaryOutput::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_i
}
this->node_state = espbt::ClientState::ESTABLISHED;
ESP_LOGD(TAG, "Found characteristic %s on device %s", this->char_uuid_.to_string().c_str(),
this->parent()->address_str().c_str());
this->parent()->address_str());
this->node_state = espbt::ClientState::ESTABLISHED;
break;
}
@@ -75,6 +74,5 @@ void BLEBinaryOutput::write_state(bool state) {
ESP_LOGW(TAG, "[%s] Write error, err=%d", this->char_uuid_.to_string().c_str(), err);
}
} // namespace ble_client
} // namespace esphome
} // namespace esphome::ble_client
#endif

View File

@@ -7,8 +7,7 @@
#ifdef USE_ESP32
#include <esp_gattc_api.h>
namespace esphome {
namespace ble_client {
namespace esphome::ble_client {
namespace espbt = esphome::esp32_ble_tracker;
@@ -36,7 +35,6 @@ class BLEBinaryOutput : public output::BinaryOutput, public BLEClientNode, publi
esp_gatt_write_type_t write_type_{};
};
} // namespace ble_client
} // namespace esphome
} // namespace esphome::ble_client
#endif

View File

@@ -5,8 +5,7 @@
#ifdef USE_ESP32
namespace esphome {
namespace ble_client {
namespace esphome::ble_client {
class BLESensorNotifyTrigger : public Trigger<float>, public BLESensor {
public:
@@ -35,7 +34,6 @@ class BLESensorNotifyTrigger : public Trigger<float>, public BLESensor {
BLESensor *sensor_;
};
} // namespace ble_client
} // namespace esphome
} // namespace esphome::ble_client
#endif

View File

@@ -6,8 +6,7 @@
#ifdef USE_ESP32
namespace esphome {
namespace ble_client {
namespace esphome::ble_client {
static const char *const TAG = "ble_rssi_sensor";
@@ -19,7 +18,7 @@ void BLEClientRSSISensor::loop() {
void BLEClientRSSISensor::dump_config() {
LOG_SENSOR("", "BLE Client RSSI Sensor", this);
ESP_LOGCONFIG(TAG, " MAC address : %s", this->parent()->address_str().c_str());
ESP_LOGCONFIG(TAG, " MAC address : %s", this->parent()->address_str());
LOG_UPDATE_INTERVAL(this);
}
@@ -69,15 +68,14 @@ void BLEClientRSSISensor::update() {
this->get_rssi_();
}
void BLEClientRSSISensor::get_rssi_() {
ESP_LOGV(TAG, "requesting rssi from %s", this->parent()->address_str().c_str());
ESP_LOGV(TAG, "requesting rssi from %s", this->parent()->address_str());
auto status = esp_ble_gap_read_rssi(this->parent()->get_remote_bda());
if (status != ESP_OK) {
ESP_LOGW(TAG, "esp_ble_gap_read_rssi error, address=%s, status=%d", this->parent()->address_str().c_str(), status);
ESP_LOGW(TAG, "esp_ble_gap_read_rssi error, address=%s, status=%d", this->parent()->address_str(), status);
this->status_set_warning();
this->publish_state(NAN);
}
}
} // namespace ble_client
} // namespace esphome
} // namespace esphome::ble_client
#endif

View File

@@ -8,8 +8,7 @@
#ifdef USE_ESP32
#include <esp_gattc_api.h>
namespace esphome {
namespace ble_client {
namespace esphome::ble_client {
namespace espbt = esphome::esp32_ble_tracker;
@@ -29,6 +28,5 @@ class BLEClientRSSISensor : public sensor::Sensor, public PollingComponent, publ
bool should_update_{false};
};
} // namespace ble_client
} // namespace esphome
} // namespace esphome::ble_client
#endif

View File

@@ -6,8 +6,7 @@
#ifdef USE_ESP32
namespace esphome {
namespace ble_client {
namespace esphome::ble_client {
static const char *const TAG = "ble_sensor";
@@ -25,7 +24,7 @@ void BLESensor::dump_config() {
" Characteristic UUID: %s\n"
" Descriptor UUID : %s\n"
" Notifications : %s",
this->parent()->address_str().c_str(), this->service_uuid_.to_string().c_str(),
this->parent()->address_str(), this->service_uuid_.to_string().c_str(),
this->char_uuid_.to_string().c_str(), this->descr_uuid_.to_string().c_str(), YESNO(this->notify_));
LOG_UPDATE_INTERVAL(this);
}
@@ -147,6 +146,5 @@ void BLESensor::update() {
}
}
} // namespace ble_client
} // namespace esphome
} // namespace esphome::ble_client
#endif

View File

@@ -10,8 +10,7 @@
#ifdef USE_ESP32
#include <esp_gattc_api.h>
namespace esphome {
namespace ble_client {
namespace esphome::ble_client {
namespace espbt = esphome::esp32_ble_tracker;
@@ -48,6 +47,5 @@ class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClie
espbt::ESPBTUUID descr_uuid_;
};
} // namespace ble_client
} // namespace esphome
} // namespace esphome::ble_client
#endif

View File

@@ -4,8 +4,7 @@
#ifdef USE_ESP32
namespace esphome {
namespace ble_client {
namespace esphome::ble_client {
static const char *const TAG = "ble_switch";
@@ -31,6 +30,5 @@ void BLEClientSwitch::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_i
void BLEClientSwitch::dump_config() { LOG_SWITCH("", "BLE Client Switch", this); }
} // namespace ble_client
} // namespace esphome
} // namespace esphome::ble_client
#endif

View File

@@ -8,8 +8,7 @@
#ifdef USE_ESP32
#include <esp_gattc_api.h>
namespace esphome {
namespace ble_client {
namespace esphome::ble_client {
namespace espbt = esphome::esp32_ble_tracker;
@@ -24,6 +23,5 @@ class BLEClientSwitch : public switch_::Switch, public Component, public BLEClie
void write_state(bool state) override;
};
} // namespace ble_client
} // namespace esphome
} // namespace esphome::ble_client
#endif

View File

@@ -5,8 +5,7 @@
#ifdef USE_ESP32
namespace esphome {
namespace ble_client {
namespace esphome::ble_client {
class BLETextSensorNotifyTrigger : public Trigger<std::string>, public BLETextSensor {
public:
@@ -33,7 +32,6 @@ class BLETextSensorNotifyTrigger : public Trigger<std::string>, public BLETextSe
BLETextSensor *sensor_;
};
} // namespace ble_client
} // namespace esphome
} // namespace esphome::ble_client
#endif

View File

@@ -7,8 +7,7 @@
#ifdef USE_ESP32
namespace esphome {
namespace ble_client {
namespace esphome::ble_client {
static const char *const TAG = "ble_text_sensor";
@@ -28,7 +27,7 @@ void BLETextSensor::dump_config() {
" Characteristic UUID: %s\n"
" Descriptor UUID : %s\n"
" Notifications : %s",
this->parent()->address_str().c_str(), this->service_uuid_.to_string().c_str(),
this->parent()->address_str(), this->service_uuid_.to_string().c_str(),
this->char_uuid_.to_string().c_str(), this->descr_uuid_.to_string().c_str(), YESNO(this->notify_));
LOG_UPDATE_INTERVAL(this);
}
@@ -138,6 +137,5 @@ void BLETextSensor::update() {
}
}
} // namespace ble_client
} // namespace esphome
} // namespace esphome::ble_client
#endif

View File

@@ -8,8 +8,7 @@
#ifdef USE_ESP32
#include <esp_gattc_api.h>
namespace esphome {
namespace ble_client {
namespace esphome::ble_client {
namespace espbt = esphome::esp32_ble_tracker;
@@ -40,6 +39,5 @@ class BLETextSensor : public text_sensor::TextSensor, public PollingComponent, p
espbt::ESPBTUUID descr_uuid_;
};
} // namespace ble_client
} // namespace esphome
} // namespace esphome::ble_client
#endif

View File

@@ -87,17 +87,21 @@ void BLENUS::setup() {
global_ble_nus = this;
#ifdef USE_LOGGER
if (logger::global_logger != nullptr && this->expose_log_) {
logger::global_logger->add_on_log_callback(
[this](int level, const char *tag, const char *message, size_t message_len) {
this->write_array(reinterpret_cast<const uint8_t *>(message), message_len);
const char c = '\n';
this->write_array(reinterpret_cast<const uint8_t *>(&c), 1);
});
logger::global_logger->add_log_listener(this);
}
#endif
}
#ifdef USE_LOGGER
void BLENUS::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) {
(void) level;
(void) tag;
this->write_array(reinterpret_cast<const uint8_t *>(message), message_len);
const char c = '\n';
this->write_array(reinterpret_cast<const uint8_t *>(&c), 1);
}
#endif
void BLENUS::dump_config() {
ESP_LOGCONFIG(TAG, "ble nus:");
ESP_LOGCONFIG(TAG, " log: %s", YESNO(this->expose_log_));

View File

@@ -2,12 +2,20 @@
#ifdef USE_ZEPHYR
#include "esphome/core/defines.h"
#include "esphome/core/component.h"
#ifdef USE_LOGGER
#include "esphome/components/logger/logger.h"
#endif
#include <shell/shell_bt_nus.h>
#include <atomic>
namespace esphome::ble_nus {
class BLENUS : public Component {
class BLENUS : public Component
#ifdef USE_LOGGER
,
public logger::LogListener
#endif
{
enum TxStatus {
TX_DISABLED,
TX_ENABLED,
@@ -20,6 +28,9 @@ class BLENUS : public Component {
void loop() override;
size_t write_array(const uint8_t *data, size_t len);
void set_expose_log(bool expose_log) { this->expose_log_ = expose_log; }
#ifdef USE_LOGGER
void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override;
#endif
protected:
static void send_enabled_callback(bt_nus_send_status status);

View File

@@ -196,8 +196,8 @@ void BluetoothConnection::send_service_for_discovery_() {
if (service_status != ESP_GATT_OK || service_count == 0) {
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service %s, status=%d, service_count=%d, offset=%d",
this->connection_index_, this->address_str().c_str(),
service_status != ESP_GATT_OK ? "error" : "missing", service_status, service_count, this->send_service_);
this->connection_index_, this->address_str(), service_status != ESP_GATT_OK ? "error" : "missing",
service_status, service_count, this->send_service_);
this->send_service_ = DONE_SENDING_SERVICES;
return;
}
@@ -312,13 +312,13 @@ void BluetoothConnection::send_service_for_discovery_() {
if (resp.services.size() > 1) {
resp.services.pop_back();
ESP_LOGD(TAG, "[%d] [%s] Service %d would exceed limit (current: %d + service: %d > %d), sending current batch",
this->connection_index_, this->address_str().c_str(), this->send_service_, current_size, service_size,
this->connection_index_, this->address_str(), this->send_service_, current_size, service_size,
MAX_PACKET_SIZE);
// Don't increment send_service_ - we'll retry this service in next batch
} else {
// This single service is too large, but we have to send it anyway
ESP_LOGV(TAG, "[%d] [%s] Service %d is too large (%d bytes) but sending anyway", this->connection_index_,
this->address_str().c_str(), this->send_service_, service_size);
this->address_str(), this->send_service_, service_size);
// Increment so we don't get stuck
this->send_service_++;
}
@@ -337,21 +337,20 @@ void BluetoothConnection::send_service_for_discovery_() {
}
void BluetoothConnection::log_connection_error_(const char *operation, esp_gatt_status_t status) {
ESP_LOGE(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str().c_str(), operation,
status);
ESP_LOGE(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str(), operation, status);
}
void BluetoothConnection::log_connection_warning_(const char *operation, esp_err_t err) {
ESP_LOGW(TAG, "[%d] [%s] %s failed, err=%d", this->connection_index_, this->address_str().c_str(), operation, err);
ESP_LOGW(TAG, "[%d] [%s] %s failed, err=%d", this->connection_index_, this->address_str(), operation, err);
}
void BluetoothConnection::log_gatt_not_connected_(const char *action, const char *type) {
ESP_LOGW(TAG, "[%d] [%s] Cannot %s GATT %s, not connected.", this->connection_index_, this->address_str().c_str(),
action, type);
ESP_LOGW(TAG, "[%d] [%s] Cannot %s GATT %s, not connected.", this->connection_index_, this->address_str(), action,
type);
}
void BluetoothConnection::log_gatt_operation_error_(const char *operation, uint16_t handle, esp_gatt_status_t status) {
ESP_LOGW(TAG, "[%d] [%s] Error %s for handle 0x%2X, status=%d", this->connection_index_, this->address_str().c_str(),
ESP_LOGW(TAG, "[%d] [%s] Error %s for handle 0x%2X, status=%d", this->connection_index_, this->address_str(),
operation, handle, status);
}
@@ -372,14 +371,14 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
case ESP_GATTC_DISCONNECT_EVT: {
// Don't reset connection yet - wait for CLOSE_EVT to ensure controller has freed resources
// This prevents race condition where we mark slot as free before controller cleanup is complete
ESP_LOGD(TAG, "[%d] [%s] Disconnect, reason=0x%02x", this->connection_index_, this->address_str_.c_str(),
ESP_LOGD(TAG, "[%d] [%s] Disconnect, reason=0x%02x", this->connection_index_, this->address_str_,
param->disconnect.reason);
// Send disconnection notification but don't free the slot yet
this->proxy_->send_device_connection(this->address_, false, 0, param->disconnect.reason);
break;
}
case ESP_GATTC_CLOSE_EVT: {
ESP_LOGD(TAG, "[%d] [%s] Close, reason=0x%02x, freeing slot", this->connection_index_, this->address_str_.c_str(),
ESP_LOGD(TAG, "[%d] [%s] Close, reason=0x%02x, freeing slot", this->connection_index_, this->address_str_,
param->close.reason);
// Now the GATT connection is fully closed and controller resources are freed
// Safe to mark the connection slot as available
@@ -463,7 +462,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
break;
}
case ESP_GATTC_NOTIFY_EVT: {
ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_NOTIFY_EVT: handle=0x%2X", this->connection_index_, this->address_str_.c_str(),
ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_NOTIFY_EVT: handle=0x%2X", this->connection_index_, this->address_str_,
param->notify.handle);
api::BluetoothGATTNotifyDataResponse resp;
resp.address = this->address_;
@@ -502,8 +501,7 @@ esp_err_t BluetoothConnection::read_characteristic(uint16_t handle) {
return ESP_GATT_NOT_CONNECTED;
}
ESP_LOGV(TAG, "[%d] [%s] Reading GATT characteristic handle %d", this->connection_index_, this->address_str_.c_str(),
handle);
ESP_LOGV(TAG, "[%d] [%s] Reading GATT characteristic handle %d", this->connection_index_, this->address_str_, handle);
esp_err_t err = esp_ble_gattc_read_char(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE);
return this->check_and_log_error_("esp_ble_gattc_read_char", err);
@@ -515,8 +513,7 @@ esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const uint8
this->log_gatt_not_connected_("write", "characteristic");
return ESP_GATT_NOT_CONNECTED;
}
ESP_LOGV(TAG, "[%d] [%s] Writing GATT characteristic handle %d", this->connection_index_, this->address_str_.c_str(),
handle);
ESP_LOGV(TAG, "[%d] [%s] Writing GATT characteristic handle %d", this->connection_index_, this->address_str_, handle);
// ESP-IDF's API requires a non-const uint8_t* but it doesn't modify the data
// The BTC layer immediately copies the data to its own buffer (see btc_gattc.c)
@@ -532,8 +529,7 @@ esp_err_t BluetoothConnection::read_descriptor(uint16_t handle) {
this->log_gatt_not_connected_("read", "descriptor");
return ESP_GATT_NOT_CONNECTED;
}
ESP_LOGV(TAG, "[%d] [%s] Reading GATT descriptor handle %d", this->connection_index_, this->address_str_.c_str(),
handle);
ESP_LOGV(TAG, "[%d] [%s] Reading GATT descriptor handle %d", this->connection_index_, this->address_str_, handle);
esp_err_t err = esp_ble_gattc_read_char_descr(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE);
return this->check_and_log_error_("esp_ble_gattc_read_char_descr", err);
@@ -544,8 +540,7 @@ esp_err_t BluetoothConnection::write_descriptor(uint16_t handle, const uint8_t *
this->log_gatt_not_connected_("write", "descriptor");
return ESP_GATT_NOT_CONNECTED;
}
ESP_LOGV(TAG, "[%d] [%s] Writing GATT descriptor handle %d", this->connection_index_, this->address_str_.c_str(),
handle);
ESP_LOGV(TAG, "[%d] [%s] Writing GATT descriptor handle %d", this->connection_index_, this->address_str_, handle);
// ESP-IDF's API requires a non-const uint8_t* but it doesn't modify the data
// The BTC layer immediately copies the data to its own buffer (see btc_gattc.c)
@@ -564,13 +559,13 @@ esp_err_t BluetoothConnection::notify_characteristic(uint16_t handle, bool enabl
if (enable) {
ESP_LOGV(TAG, "[%d] [%s] Registering for GATT characteristic notifications handle %d", this->connection_index_,
this->address_str_.c_str(), handle);
this->address_str_, handle);
esp_err_t err = esp_ble_gattc_register_for_notify(this->gattc_if_, this->remote_bda_, handle);
return this->check_and_log_error_("esp_ble_gattc_register_for_notify", err);
}
ESP_LOGV(TAG, "[%d] [%s] Unregistering for GATT characteristic notifications handle %d", this->connection_index_,
this->address_str_.c_str(), handle);
this->address_str_, handle);
esp_err_t err = esp_ble_gattc_unregister_for_notify(this->gattc_if_, this->remote_bda_, handle);
return this->check_and_log_error_("esp_ble_gattc_unregister_for_notify", err);
}

View File

@@ -27,11 +27,13 @@ void BluetoothProxy::setup() {
// Capture the configured scan mode from YAML before any API changes
this->configured_scan_active_ = this->parent_->get_scan_active();
this->parent_->add_scanner_state_callback([this](esp32_ble_tracker::ScannerState state) {
if (this->api_connection_ != nullptr) {
this->send_bluetooth_scanner_state_(state);
}
});
this->parent_->add_scanner_state_listener(this);
}
void BluetoothProxy::on_scanner_state(esp32_ble_tracker::ScannerState state) {
if (this->api_connection_ != nullptr) {
this->send_bluetooth_scanner_state_(state);
}
}
void BluetoothProxy::send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerState state) {
@@ -47,12 +49,11 @@ void BluetoothProxy::send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerSta
void BluetoothProxy::log_connection_request_ignored_(BluetoothConnection *connection, espbt::ClientState state) {
ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, state: %s", connection->get_connection_index(),
connection->address_str().c_str(), espbt::client_state_to_string(state));
connection->address_str(), espbt::client_state_to_string(state));
}
void BluetoothProxy::log_connection_info_(BluetoothConnection *connection, const char *message) {
ESP_LOGI(TAG, "[%d] [%s] Connecting %s", connection->get_connection_index(), connection->address_str().c_str(),
message);
ESP_LOGI(TAG, "[%d] [%s] Connecting %s", connection->get_connection_index(), connection->address_str(), message);
}
void BluetoothProxy::log_not_connected_gatt_(const char *action, const char *type) {
@@ -186,7 +187,7 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
}
if (!msg.has_address_type) {
ESP_LOGE(TAG, "[%d] [%s] Missing address type in connect request", connection->get_connection_index(),
connection->address_str().c_str());
connection->address_str());
this->send_device_connection(msg.address, false);
return;
}
@@ -199,7 +200,7 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
} else if (connection->state() == espbt::ClientState::CONNECTING) {
if (connection->disconnect_pending()) {
ESP_LOGW(TAG, "[%d] [%s] Connection request while pending disconnect, cancelling pending disconnect",
connection->get_connection_index(), connection->address_str().c_str());
connection->get_connection_index(), connection->address_str());
connection->cancel_pending_disconnect();
return;
}
@@ -339,7 +340,7 @@ void BluetoothProxy::bluetooth_gatt_send_services(const api::BluetoothGATTGetSer
return;
}
if (!connection->service_count_) {
ESP_LOGW(TAG, "[%d] [%s] No GATT services found", connection->connection_index_, connection->address_str().c_str());
ESP_LOGW(TAG, "[%d] [%s] No GATT services found", connection->connection_index_, connection->address_str());
this->send_gatt_services_done(msg.address);
return;
}

View File

@@ -52,7 +52,9 @@ enum BluetoothProxySubscriptionFlag : uint32_t {
SUBSCRIPTION_RAW_ADVERTISEMENTS = 1 << 0,
};
class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, public Component {
class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener,
public esp32_ble_tracker::BLEScannerStateListener,
public Component {
friend class BluetoothConnection; // Allow connection to update connections_free_response_
public:
BluetoothProxy();
@@ -108,6 +110,9 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, publ
void set_active(bool active) { this->active_ = active; }
bool has_active() { return this->active_; }
/// BLEScannerStateListener interface
void on_scanner_state(esp32_ble_tracker::ScannerState state) override;
uint32_t get_legacy_version() const {
if (this->active_) {
return LEGACY_ACTIVE_CONNECTIONS_VERSION;
@@ -130,11 +135,13 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, publ
return flags;
}
std::string get_bluetooth_mac_address_pretty() {
void get_bluetooth_mac_address_pretty(std::span<char, 18> output) {
const uint8_t *mac = esp_bt_dev_get_address();
char buf[18];
format_mac_addr_upper(mac, buf);
return std::string(buf);
if (mac != nullptr) {
format_mac_addr_upper(mac, output.data());
} else {
output[0] = '\0';
}
}
protected:

View File

@@ -0,0 +1 @@
CODEOWNERS = ["@abmantis"]

View File

@@ -0,0 +1,198 @@
#include "bm8563.h"
#include "esphome/core/log.h"
namespace esphome::bm8563 {
static const char *const TAG = "bm8563";
static constexpr uint8_t CONTROL_STATUS_1_REG = 0x00;
static constexpr uint8_t CONTROL_STATUS_2_REG = 0x01;
static constexpr uint8_t TIME_FIRST_REG = 0x02; // Time uses reg 2, 3, 4
static constexpr uint8_t DATE_FIRST_REG = 0x05; // Date uses reg 5, 6, 7, 8
static constexpr uint8_t TIMER_CONTROL_REG = 0x0E;
static constexpr uint8_t TIMER_VALUE_REG = 0x0F;
static constexpr uint8_t CLOCK_1_HZ = 0x82;
static constexpr uint8_t CLOCK_1_60_HZ = 0x83;
// Maximum duration: 255 minutes (at 1/60 Hz) = 15300 seconds
static constexpr uint32_t MAX_TIMER_DURATION_S = 255 * 60;
void BM8563::setup() {
if (!this->write_byte_16(CONTROL_STATUS_1_REG, 0)) {
this->mark_failed();
return;
}
}
void BM8563::update() { this->read_time(); }
void BM8563::dump_config() {
ESP_LOGCONFIG(TAG, "BM8563:");
LOG_I2C_DEVICE(this);
if (this->is_failed()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
}
}
void BM8563::start_timer(uint32_t duration_s) {
this->clear_irq_();
this->set_timer_irq_(duration_s);
}
void BM8563::write_time() {
auto now = time::RealTimeClock::utcnow();
if (!now.is_valid()) {
ESP_LOGE(TAG, "Invalid system time, not syncing to RTC.");
return;
}
ESP_LOGD(TAG, "Writing time: %i-%i-%i %i, %i:%i:%i", now.year, now.month, now.day_of_month, now.day_of_week, now.hour,
now.minute, now.second);
this->set_time_(now);
this->set_date_(now);
}
void BM8563::read_time() {
ESPTime rtc_time;
this->get_time_(rtc_time);
this->get_date_(rtc_time);
rtc_time.day_of_year = 1; // unused by recalc_timestamp_utc, but needs to be valid
ESP_LOGD(TAG, "Read time: %i-%i-%i %i, %i:%i:%i", rtc_time.year, rtc_time.month, rtc_time.day_of_month,
rtc_time.day_of_week, rtc_time.hour, rtc_time.minute, rtc_time.second);
rtc_time.recalc_timestamp_utc(false);
if (!rtc_time.is_valid()) {
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
return;
}
time::RealTimeClock::synchronize_epoch_(rtc_time.timestamp);
}
uint8_t BM8563::bcd2_to_byte_(uint8_t value) {
const uint8_t tmp = ((value & 0xF0) >> 0x4) * 10;
return tmp + (value & 0x0F);
}
uint8_t BM8563::byte_to_bcd2_(uint8_t value) {
const uint8_t bcdhigh = value / 10;
value -= bcdhigh * 10;
return (bcdhigh << 4) | value;
}
void BM8563::get_time_(ESPTime &time) {
uint8_t buf[3] = {0};
this->read_register(TIME_FIRST_REG, buf, 3);
time.second = this->bcd2_to_byte_(buf[0] & 0x7f);
time.minute = this->bcd2_to_byte_(buf[1] & 0x7f);
time.hour = this->bcd2_to_byte_(buf[2] & 0x3f);
}
void BM8563::set_time_(const ESPTime &time) {
uint8_t buf[3] = {this->byte_to_bcd2_(time.second), this->byte_to_bcd2_(time.minute), this->byte_to_bcd2_(time.hour)};
this->write_register_(TIME_FIRST_REG, buf, 3);
}
void BM8563::get_date_(ESPTime &time) {
uint8_t buf[4] = {0};
this->read_register(DATE_FIRST_REG, buf, sizeof(buf));
time.day_of_month = this->bcd2_to_byte_(buf[0] & 0x3f);
time.day_of_week = this->bcd2_to_byte_(buf[1] & 0x07);
time.month = this->bcd2_to_byte_(buf[2] & 0x1f);
uint8_t year_byte = this->bcd2_to_byte_(buf[3] & 0xff);
if (buf[2] & 0x80) {
time.year = 1900 + year_byte;
} else {
time.year = 2000 + year_byte;
}
}
void BM8563::set_date_(const ESPTime &time) {
uint8_t buf[4] = {
this->byte_to_bcd2_(time.day_of_month),
this->byte_to_bcd2_(time.day_of_week),
this->byte_to_bcd2_(time.month),
this->byte_to_bcd2_(time.year % 100),
};
if (time.year < 2000) {
buf[2] = buf[2] | 0x80;
}
this->write_register_(DATE_FIRST_REG, buf, 4);
}
void BM8563::write_byte_(uint8_t reg, uint8_t value) {
if (!this->write_byte(reg, value)) {
ESP_LOGE(TAG, "Failed to write byte 0x%02X with value 0x%02X", reg, value);
}
}
void BM8563::write_register_(uint8_t reg, const uint8_t *data, size_t len) {
if (auto error = this->write_register(reg, data, len); error != i2c::ErrorCode::NO_ERROR) {
ESP_LOGE(TAG, "Failed to write register 0x%02X with %zu bytes", reg, len);
}
}
optional<uint8_t> BM8563::read_register_(uint8_t reg) {
uint8_t data;
if (auto error = this->read_register(reg, &data, 1); error != i2c::ErrorCode::NO_ERROR) {
ESP_LOGE(TAG, "Failed to read register 0x%02X", reg);
return {};
}
return data;
}
void BM8563::set_timer_irq_(uint32_t duration_s) {
ESP_LOGI(TAG, "Timer Duration: %u s", duration_s);
if (duration_s > MAX_TIMER_DURATION_S) {
ESP_LOGW(TAG, "Timer duration %u s exceeds maximum %u s", duration_s, MAX_TIMER_DURATION_S);
return;
}
if (duration_s > 255) {
uint8_t duration_minutes = duration_s / 60;
this->write_byte_(TIMER_VALUE_REG, duration_minutes);
this->write_byte_(TIMER_CONTROL_REG, CLOCK_1_60_HZ);
} else {
this->write_byte_(TIMER_VALUE_REG, duration_s);
this->write_byte_(TIMER_CONTROL_REG, CLOCK_1_HZ);
}
auto maybe_ctrl_status_2 = this->read_register_(CONTROL_STATUS_2_REG);
if (!maybe_ctrl_status_2.has_value()) {
ESP_LOGE(TAG, "Failed to read CONTROL_STATUS_2_REG");
return;
}
uint8_t ctrl_status_2_reg_value = maybe_ctrl_status_2.value();
ctrl_status_2_reg_value |= (1 << 0);
ctrl_status_2_reg_value &= ~(1 << 7);
this->write_byte_(CONTROL_STATUS_2_REG, ctrl_status_2_reg_value);
}
void BM8563::clear_irq_() {
auto maybe_data = this->read_register_(CONTROL_STATUS_2_REG);
if (!maybe_data.has_value()) {
ESP_LOGE(TAG, "Failed to read CONTROL_STATUS_2_REG");
return;
}
uint8_t data = maybe_data.value();
this->write_byte_(CONTROL_STATUS_2_REG, data & 0xf3);
}
void BM8563::disable_irq_() {
this->clear_irq_();
auto maybe_data = this->read_register_(CONTROL_STATUS_2_REG);
if (!maybe_data.has_value()) {
ESP_LOGE(TAG, "Failed to read CONTROL_STATUS_2_REG");
return;
}
uint8_t data = maybe_data.value();
this->write_byte_(CONTROL_STATUS_2_REG, data & 0xfc);
}
} // namespace esphome::bm8563

View File

@@ -0,0 +1,57 @@
#pragma once
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/time/real_time_clock.h"
namespace esphome::bm8563 {
class BM8563 : public time::RealTimeClock, public i2c::I2CDevice {
public:
void setup() override;
void update() override;
void dump_config() override;
void write_time();
void read_time();
void start_timer(uint32_t duration_s);
private:
void get_time_(ESPTime &time);
void get_date_(ESPTime &time);
void set_time_(const ESPTime &time);
void set_date_(const ESPTime &time);
void set_timer_irq_(uint32_t duration_s);
void clear_irq_();
void disable_irq_();
void write_byte_(uint8_t reg, uint8_t value);
void write_register_(uint8_t reg, const uint8_t *data, size_t len);
optional<uint8_t> read_register_(uint8_t reg);
uint8_t bcd2_to_byte_(uint8_t value);
uint8_t byte_to_bcd2_(uint8_t value);
};
template<typename... Ts> class WriteAction : public Action<Ts...>, public Parented<BM8563> {
public:
void play(const Ts &...x) override { this->parent_->write_time(); }
};
template<typename... Ts> class ReadAction : public Action<Ts...>, public Parented<BM8563> {
public:
void play(const Ts &...x) override { this->parent_->read_time(); }
};
template<typename... Ts> class TimerAction : public Action<Ts...>, public Parented<BM8563> {
public:
TEMPLATABLE_VALUE(uint32_t, duration)
void play(const Ts &...x) override {
auto duration = this->duration_.value(x...);
this->parent_->start_timer(duration);
}
};
} // namespace esphome::bm8563

View File

@@ -0,0 +1,80 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components import i2c, time
import esphome.config_validation as cv
from esphome.const import CONF_DURATION, CONF_ID
DEPENDENCIES = ["i2c"]
I2C_ADDR = 0x51
bm8563_ns = cg.esphome_ns.namespace("bm8563")
BM8563 = bm8563_ns.class_("BM8563", time.RealTimeClock, i2c.I2CDevice)
WriteAction = bm8563_ns.class_("WriteAction", automation.Action)
ReadAction = bm8563_ns.class_("ReadAction", automation.Action)
TimerAction = bm8563_ns.class_("TimerAction", automation.Action)
CONFIG_SCHEMA = (
time.TIME_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(BM8563),
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(i2c.i2c_device_schema(I2C_ADDR))
)
@automation.register_action(
"bm8563.write_time",
WriteAction,
automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(BM8563),
}
),
)
async def bm8563_write_time_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
@automation.register_action(
"bm8563.start_timer",
TimerAction,
cv.Schema(
{
cv.GenerateID(): cv.use_id(BM8563),
cv.Required(CONF_DURATION): cv.templatable(cv.positive_time_period_seconds),
}
),
)
async def bm8563_start_timer_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
template_ = await cg.templatable(config[CONF_DURATION], args, cg.uint32)
cg.add(var.set_duration(template_))
return var
@automation.register_action(
"bm8563.read_time",
ReadAction,
automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(BM8563),
}
),
)
async def bm8563_read_time_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
await time.register_time(var, config)

View File

@@ -100,18 +100,18 @@ void BME280Component::setup() {
if (!this->read_byte(BME280_REGISTER_CHIPID, &chip_id)) {
this->error_code_ = COMMUNICATION_FAILED;
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
if (chip_id != 0x60) {
this->error_code_ = WRONG_CHIP_ID;
this->mark_failed(BME280_ERROR_WRONG_CHIP_ID);
this->mark_failed(LOG_STR(BME280_ERROR_WRONG_CHIP_ID));
return;
}
// Send a soft reset.
if (!this->write_byte(BME280_REGISTER_RESET, BME280_SOFT_RESET)) {
this->mark_failed("Reset failed");
this->mark_failed(LOG_STR("Reset failed"));
return;
}
// Wait until the NVM data has finished loading.
@@ -120,12 +120,12 @@ void BME280Component::setup() {
do { // NOLINT
delay(2);
if (!this->read_byte(BME280_REGISTER_STATUS, &status)) {
this->mark_failed("Error reading status register");
this->mark_failed(LOG_STR("Error reading status register"));
return;
}
} while ((status & BME280_STATUS_IM_UPDATE) && (--retry));
if (status & BME280_STATUS_IM_UPDATE) {
this->mark_failed("Timeout loading NVM");
this->mark_failed(LOG_STR("Timeout loading NVM"));
return;
}
@@ -153,26 +153,26 @@ void BME280Component::setup() {
uint8_t humid_control_val = 0;
if (!this->read_byte(BME280_REGISTER_CONTROLHUMID, &humid_control_val)) {
this->mark_failed("Read humidity control");
this->mark_failed(LOG_STR("Read humidity control"));
return;
}
humid_control_val &= ~0b00000111;
humid_control_val |= this->humidity_oversampling_ & 0b111;
if (!this->write_byte(BME280_REGISTER_CONTROLHUMID, humid_control_val)) {
this->mark_failed("Write humidity control");
this->mark_failed(LOG_STR("Write humidity control"));
return;
}
uint8_t config_register = 0;
if (!this->read_byte(BME280_REGISTER_CONFIG, &config_register)) {
this->mark_failed("Read config");
this->mark_failed(LOG_STR("Read config"));
return;
}
config_register &= ~0b11111100;
config_register |= 0b101 << 5; // 1000 ms standby time
config_register |= (this->iir_filter_ & 0b111) << 2;
if (!this->write_byte(BME280_REGISTER_CONFIG, config_register)) {
this->mark_failed("Write config");
this->mark_failed(LOG_STR("Write config"));
return;
}
}

View File

@@ -69,7 +69,7 @@ CONFIG_SCHEMA = cv.All(
cv.only_on_esp8266,
cv.All(
cv.only_on_esp32,
esp32.only_on_variant(supported=[esp32.const.VARIANT_ESP32]),
esp32.only_on_variant(supported=[esp32.VARIANT_ESP32]),
),
),
)

View File

@@ -70,6 +70,9 @@ void BME68xBSEC2Component::dump_config() {
if (this->is_failed()) {
ESP_LOGE(TAG, "Communication failed (BSEC2 status: %d, BME68X status: %d)", this->bsec_status_,
this->bme68x_status_);
if (this->bsec_status_ == BSEC_I_SU_SUBSCRIBEDOUTPUTGATES) {
ESP_LOGE(TAG, "No sensors, add at least one sensor to the config");
}
}
if (this->algorithm_output_ != ALGORITHM_OUTPUT_IAQ) {

View File

@@ -11,6 +11,7 @@ CODEOWNERS = ["@neffs", "@kbx81"]
AUTO_LOAD = ["bme68x_bsec2"]
DEPENDENCIES = ["i2c"]
MULTI_CONF = True
bme68x_bsec2_i2c_ns = cg.esphome_ns.namespace("bme68x_bsec2_i2c")
BME68xBSEC2I2CComponent = bme68x_bsec2_i2c_ns.class_(

View File

@@ -65,23 +65,23 @@ void BMP280Component::setup() {
// https://community.st.com/t5/stm32-mcus-products/issue-with-reading-bmp280-chip-id-using-spi/td-p/691855
if (!this->bmp_read_byte(0xD0, &chip_id)) {
this->error_code_ = COMMUNICATION_FAILED;
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
if (!this->bmp_read_byte(0xD0, &chip_id)) {
this->error_code_ = COMMUNICATION_FAILED;
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
if (chip_id != 0x58) {
this->error_code_ = WRONG_CHIP_ID;
this->mark_failed(BMP280_ERROR_WRONG_CHIP_ID);
this->mark_failed(LOG_STR(BMP280_ERROR_WRONG_CHIP_ID));
return;
}
// Send a soft reset.
if (!this->bmp_write_byte(BMP280_REGISTER_RESET, BMP280_SOFT_RESET)) {
this->mark_failed("Reset failed");
this->mark_failed(LOG_STR("Reset failed"));
return;
}
// Wait until the NVM data has finished loading.
@@ -90,12 +90,12 @@ void BMP280Component::setup() {
do {
delay(2);
if (!this->bmp_read_byte(BMP280_REGISTER_STATUS, &status)) {
this->mark_failed("Error reading status register");
this->mark_failed(LOG_STR("Error reading status register"));
return;
}
} while ((status & BMP280_STATUS_IM_UPDATE) && (--retry));
if (status & BMP280_STATUS_IM_UPDATE) {
this->mark_failed("Timeout loading NVM");
this->mark_failed(LOG_STR("Timeout loading NVM"));
return;
}
@@ -116,14 +116,14 @@ void BMP280Component::setup() {
uint8_t config_register = 0;
if (!this->bmp_read_byte(BMP280_REGISTER_CONFIG, &config_register)) {
this->mark_failed("Read config");
this->mark_failed(LOG_STR("Read config"));
return;
}
config_register &= ~0b11111100;
config_register |= 0b000 << 5; // 0.5 ms standby time
config_register |= (this->iir_filter_ & 0b111) << 2;
if (!this->bmp_write_byte(BMP280_REGISTER_CONFIG, config_register)) {
this->mark_failed("Write config");
this->mark_failed(LOG_STR("Write config"));
return;
}
}

View File

@@ -4,8 +4,7 @@
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
namespace esphome {
namespace button {
namespace esphome::button {
template<typename... Ts> class PressAction : public Action<Ts...> {
public:
@@ -24,5 +23,4 @@ class ButtonPressTrigger : public Trigger<> {
}
};
} // namespace button
} // namespace esphome
} // namespace esphome::button

View File

@@ -1,8 +1,7 @@
#include "button.h"
#include "esphome/core/log.h"
namespace esphome {
namespace button {
namespace esphome::button {
static const char *const TAG = "button";
@@ -26,5 +25,4 @@ void Button::press() {
}
void Button::add_on_press_callback(std::function<void()> &&callback) { this->press_callback_.add(std::move(callback)); }
} // namespace button
} // namespace esphome
} // namespace esphome::button

View File

@@ -4,8 +4,7 @@
#include "esphome/core/entity_base.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace button {
namespace esphome::button {
class Button;
void log_button(const char *tag, const char *prefix, const char *type, Button *obj);
@@ -45,5 +44,4 @@ class Button : public EntityBase, public EntityBase_DeviceClass {
CallbackManager<void()> press_callback_{};
};
} // namespace button
} // namespace esphome
} // namespace esphome::button

View File

@@ -8,7 +8,7 @@ Camera *Camera::global_camera = nullptr;
Camera::Camera() {
if (global_camera != nullptr) {
this->status_set_error("Multiple cameras are configured, but only one is supported.");
this->status_set_error(LOG_STR("Multiple cameras are configured, but only one is supported."));
this->mark_failed();
return;
}

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