1
0
mirror of https://github.com/esphome/esphome.git synced 2026-02-11 18:21:52 +00:00

Compare commits

..

204 Commits

Author SHA1 Message Date
J. Nick Koston
36b5f0aaf0 [api] Eliminate heap allocations in Home Assistant state callbacks 2025-12-30 10:17:17 -10:00
Jonathan Swoboda
96c47f3b4d Merge branch 'release' into dev 2025-12-30 09:31:44 -05:00
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
Samuel Sieb
a615b28ecf [bme68x_bsec2] add id: to allow extending (#12649) 2025-12-29 23:22:36 -08:00
bakroistvan
468bd7b04f [dallas_temp] higher precision for logged temperature (#12695) 2025-12-29 22:53:28 -08:00
Jonathan Swoboda
4c16afeacb [esp32] Add IDF framework source for Arduino builds (#12731)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2025-12-29 22:25:26 -05:00
J. Nick Koston
d86c05bfe6 [esp32] Breaking Change: Change default framework to ESP-IDF (#12746)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-30 03:23:41 +00:00
J. Nick Koston
63464a13c3 [core] Fix incremental build failures when adding components on ESP32-Arduino (#12745) 2025-12-29 16:57:22 -10:00
Clyde Stubbs
20e43398fa [cli] Report program path on host (#12743) 2025-12-30 13:21:30 +11:00
hsand
2e7cdad532 [pvvx_mithermometer] fix displaying negative numbers (#12735) 2025-12-29 13:58:38 -08:00
dependabot[bot]
636cccc6a3 Bump aioesphomeapi from 43.9.0 to 43.9.1 (#12724)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-29 09:55:26 -10:00
Thomas Rupprecht
93e2a1bd1a [tests] improve mipi_spi variable naming (#12716) 2025-12-29 14:21:07 -05:00
Thomas Rupprecht
dd3beb5841 [tests] fix typo mipi tests (#12715) 2025-12-29 14:20:38 -05:00
Thomas Rupprecht
97af01c5ed [usb_host] sort esp32 variants (#12720) 2025-12-29 14:19:36 -05:00
J. Nick Koston
7e362cdafc [ota] Use precision format specifier for auth logging (#12706)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-29 08:43:54 -10:00
Jonathan Swoboda
890d531cea [esp32] Bump to ESP-IDF 5.5.2, Arduino 3.3.5, platform 55.3.35 (#12681)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-12-29 11:35:54 -05:00
Swaptor
6a6c6b648f [internal_temperature] Add ESP32-C5 support (#12713)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-12-29 11:32:32 -05:00
dependabot[bot]
d0673122a8 Bump aioesphomeapi from 43.8.0 to 43.9.0 (#12702) 2025-12-28 18:15:06 -10:00
dependabot[bot]
5cbef3ef95 Bump aioesphomeapi from 43.7.0 to 43.8.0 (#12701) 2025-12-29 03:15:40 +00:00
dependabot[bot]
a1e0121330 Bump bleak from 2.0.0 to 2.1.0 (#12700)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-28 16:48:20 -10:00
dependabot[bot]
eb050ff13e Bump aioesphomeapi from 43.6.0 to 43.7.0 (#12699)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-28 16:48:08 -10:00
Jonathan Swoboda
45e61f100c [core] Replace USE_ESP_IDF with USE_ESP32 across components (#12673)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-12-27 11:59:55 -10:00
J. Nick Koston
5e99dd14ae [ethernet] Eliminate heap allocations in dump_config logging (#12665) 2025-12-27 08:36:35 -10:00
J. Nick Koston
a6097f4a0f [wifi] Eliminate heap allocations in dump_config logging (#12664)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-27 08:36:19 -10:00
J. Nick Koston
f243e609a5 [wifi] Use StringRef and std::span in WiFiConnectStateListener to avoid allocations (#12672) 2025-12-27 08:35:58 -10:00
J. Nick Koston
be0bf1e5b9 [lvgl] Fix lambdas in canvas actions called from outside LVGL context (#12671) 2025-12-27 08:35:36 -10:00
J. Nick Koston
a275f37135 [udp] Use stack buffer for listen address logging in dump_config (#12667) 2025-12-27 08:35:16 -10:00
J. Nick Koston
e9f2d75aab [core] Add format_hex_to helper for zero-allocation hex formatting (#12670) 2025-12-27 08:34:45 -10:00
J. Nick Koston
34067f8b15 [esp8266] Native OTA backend to reduce Arduino dependencies (#12675)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-27 08:29:15 -10:00
J. Nick Koston
bdc087148a [wifi_info] Reduce heap allocations in text sensor formatting (#12660) 2025-12-26 12:52:41 -10:00
J. Nick Koston
5a2e0612a8 [web_server] Use C++17 nested namespace syntax (#12663) 2025-12-26 08:44:34 -10:00
J. Nick Koston
f1fecd22e3 [web_server] Move HTTP header strings to flash on ESP8266 (#12668) 2025-12-26 08:44:17 -10:00
J. Nick Koston
0919017d49 [wifi] Avoid unnecessary string copy in failed connection logging (#12659) 2025-12-26 08:44:03 -10:00
J. Nick Koston
963f594c9e [text_sensor] Return state by const reference to avoid copies (#12661) 2025-12-26 07:58:46 -10:00
J. Nick Koston
4f70663658 [alarm_control_panel] Use C++17 nested namespace and remove unused include (#12662) 2025-12-26 07:57:33 -10:00
dependabot[bot]
958a35e262 Bump aioesphomeapi from 43.5.0 to 43.6.0 (#12644)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-24 14:17:52 -10:00
J. Nick Koston
0c566c6f00 [core] Deprecate get_object_id() and migrate remaining usages to get_object_id_to() (#12629) 2025-12-23 06:59:07 -10:00
Jonathan Swoboda
ba73289b28 Merge branch 'release' into dev 2025-12-23 11:17:15 -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
ffefa8929e [cc1101] Fix packet mode RSSI/LQI (#12630)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-23 09:05:48 -05:00
J. Nick Koston
7d5342bca5 [logger] Host: Use fwrite() with explicit length and remove platform branching (#12628) 2025-12-22 16:45:22 -10:00
J. Nick Koston
b4c92dd8cb [logger] Zephyr: Use k_str_out() with known length instead of printk() (#12619) 2025-12-22 14:29:47 -10:00
eoasmxd
1b31253287 Add Event Component to UART (#11765)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2025-12-22 12:19:48 -10:00
J. Nick Koston
af0d4d2c2c [web_server] Use stack buffers for value formatting to reduce flash usage (#12575) 2025-12-22 21:56:07 +00:00
J. Nick Koston
f238f93312 [core] Move comment to PROGMEM on ESP8266 (#12554) 2025-12-22 21:37:51 +00:00
J. Nick Koston
bdbe72b7f1 [web_server] Make internal JSON helper methods private (#12624) 2025-12-22 11:14:11 -10:00
J. Nick Koston
c8b531ac06 [safe_mode] Defer preference sync in clean_rtc to avoid blocking event loop (#12625) 2025-12-22 11:13:51 -10:00
Jonathan Swoboda
918bc4b74f [esp32] Remove remaining using_esp_idf checks (#12623)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-22 15:41:14 -05:00
Keith Burzinski
08c0f65f30 [sprinkler] Remove internal latching valve support (#12603) 2025-12-22 14:13:18 -05:00
Keith Burzinski
cd45fe0c3a [thermostat] Optimizations to reduce binary size (#12621) 2025-12-22 14:13:03 -05:00
Jonathan Swoboda
84b5d9b21c [core] Remove deprecated config options from before 2025 (#12622)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-22 14:00:12 -05:00
J. Nick Koston
6383fe4598 [core] Add zero-allocation object_id methods (#12578) 2025-12-22 07:56:33 -10:00
J. Nick Koston
265ad9d264 [esp32_camera] Reduce loop overhead and improve frame latency with wake_loop_threadsafe (#12601) 2025-12-22 07:55:28 -10:00
J. Nick Koston
1bdbc4cb85 [esp32_ble] Avoid string allocation when setting BLE device name (#12579) 2025-12-22 07:54:55 -10:00
J. Nick Koston
1756fc31b0 [api] Use union for iterators to reduce APIConnection size by ~16 bytes (#12563) 2025-12-22 07:54:17 -10:00
J. Nick Koston
74b075d3cf [codegen] Add static storage class to global variables for size optimization (#12616) 2025-12-22 07:03:17 -10:00
Clint Armstrong
52eb08f48f [thermostat] Enhance timer behavior for immediate response to duration changes (#12610) 2025-12-21 23:52:17 -06:00
J. Nick Koston
0d993691d4 [logger] RP2040: Use write() with known length instead of println() (#12615) 2025-12-21 17:59:30 -10:00
Douwe
39926909af [water_heater] (1/4) Implement API/Core/component for new water_heater component (#12498)
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+github@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-12-21 11:36:34 -10:00
J. Nick Koston
637e032528 [esp32_camera] Throttle frame logging to reduce overhead and improve throughput (#12586) 2025-12-21 09:04:43 -10:00
Anna Oake
d89eaf5bf6 [cc1101] Fix option defaults and move them to YAML (#12608) 2025-12-21 13:04:17 -05:00
J. Nick Koston
bf617c3279 [web_server] Replace str_sprintf with stack buffers (#12592) 2025-12-21 07:32:05 -10:00
J. Nick Koston
c70eab931e [api] Add zero-copy support for Home Assistant state response messages (#12585) 2025-12-21 07:31:54 -10:00
J. Nick Koston
a799ac6488 [syslog] Eliminate heap allocations in log path (#12589) 2025-12-21 07:10:27 -10:00
polyfloyd
5a36cea5ec Add nix files to gitignore (#12604) 2025-12-21 09:26:03 -05:00
J. Nick Koston
60756db06d [syslog] Use C++17 nested namespace syntax (#12594) 2025-12-21 02:47:37 -06:00
Keith Burzinski
2113858f89 [sprinkler] Squash a few bugs + minor optimization (#12436) 2025-12-21 02:45:24 -06:00
dependabot[bot]
e89fe9b945 Bump aioesphomeapi from 43.4.0 to 43.5.0 (#12599)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-20 23:59:48 +00:00
dependabot[bot]
f1362cd9fe Bump aioesphomeapi from 43.3.0 to 43.4.0 (#12597)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-20 22:37:10 +00:00
Frédéric Metrich
bf554a58ef [const] Add CONF_ON_DATA and consolidate definitions (#12595)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-20 12:17:09 -10:00
J. Nick Koston
644e806afd [zwave_proxy] Add missing USE_API guards for clang-tidy (#12590) 2025-12-20 10:40:43 -10:00
Leo Bergolth
6c2d255230 send NIL ("-") as timestamp if time source is not valid (#12588) 2025-12-20 10:04:59 -10:00
Stuart Parmenter
6f3bfc2060 [hub75] Bump esp-hub75 version to 0.1.7 (#12564) 2025-12-20 13:18:20 -05:00
J. Nick Koston
40eb898814 [api] Add zero-copy support for noise encryption key requests (#12405) 2025-12-20 06:47:30 -10:00
J. Nick Koston
64269334ce [text_sensor] Avoid string copies in callbacks by passing const ref (#12503)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-12-20 06:46:13 -10:00
Eduard Llull
121375ff39 [display_menu_base] Call on_value_ after updating the select (#12584) 2025-12-20 10:59:14 -05:00
J. Nick Koston
48cdf9e036 [tests] Fix race condition in alarm control panel state transitions test (#12581) 2025-12-20 10:47:29 -05:00
J. Nick Koston
3e313014e1 [core] Migrate entities to use lazy callbacks (#12580) 2025-12-19 19:04:21 -10:00
Martin Ebner
be6c1e4ec0 [sen5x][sgp4x] Move configuration keys from SEN5x and SGP4x to const.py (#12567)
Co-authored-by: Martin Ebner <martinebner@me.com>
2025-12-19 21:29:02 -05:00
Keith Burzinski
730bf206de [wifi] Fix for wifi_info when static IP is configured (#12576) 2025-12-19 21:25:16 -05:00
J. Nick Koston
c9fccdff25 [fan] Add zero-copy support for API preset mode commands (#12404)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-19 22:05:52 +00:00
J. Nick Koston
ada6c42f3f [alarm_control_panel] Remove redundant per-state callbacks (#12171)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-19 11:48:14 -10:00
J. Nick Koston
988b888c63 [ota] Replace std::function callbacks with listener interface (#12167) 2025-12-19 11:19:07 -10:00
J. Nick Koston
940afdbb12 [climate] Add zero-copy support for API custom fan mode and preset commands (#12402) 2025-12-19 11:18:50 -10:00
J. Nick Koston
81e91c2a8f [esp32_ble] Add stack-based UUID formatting to avoid heap allocations (#12510) 2025-12-19 11:18:32 -10:00
J. Nick Koston
ebc3d28ade [wifi] Replace optional with sentinel values to reduce RAM and clarify API (#12446) 2025-12-19 11:18:15 -10:00
Rene Guca
25cebedcfc [dht] Fix "Falling edge for bit 39 failed!" for Sonoff THS01 (#9225)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-12-19 15:42:39 -05:00
dependabot[bot]
98ed679b19 Bump ruff from 0.14.9 to 0.14.10 (#12572)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-12-19 19:22:56 +00:00
dependabot[bot]
59b38d79b4 Bump docker/setup-buildx-action from 3.11.1 to 3.12.0 in the docker-actions group (#12574)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-19 09:18:52 -10:00
dependabot[bot]
26c16f4ca2 Bump voluptuous from 0.15.2 to 0.16.0 (#12573)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-19 09:18:33 -10:00
Jas Strong
940e619481 [aqi, hm3301, pmsx003] Air Quality Index improvements (#12203)
Co-authored-by: jas <jas@asspa.in>
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-19 13:42:11 -05:00
Jonathan Swoboda
eaca81c3ab Merge branch 'release' into dev 2025-12-19 10:53:18 -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
pixelgrb
f962497db1 [mmc5603] enable AUTO_SR_en to compensate for temperature drift (#12556)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-12-18 23:13:36 -05:00
Keith Burzinski
7ae3a11d6b [esp32_ble, esp32_ble_tracker] Fix crash, error messages when ble.disable called during boot (#12560) 2025-12-18 19:42:47 -06:00
dependabot[bot]
1c50c2b672 Bump ruamel-yaml from 0.18.16 to 0.18.17 (#12555)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-18 11:19:19 -10:00
Jonathan Swoboda
41fd1762e9 [core] Deprecate custom_components folder (#12552)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-18 11:46:16 -05:00
J. Nick Koston
2cf6ed2af7 [socket] Refactor socket implementations for memory efficiency and code quality (#12550) 2025-12-18 09:07:35 -07:00
J. Nick Koston
b47b7d43fd [api] Remove unused force parameter from encode_message (#12551) 2025-12-18 09:06:16 -07:00
Jonathan Swoboda
663a4304e0 [libretiny] Fix millis() ambiguity on BK72XX (#12534)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-18 07:50:31 -05:00
Jonathan Swoboda
ca47bad90a [template.alarm_control_panel] Fix compile without binary_sensor (#12548)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-17 23:34:04 -05:00
J. Nick Koston
4f821a6d76 [wifi] Reduce scan logging to prevent blocking loop during connection (#12544) 2025-12-17 18:21:46 -10:00
J. Nick Koston
426305836d [esp32][libretiny] Avoid duplicate snprintf when syncing preferences (#12542) 2025-12-17 18:16:14 -10:00
dependabot[bot]
1b5af7d21d Bump github/codeql-action from 4.31.8 to 4.31.9 (#12524)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-17 15:22:19 -10:00
David Woodhouse
9de7df7b5b Add build info to image (#12425)
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>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-18 00:06:52 +00:00
Jonathan Swoboda
2b337aa306 [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-17 17:37:59 -05:00
Jonathan Swoboda
4ddaff4027 [esp32] Dynamically embed managed component server certificates (#12509)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-12-17 17:26:56 -05:00
J. Nick Koston
91c504061b [select] Eliminate string allocation in state callbacks (#12505)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-12-17 12:19:26 -10:00
Jonathan Swoboda
dc8f7abce2 [bme68x_bsec2_i2c] Add MULTI_CONF support for multiple sensors (#12535)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-17 17:07:42 -05:00
Jonathan Swoboda
3d673ac55e [ci] Check changed headers in clang-tidy when using --changed (#12540)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-17 11:13:18 -10:00
Jack Wilsdon
b02696edc0 [pm1006] Fix "never" update interval detection (#12529) 2025-12-18 07:40:31 +11:00
Anna Oake
f9720026d0 [cc1101] Fix default frequencies (#12539) 2025-12-17 14:19:18 -05:00
Jonathan Swoboda
d7b04a3d18 [nextion] Fix clang-tidy error on Zephyr for HTTPClient (#12538)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-17 13:59:49 -05:00
Jonathan Swoboda
0e71fa97a7 [spi] Add SPIInterface stub for clang-tidy on unsupported platforms (#12532)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-17 12:18:25 -05:00
J. Nick Koston
42e061c9ae [text] Avoid string copies in callbacks by passing const ref (#12504) 2025-12-17 12:00:19 -05:00
J. Nick Koston
94763ebdab [libretiny] Store preference keys as uint32_t, convert to string only at FlashDB boundary (#12500) 2025-12-17 11:59:40 -05:00
J. Nick Koston
f32bb618ac [esp32] Store preference keys as uint32_t, convert to string only at NVS boundary (#12494) 2025-12-17 11:59:35 -05:00
Edward Firmo
0707f383a6 [nextion] Use ESP-IDF for ESP32 Arduino (#9429)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-12-17 11:45:17 -05:00
Piotr Szulc
e91c6a79ea [deep_sleep] Deep sleep for BK72xx (#12267)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-12-17 11:45:05 -05:00
J. Nick Koston
63fc8b4e5a [core] Refactor str_snake_case and str_sanitize to use constexpr helpers (#12454) 2025-12-17 11:30:12 -05:00
J. Nick Koston
ab73ed76b8 [esphome] Improve OTA field alignment to save 4 bytes on 32-bit (#12461) 2025-12-17 11:29:58 -05:00
J. Nick Koston
bf6a03d1cf [factory_reset] Optimize memory by storing interval as uint16_t seconds (#12462) 2025-12-17 11:29:51 -05:00
J. Nick Koston
9928ab09cf [time] Convert to C++17 nested namespace syntax (#12463) 2025-12-17 11:29:43 -05:00
Thomas Rupprecht
56c1691d72 [pca9685,sx126x,sx127x] Use frequency/float_range check (#12490)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-12-16 22:52:28 -05:00
Roger Fachini
a065990ab9 [update] Add check action to trigger update checks (#12415) 2025-12-16 22:20:12 -05:00
Stuart Parmenter
084f517a20 [hub75] Add set_brightness action (#12521) 2025-12-16 22:12:33 -05:00
Jonathan Swoboda
1122ec354f [esp32] Add OTA rollback support (#12460)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-16 20:07:57 -05:00
Jonathan Swoboda
431183eebc [ledc,mqtt,resampler] Remove unnecessary ESP-IDF framework restrictions (#12442)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-16 20:07:09 -05:00
Jonathan Swoboda
08beaf8750 [esp32] Remove Arduino-specific code from core.cpp (#12501)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-16 20:06:12 -05:00
Jonathan Swoboda
18814f12dc [http_request] Use ESP-IDF for ESP32 Arduino (#12428)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-16 19:44:14 -05:00
Jonathan Swoboda
9cd888cef6 [spi] Use ESP-IDF driver for ESP32 Arduino (#12420)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-16 19:44:01 -05:00
Thomas Rupprecht
9727c7135c [openthread] channel range, fix typo, use C++17 nested namespace syntax (#12422) 2025-12-16 19:43:18 -05:00
Thomas Rupprecht
93621d85b0 [climate] Improve temperature unit regex (#12032) 2025-12-16 19:43:10 -05:00
Thomas Rupprecht
046ea922e8 [esp32] improve types and variable naming (#12423) 2025-12-16 19:42:52 -05:00
Jeff Zigler
fab4efb469 [esp32] Fix serial logging on h2, c2 & c61 (#12522)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-12-16 19:42:12 -05:00
Jonathan Swoboda
efc5672567 Merge branch 'release' into dev 2025-12-16 18:57:37 -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
5e630e9255 Merge branch 'beta' into dev 2025-12-16 11:26:08 -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
1897551b28 [uart] Fix UART on default UART0 pins for ESP-IDF (#12519)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-16 10:17:17 -05:00
J. Nick Koston
ead60bc5c4 [socket] Fix getpeername() returning local address instead of remote in LWIP raw TCP (#12475) 2025-12-16 00:48:30 -06:00
Jonathan Swoboda
7fe8e53f82 Merge branch 'beta' into dev 2025-12-15 19:01:12 -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
David Woodhouse
839139df36 Add FNV-1a hash functions (#12502) 2025-12-15 20:23:54 +00:00
dependabot[bot]
24d7e9dd23 Bump tornado from 6.5.3 to 6.5.4 (#12508)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-15 20:08:16 +00:00
dependabot[bot]
1214bb6bad Bump aioesphomeapi from 43.2.1 to 43.3.0 (#12507)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-15 20:07:20 +00:00
Pascal Vizeli
260ffba2a5 [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 12:54:12 -05:00
Jonathan Swoboda
2e899dd010 [esp32] Support all IDF component version operators in shorthand syntax (#12499)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-15 12:07:02 -05:00
David Woodhouse
61cbd07e1d Add hmac-sha256 support (#12437)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2025-12-15 10:55:03 -06:00
Jonathan Swoboda
450962850a [remote_base] Fix crash when ABBWelcome action has no data field (#12493)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-15 09:29:51 -05:00
Jonathan Swoboda
6b088caf5d Merge branch 'beta' into dev 2025-12-14 19:18:10 -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
J. Nick Koston
ffce80f96c [wifi] Fix WiFi recovery after failed connection attempts (#12483) 2025-12-14 16:26:34 -06:00
mbohdal
fa5b14fad4 [ethernet] fix used pins validation in configuration of RMII pins (#12486) 2025-12-14 16:40:08 -05:00
guillempages
cee532a1e3 [core] Use Arduino string macros only on ESP8266 (#12471) 2025-12-15 07:15:19 +11:00
J. Nick Koston
8524b894d6 [ota] Match client timeout to device timeout to prevent premature failures (#12484) 2025-12-14 13:47:11 -06:00
J. Nick Koston
3a5e708c13 [web_server_idf] Always enable LRU purge to prevent socket exhaustion (#12481) 2025-12-14 13:31:19 -06:00
J. Nick Koston
96e418a8ca [wifi_signal] Skip publishing disconnected RSSI value (#12482) 2025-12-14 13:31:07 -06:00
J. Nick Koston
780a407b10 [dashboard] Add ESPHOME_TRUSTED_DOMAINS support to events WebSocket (#12479) 2025-12-14 13:30:55 -06:00
Jonathan Swoboda
cfc0d8bdfc [cc1101] Add packet mode support (#12474)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-14 13:22:55 -05:00
Jonathan Swoboda
786d7266f5 [core] Fix polling_component_schema and type consistency (#12478)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-14 12:47:52 -05:00
Clyde Stubbs
ede64a9f47 [packet_transport] Ensure retransmission at update intervals (#12472)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-14 12:47:15 -05:00
J. Nick Koston
e0ce66e011 [core] Fix CORE.raw_config not updated after package merge (#12456) 2025-12-13 07:38:31 -06:00
David Woodhouse
6fce0a6104 Add host platform support to MD5 component (#12458) 2025-12-13 02:50:34 +00:00
425 changed files with 8209 additions and 3318 deletions

View File

@@ -1 +1 @@
766420905c06eeb6c5f360f68fd965e5ddd9c4a5db6b823263d3ad3accb64a07 94557f94be073390342833aff12ef8676a8b597db5fa770a5a1232e9425cb48f

View File

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

View File

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

View File

@@ -99,7 +99,7 @@ jobs:
python-version: "3.11" python-version: "3.11"
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Log in to docker hub - name: Log in to docker hub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
@@ -178,7 +178,7 @@ jobs:
merge-multiple: true merge-multiple: true
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Log in to docker hub - name: Log in to docker hub
if: matrix.registry == 'dockerhub' if: matrix.registry == 'dockerhub'

4
.gitignore vendored
View File

@@ -91,6 +91,10 @@ venv-*/
# mypy # mypy
.mypy_cache/ .mypy_cache/
# nix
/default.nix
/shell.nix
.pioenvs .pioenvs
.piolibdeps .piolibdeps
.pio .pio

View File

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

View File

@@ -42,6 +42,7 @@ esphome/components/animation/* @syndlex
esphome/components/anova/* @buxtronix esphome/components/anova/* @buxtronix
esphome/components/apds9306/* @aodrenah esphome/components/apds9306/* @aodrenah
esphome/components/api/* @esphome/core esphome/components/api/* @esphome/core
esphome/components/aqi/* @freekode @jasstrong @ximex
esphome/components/as5600/* @ammmze esphome/components/as5600/* @ammmze
esphome/components/as5600/sensor/* @ammmze esphome/components/as5600/sensor/* @ammmze
esphome/components/as7341/* @mrgnr esphome/components/as7341/* @mrgnr
@@ -215,6 +216,7 @@ esphome/components/hlk_fm22x/* @OnFreund
esphome/components/hlw8032/* @rici4kubicek esphome/components/hlw8032/* @rici4kubicek
esphome/components/hm3301/* @freekode esphome/components/hm3301/* @freekode
esphome/components/hmac_md5/* @dwmw2 esphome/components/hmac_md5/* @dwmw2
esphome/components/hmac_sha256/* @dwmw2
esphome/components/homeassistant/* @esphome/core @OttoWinter esphome/components/homeassistant/* @esphome/core @OttoWinter
esphome/components/homeassistant/number/* @landonr esphome/components/homeassistant/number/* @landonr
esphome/components/homeassistant/switch/* @Links2004 esphome/components/homeassistant/switch/* @Links2004
@@ -517,6 +519,7 @@ esphome/components/tuya/switch/* @jesserockz
esphome/components/tuya/text_sensor/* @dentra esphome/components/tuya/text_sensor/* @dentra
esphome/components/uart/* @esphome/core esphome/components/uart/* @esphome/core
esphome/components/uart/button/* @ssieb esphome/components/uart/button/* @ssieb
esphome/components/uart/event/* @eoasmxd
esphome/components/uart/packet_transport/* @clydebarrow esphome/components/uart/packet_transport/* @clydebarrow
esphome/components/udp/* @clydebarrow esphome/components/udp/* @clydebarrow
esphome/components/ufire_ec/* @pvizeli esphome/components/ufire_ec/* @pvizeli
@@ -535,6 +538,7 @@ esphome/components/version/* @esphome/core
esphome/components/voice_assistant/* @jesserockz @kahrendt esphome/components/voice_assistant/* @jesserockz @kahrendt
esphome/components/wake_on_lan/* @clydebarrow @willwill2will54 esphome/components/wake_on_lan/* @clydebarrow @willwill2will54
esphome/components/watchdog/* @oarcher esphome/components/watchdog/* @oarcher
esphome/components/water_heater/* @dhoeben
esphome/components/waveshare_epaper/* @clydebarrow esphome/components/waveshare_epaper/* @clydebarrow
esphome/components/web_server/ota/* @esphome/core esphome/components/web_server/ota/* @esphome/core
esphome/components/web_server_base/* @esphome/core esphome/components/web_server_base/* @esphome/core

View File

@@ -518,10 +518,49 @@ def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
rc = platformio_api.run_compile(config, CORE.verbose) rc = platformio_api.run_compile(config, CORE.verbose)
if rc != 0: if rc != 0:
return rc return rc
# Check if firmware was rebuilt and emit build_info + create manifest
_check_and_emit_build_info()
idedata = platformio_api.get_idedata(config) idedata = platformio_api.get_idedata(config)
return 0 if idedata is not None else 1 return 0 if idedata is not None else 1
def _check_and_emit_build_info() -> None:
"""Check if firmware was rebuilt and emit build_info."""
import json
firmware_path = CORE.firmware_bin
build_info_json_path = CORE.relative_build_path("build_info.json")
# Check if both files exist
if not firmware_path.exists() or not build_info_json_path.exists():
return
# Check if firmware is newer than build_info (indicating a relink occurred)
if firmware_path.stat().st_mtime <= build_info_json_path.stat().st_mtime:
return
# Read build_info from JSON
try:
with open(build_info_json_path, encoding="utf-8") as f:
build_info = json.load(f)
except (OSError, json.JSONDecodeError) as e:
_LOGGER.debug("Failed to read build_info: %s", e)
return
config_hash = build_info.get("config_hash")
build_time_str = build_info.get("build_time_str")
if config_hash is None or build_time_str is None:
return
# Emit build_info with human-readable time
_LOGGER.info(
"Build Info: config_hash=0x%08x build_time_str=%s", config_hash, build_time_str
)
def upload_using_esptool( def upload_using_esptool(
config: ConfigType, port: str, file: str, speed: int config: ConfigType, port: str, file: str, speed: int
) -> str | int: ) -> str | int:
@@ -741,13 +780,6 @@ def command_vscode(args: ArgsProtocol) -> int | None:
def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None: def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
# Set memory analysis options in config
if args.analyze_memory:
config.setdefault(CONF_ESPHOME, {})["analyze_memory"] = True
if args.memory_report:
config.setdefault(CONF_ESPHOME, {})["memory_report_file"] = args.memory_report
exit_code = write_cpp(config) exit_code = write_cpp(config)
if exit_code != 0: if exit_code != 0:
return exit_code return exit_code
@@ -757,7 +789,13 @@ def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
exit_code = compile_program(args, config) exit_code = compile_program(args, config)
if exit_code != 0: if exit_code != 0:
return exit_code return exit_code
_LOGGER.info("Successfully compiled program.") if CORE.is_host:
from esphome.platformio_api import get_idedata
program_path = str(get_idedata(config).firmware_elf_path)
_LOGGER.info("Successfully compiled program to path '%s'", program_path)
else:
_LOGGER.info("Successfully compiled program.")
return 0 return 0
@@ -807,10 +845,8 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
if CORE.is_host: if CORE.is_host:
from esphome.platformio_api import get_idedata from esphome.platformio_api import get_idedata
idedata = get_idedata(config) program_path = str(get_idedata(config).firmware_elf_path)
if idedata is None: _LOGGER.info("Running program from path '%s'", program_path)
return 1
program_path = idedata.raw["prog_path"]
return run_external_process(program_path) return run_external_process(program_path)
# Get devices, resolving special identifiers like OTA # Get devices, resolving special identifiers like OTA
@@ -1227,17 +1263,6 @@ def parse_args(argv):
help="Only generate source code, do not compile.", help="Only generate source code, do not compile.",
action="store_true", action="store_true",
) )
parser_compile.add_argument(
"--analyze-memory",
help="Analyze and display memory usage by component after compilation.",
action="store_true",
)
parser_compile.add_argument(
"--memory-report",
help="Save memory analysis report to a file (supports .json or .txt).",
type=str,
metavar="FILE",
)
parser_upload = subparsers.add_parser( parser_upload = subparsers.add_parser(
"upload", "upload",

View File

@@ -1,7 +1,6 @@
"""CLI interface for memory analysis with report generation.""" """CLI interface for memory analysis with report generation."""
from collections import defaultdict from collections import defaultdict
import json
import sys import sys
from . import ( from . import (
@@ -298,28 +297,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
return "\n".join(lines) return "\n".join(lines)
def to_json(self) -> str:
"""Export analysis results as JSON."""
data = {
"components": {
name: {
"text": mem.text_size,
"rodata": mem.rodata_size,
"data": mem.data_size,
"bss": mem.bss_size,
"flash_total": mem.flash_total,
"ram_total": mem.ram_total,
"symbol_count": mem.symbol_count,
}
for name, mem in self.components.items()
},
"totals": {
"flash": sum(c.flash_total for c in self.components.values()),
"ram": sum(c.ram_total for c in self.components.values()),
},
}
return json.dumps(data, indent=2)
def dump_uncategorized_symbols(self, output_file: str | None = None) -> None: def dump_uncategorized_symbols(self, output_file: str | None = None) -> None:
"""Dump uncategorized symbols for analysis.""" """Dump uncategorized symbols for analysis."""
# Sort by size descending # Sort by size descending

View File

@@ -227,7 +227,7 @@ CONFIG_SCHEMA = cv.All(
{ {
cv.GenerateID(): cv.declare_id(ADE7880), cv.GenerateID(): cv.declare_id(ADE7880),
cv.Optional(CONF_FREQUENCY, default="50Hz"): cv.All( 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.Optional(CONF_IRQ0_PIN): pins.internal_gpio_input_pin_schema,
cv.Required(CONF_IRQ1_PIN): pins.internal_gpio_input_pin_schema, cv.Required(CONF_IRQ1_PIN): pins.internal_gpio_input_pin_schema,

View File

@@ -8,8 +8,7 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome { namespace esphome::alarm_control_panel {
namespace alarm_control_panel {
static const char *const TAG = "alarm_control_panel"; static const char *const TAG = "alarm_control_panel";
@@ -115,5 +114,4 @@ void AlarmControlPanel::disarm(optional<std::string> code) {
call.perform(); call.perform();
} }
} // namespace alarm_control_panel } // namespace esphome::alarm_control_panel
} // namespace esphome

View File

@@ -1,7 +1,5 @@
#pragma once #pragma once
#include <map>
#include "alarm_control_panel_call.h" #include "alarm_control_panel_call.h"
#include "alarm_control_panel_state.h" #include "alarm_control_panel_state.h"
@@ -9,8 +7,7 @@
#include "esphome/core/entity_base.h" #include "esphome/core/entity_base.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome { namespace esphome::alarm_control_panel {
namespace alarm_control_panel {
enum AlarmControlPanelFeature : uint8_t { enum AlarmControlPanelFeature : uint8_t {
// Matches Home Assistant values // Matches Home Assistant values
@@ -132,14 +129,13 @@ class AlarmControlPanel : public EntityBase {
// the call control function // the call control function
virtual void control(const AlarmControlPanelCall &call) = 0; virtual void control(const AlarmControlPanelCall &call) = 0;
// state callback - triggers check get_state() for specific state // state callback - triggers check get_state() for specific state
CallbackManager<void()> state_callback_{}; LazyCallbackManager<void()> state_callback_{};
// clear callback - fires when leaving TRIGGERED state // clear callback - fires when leaving TRIGGERED state
CallbackManager<void()> cleared_callback_{}; LazyCallbackManager<void()> cleared_callback_{};
// chime callback // chime callback
CallbackManager<void()> chime_callback_{}; LazyCallbackManager<void()> chime_callback_{};
// ready callback // ready callback
CallbackManager<void()> ready_callback_{}; LazyCallbackManager<void()> ready_callback_{};
}; };
} // namespace alarm_control_panel } // namespace esphome::alarm_control_panel
} // namespace esphome

View File

@@ -4,8 +4,7 @@
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome { namespace esphome::alarm_control_panel {
namespace alarm_control_panel {
static const char *const TAG = "alarm_control_panel"; static const char *const TAG = "alarm_control_panel";
@@ -99,5 +98,4 @@ void AlarmControlPanelCall::perform() {
} }
} }
} // namespace alarm_control_panel } // namespace esphome::alarm_control_panel
} // namespace esphome

View File

@@ -6,8 +6,7 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
namespace esphome { namespace esphome::alarm_control_panel {
namespace alarm_control_panel {
class AlarmControlPanel; class AlarmControlPanel;
@@ -36,5 +35,4 @@ class AlarmControlPanelCall {
void validate_(); void validate_();
}; };
} // namespace alarm_control_panel } // namespace esphome::alarm_control_panel
} // namespace esphome

View File

@@ -1,7 +1,6 @@
#include "alarm_control_panel_state.h" #include "alarm_control_panel_state.h"
namespace esphome { namespace esphome::alarm_control_panel {
namespace alarm_control_panel {
const LogString *alarm_control_panel_state_to_string(AlarmControlPanelState state) { const LogString *alarm_control_panel_state_to_string(AlarmControlPanelState state) {
switch (state) { switch (state) {
@@ -30,5 +29,4 @@ const LogString *alarm_control_panel_state_to_string(AlarmControlPanelState stat
} }
} }
} // namespace alarm_control_panel } // namespace esphome::alarm_control_panel
} // namespace esphome

View File

@@ -3,8 +3,7 @@
#include <cstdint> #include <cstdint>
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome { namespace esphome::alarm_control_panel {
namespace alarm_control_panel {
enum AlarmControlPanelState : uint8_t { enum AlarmControlPanelState : uint8_t {
ACP_STATE_DISARMED = 0, ACP_STATE_DISARMED = 0,
@@ -25,5 +24,4 @@ enum AlarmControlPanelState : uint8_t {
*/ */
const LogString *alarm_control_panel_state_to_string(AlarmControlPanelState state); const LogString *alarm_control_panel_state_to_string(AlarmControlPanelState state);
} // namespace alarm_control_panel } // namespace esphome::alarm_control_panel
} // namespace esphome

View File

@@ -3,8 +3,7 @@
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
#include "alarm_control_panel.h" #include "alarm_control_panel.h"
namespace esphome { namespace esphome::alarm_control_panel {
namespace alarm_control_panel {
/// Trigger on any state change /// Trigger on any state change
class StateTrigger : public Trigger<> { class StateTrigger : public Trigger<> {
@@ -165,5 +164,4 @@ template<typename... Ts> class AlarmControlPanelCondition : public Condition<Ts.
AlarmControlPanel *parent_; AlarmControlPanel *parent_;
}; };
} // namespace alarm_control_panel } // namespace esphome::alarm_control_panel
} // namespace esphome

View File

@@ -477,7 +477,7 @@ message FanCommandRequest {
bool has_speed_level = 10; bool has_speed_level = 10;
int32 speed_level = 11; int32 speed_level = 11;
bool has_preset_mode = 12; bool has_preset_mode = 12;
string preset_mode = 13; string preset_mode = 13 [(pointer_to_buffer) = true];
uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"]; uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"];
} }
@@ -747,7 +747,7 @@ message NoiseEncryptionSetKeyRequest {
option (source) = SOURCE_CLIENT; option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_API_NOISE"; option (ifdef) = "USE_API_NOISE";
bytes key = 1; bytes key = 1 [(pointer_to_buffer) = true];
} }
message NoiseEncryptionSetKeyResponse { message NoiseEncryptionSetKeyResponse {
@@ -824,9 +824,9 @@ message HomeAssistantStateResponse {
option (no_delay) = true; option (no_delay) = true;
option (ifdef) = "USE_API_HOMEASSISTANT_STATES"; option (ifdef) = "USE_API_HOMEASSISTANT_STATES";
string entity_id = 1; string entity_id = 1 [(pointer_to_buffer) = true];
string state = 2; string state = 2 [(pointer_to_buffer) = true];
string attribute = 3; string attribute = 3 [(pointer_to_buffer) = true];
} }
// ==================== IMPORT TIME ==================== // ==================== IMPORT TIME ====================
@@ -1091,16 +1091,95 @@ message ClimateCommandRequest {
bool has_swing_mode = 14; bool has_swing_mode = 14;
ClimateSwingMode swing_mode = 15; ClimateSwingMode swing_mode = 15;
bool has_custom_fan_mode = 16; bool has_custom_fan_mode = 16;
string custom_fan_mode = 17; string custom_fan_mode = 17 [(pointer_to_buffer) = true];
bool has_preset = 18; bool has_preset = 18;
ClimatePreset preset = 19; ClimatePreset preset = 19;
bool has_custom_preset = 20; bool has_custom_preset = 20;
string custom_preset = 21; string custom_preset = 21 [(pointer_to_buffer) = true];
bool has_target_humidity = 22; bool has_target_humidity = 22;
float target_humidity = 23; float target_humidity = 23;
uint32 device_id = 24 [(field_ifdef) = "USE_DEVICES"]; uint32 device_id = 24 [(field_ifdef) = "USE_DEVICES"];
} }
// ==================== WATER_HEATER ====================
enum WaterHeaterMode {
WATER_HEATER_MODE_OFF = 0;
WATER_HEATER_MODE_ECO = 1;
WATER_HEATER_MODE_ELECTRIC = 2;
WATER_HEATER_MODE_PERFORMANCE = 3;
WATER_HEATER_MODE_HIGH_DEMAND = 4;
WATER_HEATER_MODE_HEAT_PUMP = 5;
WATER_HEATER_MODE_GAS = 6;
}
message ListEntitiesWaterHeaterResponse {
option (id) = 132;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_WATER_HEATER";
string object_id = 1;
fixed32 key = 2;
string name = 3;
string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON"];
bool disabled_by_default = 5;
EntityCategory entity_category = 6;
uint32 device_id = 7 [(field_ifdef) = "USE_DEVICES"];
float min_temperature = 8;
float max_temperature = 9;
float target_temperature_step = 10;
repeated WaterHeaterMode supported_modes = 11 [(container_pointer_no_template) = "water_heater::WaterHeaterModeMask"];
// Bitmask of WaterHeaterFeature flags
uint32 supported_features = 12;
}
message WaterHeaterStateResponse {
option (id) = 133;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_WATER_HEATER";
option (no_delay) = true;
fixed32 key = 1;
float current_temperature = 2;
float target_temperature = 3;
WaterHeaterMode mode = 4;
uint32 device_id = 5 [(field_ifdef) = "USE_DEVICES"];
// Bitmask of current state flags (bit 0 = away, bit 1 = on)
uint32 state = 6;
float target_temperature_low = 7;
float target_temperature_high = 8;
}
// Bitmask for WaterHeaterCommandRequest.has_fields
enum WaterHeaterCommandHasField {
WATER_HEATER_COMMAND_HAS_NONE = 0;
WATER_HEATER_COMMAND_HAS_MODE = 1;
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE = 2;
WATER_HEATER_COMMAND_HAS_STATE = 4;
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8;
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH = 16;
}
message WaterHeaterCommandRequest {
option (id) = 134;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_WATER_HEATER";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
// Bitmask of which fields are set (see WaterHeaterCommandHasField)
uint32 has_fields = 2;
WaterHeaterMode mode = 3;
float target_temperature = 4;
uint32 device_id = 5 [(field_ifdef) = "USE_DEVICES"];
// State flags bitmask (bit 0 = away, bit 1 = on)
uint32 state = 6;
float target_temperature_low = 7;
float target_temperature_high = 8;
}
// ==================== NUMBER ==================== // ==================== NUMBER ====================
enum NumberMode { enum NumberMode {
NUMBER_MODE_AUTO = 0; NUMBER_MODE_AUTO = 0;

View File

@@ -13,6 +13,7 @@
#include <cinttypes> #include <cinttypes>
#include <functional> #include <functional>
#include <limits> #include <limits>
#include <new>
#include <utility> #include <utility>
#ifdef USE_ESP8266 #ifdef USE_ESP8266
#include <pgmspace.h> #include <pgmspace.h>
@@ -42,6 +43,9 @@
#ifdef USE_ZWAVE_PROXY #ifdef USE_ZWAVE_PROXY
#include "esphome/components/zwave_proxy/zwave_proxy.h" #include "esphome/components/zwave_proxy/zwave_proxy.h"
#endif #endif
#ifdef USE_WATER_HEATER
#include "esphome/components/water_heater/water_heater.h"
#endif
namespace esphome::api { namespace esphome::api {
@@ -93,8 +97,7 @@ static const int CAMERA_STOP_STREAM = 5000;
return; return;
#endif // USE_DEVICES #endif // USE_DEVICES
APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent) APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent) : parent_(parent) {
: parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) {
#if defined(USE_API_PLAINTEXT) && defined(USE_API_NOISE) #if defined(USE_API_PLAINTEXT) && defined(USE_API_NOISE)
auto &noise_ctx = parent->get_noise_ctx(); auto &noise_ctx = parent->get_noise_ctx();
if (noise_ctx.has_psk()) { if (noise_ctx.has_psk()) {
@@ -133,6 +136,7 @@ void APIConnection::start() {
} }
APIConnection::~APIConnection() { APIConnection::~APIConnection() {
this->destroy_active_iterator_();
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
if (bluetooth_proxy::global_bluetooth_proxy->get_api_connection() == this) { if (bluetooth_proxy::global_bluetooth_proxy->get_api_connection() == this) {
bluetooth_proxy::global_bluetooth_proxy->unsubscribe_api_connection(this); bluetooth_proxy::global_bluetooth_proxy->unsubscribe_api_connection(this);
@@ -145,6 +149,32 @@ APIConnection::~APIConnection() {
#endif #endif
} }
void APIConnection::destroy_active_iterator_() {
switch (this->active_iterator_) {
case ActiveIterator::LIST_ENTITIES:
this->iterator_storage_.list_entities.~ListEntitiesIterator();
break;
case ActiveIterator::INITIAL_STATE:
this->iterator_storage_.initial_state.~InitialStateIterator();
break;
case ActiveIterator::NONE:
break;
}
this->active_iterator_ = ActiveIterator::NONE;
}
void APIConnection::begin_iterator_(ActiveIterator type) {
this->destroy_active_iterator_();
this->active_iterator_ = type;
if (type == ActiveIterator::LIST_ENTITIES) {
new (&this->iterator_storage_.list_entities) ListEntitiesIterator(this);
this->iterator_storage_.list_entities.begin();
} else {
new (&this->iterator_storage_.initial_state) InitialStateIterator(this);
this->iterator_storage_.initial_state.begin();
}
}
void APIConnection::loop() { void APIConnection::loop() {
if (this->flags_.next_close) { if (this->flags_.next_close) {
// requested a disconnect // requested a disconnect
@@ -187,23 +217,35 @@ void APIConnection::loop() {
this->process_batch_(); this->process_batch_();
} }
if (!this->list_entities_iterator_.completed()) { switch (this->active_iterator_) {
this->process_iterator_batch_(this->list_entities_iterator_); case ActiveIterator::LIST_ENTITIES:
} else if (!this->initial_state_iterator_.completed()) { if (this->iterator_storage_.list_entities.completed()) {
this->process_iterator_batch_(this->initial_state_iterator_); this->destroy_active_iterator_();
if (this->flags_.state_subscription) {
// If we've completed initial states, process any remaining and clear the flag this->begin_iterator_(ActiveIterator::INITIAL_STATE);
if (this->initial_state_iterator_.completed()) { }
// Process any remaining batched messages immediately } else {
if (!this->deferred_batch_.empty()) { this->process_iterator_batch_(this->iterator_storage_.list_entities);
this->process_batch_();
} }
// Now that everything is sent, enable immediate sending for future state changes break;
this->flags_.should_try_send_immediately = true; case ActiveIterator::INITIAL_STATE:
// Release excess memory from buffers that grew during initial sync if (this->iterator_storage_.initial_state.completed()) {
this->deferred_batch_.release_buffer(); this->destroy_active_iterator_();
this->helper_->release_buffers(); // Process any remaining batched messages immediately
} if (!this->deferred_batch_.empty()) {
this->process_batch_();
}
// 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();
} else {
this->process_iterator_batch_(this->iterator_storage_.initial_state);
}
break;
case ActiveIterator::NONE:
break;
} }
if (this->flags_.sent_ping) { if (this->flags_.sent_ping) {
@@ -228,33 +270,17 @@ void APIConnection::loop() {
} }
} }
#ifdef USE_CAMERA
if (this->image_reader_ && this->image_reader_->available() && this->helper_->can_write_without_blocking()) {
uint32_t to_send = std::min((size_t) MAX_BATCH_PACKET_SIZE, this->image_reader_->available());
bool done = this->image_reader_->available() == to_send;
CameraImageResponse msg;
msg.key = camera::Camera::instance()->get_object_id_hash();
msg.set_data(this->image_reader_->peek_data_buffer(), to_send);
msg.done = done;
#ifdef USE_DEVICES
msg.device_id = camera::Camera::instance()->get_device_id();
#endif
if (this->send_message_(msg, CameraImageResponse::MESSAGE_TYPE)) {
this->image_reader_->consume_data(to_send);
if (done) {
this->image_reader_->return_image();
}
}
}
#endif
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
if (state_subs_at_ >= 0) { if (state_subs_at_ >= 0) {
this->process_state_subscriptions_(); this->process_state_subscriptions_();
} }
#endif #endif
#ifdef USE_CAMERA
// Process camera last - state updates are higher priority
// (missing a frame is fine, missing a state update is not)
this->try_send_camera_image_();
#endif
} }
bool APIConnection::send_disconnect_response(const DisconnectRequest &msg) { bool APIConnection::send_disconnect_response(const DisconnectRequest &msg) {
@@ -447,7 +473,7 @@ void APIConnection::fan_command(const FanCommandRequest &msg) {
if (msg.has_direction) if (msg.has_direction)
call.set_direction(static_cast<fan::FanDirection>(msg.direction)); call.set_direction(static_cast<fan::FanDirection>(msg.direction));
if (msg.has_preset_mode) if (msg.has_preset_mode)
call.set_preset_mode(msg.preset_mode); call.set_preset_mode(reinterpret_cast<const char *>(msg.preset_mode), msg.preset_mode_len);
call.perform(); call.perform();
} }
#endif #endif
@@ -712,11 +738,11 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) {
if (msg.has_fan_mode) if (msg.has_fan_mode)
call.set_fan_mode(static_cast<climate::ClimateFanMode>(msg.fan_mode)); call.set_fan_mode(static_cast<climate::ClimateFanMode>(msg.fan_mode));
if (msg.has_custom_fan_mode) if (msg.has_custom_fan_mode)
call.set_fan_mode(msg.custom_fan_mode); call.set_fan_mode(reinterpret_cast<const char *>(msg.custom_fan_mode), msg.custom_fan_mode_len);
if (msg.has_preset) if (msg.has_preset)
call.set_preset(static_cast<climate::ClimatePreset>(msg.preset)); call.set_preset(static_cast<climate::ClimatePreset>(msg.preset));
if (msg.has_custom_preset) if (msg.has_custom_preset)
call.set_preset(msg.custom_preset); call.set_preset(reinterpret_cast<const char *>(msg.custom_preset), msg.custom_preset_len);
if (msg.has_swing_mode) if (msg.has_swing_mode)
call.set_swing_mode(static_cast<climate::ClimateSwingMode>(msg.swing_mode)); call.set_swing_mode(static_cast<climate::ClimateSwingMode>(msg.swing_mode));
call.perform(); call.perform();
@@ -1057,6 +1083,36 @@ void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) {
#endif #endif
#ifdef USE_CAMERA #ifdef USE_CAMERA
void APIConnection::try_send_camera_image_() {
if (!this->image_reader_)
return;
// Send as many chunks as possible without blocking
while (this->image_reader_->available()) {
if (!this->helper_->can_write_without_blocking())
return;
uint32_t to_send = std::min((size_t) MAX_BATCH_PACKET_SIZE, this->image_reader_->available());
bool done = this->image_reader_->available() == to_send;
CameraImageResponse msg;
msg.key = camera::Camera::instance()->get_object_id_hash();
msg.set_data(this->image_reader_->peek_data_buffer(), to_send);
msg.done = done;
#ifdef USE_DEVICES
msg.device_id = camera::Camera::instance()->get_device_id();
#endif
if (!this->send_message_(msg, CameraImageResponse::MESSAGE_TYPE)) {
return; // Send failed, try again later
}
this->image_reader_->consume_data(to_send);
if (done) {
this->image_reader_->return_image();
return;
}
}
}
void APIConnection::set_camera_state(std::shared_ptr<camera::CameraImage> image) { void APIConnection::set_camera_state(std::shared_ptr<camera::CameraImage> image) {
if (!this->flags_.state_subscription) if (!this->flags_.state_subscription)
return; return;
@@ -1064,8 +1120,11 @@ void APIConnection::set_camera_state(std::shared_ptr<camera::CameraImage> image)
return; return;
if (this->image_reader_->available()) if (this->image_reader_->available())
return; return;
if (image->was_requested_by(esphome::camera::API_REQUESTER) || image->was_requested_by(esphome::camera::IDLE)) if (image->was_requested_by(esphome::camera::API_REQUESTER) || image->was_requested_by(esphome::camera::IDLE)) {
this->image_reader_->set_image(std::move(image)); this->image_reader_->set_image(std::move(image));
// Try to send immediately to reduce latency
this->try_send_camera_image_();
}
} }
uint16_t APIConnection::try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) { bool is_single) {
@@ -1306,6 +1365,57 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe
} }
#endif #endif
#ifdef USE_WATER_HEATER
bool APIConnection::send_water_heater_state(water_heater::WaterHeater *water_heater) {
return this->send_message_smart_(water_heater, &APIConnection::try_send_water_heater_state,
WaterHeaterStateResponse::MESSAGE_TYPE, WaterHeaterStateResponse::ESTIMATED_SIZE);
}
uint16_t APIConnection::try_send_water_heater_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) {
auto *wh = static_cast<water_heater::WaterHeater *>(entity);
WaterHeaterStateResponse resp;
resp.mode = static_cast<enums::WaterHeaterMode>(wh->get_mode());
resp.current_temperature = wh->get_current_temperature();
resp.target_temperature = wh->get_target_temperature();
resp.target_temperature_low = wh->get_target_temperature_low();
resp.target_temperature_high = wh->get_target_temperature_high();
resp.state = wh->get_state();
resp.key = wh->get_object_id_hash();
return encode_message_to_buffer(resp, WaterHeaterStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
}
uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) {
auto *wh = static_cast<water_heater::WaterHeater *>(entity);
ListEntitiesWaterHeaterResponse msg;
auto traits = wh->get_traits();
msg.min_temperature = traits.get_min_temperature();
msg.max_temperature = traits.get_max_temperature();
msg.target_temperature_step = traits.get_target_temperature_step();
msg.supported_modes = &traits.get_supported_modes();
msg.supported_features = traits.get_feature_flags();
return fill_and_encode_entity_info(wh, msg, ListEntitiesWaterHeaterResponse::MESSAGE_TYPE, conn, remaining_size,
is_single);
}
void APIConnection::on_water_heater_command_request(const WaterHeaterCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(water_heater::WaterHeater, water_heater, water_heater)
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_MODE)
call.set_mode(static_cast<water_heater::WaterHeaterMode>(msg.mode));
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE)
call.set_target_temperature(msg.target_temperature);
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW)
call.set_target_temperature_low(msg.target_temperature_low);
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH)
call.set_target_temperature_high(msg.target_temperature_high);
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_STATE) {
call.set_away((msg.state & water_heater::WATER_HEATER_STATE_AWAY) != 0);
call.set_on((msg.state & water_heater::WATER_HEATER_STATE_ON) != 0);
}
call.perform();
}
#endif
#ifdef USE_EVENT #ifdef USE_EVENT
void APIConnection::send_event(event::Event *event, const char *event_type) { void APIConnection::send_event(event::Event *event, const char *event_type) {
this->send_message_smart_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE, this->send_message_smart_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE,
@@ -1472,7 +1582,10 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
resp.set_esphome_version(ESPHOME_VERSION_REF); resp.set_esphome_version(ESPHOME_VERSION_REF);
resp.set_compilation_time(App.get_compilation_time_ref()); // Stack buffer for build time string
char build_time_str[Application::BUILD_TIME_STR_SIZE];
App.get_build_time_string(build_time_str);
resp.set_compilation_time(StringRef(build_time_str));
// Manufacturer string - define once, handle ESP8266 PROGMEM separately // Manufacturer string - define once, handle ESP8266 PROGMEM separately
#if defined(USE_ESP8266) || defined(USE_ESP32) #if defined(USE_ESP8266) || defined(USE_ESP32)
@@ -1579,15 +1692,28 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
void APIConnection::on_home_assistant_state_response(const HomeAssistantStateResponse &msg) { void APIConnection::on_home_assistant_state_response(const HomeAssistantStateResponse &msg) {
for (auto &it : this->parent_->get_state_subs()) { // Skip if entity_id is empty (invalid message)
// Compare entity_id and attribute with message fields if (msg.entity_id_len == 0) {
bool entity_match = (strcmp(it.entity_id, msg.entity_id.c_str()) == 0); return;
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) { for (auto &it : this->parent_->get_state_subs()) {
it.callback(msg.state); // Compare entity_id: check length matches and content matches
size_t entity_id_len = strlen(it.entity_id);
if (entity_id_len != msg.entity_id_len || memcmp(it.entity_id, msg.entity_id, msg.entity_id_len) != 0) {
continue;
} }
// Compare attribute: either both have matching attribute, or both have none
size_t sub_attr_len = it.attribute != nullptr ? strlen(it.attribute) : 0;
if (sub_attr_len != msg.attribute_len ||
(sub_attr_len > 0 && memcmp(it.attribute, msg.attribute, sub_attr_len) != 0)) {
continue;
}
// Create StringRef directly from message data (zero allocation)
StringRef state(reinterpret_cast<const char *>(msg.state), msg.state_len);
it.callback(state);
} }
} }
#endif #endif
@@ -1663,13 +1789,13 @@ bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryption
resp.success = false; resp.success = false;
psk_t psk{}; psk_t psk{};
if (msg.key.empty()) { if (msg.key_len == 0) {
if (this->parent_->clear_noise_psk(true)) { if (this->parent_->clear_noise_psk(true)) {
resp.success = true; resp.success = true;
} else { } else {
ESP_LOGW(TAG, "Failed to clear encryption key"); ESP_LOGW(TAG, "Failed to clear encryption key");
} }
} else if (base64_decode(msg.key, psk.data(), psk.size()) != psk.size()) { } else if (base64_decode(msg.key, msg.key_len, psk.data(), psk.size()) != psk.size()) {
ESP_LOGW(TAG, "Invalid encryption key length"); ESP_LOGW(TAG, "Invalid encryption key length");
} else if (!this->parent_->save_noise_psk(psk, true)) { } else if (!this->parent_->save_noise_psk(psk, true)) {
ESP_LOGW(TAG, "Failed to save encryption key"); ESP_LOGW(TAG, "Failed to save encryption key");

View File

@@ -176,6 +176,11 @@ class APIConnection final : public APIServerConnection {
void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override; void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override;
#endif #endif
#ifdef USE_WATER_HEATER
bool send_water_heater_state(water_heater::WaterHeater *water_heater);
void on_water_heater_command_request(const WaterHeaterCommandRequest &msg) override;
#endif
#ifdef USE_EVENT #ifdef USE_EVENT
void send_event(event::Event *event, const char *event_type); void send_event(event::Event *event, const char *event_type);
#endif #endif
@@ -203,10 +208,14 @@ class APIConnection final : public APIServerConnection {
bool send_disconnect_response(const DisconnectRequest &msg) override; bool send_disconnect_response(const DisconnectRequest &msg) override;
bool send_ping_response(const PingRequest &msg) override; bool send_ping_response(const PingRequest &msg) override;
bool send_device_info_response(const DeviceInfoRequest &msg) override; bool send_device_info_response(const DeviceInfoRequest &msg) override;
void list_entities(const ListEntitiesRequest &msg) override { this->list_entities_iterator_.begin(); } void list_entities(const ListEntitiesRequest &msg) override { this->begin_iterator_(ActiveIterator::LIST_ENTITIES); }
void subscribe_states(const SubscribeStatesRequest &msg) override { void subscribe_states(const SubscribeStatesRequest &msg) override {
this->flags_.state_subscription = true; this->flags_.state_subscription = true;
this->initial_state_iterator_.begin(); // Start initial state iterator only if no iterator is active
// If list_entities is running, we'll start initial_state when it completes
if (this->active_iterator_ == ActiveIterator::NONE) {
this->begin_iterator_(ActiveIterator::INITIAL_STATE);
}
} }
void subscribe_logs(const SubscribeLogsRequest &msg) override { void subscribe_logs(const SubscribeLogsRequest &msg) override {
this->flags_.log_subscription = msg.level; this->flags_.log_subscription = msg.level;
@@ -287,6 +296,10 @@ class APIConnection final : public APIServerConnection {
// Helper function to handle authentication completion // Helper function to handle authentication completion
void complete_authentication_(); void complete_authentication_();
#ifdef USE_CAMERA
void try_send_camera_image_();
#endif
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
void process_state_subscriptions_(); void process_state_subscriptions_();
#endif #endif
@@ -310,17 +323,10 @@ class APIConnection final : public APIServerConnection {
APIConnection *conn, uint32_t remaining_size, bool is_single) { APIConnection *conn, uint32_t remaining_size, bool is_single) {
// Set common fields that are shared by all entity types // Set common fields that are shared by all entity types
msg.key = entity->get_object_id_hash(); msg.key = entity->get_object_id_hash();
// Try to use static reference first to avoid allocation // Get object_id with zero heap allocation
StringRef static_ref = entity->get_object_id_ref_for_api_(); // Static case returns direct reference, dynamic case uses buffer
// Store dynamic string outside the if-else to maintain lifetime char object_id_buf[OBJECT_ID_MAX_LEN];
std::string object_id; msg.set_object_id(entity->get_object_id_to(object_id_buf));
if (!static_ref.empty()) {
msg.set_object_id(static_ref);
} else {
// Dynamic case - need to allocate
object_id = entity->get_object_id();
msg.set_object_id(StringRef(object_id));
}
if (entity->has_own_name()) { if (entity->has_own_name()) {
msg.set_name(entity->get_name()); msg.set_name(entity->get_name());
@@ -456,6 +462,12 @@ class APIConnection final : public APIServerConnection {
static uint16_t try_send_alarm_control_panel_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, static uint16_t try_send_alarm_control_panel_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single); bool is_single);
#endif #endif
#ifdef USE_WATER_HEATER
static uint16_t try_send_water_heater_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single);
static uint16_t try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single);
#endif
#ifdef USE_EVENT #ifdef USE_EVENT
static uint16_t try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn, static uint16_t try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn,
uint32_t remaining_size, bool is_single); uint32_t remaining_size, bool is_single);
@@ -490,10 +502,22 @@ class APIConnection final : public APIServerConnection {
std::unique_ptr<APIFrameHelper> helper_; std::unique_ptr<APIFrameHelper> helper_;
APIServer *parent_; APIServer *parent_;
// Group 2: Larger objects (must be 4-byte aligned) // Group 2: Iterator union (saves ~16 bytes vs separate iterators)
// These contain vectors/pointers internally, so putting them early ensures good alignment // These iterators are never active simultaneously - list_entities runs to completion
InitialStateIterator initial_state_iterator_; // before initial_state begins, so we use a union with explicit construction/destruction.
ListEntitiesIterator list_entities_iterator_; enum class ActiveIterator : uint8_t { NONE, LIST_ENTITIES, INITIAL_STATE };
union IteratorUnion {
ListEntitiesIterator list_entities;
InitialStateIterator initial_state;
// Constructor/destructor do nothing - use placement new/explicit destructor
IteratorUnion() {}
~IteratorUnion() {}
} iterator_storage_;
// Helper methods for iterator lifecycle management
void destroy_active_iterator_();
void begin_iterator_(ActiveIterator type);
#ifdef USE_CAMERA #ifdef USE_CAMERA
std::unique_ptr<camera::CameraImageReader> image_reader_; std::unique_ptr<camera::CameraImageReader> image_reader_;
#endif #endif
@@ -608,7 +632,9 @@ class APIConnection final : public APIServerConnection {
// 2-byte types immediately after flags_ (no padding between them) // 2-byte types immediately after flags_ (no padding between them)
uint16_t client_api_version_major_{0}; uint16_t client_api_version_major_{0};
uint16_t client_api_version_minor_{0}; uint16_t client_api_version_minor_{0};
// Total: 2 (flags) + 2 + 2 = 6 bytes, then 2 bytes padding to next 4-byte boundary // 1-byte type to fill padding
ActiveIterator active_iterator_{ActiveIterator::NONE};
// Total: 2 (flags) + 2 + 2 + 1 = 7 bytes, then 1 byte padding to next 4-byte boundary
uint32_t get_batch_delay_ms_() const; uint32_t get_batch_delay_ms_() const;
// Message will use 8 more bytes than the minimum size, and typical // Message will use 8 more bytes than the minimum size, and typical

View File

@@ -124,12 +124,12 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const {
#endif #endif
#ifdef USE_DEVICES #ifdef USE_DEVICES
for (const auto &it : this->devices) { for (const auto &it : this->devices) {
buffer.encode_message(20, it, true); buffer.encode_message(20, it);
} }
#endif #endif
#ifdef USE_AREAS #ifdef USE_AREAS
for (const auto &it : this->areas) { for (const auto &it : this->areas) {
buffer.encode_message(21, it, true); buffer.encode_message(21, it);
} }
#endif #endif
#ifdef USE_AREAS #ifdef USE_AREAS
@@ -447,9 +447,12 @@ bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
} }
bool FanCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { bool FanCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) { switch (field_id) {
case 13: case 13: {
this->preset_mode = value.as_string(); // Use raw data directly to avoid allocation
this->preset_mode = value.data();
this->preset_mode_len = value.size();
break; break;
}
default: default:
return false; return false;
} }
@@ -855,9 +858,12 @@ void SubscribeLogsResponse::calculate_size(ProtoSize &size) const {
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
bool NoiseEncryptionSetKeyRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { bool NoiseEncryptionSetKeyRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) { switch (field_id) {
case 1: case 1: {
this->key = value.as_string(); // Use raw data directly to avoid allocation
this->key = value.data();
this->key_len = value.size();
break; break;
}
default: default:
return false; return false;
} }
@@ -878,13 +884,13 @@ void HomeassistantServiceMap::calculate_size(ProtoSize &size) const {
void HomeassistantActionRequest::encode(ProtoWriteBuffer buffer) const { void HomeassistantActionRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->service_ref_); buffer.encode_string(1, this->service_ref_);
for (auto &it : this->data) { for (auto &it : this->data) {
buffer.encode_message(2, it, true); buffer.encode_message(2, it);
} }
for (auto &it : this->data_template) { for (auto &it : this->data_template) {
buffer.encode_message(3, it, true); buffer.encode_message(3, it);
} }
for (auto &it : this->variables) { for (auto &it : this->variables) {
buffer.encode_message(4, it, true); buffer.encode_message(4, it);
} }
buffer.encode_bool(5, this->is_event); buffer.encode_bool(5, this->is_event);
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
@@ -960,15 +966,24 @@ void SubscribeHomeAssistantStateResponse::calculate_size(ProtoSize &size) const
} }
bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) { switch (field_id) {
case 1: case 1: {
this->entity_id = value.as_string(); // Use raw data directly to avoid allocation
this->entity_id = value.data();
this->entity_id_len = value.size();
break; break;
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; break;
case 3: }
this->attribute = value.as_string(); case 3: {
// Use raw data directly to avoid allocation
this->attribute = value.data();
this->attribute_len = value.size();
break; break;
}
default: default:
return false; return false;
} }
@@ -1011,7 +1026,7 @@ void ListEntitiesServicesResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->name_ref_); buffer.encode_string(1, this->name_ref_);
buffer.encode_fixed32(2, this->key); buffer.encode_fixed32(2, this->key);
for (auto &it : this->args) { for (auto &it : this->args) {
buffer.encode_message(3, it, true); buffer.encode_message(3, it);
} }
buffer.encode_uint32(4, static_cast<uint32_t>(this->supports_response)); buffer.encode_uint32(4, static_cast<uint32_t>(this->supports_response));
} }
@@ -1392,12 +1407,18 @@ bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value)
} }
bool ClimateCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { bool ClimateCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) { switch (field_id) {
case 17: case 17: {
this->custom_fan_mode = value.as_string(); // Use raw data directly to avoid allocation
this->custom_fan_mode = value.data();
this->custom_fan_mode_len = value.size();
break; break;
case 21: }
this->custom_preset = value.as_string(); case 21: {
// Use raw data directly to avoid allocation
this->custom_preset = value.data();
this->custom_preset_len = value.size();
break; break;
}
default: default:
return false; return false;
} }
@@ -1426,6 +1447,114 @@ bool ClimateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
return true; return true;
} }
#endif #endif
#ifdef USE_WATER_HEATER
void ListEntitiesWaterHeaterResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->object_id_ref_);
buffer.encode_fixed32(2, this->key);
buffer.encode_string(3, this->name_ref_);
#ifdef USE_ENTITY_ICON
buffer.encode_string(4, this->icon_ref_);
#endif
buffer.encode_bool(5, this->disabled_by_default);
buffer.encode_uint32(6, static_cast<uint32_t>(this->entity_category));
#ifdef USE_DEVICES
buffer.encode_uint32(7, this->device_id);
#endif
buffer.encode_float(8, this->min_temperature);
buffer.encode_float(9, this->max_temperature);
buffer.encode_float(10, this->target_temperature_step);
for (const auto &it : *this->supported_modes) {
buffer.encode_uint32(11, static_cast<uint32_t>(it), true);
}
buffer.encode_uint32(12, this->supported_features);
}
void ListEntitiesWaterHeaterResponse::calculate_size(ProtoSize &size) const {
size.add_length(1, this->object_id_ref_.size());
size.add_fixed32(1, this->key);
size.add_length(1, this->name_ref_.size());
#ifdef USE_ENTITY_ICON
size.add_length(1, this->icon_ref_.size());
#endif
size.add_bool(1, this->disabled_by_default);
size.add_uint32(1, static_cast<uint32_t>(this->entity_category));
#ifdef USE_DEVICES
size.add_uint32(1, this->device_id);
#endif
size.add_float(1, this->min_temperature);
size.add_float(1, this->max_temperature);
size.add_float(1, this->target_temperature_step);
if (!this->supported_modes->empty()) {
for (const auto &it : *this->supported_modes) {
size.add_uint32_force(1, static_cast<uint32_t>(it));
}
}
size.add_uint32(1, this->supported_features);
}
void WaterHeaterStateResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_fixed32(1, this->key);
buffer.encode_float(2, this->current_temperature);
buffer.encode_float(3, this->target_temperature);
buffer.encode_uint32(4, static_cast<uint32_t>(this->mode));
#ifdef USE_DEVICES
buffer.encode_uint32(5, this->device_id);
#endif
buffer.encode_uint32(6, this->state);
buffer.encode_float(7, this->target_temperature_low);
buffer.encode_float(8, this->target_temperature_high);
}
void WaterHeaterStateResponse::calculate_size(ProtoSize &size) const {
size.add_fixed32(1, this->key);
size.add_float(1, this->current_temperature);
size.add_float(1, this->target_temperature);
size.add_uint32(1, static_cast<uint32_t>(this->mode));
#ifdef USE_DEVICES
size.add_uint32(1, this->device_id);
#endif
size.add_uint32(1, this->state);
size.add_float(1, this->target_temperature_low);
size.add_float(1, this->target_temperature_high);
}
bool WaterHeaterCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 2:
this->has_fields = value.as_uint32();
break;
case 3:
this->mode = static_cast<enums::WaterHeaterMode>(value.as_uint32());
break;
#ifdef USE_DEVICES
case 5:
this->device_id = value.as_uint32();
break;
#endif
case 6:
this->state = value.as_uint32();
break;
default:
return false;
}
return true;
}
bool WaterHeaterCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
switch (field_id) {
case 1:
this->key = value.as_fixed32();
break;
case 4:
this->target_temperature = value.as_float();
break;
case 7:
this->target_temperature_low = value.as_float();
break;
case 8:
this->target_temperature_high = value.as_float();
break;
default:
return false;
}
return true;
}
#endif
#ifdef USE_NUMBER #ifdef USE_NUMBER
void ListEntitiesNumberResponse::encode(ProtoWriteBuffer buffer) const { void ListEntitiesNumberResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->object_id_ref_); buffer.encode_string(1, this->object_id_ref_);
@@ -1867,7 +1996,7 @@ void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_uint32(7, static_cast<uint32_t>(this->entity_category)); buffer.encode_uint32(7, static_cast<uint32_t>(this->entity_category));
buffer.encode_bool(8, this->supports_pause); buffer.encode_bool(8, this->supports_pause);
for (auto &it : this->supported_formats) { for (auto &it : this->supported_formats) {
buffer.encode_message(9, it, true); buffer.encode_message(9, it);
} }
#ifdef USE_DEVICES #ifdef USE_DEVICES
buffer.encode_uint32(10, this->device_id); buffer.encode_uint32(10, this->device_id);
@@ -1987,7 +2116,7 @@ void BluetoothLERawAdvertisement::calculate_size(ProtoSize &size) const {
} }
void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer buffer) const { void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer buffer) const {
for (uint16_t i = 0; i < this->advertisements_len; i++) { for (uint16_t i = 0; i < this->advertisements_len; i++) {
buffer.encode_message(1, this->advertisements[i], true); buffer.encode_message(1, this->advertisements[i]);
} }
} }
void BluetoothLERawAdvertisementsResponse::calculate_size(ProtoSize &size) const { void BluetoothLERawAdvertisementsResponse::calculate_size(ProtoSize &size) const {
@@ -2060,7 +2189,7 @@ void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer buffer) const {
buffer.encode_uint32(2, this->handle); buffer.encode_uint32(2, this->handle);
buffer.encode_uint32(3, this->properties); buffer.encode_uint32(3, this->properties);
for (auto &it : this->descriptors) { for (auto &it : this->descriptors) {
buffer.encode_message(4, it, true); buffer.encode_message(4, it);
} }
buffer.encode_uint32(5, this->short_uuid); buffer.encode_uint32(5, this->short_uuid);
} }
@@ -2081,7 +2210,7 @@ void BluetoothGATTService::encode(ProtoWriteBuffer buffer) const {
} }
buffer.encode_uint32(2, this->handle); buffer.encode_uint32(2, this->handle);
for (auto &it : this->characteristics) { for (auto &it : this->characteristics) {
buffer.encode_message(3, it, true); buffer.encode_message(3, it);
} }
buffer.encode_uint32(4, this->short_uuid); buffer.encode_uint32(4, this->short_uuid);
} }
@@ -2097,7 +2226,7 @@ void BluetoothGATTService::calculate_size(ProtoSize &size) const {
void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer buffer) const { void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_uint64(1, this->address); buffer.encode_uint64(1, this->address);
for (auto &it : this->services) { for (auto &it : this->services) {
buffer.encode_message(2, it, true); buffer.encode_message(2, it);
} }
} }
void BluetoothGATTGetServicesResponse::calculate_size(ProtoSize &size) const { void BluetoothGATTGetServicesResponse::calculate_size(ProtoSize &size) const {
@@ -2557,7 +2686,7 @@ bool VoiceAssistantConfigurationRequest::decode_length(uint32_t field_id, ProtoL
} }
void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer buffer) const { void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer buffer) const {
for (auto &it : this->available_wake_words) { for (auto &it : this->available_wake_words) {
buffer.encode_message(1, it, true); buffer.encode_message(1, it);
} }
for (const auto &it : *this->active_wake_words) { for (const auto &it : *this->active_wake_words) {
buffer.encode_string(2, it, true); buffer.encode_string(2, it, true);

View File

@@ -129,6 +129,25 @@ enum ClimatePreset : uint32_t {
CLIMATE_PRESET_ACTIVITY = 7, CLIMATE_PRESET_ACTIVITY = 7,
}; };
#endif #endif
#ifdef USE_WATER_HEATER
enum WaterHeaterMode : uint32_t {
WATER_HEATER_MODE_OFF = 0,
WATER_HEATER_MODE_ECO = 1,
WATER_HEATER_MODE_ELECTRIC = 2,
WATER_HEATER_MODE_PERFORMANCE = 3,
WATER_HEATER_MODE_HIGH_DEMAND = 4,
WATER_HEATER_MODE_HEAT_PUMP = 5,
WATER_HEATER_MODE_GAS = 6,
};
#endif
enum WaterHeaterCommandHasField : uint32_t {
WATER_HEATER_COMMAND_HAS_NONE = 0,
WATER_HEATER_COMMAND_HAS_MODE = 1,
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE = 2,
WATER_HEATER_COMMAND_HAS_STATE = 4,
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8,
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH = 16,
};
#ifdef USE_NUMBER #ifdef USE_NUMBER
enum NumberMode : uint32_t { enum NumberMode : uint32_t {
NUMBER_MODE_AUTO = 0, NUMBER_MODE_AUTO = 0,
@@ -765,7 +784,7 @@ class FanStateResponse final : public StateResponseProtoMessage {
class FanCommandRequest final : public CommandProtoMessage { class FanCommandRequest final : public CommandProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 31; static constexpr uint8_t MESSAGE_TYPE = 31;
static constexpr uint8_t ESTIMATED_SIZE = 38; static constexpr uint8_t ESTIMATED_SIZE = 48;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "fan_command_request"; } const char *message_name() const override { return "fan_command_request"; }
#endif #endif
@@ -778,7 +797,8 @@ class FanCommandRequest final : public CommandProtoMessage {
bool has_speed_level{false}; bool has_speed_level{false};
int32_t speed_level{0}; int32_t speed_level{0};
bool has_preset_mode{false}; bool has_preset_mode{false};
std::string preset_mode{}; const uint8_t *preset_mode{nullptr};
uint16_t preset_mode_len{0};
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -1053,11 +1073,12 @@ class SubscribeLogsResponse final : public ProtoMessage {
class NoiseEncryptionSetKeyRequest final : public ProtoDecodableMessage { class NoiseEncryptionSetKeyRequest final : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 124; static constexpr uint8_t MESSAGE_TYPE = 124;
static constexpr uint8_t ESTIMATED_SIZE = 9; static constexpr uint8_t ESTIMATED_SIZE = 19;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "noise_encryption_set_key_request"; } const char *message_name() const override { return "noise_encryption_set_key_request"; }
#endif #endif
std::string key{}; const uint8_t *key{nullptr};
uint16_t key_len{0};
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -1201,13 +1222,16 @@ class SubscribeHomeAssistantStateResponse final : public ProtoMessage {
class HomeAssistantStateResponse final : public ProtoDecodableMessage { class HomeAssistantStateResponse final : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 40; static constexpr uint8_t MESSAGE_TYPE = 40;
static constexpr uint8_t ESTIMATED_SIZE = 27; static constexpr uint8_t ESTIMATED_SIZE = 57;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "home_assistant_state_response"; } const char *message_name() const override { return "home_assistant_state_response"; }
#endif #endif
std::string entity_id{}; const uint8_t *entity_id{nullptr};
std::string state{}; uint16_t entity_id_len{0};
std::string attribute{}; const uint8_t *state{nullptr};
uint16_t state_len{0};
const uint8_t *attribute{nullptr};
uint16_t attribute_len{0};
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -1475,7 +1499,7 @@ class ClimateStateResponse final : public StateResponseProtoMessage {
class ClimateCommandRequest final : public CommandProtoMessage { class ClimateCommandRequest final : public CommandProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 48; static constexpr uint8_t MESSAGE_TYPE = 48;
static constexpr uint8_t ESTIMATED_SIZE = 84; static constexpr uint8_t ESTIMATED_SIZE = 104;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "climate_command_request"; } const char *message_name() const override { return "climate_command_request"; }
#endif #endif
@@ -1492,11 +1516,13 @@ class ClimateCommandRequest final : public CommandProtoMessage {
bool has_swing_mode{false}; bool has_swing_mode{false};
enums::ClimateSwingMode swing_mode{}; enums::ClimateSwingMode swing_mode{};
bool has_custom_fan_mode{false}; bool has_custom_fan_mode{false};
std::string custom_fan_mode{}; const uint8_t *custom_fan_mode{nullptr};
uint16_t custom_fan_mode_len{0};
bool has_preset{false}; bool has_preset{false};
enums::ClimatePreset preset{}; enums::ClimatePreset preset{};
bool has_custom_preset{false}; bool has_custom_preset{false};
std::string custom_preset{}; const uint8_t *custom_preset{nullptr};
uint16_t custom_preset_len{0};
bool has_target_humidity{false}; bool has_target_humidity{false};
float target_humidity{0.0f}; float target_humidity{0.0f};
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@@ -1509,6 +1535,70 @@ class ClimateCommandRequest final : public CommandProtoMessage {
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
#endif #endif
#ifdef USE_WATER_HEATER
class ListEntitiesWaterHeaterResponse final : public InfoResponseProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 132;
static constexpr uint8_t ESTIMATED_SIZE = 63;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_water_heater_response"; }
#endif
float min_temperature{0.0f};
float max_temperature{0.0f};
float target_temperature_step{0.0f};
const water_heater::WaterHeaterModeMask *supported_modes{};
uint32_t supported_features{0};
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:
};
class WaterHeaterStateResponse final : public StateResponseProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 133;
static constexpr uint8_t ESTIMATED_SIZE = 35;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "water_heater_state_response"; }
#endif
float current_temperature{0.0f};
float target_temperature{0.0f};
enums::WaterHeaterMode mode{};
uint32_t state{0};
float target_temperature_low{0.0f};
float target_temperature_high{0.0f};
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:
};
class WaterHeaterCommandRequest final : public CommandProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 134;
static constexpr uint8_t ESTIMATED_SIZE = 34;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "water_heater_command_request"; }
#endif
uint32_t has_fields{0};
enums::WaterHeaterMode mode{};
float target_temperature{0.0f};
uint32_t state{0};
float target_temperature_low{0.0f};
float target_temperature_high{0.0f};
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
#endif
#ifdef USE_NUMBER #ifdef USE_NUMBER
class ListEntitiesNumberResponse final : public InfoResponseProtoMessage { class ListEntitiesNumberResponse final : public InfoResponseProtoMessage {
public: public:

View File

@@ -348,6 +348,47 @@ template<> const char *proto_enum_to_string<enums::ClimatePreset>(enums::Climate
} }
} }
#endif #endif
#ifdef USE_WATER_HEATER
template<> const char *proto_enum_to_string<enums::WaterHeaterMode>(enums::WaterHeaterMode value) {
switch (value) {
case enums::WATER_HEATER_MODE_OFF:
return "WATER_HEATER_MODE_OFF";
case enums::WATER_HEATER_MODE_ECO:
return "WATER_HEATER_MODE_ECO";
case enums::WATER_HEATER_MODE_ELECTRIC:
return "WATER_HEATER_MODE_ELECTRIC";
case enums::WATER_HEATER_MODE_PERFORMANCE:
return "WATER_HEATER_MODE_PERFORMANCE";
case enums::WATER_HEATER_MODE_HIGH_DEMAND:
return "WATER_HEATER_MODE_HIGH_DEMAND";
case enums::WATER_HEATER_MODE_HEAT_PUMP:
return "WATER_HEATER_MODE_HEAT_PUMP";
case enums::WATER_HEATER_MODE_GAS:
return "WATER_HEATER_MODE_GAS";
default:
return "UNKNOWN";
}
}
#endif
template<>
const char *proto_enum_to_string<enums::WaterHeaterCommandHasField>(enums::WaterHeaterCommandHasField value) {
switch (value) {
case enums::WATER_HEATER_COMMAND_HAS_NONE:
return "WATER_HEATER_COMMAND_HAS_NONE";
case enums::WATER_HEATER_COMMAND_HAS_MODE:
return "WATER_HEATER_COMMAND_HAS_MODE";
case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE:
return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE";
case enums::WATER_HEATER_COMMAND_HAS_STATE:
return "WATER_HEATER_COMMAND_HAS_STATE";
case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW:
return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW";
case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH:
return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH";
default:
return "UNKNOWN";
}
}
#ifdef USE_NUMBER #ifdef USE_NUMBER
template<> const char *proto_enum_to_string<enums::NumberMode>(enums::NumberMode value) { template<> const char *proto_enum_to_string<enums::NumberMode>(enums::NumberMode value) {
switch (value) { switch (value) {
@@ -923,7 +964,9 @@ void FanCommandRequest::dump_to(std::string &out) const {
dump_field(out, "has_speed_level", this->has_speed_level); dump_field(out, "has_speed_level", this->has_speed_level);
dump_field(out, "speed_level", this->speed_level); dump_field(out, "speed_level", this->speed_level);
dump_field(out, "has_preset_mode", this->has_preset_mode); dump_field(out, "has_preset_mode", this->has_preset_mode);
dump_field(out, "preset_mode", this->preset_mode); out.append(" preset_mode: ");
out.append(format_hex_pretty(this->preset_mode, this->preset_mode_len));
out.append("\n");
#ifdef USE_DEVICES #ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id); dump_field(out, "device_id", this->device_id);
#endif #endif
@@ -1113,7 +1156,7 @@ void SubscribeLogsResponse::dump_to(std::string &out) const {
void NoiseEncryptionSetKeyRequest::dump_to(std::string &out) const { void NoiseEncryptionSetKeyRequest::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "NoiseEncryptionSetKeyRequest"); MessageDumpHelper helper(out, "NoiseEncryptionSetKeyRequest");
out.append(" key: "); out.append(" key: ");
out.append(format_hex_pretty(reinterpret_cast<const uint8_t *>(this->key.data()), this->key.size())); out.append(format_hex_pretty(this->key, this->key_len));
out.append("\n"); out.append("\n");
} }
void NoiseEncryptionSetKeyResponse::dump_to(std::string &out) const { dump_field(out, "success", this->success); } void NoiseEncryptionSetKeyResponse::dump_to(std::string &out) const { dump_field(out, "success", this->success); }
@@ -1182,9 +1225,15 @@ void SubscribeHomeAssistantStateResponse::dump_to(std::string &out) const {
} }
void HomeAssistantStateResponse::dump_to(std::string &out) const { void HomeAssistantStateResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "HomeAssistantStateResponse"); MessageDumpHelper helper(out, "HomeAssistantStateResponse");
dump_field(out, "entity_id", this->entity_id); out.append(" entity_id: ");
dump_field(out, "state", this->state); out.append(format_hex_pretty(this->entity_id, this->entity_id_len));
dump_field(out, "attribute", this->attribute); out.append("\n");
out.append(" state: ");
out.append(format_hex_pretty(this->state, this->state_len));
out.append("\n");
out.append(" attribute: ");
out.append(format_hex_pretty(this->attribute, this->attribute_len));
out.append("\n");
} }
#endif #endif
void GetTimeRequest::dump_to(std::string &out) const { out.append("GetTimeRequest {}"); } void GetTimeRequest::dump_to(std::string &out) const { out.append("GetTimeRequest {}"); }
@@ -1374,11 +1423,15 @@ void ClimateCommandRequest::dump_to(std::string &out) const {
dump_field(out, "has_swing_mode", this->has_swing_mode); dump_field(out, "has_swing_mode", this->has_swing_mode);
dump_field(out, "swing_mode", static_cast<enums::ClimateSwingMode>(this->swing_mode)); dump_field(out, "swing_mode", static_cast<enums::ClimateSwingMode>(this->swing_mode));
dump_field(out, "has_custom_fan_mode", this->has_custom_fan_mode); dump_field(out, "has_custom_fan_mode", this->has_custom_fan_mode);
dump_field(out, "custom_fan_mode", this->custom_fan_mode); out.append(" custom_fan_mode: ");
out.append(format_hex_pretty(this->custom_fan_mode, this->custom_fan_mode_len));
out.append("\n");
dump_field(out, "has_preset", this->has_preset); dump_field(out, "has_preset", this->has_preset);
dump_field(out, "preset", static_cast<enums::ClimatePreset>(this->preset)); dump_field(out, "preset", static_cast<enums::ClimatePreset>(this->preset));
dump_field(out, "has_custom_preset", this->has_custom_preset); dump_field(out, "has_custom_preset", this->has_custom_preset);
dump_field(out, "custom_preset", this->custom_preset); out.append(" custom_preset: ");
out.append(format_hex_pretty(this->custom_preset, this->custom_preset_len));
out.append("\n");
dump_field(out, "has_target_humidity", this->has_target_humidity); dump_field(out, "has_target_humidity", this->has_target_humidity);
dump_field(out, "target_humidity", this->target_humidity); dump_field(out, "target_humidity", this->target_humidity);
#ifdef USE_DEVICES #ifdef USE_DEVICES
@@ -1386,6 +1439,55 @@ void ClimateCommandRequest::dump_to(std::string &out) const {
#endif #endif
} }
#endif #endif
#ifdef USE_WATER_HEATER
void ListEntitiesWaterHeaterResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ListEntitiesWaterHeaterResponse");
dump_field(out, "object_id", this->object_id_ref_);
dump_field(out, "key", this->key);
dump_field(out, "name", this->name_ref_);
#ifdef USE_ENTITY_ICON
dump_field(out, "icon", this->icon_ref_);
#endif
dump_field(out, "disabled_by_default", this->disabled_by_default);
dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category));
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
#endif
dump_field(out, "min_temperature", this->min_temperature);
dump_field(out, "max_temperature", this->max_temperature);
dump_field(out, "target_temperature_step", this->target_temperature_step);
for (const auto &it : *this->supported_modes) {
dump_field(out, "supported_modes", static_cast<enums::WaterHeaterMode>(it), 4);
}
dump_field(out, "supported_features", this->supported_features);
}
void WaterHeaterStateResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "WaterHeaterStateResponse");
dump_field(out, "key", this->key);
dump_field(out, "current_temperature", this->current_temperature);
dump_field(out, "target_temperature", this->target_temperature);
dump_field(out, "mode", static_cast<enums::WaterHeaterMode>(this->mode));
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
#endif
dump_field(out, "state", this->state);
dump_field(out, "target_temperature_low", this->target_temperature_low);
dump_field(out, "target_temperature_high", this->target_temperature_high);
}
void WaterHeaterCommandRequest::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "WaterHeaterCommandRequest");
dump_field(out, "key", this->key);
dump_field(out, "has_fields", this->has_fields);
dump_field(out, "mode", static_cast<enums::WaterHeaterMode>(this->mode));
dump_field(out, "target_temperature", this->target_temperature);
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
#endif
dump_field(out, "state", this->state);
dump_field(out, "target_temperature_low", this->target_temperature_low);
dump_field(out, "target_temperature_high", this->target_temperature_high);
}
#endif
#ifdef USE_NUMBER #ifdef USE_NUMBER
void ListEntitiesNumberResponse::dump_to(std::string &out) const { void ListEntitiesNumberResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ListEntitiesNumberResponse"); MessageDumpHelper helper(out, "ListEntitiesNumberResponse");

View File

@@ -10,6 +10,10 @@
#include "esphome/components/climate/climate_traits.h" #include "esphome/components/climate/climate_traits.h"
#endif #endif
#ifdef USE_WATER_HEATER
#include "esphome/components/water_heater/water_heater.h"
#endif
#ifdef USE_LIGHT #ifdef USE_LIGHT
#include "esphome/components/light/light_traits.h" #include "esphome/components/light/light_traits.h"
#endif #endif

View File

@@ -621,6 +621,17 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
this->on_homeassistant_action_response(msg); this->on_homeassistant_action_response(msg);
break; break;
} }
#endif
#ifdef USE_WATER_HEATER
case WaterHeaterCommandRequest::MESSAGE_TYPE: {
WaterHeaterCommandRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_water_heater_command_request: %s", msg.dump().c_str());
#endif
this->on_water_heater_command_request(msg);
break;
}
#endif #endif
default: default:
break; break;

View File

@@ -91,6 +91,10 @@ class APIServerConnectionBase : public ProtoService {
virtual void on_climate_command_request(const ClimateCommandRequest &value){}; virtual void on_climate_command_request(const ClimateCommandRequest &value){};
#endif #endif
#ifdef USE_WATER_HEATER
virtual void on_water_heater_command_request(const WaterHeaterCommandRequest &value){};
#endif
#ifdef USE_NUMBER #ifdef USE_NUMBER
virtual void on_number_command_request(const NumberCommandRequest &value){}; virtual void on_number_command_request(const NumberCommandRequest &value){};
#endif #endif

View File

@@ -335,6 +335,10 @@ API_DISPATCH_UPDATE(valve::Valve, valve)
API_DISPATCH_UPDATE(media_player::MediaPlayer, media_player) API_DISPATCH_UPDATE(media_player::MediaPlayer, media_player)
#endif #endif
#ifdef USE_WATER_HEATER
API_DISPATCH_UPDATE(water_heater::WaterHeater, water_heater)
#endif
#ifdef USE_EVENT #ifdef USE_EVENT
// Event is a special case - unlike other entities with simple state fields, // Event is a special case - unlike other entities with simple state fields,
// events store their state in a member accessed via obj->get_last_event_type() // events store their state in a member accessed via obj->get_last_event_type()
@@ -419,18 +423,18 @@ void APIServer::handle_action_response(uint32_t call_id, bool success, const std
#endif // USE_API_HOMEASSISTANT_SERVICES #endif // USE_API_HOMEASSISTANT_SERVICES
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
// Helper to add subscription (reduces duplication) // Helper to add subscription (reduces duplication) - const char* version (zero allocation)
void APIServer::add_state_subscription_(const char *entity_id, const char *attribute, void APIServer::add_state_subscription_(const char *entity_id, const char *attribute, std::function<void(StringRef)> f,
std::function<void(std::string)> f, bool once) { bool once) {
this->state_subs_.push_back(HomeAssistantStateSubscription{ this->state_subs_.push_back(HomeAssistantStateSubscription{
.entity_id = entity_id, .attribute = attribute, .callback = std::move(f), .once = once, .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) // entity_id_dynamic_storage and attribute_dynamic_storage remain nullptr (no heap allocation)
}); });
} }
// Helper to add subscription with heap-allocated strings (reduces duplication) // Helper to add subscription with heap-allocated strings and StringRef callback
void APIServer::add_state_subscription_(std::string entity_id, optional<std::string> attribute, void APIServer::add_state_subscription_(std::string entity_id, optional<std::string> attribute,
std::function<void(std::string)> f, bool once) { std::function<void(StringRef)> f, bool once) {
HomeAssistantStateSubscription sub; HomeAssistantStateSubscription sub;
// Allocate heap storage for the strings // Allocate heap storage for the strings
sub.entity_id_dynamic_storage = std::make_unique<std::string>(std::move(entity_id)); sub.entity_id_dynamic_storage = std::make_unique<std::string>(std::move(entity_id));
@@ -448,25 +452,45 @@ void APIServer::add_state_subscription_(std::string entity_id, optional<std::str
this->state_subs_.push_back(std::move(sub)); this->state_subs_.push_back(std::move(sub));
} }
// Legacy helper: wraps std::string callback and delegates to StringRef version
void APIServer::add_state_subscription_(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f, bool once) {
// Wrap callback to convert StringRef -> std::string, then delegate
this->add_state_subscription_(std::move(entity_id), std::move(attribute),
std::function<void(StringRef)>([f = std::move(f)](StringRef state) { f(state.str()); }),
once);
}
// New const char* overload (for internal components - zero allocation) // New const char* overload (for internal components - zero allocation)
void APIServer::subscribe_home_assistant_state(const char *entity_id, const char *attribute, void APIServer::subscribe_home_assistant_state(const char *entity_id, const char *attribute,
std::function<void(std::string)> f) { std::function<void(StringRef)> f) {
this->add_state_subscription_(entity_id, attribute, std::move(f), false); this->add_state_subscription_(entity_id, attribute, std::move(f), false);
} }
void APIServer::get_home_assistant_state(const char *entity_id, const char *attribute, void APIServer::get_home_assistant_state(const char *entity_id, const char *attribute,
std::function<void(std::string)> f) { std::function<void(StringRef)> f) {
this->add_state_subscription_(entity_id, attribute, std::move(f), true); this->add_state_subscription_(entity_id, attribute, std::move(f), true);
} }
// Existing std::string overload (for custom_api_device.h - heap allocation) // std::string overload with StringRef callback (zero-allocation callback)
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute, void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(std::string)> f) { std::function<void(StringRef)> f) {
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), 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, void APIServer::get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(std::string)> f) { std::function<void(StringRef)> f) {
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), true);
}
// Legacy std::string overload (for custom_api_device.h - converts StringRef to std::string)
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f) {
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), false);
}
void APIServer::get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f) {
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), true); this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), true);
} }

View File

@@ -133,6 +133,9 @@ class APIServer : public Component,
#ifdef USE_MEDIA_PLAYER #ifdef USE_MEDIA_PLAYER
void on_media_player_update(media_player::MediaPlayer *obj) override; void on_media_player_update(media_player::MediaPlayer *obj) override;
#endif #endif
#ifdef USE_WATER_HEATER
void on_water_heater_update(water_heater::WaterHeater *obj) override;
#endif
#ifdef USE_API_HOMEASSISTANT_SERVICES #ifdef USE_API_HOMEASSISTANT_SERVICES
void send_homeassistant_action(const HomeassistantActionRequest &call); void send_homeassistant_action(const HomeassistantActionRequest &call);
@@ -192,7 +195,7 @@ class APIServer : public Component,
struct HomeAssistantStateSubscription { struct HomeAssistantStateSubscription {
const char *entity_id; // Pointer to flash (internal) or heap (external) const char *entity_id; // Pointer to flash (internal) or heap (external)
const char *attribute; // Pointer to flash or nullptr (nullptr means no attribute) const char *attribute; // Pointer to flash or nullptr (nullptr means no attribute)
std::function<void(std::string)> callback; std::function<void(StringRef)> callback;
bool once; bool once;
// Dynamic storage for external components using std::string API (custom_api_device.h) // Dynamic storage for external components using std::string API (custom_api_device.h)
@@ -202,14 +205,20 @@ class APIServer : public Component,
}; };
// New const char* overload (for internal components - zero allocation) // 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 subscribe_home_assistant_state(const char *entity_id, const char *attribute, std::function<void(StringRef)> f);
void get_home_assistant_state(const char *entity_id, const char *attribute, std::function<void(std::string)> f); void get_home_assistant_state(const char *entity_id, const char *attribute, std::function<void(StringRef)> f);
// Existing std::string overload (for custom_api_device.h - heap allocation) // std::string overload with StringRef callback (for custom_api_device.h with zero-allocation callback)
void subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute, void subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(std::string)> f); std::function<void(StringRef)> f);
void get_home_assistant_state(std::string entity_id, optional<std::string> attribute, void get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(std::string)> f); std::function<void(StringRef)> f);
// Legacy std::string overload (for custom_api_device.h - converts StringRef to std::string for callback)
void subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f);
void get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f);
const std::vector<HomeAssistantStateSubscription> &get_state_subs() const; const std::vector<HomeAssistantStateSubscription> &get_state_subs() const;
#endif #endif
@@ -233,10 +242,12 @@ class APIServer : public Component,
#endif // USE_API_NOISE #endif // USE_API_NOISE
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
// Helper methods to reduce code duplication // Helper methods to reduce code duplication
void add_state_subscription_(const char *entity_id, const char *attribute, std::function<void(std::string)> f, void add_state_subscription_(const char *entity_id, const char *attribute, std::function<void(StringRef)> f,
bool once);
void add_state_subscription_(std::string entity_id, optional<std::string> attribute, std::function<void(StringRef)> f,
bool once); bool once);
void add_state_subscription_(std::string entity_id, optional<std::string> attribute, void add_state_subscription_(std::string entity_id, optional<std::string> attribute,
std::function<void(std::string)> f, bool once); std::function<void(const std::string &)> f, bool once);
#endif // USE_API_HOMEASSISTANT_STATES #endif // USE_API_HOMEASSISTANT_STATES
// Pointers and pointer-like types first (4 bytes each) // Pointers and pointer-like types first (4 bytes each)
std::unique_ptr<socket::Socket> socket_ = nullptr; std::unique_ptr<socket::Socket> socket_ = nullptr;

View File

@@ -122,17 +122,29 @@ class CustomAPIDevice {
* subscribe_homeassistant_state(&CustomNativeAPI::on_state_changed, "climate.kitchen", "current_temperature"); * subscribe_homeassistant_state(&CustomNativeAPI::on_state_changed, "climate.kitchen", "current_temperature");
* } * }
* *
* void on_state_changed(std::string state) { * void on_state_changed(StringRef state) {
* // State of sensor.weather_forecast is `state` * // State of climate.kitchen current_temperature is `state`
* // Use state.c_str() for C string, state.str() for std::string
* } * }
* ``` * ```
* *
* @tparam T The class type creating the service, automatically deduced from the function pointer. * @tparam T The class type creating the service, automatically deduced from the function pointer.
* @param callback The member function to call when the entity state changes. * @param callback The member function to call when the entity state changes (zero-allocation).
* @param entity_id The entity_id to track. * @param entity_id The entity_id to track.
* @param attribute The entity state attribute to track. * @param attribute The entity state attribute to track.
*/ */
template<typename T> template<typename T>
void subscribe_homeassistant_state(void (T::*callback)(StringRef), const std::string &entity_id,
const std::string &attribute = "") {
auto f = std::bind(callback, (T *) this, std::placeholders::_1);
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), std::move(f));
}
/** Subscribe to the state (or attribute state) of an entity from Home Assistant (legacy std::string version).
*
* @deprecated Use the StringRef overload for zero-allocation callbacks.
*/
template<typename T>
void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id, void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id,
const std::string &attribute = "") { const std::string &attribute = "") {
auto f = std::bind(callback, (T *) this, std::placeholders::_1); auto f = std::bind(callback, (T *) this, std::placeholders::_1);
@@ -148,23 +160,42 @@ class CustomAPIDevice {
* subscribe_homeassistant_state(&CustomNativeAPI::on_state_changed, "sensor.weather_forecast"); * subscribe_homeassistant_state(&CustomNativeAPI::on_state_changed, "sensor.weather_forecast");
* } * }
* *
* void on_state_changed(std::string entity_id, std::string state) { * void on_state_changed(const std::string &entity_id, StringRef state) {
* // State of `entity_id` is `state` * // State of `entity_id` is `state`
* } * }
* ``` * ```
* *
* @tparam T The class type creating the service, automatically deduced from the function pointer. * @tparam T The class type creating the service, automatically deduced from the function pointer.
* @param callback The member function to call when the entity state changes. * @param callback The member function to call when the entity state changes (zero-allocation for state).
* @param entity_id The entity_id to track. * @param entity_id The entity_id to track.
* @param attribute The entity state attribute to track. * @param attribute The entity state attribute to track.
*/ */
template<typename T> template<typename T>
void subscribe_homeassistant_state(void (T::*callback)(const std::string &, StringRef), const std::string &entity_id,
const std::string &attribute = "") {
auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1);
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), std::move(f));
}
/** Subscribe to the state (or attribute state) of an entity from Home Assistant (legacy std::string version).
*
* @deprecated Use the StringRef overload for zero-allocation callbacks.
*/
template<typename T>
void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id, void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id,
const std::string &attribute = "") { const std::string &attribute = "") {
auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1); auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1);
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), f); global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), f);
} }
#else #else
template<typename T>
void subscribe_homeassistant_state(void (T::*callback)(StringRef), const std::string &entity_id,
const std::string &attribute = "") {
static_assert(sizeof(T) == 0,
"subscribe_homeassistant_state() requires 'homeassistant_states: true' in the 'api:' section "
"of your YAML configuration");
}
template<typename T> template<typename T>
void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id, void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id,
const std::string &attribute = "") { const std::string &attribute = "") {
@@ -173,6 +204,14 @@ class CustomAPIDevice {
"of your YAML configuration"); "of your YAML configuration");
} }
template<typename T>
void subscribe_homeassistant_state(void (T::*callback)(const std::string &, StringRef), const std::string &entity_id,
const std::string &attribute = "") {
static_assert(sizeof(T) == 0,
"subscribe_homeassistant_state() requires 'homeassistant_states: true' in the 'api:' section "
"of your YAML configuration");
}
template<typename T> template<typename T>
void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id, void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id,
const std::string &attribute = "") { const std::string &attribute = "") {

View File

@@ -73,6 +73,9 @@ LIST_ENTITIES_HANDLER(media_player, media_player::MediaPlayer, ListEntitiesMedia
LIST_ENTITIES_HANDLER(alarm_control_panel, alarm_control_panel::AlarmControlPanel, LIST_ENTITIES_HANDLER(alarm_control_panel, alarm_control_panel::AlarmControlPanel,
ListEntitiesAlarmControlPanelResponse) ListEntitiesAlarmControlPanelResponse)
#endif #endif
#ifdef USE_WATER_HEATER
LIST_ENTITIES_HANDLER(water_heater, water_heater::WaterHeater, ListEntitiesWaterHeaterResponse)
#endif
#ifdef USE_EVENT #ifdef USE_EVENT
LIST_ENTITIES_HANDLER(event, event::Event, ListEntitiesEventResponse) LIST_ENTITIES_HANDLER(event, event::Event, ListEntitiesEventResponse)
#endif #endif

View File

@@ -82,6 +82,9 @@ class ListEntitiesIterator : public ComponentIterator {
#ifdef USE_ALARM_CONTROL_PANEL #ifdef USE_ALARM_CONTROL_PANEL
bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *entity) override; bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *entity) override;
#endif #endif
#ifdef USE_WATER_HEATER
bool on_water_heater(water_heater::WaterHeater *entity) override;
#endif
#ifdef USE_EVENT #ifdef USE_EVENT
bool on_event(event::Event *entity) override; bool on_event(event::Event *entity) override;
#endif #endif

View File

@@ -334,7 +334,7 @@ class ProtoWriteBuffer {
void encode_sint64(uint32_t field_id, int64_t value, bool force = false) { void encode_sint64(uint32_t field_id, int64_t value, bool force = false) {
this->encode_uint64(field_id, encode_zigzag64(value), force); this->encode_uint64(field_id, encode_zigzag64(value), force);
} }
void encode_message(uint32_t field_id, const ProtoMessage &value, bool force = false); void encode_message(uint32_t field_id, const ProtoMessage &value);
std::vector<uint8_t> *get_buffer() const { return buffer_; } std::vector<uint8_t> *get_buffer() const { return buffer_; }
protected: protected:
@@ -795,7 +795,7 @@ class ProtoSize {
}; };
// Implementation of encode_message - must be after ProtoMessage is defined // Implementation of encode_message - must be after ProtoMessage is defined
inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value, bool force) { inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value) {
this->encode_field_raw(field_id, 2); // type 2: Length-delimited message this->encode_field_raw(field_id, 2); // type 2: Length-delimited message
// Calculate the message size first // Calculate the message size first

View File

@@ -60,6 +60,9 @@ INITIAL_STATE_HANDLER(media_player, media_player::MediaPlayer)
#ifdef USE_ALARM_CONTROL_PANEL #ifdef USE_ALARM_CONTROL_PANEL
INITIAL_STATE_HANDLER(alarm_control_panel, alarm_control_panel::AlarmControlPanel) INITIAL_STATE_HANDLER(alarm_control_panel, alarm_control_panel::AlarmControlPanel)
#endif #endif
#ifdef USE_WATER_HEATER
INITIAL_STATE_HANDLER(water_heater, water_heater::WaterHeater)
#endif
#ifdef USE_UPDATE #ifdef USE_UPDATE
INITIAL_STATE_HANDLER(update, update::UpdateEntity) INITIAL_STATE_HANDLER(update, update::UpdateEntity)
#endif #endif

View File

@@ -76,6 +76,9 @@ class InitialStateIterator : public ComponentIterator {
#ifdef USE_ALARM_CONTROL_PANEL #ifdef USE_ALARM_CONTROL_PANEL
bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *entity) override; bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *entity) override;
#endif #endif
#ifdef USE_WATER_HEATER
bool on_water_heater(water_heater::WaterHeater *entity) override;
#endif
#ifdef USE_EVENT #ifdef USE_EVENT
bool on_event(event::Event *event) override { return true; }; bool on_event(event::Event *event) override { return true; };
#endif #endif

View File

@@ -0,0 +1,14 @@
import esphome.codegen as cg
CODEOWNERS = ["@jasstrong", "@ximex", "@freekode"]
aqi_ns = cg.esphome_ns.namespace("aqi")
AQICalculatorType = aqi_ns.enum("AQICalculatorType")
CONF_AQI = "aqi"
CONF_CALCULATION_TYPE = "calculation_type"
AQI_CALCULATION_TYPE = {
"CAQI": AQICalculatorType.CAQI_TYPE,
"AQI": AQICalculatorType.AQI_TYPE,
}

View File

@@ -2,13 +2,11 @@
#include <cstdint> #include <cstdint>
namespace esphome { namespace esphome::aqi {
namespace hm3301 {
class AbstractAQICalculator { class AbstractAQICalculator {
public: public:
virtual uint16_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) = 0; virtual uint16_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) = 0;
}; };
} // namespace hm3301 } // namespace esphome::aqi
} // namespace esphome

View File

@@ -1,10 +1,11 @@
#pragma once #pragma once
#include <climits> #include <climits>
#include "abstract_aqi_calculator.h" #include "abstract_aqi_calculator.h"
// https://document.airnow.gov/technical-assistance-document-for-the-reporting-of-daily-air-quailty.pdf // https://document.airnow.gov/technical-assistance-document-for-the-reporting-of-daily-air-quailty.pdf
namespace esphome { namespace esphome::aqi {
namespace hm3301 {
class AQICalculator : public AbstractAQICalculator { class AQICalculator : public AbstractAQICalculator {
public: public:
@@ -28,6 +29,9 @@ class AQICalculator : public AbstractAQICalculator {
int calculate_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) { int calculate_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) {
int grid_index = get_grid_index_(value, array); int grid_index = get_grid_index_(value, array);
if (grid_index == -1) {
return -1;
}
int aqi_lo = index_grid_[grid_index][0]; int aqi_lo = index_grid_[grid_index][0];
int aqi_hi = index_grid_[grid_index][1]; int aqi_hi = index_grid_[grid_index][1];
int conc_lo = array[grid_index][0]; int conc_lo = array[grid_index][0];
@@ -46,5 +50,4 @@ class AQICalculator : public AbstractAQICalculator {
} }
}; };
} // namespace hm3301 } // namespace esphome::aqi
} // namespace esphome

View File

@@ -3,8 +3,7 @@
#include "caqi_calculator.h" #include "caqi_calculator.h"
#include "aqi_calculator.h" #include "aqi_calculator.h"
namespace esphome { namespace esphome::aqi {
namespace hm3301 {
enum AQICalculatorType { CAQI_TYPE = 0, AQI_TYPE = 1 }; enum AQICalculatorType { CAQI_TYPE = 0, AQI_TYPE = 1 };
@@ -12,18 +11,17 @@ class AQICalculatorFactory {
public: public:
AbstractAQICalculator *get_calculator(AQICalculatorType type) { AbstractAQICalculator *get_calculator(AQICalculatorType type) {
if (type == 0) { if (type == 0) {
return caqi_calculator_; return &this->caqi_calculator_;
} else if (type == 1) { } else if (type == 1) {
return aqi_calculator_; return &this->aqi_calculator_;
} }
return nullptr; return nullptr;
} }
protected: protected:
CAQICalculator *caqi_calculator_ = new CAQICalculator(); CAQICalculator caqi_calculator_;
AQICalculator *aqi_calculator_ = new AQICalculator(); AQICalculator aqi_calculator_;
}; };
} // namespace hm3301 } // namespace esphome::aqi
} // namespace esphome

View File

@@ -3,8 +3,7 @@
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "abstract_aqi_calculator.h" #include "abstract_aqi_calculator.h"
namespace esphome { namespace esphome::aqi {
namespace hm3301 {
class CAQICalculator : public AbstractAQICalculator { class CAQICalculator : public AbstractAQICalculator {
public: public:
@@ -48,5 +47,4 @@ class CAQICalculator : public AbstractAQICalculator {
} }
}; };
} // namespace hm3301 } // namespace esphome::aqi
} // namespace esphome

View File

@@ -1,6 +1,6 @@
#include "audio_reader.h" #include "audio_reader.h"
#ifdef USE_ESP_IDF #ifdef USE_ESP32
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"

View File

@@ -1,6 +1,6 @@
#pragma once #pragma once
#ifdef USE_ESP_IDF #ifdef USE_ESP32
#include "audio.h" #include "audio.h"
#include "audio_transfer_buffer.h" #include "audio_transfer_buffer.h"

View File

@@ -1,12 +1,7 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import ble_client, climate from esphome.components import climate
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import CONF_HEAT_MODE, CONF_TEMPERATURE_SOURCE
CONF_HEAT_MODE,
CONF_RECEIVE_TIMEOUT,
CONF_TEMPERATURE_SOURCE,
CONF_TIME_ID,
)
from .. import BEDJET_CLIENT_SCHEMA, bedjet_ns, register_bedjet_child from .. import BEDJET_CLIENT_SCHEMA, bedjet_ns, register_bedjet_child
@@ -38,22 +33,6 @@ CONFIG_SCHEMA = (
} }
) )
.extend(cv.polling_component_schema("60s")) .extend(cv.polling_component_schema("60s"))
.extend(
# TODO: remove compat layer.
{
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/"
),
cv.Optional(CONF_TIME_ID): cv.invalid(
"The 'time_id' option has been moved to the `bedjet` component."
),
cv.Optional(CONF_RECEIVE_TIMEOUT): cv.invalid(
"The 'receive_timeout' option has been moved to the `bedjet` component."
),
}
)
.extend(BEDJET_CLIENT_SCHEMA) .extend(BEDJET_CLIENT_SCHEMA)
) )

View File

@@ -1,8 +1,8 @@
#include "bh1750.h" #include "bh1750.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome::bh1750 { namespace esphome {
namespace bh1750 {
static const char *const TAG = "bh1750.sensor"; static const char *const TAG = "bh1750.sensor";
@@ -13,31 +13,6 @@ static const uint8_t BH1750_COMMAND_ONE_TIME_L = 0b00100011;
static const uint8_t BH1750_COMMAND_ONE_TIME_H = 0b00100000; static const uint8_t BH1750_COMMAND_ONE_TIME_H = 0b00100000;
static const uint8_t BH1750_COMMAND_ONE_TIME_H2 = 0b00100001; static const uint8_t BH1750_COMMAND_ONE_TIME_H2 = 0b00100001;
static constexpr uint32_t MEASUREMENT_TIMEOUT_MS = 2000;
static constexpr float HIGH_LIGHT_THRESHOLD_LX = 7000.0f;
// Measurement time constants (datasheet values)
static constexpr uint16_t MTREG_DEFAULT = 69;
static constexpr uint16_t MTREG_MIN = 31;
static constexpr uint16_t MTREG_MAX = 254;
static constexpr uint16_t MEAS_TIME_L_MS = 24; // L-resolution max measurement time @ mtreg=69
static constexpr uint16_t MEAS_TIME_H_MS = 180; // H/H2-resolution max measurement time @ mtreg=69
// Conversion constants (datasheet formulas)
static constexpr float RESOLUTION_DIVISOR = 1.2f; // counts to lux conversion divisor
static constexpr float MODE_H2_DIVISOR = 2.0f; // H2 mode has 2x higher resolution
// MTreg calculation constants
static constexpr int COUNTS_TARGET = 50000; // Target counts for optimal range (avoid saturation)
static constexpr int COUNTS_NUMERATOR = 10;
static constexpr int COUNTS_DENOMINATOR = 12;
// MTreg register bit manipulation constants
static constexpr uint8_t MTREG_HI_SHIFT = 5; // High 3 bits start at bit 5
static constexpr uint8_t MTREG_HI_MASK = 0b111; // 3-bit mask for high bits
static constexpr uint8_t MTREG_LO_SHIFT = 0; // Low 5 bits start at bit 0
static constexpr uint8_t MTREG_LO_MASK = 0b11111; // 5-bit mask for low bits
/* /*
bh1750 properties: bh1750 properties:
@@ -68,7 +43,74 @@ void BH1750Sensor::setup() {
this->mark_failed(); this->mark_failed();
return; return;
} }
this->state_ = IDLE; }
void BH1750Sensor::read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function<void(float)> &f) {
// turn on (after one-shot sensor automatically powers down)
uint8_t turn_on = BH1750_COMMAND_POWER_ON;
if (this->write(&turn_on, 1) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Power on failed");
f(NAN);
return;
}
if (active_mtreg_ != mtreg) {
// set mtreg
uint8_t mtreg_hi = BH1750_COMMAND_MT_REG_HI | ((mtreg >> 5) & 0b111);
uint8_t mtreg_lo = BH1750_COMMAND_MT_REG_LO | ((mtreg >> 0) & 0b11111);
if (this->write(&mtreg_hi, 1) != i2c::ERROR_OK || this->write(&mtreg_lo, 1) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Set measurement time failed");
active_mtreg_ = 0;
f(NAN);
return;
}
active_mtreg_ = mtreg;
}
uint8_t cmd;
uint16_t meas_time;
switch (mode) {
case BH1750_MODE_L:
cmd = BH1750_COMMAND_ONE_TIME_L;
meas_time = 24 * mtreg / 69;
break;
case BH1750_MODE_H:
cmd = BH1750_COMMAND_ONE_TIME_H;
meas_time = 180 * mtreg / 69;
break;
case BH1750_MODE_H2:
cmd = BH1750_COMMAND_ONE_TIME_H2;
meas_time = 180 * mtreg / 69;
break;
default:
f(NAN);
return;
}
if (this->write(&cmd, 1) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Start measurement failed");
f(NAN);
return;
}
// probably not needed, but adjust for rounding
meas_time++;
this->set_timeout("read", meas_time, [this, mode, mtreg, f]() {
uint16_t raw_value;
if (this->read(reinterpret_cast<uint8_t *>(&raw_value), 2) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Read data failed");
f(NAN);
return;
}
raw_value = i2c::i2ctohs(raw_value);
float lx = float(raw_value) / 1.2f;
lx *= 69.0f / mtreg;
if (mode == BH1750_MODE_H2)
lx /= 2.0f;
f(lx);
});
} }
void BH1750Sensor::dump_config() { void BH1750Sensor::dump_config() {
@@ -82,189 +124,45 @@ void BH1750Sensor::dump_config() {
} }
void BH1750Sensor::update() { void BH1750Sensor::update() {
const uint32_t now = millis(); // first do a quick measurement in L-mode with full range
// to find right range
// Start coarse measurement to determine optimal mode/mtreg this->read_lx_(BH1750_MODE_L, 31, [this](float val) {
if (this->state_ != IDLE) { if (std::isnan(val)) {
// Safety timeout: reset if stuck this->status_set_warning();
if (now - this->measurement_start_time_ > MEASUREMENT_TIMEOUT_MS) { this->publish_state(NAN);
ESP_LOGW(TAG, "Measurement timeout, resetting state");
this->state_ = IDLE;
} else {
ESP_LOGW(TAG, "Previous measurement not complete, skipping update");
return; return;
} }
}
if (!this->start_measurement_(BH1750_MODE_L, MTREG_MIN, now)) { BH1750Mode use_mode;
this->status_set_warning(); uint8_t use_mtreg;
this->publish_state(NAN); if (val <= 7000) {
return; use_mode = BH1750_MODE_H2;
} use_mtreg = 254;
} else {
this->state_ = WAITING_COARSE_MEASUREMENT; use_mode = BH1750_MODE_H;
this->enable_loop(); // Enable loop while measurement in progress // lx = counts / 1.2 * (69 / mtreg)
} // -> mtreg = counts / 1.2 * (69 / lx)
// calculate for counts=50000 (allow some range to not saturate, but maximize mtreg)
void BH1750Sensor::loop() { // -> mtreg = 50000*(10/12)*(69/lx)
const uint32_t now = App.get_loop_component_start_time(); int ideal_mtreg = 50000 * 10 * 69 / (12 * (int) val);
use_mtreg = std::min(254, std::max(31, ideal_mtreg));
switch (this->state_) {
case IDLE:
// Disable loop when idle to save cycles
this->disable_loop();
break;
case WAITING_COARSE_MEASUREMENT:
if (now - this->measurement_start_time_ >= this->measurement_duration_) {
this->state_ = READING_COARSE_RESULT;
}
break;
case READING_COARSE_RESULT: {
float lx;
if (!this->read_measurement_(lx)) {
this->fail_and_reset_();
break;
}
this->process_coarse_result_(lx);
// Start fine measurement with optimal settings
// fetch millis() again since the read can take a bit
if (!this->start_measurement_(this->fine_mode_, this->fine_mtreg_, millis())) {
this->fail_and_reset_();
break;
}
this->state_ = WAITING_FINE_MEASUREMENT;
break;
} }
ESP_LOGV(TAG, "L result: %f -> Calculated mode=%d, mtreg=%d", val, (int) use_mode, use_mtreg);
case WAITING_FINE_MEASUREMENT: this->read_lx_(use_mode, use_mtreg, [this](float val) {
if (now - this->measurement_start_time_ >= this->measurement_duration_) { if (std::isnan(val)) {
this->state_ = READING_FINE_RESULT; this->status_set_warning();
this->publish_state(NAN);
return;
} }
break; ESP_LOGD(TAG, "'%s': Illuminance=%.1flx", this->get_name().c_str(), val);
case READING_FINE_RESULT: {
float lx;
if (!this->read_measurement_(lx)) {
this->fail_and_reset_();
break;
}
ESP_LOGD(TAG, "'%s': Illuminance=%.1flx", this->get_name().c_str(), lx);
this->status_clear_warning(); this->status_clear_warning();
this->publish_state(lx); this->publish_state(val);
this->state_ = IDLE; });
break; });
}
}
}
bool BH1750Sensor::start_measurement_(BH1750Mode mode, uint8_t mtreg, uint32_t now) {
// Power on
uint8_t turn_on = BH1750_COMMAND_POWER_ON;
if (this->write(&turn_on, 1) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Power on failed");
return false;
}
// Set MTreg if changed
if (this->active_mtreg_ != mtreg) {
uint8_t mtreg_hi = BH1750_COMMAND_MT_REG_HI | ((mtreg >> MTREG_HI_SHIFT) & MTREG_HI_MASK);
uint8_t mtreg_lo = BH1750_COMMAND_MT_REG_LO | ((mtreg >> MTREG_LO_SHIFT) & MTREG_LO_MASK);
if (this->write(&mtreg_hi, 1) != i2c::ERROR_OK || this->write(&mtreg_lo, 1) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Set measurement time failed");
this->active_mtreg_ = 0;
return false;
}
this->active_mtreg_ = mtreg;
}
// Start measurement
uint8_t cmd;
uint16_t meas_time;
switch (mode) {
case BH1750_MODE_L:
cmd = BH1750_COMMAND_ONE_TIME_L;
meas_time = MEAS_TIME_L_MS * mtreg / MTREG_DEFAULT;
break;
case BH1750_MODE_H:
cmd = BH1750_COMMAND_ONE_TIME_H;
meas_time = MEAS_TIME_H_MS * mtreg / MTREG_DEFAULT;
break;
case BH1750_MODE_H2:
cmd = BH1750_COMMAND_ONE_TIME_H2;
meas_time = MEAS_TIME_H_MS * mtreg / MTREG_DEFAULT;
break;
default:
return false;
}
if (this->write(&cmd, 1) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Start measurement failed");
return false;
}
// Store current measurement parameters
this->current_mode_ = mode;
this->current_mtreg_ = mtreg;
this->measurement_start_time_ = now;
this->measurement_duration_ = meas_time + 1; // Add 1ms for safety
return true;
}
bool BH1750Sensor::read_measurement_(float &lx_out) {
uint16_t raw_value;
if (this->read(reinterpret_cast<uint8_t *>(&raw_value), 2) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Read data failed");
return false;
}
raw_value = i2c::i2ctohs(raw_value);
float lx = float(raw_value) / RESOLUTION_DIVISOR;
lx *= float(MTREG_DEFAULT) / this->current_mtreg_;
if (this->current_mode_ == BH1750_MODE_H2) {
lx /= MODE_H2_DIVISOR;
}
lx_out = lx;
return true;
}
void BH1750Sensor::process_coarse_result_(float lx) {
if (std::isnan(lx)) {
// Use defaults if coarse measurement failed
this->fine_mode_ = BH1750_MODE_H2;
this->fine_mtreg_ = MTREG_MAX;
return;
}
if (lx <= HIGH_LIGHT_THRESHOLD_LX) {
this->fine_mode_ = BH1750_MODE_H2;
this->fine_mtreg_ = MTREG_MAX;
} else {
this->fine_mode_ = BH1750_MODE_H;
// lx = counts / 1.2 * (69 / mtreg)
// -> mtreg = counts / 1.2 * (69 / lx)
// calculate for counts=50000 (allow some range to not saturate, but maximize mtreg)
// -> mtreg = 50000*(10/12)*(69/lx)
int ideal_mtreg = COUNTS_TARGET * COUNTS_NUMERATOR * MTREG_DEFAULT / (COUNTS_DENOMINATOR * (int) lx);
this->fine_mtreg_ = std::min((int) MTREG_MAX, std::max((int) MTREG_MIN, ideal_mtreg));
}
ESP_LOGV(TAG, "L result: %.1f -> Calculated mode=%d, mtreg=%d", lx, (int) this->fine_mode_, this->fine_mtreg_);
}
void BH1750Sensor::fail_and_reset_() {
this->status_set_warning();
this->publish_state(NAN);
this->state_ = IDLE;
} }
float BH1750Sensor::get_setup_priority() const { return setup_priority::DATA; } float BH1750Sensor::get_setup_priority() const { return setup_priority::DATA; }
} // namespace esphome::bh1750 } // namespace bh1750
} // namespace esphome

View File

@@ -4,9 +4,10 @@
#include "esphome/components/sensor/sensor.h" #include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h" #include "esphome/components/i2c/i2c.h"
namespace esphome::bh1750 { namespace esphome {
namespace bh1750 {
enum BH1750Mode : uint8_t { enum BH1750Mode {
BH1750_MODE_L, BH1750_MODE_L,
BH1750_MODE_H, BH1750_MODE_H,
BH1750_MODE_H2, BH1750_MODE_H2,
@@ -20,36 +21,13 @@ class BH1750Sensor : public sensor::Sensor, public PollingComponent, public i2c:
void setup() override; void setup() override;
void dump_config() override; void dump_config() override;
void update() override; void update() override;
void loop() override;
float get_setup_priority() const override; float get_setup_priority() const override;
protected: protected:
// State machine states void read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function<void(float)> &f);
enum State : uint8_t {
IDLE,
WAITING_COARSE_MEASUREMENT,
READING_COARSE_RESULT,
WAITING_FINE_MEASUREMENT,
READING_FINE_RESULT,
};
// 4-byte aligned members
uint32_t measurement_start_time_{0};
uint32_t measurement_duration_{0};
// 1-byte members grouped together to minimize padding
State state_{IDLE};
BH1750Mode current_mode_{BH1750_MODE_L};
uint8_t current_mtreg_{31};
BH1750Mode fine_mode_{BH1750_MODE_H2};
uint8_t fine_mtreg_{254};
uint8_t active_mtreg_{0}; uint8_t active_mtreg_{0};
// Helper methods
bool start_measurement_(BH1750Mode mode, uint8_t mtreg, uint32_t now);
bool read_measurement_(float &lx_out);
void process_coarse_result_(float lx);
void fail_and_reset_();
}; };
} // namespace esphome::bh1750 } // namespace bh1750
} // namespace esphome

View File

@@ -20,16 +20,6 @@ CONFIG_SCHEMA = (
device_class=DEVICE_CLASS_ILLUMINANCE, device_class=DEVICE_CLASS_ILLUMINANCE,
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
) )
.extend(
{
cv.Optional("resolution"): cv.invalid(
"The 'resolution' option has been removed. The optimal value is now dynamically calculated."
),
cv.Optional("measurement_duration"): cv.invalid(
"The 'measurement_duration' option has been removed. The optimal value is now dynamically calculated."
),
}
)
.extend(cv.polling_component_schema("60s")) .extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x23)) .extend(i2c.i2c_device_schema(0x23))
) )

View File

@@ -50,6 +50,7 @@ TYPES = [
CONFIG_SCHEMA = cv.Schema( CONFIG_SCHEMA = cv.Schema(
{ {
cv.GenerateID(): cv.declare_id(cg.Component),
cv.GenerateID(CONF_BME68X_BSEC2_ID): cv.use_id(BME68xBSEC2Component), cv.GenerateID(CONF_BME68X_BSEC2_ID): cv.use_id(BME68xBSEC2Component),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS, unit_of_measurement=UNIT_CELSIUS,

View File

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

View File

@@ -41,7 +41,7 @@ class Button : public EntityBase, public EntityBase_DeviceClass {
*/ */
virtual void press_action() = 0; virtual void press_action() = 0;
CallbackManager<void()> press_callback_{}; LazyCallbackManager<void()> press_callback_{};
}; };
} // namespace esphome::button } // namespace esphome::button

View File

@@ -25,7 +25,7 @@ _LOGGER = logging.getLogger(__name__)
def AUTO_LOAD() -> list[str]: def AUTO_LOAD() -> list[str]:
auto_load = ["web_server_base", "ota.web_server"] auto_load = ["web_server_base", "ota.web_server"]
if CORE.using_esp_idf: if CORE.is_esp32:
auto_load.append("socket") auto_load.append("socket")
return auto_load return auto_load
@@ -97,10 +97,6 @@ async def to_code(config):
cg.add_define("USE_CAPTIVE_PORTAL") cg.add_define("USE_CAPTIVE_PORTAL")
if CORE.using_arduino: if CORE.using_arduino:
if CORE.is_esp32:
cg.add_library("ESP32 Async UDP", None)
cg.add_library("DNSServer", None)
cg.add_library("WiFi", None)
if CORE.is_esp8266: if CORE.is_esp8266:
cg.add_library("DNSServer", None) cg.add_library("DNSServer", None)
if CORE.is_libretiny: if CORE.is_libretiny:
@@ -110,6 +106,9 @@ async def to_code(config):
# Only compile the ESP-IDF DNS server when using ESP-IDF framework # Only compile the ESP-IDF DNS server when using ESP-IDF framework
FILTER_SOURCE_FILES = filter_source_files_from_platform( FILTER_SOURCE_FILES = filter_source_files_from_platform(
{ {
"dns_server_esp32_idf.cpp": {PlatformFramework.ESP32_IDF}, "dns_server_esp32_idf.cpp": {
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF,
},
} }
) )

View File

@@ -65,22 +65,15 @@ void CaptivePortal::start() {
this->base_->init(); this->base_->init();
if (!this->initialized_) { if (!this->initialized_) {
this->base_->add_handler(this); this->base_->add_handler(this);
#ifdef USE_ESP32
// Enable LRU socket purging to handle captive portal detection probe bursts
// OS captive portal detection makes many simultaneous HTTP requests which can
// exhaust sockets. LRU purging automatically closes oldest idle connections.
this->base_->get_server()->set_lru_purge_enable(true);
#endif
} }
network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip(); network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip();
#ifdef USE_ESP_IDF #if defined(USE_ESP32)
// Create DNS server instance for ESP-IDF // Create DNS server instance for ESP-IDF
this->dns_server_ = make_unique<DNSServer>(); this->dns_server_ = make_unique<DNSServer>();
this->dns_server_->start(ip); this->dns_server_->start(ip);
#endif #elif defined(USE_ARDUINO)
#ifdef USE_ARDUINO
this->dns_server_ = make_unique<DNSServer>(); this->dns_server_ = make_unique<DNSServer>();
this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError); this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError);
this->dns_server_->start(53, ESPHOME_F("*"), ip); this->dns_server_->start(53, ESPHOME_F("*"), ip);

View File

@@ -2,11 +2,10 @@
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#ifdef USE_CAPTIVE_PORTAL #ifdef USE_CAPTIVE_PORTAL
#include <memory> #include <memory>
#ifdef USE_ARDUINO #if defined(USE_ESP32)
#include <DNSServer.h>
#endif
#ifdef USE_ESP_IDF
#include "dns_server_esp32_idf.h" #include "dns_server_esp32_idf.h"
#elif defined(USE_ARDUINO)
#include <DNSServer.h>
#endif #endif
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
@@ -23,15 +22,14 @@ class CaptivePortal : public AsyncWebHandler, public Component {
void setup() override; void setup() override;
void dump_config() override; void dump_config() override;
void loop() override { void loop() override {
#ifdef USE_ARDUINO #if defined(USE_ESP32)
if (this->dns_server_ != nullptr) {
this->dns_server_->processNextRequest();
}
#endif
#ifdef USE_ESP_IDF
if (this->dns_server_ != nullptr) { if (this->dns_server_ != nullptr) {
this->dns_server_->process_next_request(); this->dns_server_->process_next_request();
} }
#elif defined(USE_ARDUINO)
if (this->dns_server_ != nullptr) {
this->dns_server_->processNextRequest();
}
#endif #endif
} }
float get_setup_priority() const override; float get_setup_priority() const override;
@@ -40,10 +38,6 @@ class CaptivePortal : public AsyncWebHandler, public Component {
void end() { void end() {
this->active_ = false; this->active_ = false;
this->disable_loop(); // Stop processing DNS requests this->disable_loop(); // Stop processing DNS requests
#ifdef USE_ESP32
// Disable LRU socket purging now that captive portal is done
this->base_->get_server()->set_lru_purge_enable(false);
#endif
this->base_->deinit(); this->base_->deinit();
if (this->dns_server_ != nullptr) { if (this->dns_server_ != nullptr) {
this->dns_server_->stop(); this->dns_server_->stop();
@@ -68,7 +62,7 @@ class CaptivePortal : public AsyncWebHandler, public Component {
web_server_base::WebServerBase *base_; web_server_base::WebServerBase *base_;
bool initialized_{false}; bool initialized_{false};
bool active_{false}; bool active_{false};
#if defined(USE_ARDUINO) || defined(USE_ESP_IDF) #if defined(USE_ARDUINO) || defined(USE_ESP32)
std::unique_ptr<DNSServer> dns_server_{nullptr}; std::unique_ptr<DNSServer> dns_server_{nullptr};
#endif #endif
}; };

View File

@@ -1,5 +1,5 @@
#include "dns_server_esp32_idf.h" #include "dns_server_esp32_idf.h"
#ifdef USE_ESP_IDF #ifdef USE_ESP32
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
@@ -202,4 +202,4 @@ void DNSServer::process_next_request() {
} // namespace esphome::captive_portal } // namespace esphome::captive_portal
#endif // USE_ESP_IDF #endif // USE_ESP32

View File

@@ -1,5 +1,5 @@
#pragma once #pragma once
#ifdef USE_ESP_IDF #ifdef USE_ESP32
#include <memory> #include <memory>
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
@@ -24,4 +24,4 @@ class DNSServer {
} // namespace esphome::captive_portal } // namespace esphome::captive_portal
#endif // USE_ESP_IDF #endif // USE_ESP32

View File

@@ -1,9 +1,17 @@
from esphome import automation from esphome import automation, pins
from esphome.automation import maybe_simple_id from esphome.automation import maybe_simple_id
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import spi from esphome.components import spi
from esphome.components.const import CONF_CRC_ENABLE, CONF_ON_PACKET
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_CHANNEL, CONF_FREQUENCY, CONF_ID, CONF_WAIT_TIME from esphome.const import (
CONF_CHANNEL,
CONF_DATA,
CONF_FREQUENCY,
CONF_ID,
CONF_WAIT_TIME,
)
from esphome.core import ID
CODEOWNERS = ["@lygris", "@gabest11"] CODEOWNERS = ["@lygris", "@gabest11"]
DEPENDENCIES = ["spi"] DEPENDENCIES = ["spi"]
@@ -29,7 +37,6 @@ CONF_MANCHESTER = "manchester"
CONF_NUM_PREAMBLE = "num_preamble" CONF_NUM_PREAMBLE = "num_preamble"
CONF_SYNC1 = "sync1" CONF_SYNC1 = "sync1"
CONF_SYNC0 = "sync0" CONF_SYNC0 = "sync0"
CONF_PKTLEN = "pktlen"
CONF_MAGN_TARGET = "magn_target" CONF_MAGN_TARGET = "magn_target"
CONF_MAX_LNA_GAIN = "max_lna_gain" CONF_MAX_LNA_GAIN = "max_lna_gain"
CONF_MAX_DVGA_GAIN = "max_dvga_gain" CONF_MAX_DVGA_GAIN = "max_dvga_gain"
@@ -41,6 +48,12 @@ CONF_FILTER_LENGTH_ASK_OOK = "filter_length_ask_ook"
CONF_FREEZE = "freeze" CONF_FREEZE = "freeze"
CONF_HYST_LEVEL = "hyst_level" CONF_HYST_LEVEL = "hyst_level"
# Packet mode config keys
CONF_PACKET_MODE = "packet_mode"
CONF_PACKET_LENGTH = "packet_length"
CONF_WHITENING = "whitening"
CONF_GDO0_PIN = "gdo0_pin"
# Enums # Enums
SyncMode = ns.enum("SyncMode", True) SyncMode = ns.enum("SyncMode", True)
SYNC_MODE = { SYNC_MODE = {
@@ -147,45 +160,89 @@ HYST_LEVEL = {
"High": HystLevel.HYST_LEVEL_HIGH, "High": HystLevel.HYST_LEVEL_HIGH,
} }
# Config key -> Validator mapping # Optional settings to generate setter calls for
CONFIG_MAP = { CONFIG_MAP = {
CONF_OUTPUT_POWER: cv.float_range(min=-30.0, max=11.0), cv.Optional(CONF_OUTPUT_POWER, default=10): cv.float_range(min=-30.0, max=11.0),
CONF_RX_ATTENUATION: cv.enum(RX_ATTENUATION, upper=False), cv.Optional(CONF_RX_ATTENUATION, default="0dB"): cv.enum(
CONF_DC_BLOCKING_FILTER: cv.boolean, RX_ATTENUATION, upper=False
CONF_FREQUENCY: cv.All(cv.frequency, cv.float_range(min=300000000, max=928000000)), ),
CONF_IF_FREQUENCY: cv.All(cv.frequency, cv.float_range(min=25000, max=788000)), cv.Optional(CONF_DC_BLOCKING_FILTER, default=True): cv.boolean,
CONF_FILTER_BANDWIDTH: cv.All(cv.frequency, cv.float_range(min=58000, max=812000)), cv.Optional(CONF_FREQUENCY, default="433.92MHz"): cv.All(
CONF_CHANNEL: cv.uint8_t, cv.frequency, cv.float_range(min=300.0e6, max=928.0e6)
CONF_CHANNEL_SPACING: cv.All(cv.frequency, cv.float_range(min=25000, max=405000)), ),
CONF_FSK_DEVIATION: cv.All(cv.frequency, cv.float_range(min=1500, max=381000)), cv.Optional(CONF_IF_FREQUENCY, default="153kHz"): cv.All(
CONF_MSK_DEVIATION: cv.int_range(min=1, max=8), cv.frequency, cv.float_range(min=25000, max=788000)
CONF_SYMBOL_RATE: cv.float_range(min=600, max=500000), ),
CONF_SYNC_MODE: cv.enum(SYNC_MODE, upper=False), cv.Optional(CONF_FILTER_BANDWIDTH, default="203kHz"): cv.All(
CONF_CARRIER_SENSE_ABOVE_THRESHOLD: cv.boolean, cv.frequency, cv.float_range(min=58000, max=812000)
CONF_MODULATION_TYPE: cv.enum(MODULATION, upper=False), ),
CONF_MANCHESTER: cv.boolean, cv.Optional(CONF_CHANNEL, default=0): cv.uint8_t,
CONF_NUM_PREAMBLE: cv.int_range(min=0, max=7), cv.Optional(CONF_CHANNEL_SPACING, default="200kHz"): cv.All(
CONF_SYNC1: cv.hex_uint8_t, cv.frequency, cv.float_range(min=25000, max=405000)
CONF_SYNC0: cv.hex_uint8_t, ),
CONF_PKTLEN: cv.uint8_t, cv.Optional(CONF_FSK_DEVIATION): cv.All(
CONF_MAGN_TARGET: cv.enum(MAGN_TARGET, upper=False), cv.frequency, cv.float_range(min=1500, max=381000)
CONF_MAX_LNA_GAIN: cv.enum(MAX_LNA_GAIN, upper=False), ),
CONF_MAX_DVGA_GAIN: cv.enum(MAX_DVGA_GAIN, upper=False), cv.Optional(CONF_MSK_DEVIATION): cv.int_range(min=1, max=8),
CONF_CARRIER_SENSE_ABS_THR: cv.int_range(min=-8, max=7), cv.Optional(CONF_SYMBOL_RATE, default=5000): cv.float_range(min=600, max=500000),
CONF_CARRIER_SENSE_REL_THR: cv.enum(CARRIER_SENSE_REL_THR, upper=False), cv.Optional(CONF_SYNC_MODE, default="16/16"): cv.enum(SYNC_MODE, upper=False),
CONF_LNA_PRIORITY: cv.boolean, cv.Optional(CONF_CARRIER_SENSE_ABOVE_THRESHOLD, default=False): cv.boolean,
CONF_FILTER_LENGTH_FSK_MSK: cv.enum(FILTER_LENGTH_FSK_MSK, upper=False), cv.Optional(CONF_MODULATION_TYPE, default="ASK/OOK"): cv.enum(
CONF_FILTER_LENGTH_ASK_OOK: cv.enum(FILTER_LENGTH_ASK_OOK, upper=False), MODULATION, upper=False
CONF_FREEZE: cv.enum(FREEZE, upper=False), ),
CONF_WAIT_TIME: cv.enum(WAIT_TIME, upper=False), cv.Optional(CONF_MANCHESTER, default=False): cv.boolean,
CONF_HYST_LEVEL: cv.enum(HYST_LEVEL, upper=False), cv.Optional(CONF_NUM_PREAMBLE, default=2): cv.int_range(min=0, max=7),
cv.Optional(CONF_SYNC1, default=0xD3): cv.hex_uint8_t,
cv.Optional(CONF_SYNC0, default=0x91): cv.hex_uint8_t,
cv.Optional(CONF_MAGN_TARGET, default="42dB"): cv.enum(MAGN_TARGET, upper=False),
cv.Optional(CONF_MAX_LNA_GAIN, default="Default"): cv.enum(
MAX_LNA_GAIN, upper=False
),
cv.Optional(CONF_MAX_DVGA_GAIN, default="-3"): cv.enum(MAX_DVGA_GAIN, upper=False),
cv.Optional(CONF_CARRIER_SENSE_ABS_THR): cv.int_range(min=-8, max=7),
cv.Optional(CONF_CARRIER_SENSE_REL_THR): cv.enum(
CARRIER_SENSE_REL_THR, upper=False
),
cv.Optional(CONF_LNA_PRIORITY, default=False): cv.boolean,
cv.Optional(CONF_FILTER_LENGTH_FSK_MSK): cv.enum(
FILTER_LENGTH_FSK_MSK, upper=False
),
cv.Optional(CONF_FILTER_LENGTH_ASK_OOK): cv.enum(
FILTER_LENGTH_ASK_OOK, upper=False
),
cv.Optional(CONF_FREEZE): cv.enum(FREEZE, upper=False),
cv.Optional(CONF_WAIT_TIME, default="32"): cv.enum(WAIT_TIME, upper=False),
cv.Optional(CONF_HYST_LEVEL): cv.enum(HYST_LEVEL, upper=False),
cv.Optional(CONF_PACKET_MODE, default=False): cv.boolean,
cv.Optional(CONF_PACKET_LENGTH): cv.uint8_t,
cv.Optional(CONF_CRC_ENABLE, default=False): cv.boolean,
cv.Optional(CONF_WHITENING, default=False): cv.boolean,
} }
CONFIG_SCHEMA = (
cv.Schema({cv.GenerateID(): cv.declare_id(CC1101Component)}) def _validate_packet_mode(config):
.extend({cv.Optional(key): validator for key, validator in CONFIG_MAP.items()}) if config.get(CONF_PACKET_MODE, False):
if CONF_GDO0_PIN not in config:
raise cv.Invalid("gdo0_pin is required when packet_mode is enabled")
if CONF_PACKET_LENGTH not in config:
raise cv.Invalid("packet_length is required when packet_mode is enabled")
if config[CONF_PACKET_LENGTH] > 64:
raise cv.Invalid("packet_length must be <= 64 (FIFO size)")
return config
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(CC1101Component),
cv.Optional(CONF_GDO0_PIN): pins.internal_gpio_input_pin_schema,
cv.Optional(CONF_ON_PACKET): automation.validate_automation(single=True),
}
)
.extend(CONFIG_MAP)
.extend(cv.COMPONENT_SCHEMA) .extend(cv.COMPONENT_SCHEMA)
.extend(spi.spi_device_schema(cs_pin_required=True)) .extend(spi.spi_device_schema(cs_pin_required=True)),
_validate_packet_mode,
) )
@@ -194,16 +251,34 @@ async def to_code(config):
await cg.register_component(var, config) await cg.register_component(var, config)
await spi.register_spi_device(var, config) await spi.register_spi_device(var, config)
for key in CONFIG_MAP: for opt in CONFIG_MAP:
key = opt.schema
if key in config: if key in config:
cg.add(getattr(var, f"set_{key}")(config[key])) cg.add(getattr(var, f"set_{key}")(config[key]))
if CONF_GDO0_PIN in config:
gdo0_pin = await cg.gpio_pin_expression(config[CONF_GDO0_PIN])
cg.add(var.set_gdo0_pin(gdo0_pin))
if CONF_ON_PACKET in config:
await automation.build_automation(
var.get_packet_trigger(),
[
(cg.std_vector.template(cg.uint8), "x"),
(cg.float_, "rssi"),
(cg.uint8, "lqi"),
],
config[CONF_ON_PACKET],
)
# Actions # Actions
BeginTxAction = ns.class_("BeginTxAction", automation.Action) BeginTxAction = ns.class_("BeginTxAction", automation.Action)
BeginRxAction = ns.class_("BeginRxAction", automation.Action) BeginRxAction = ns.class_("BeginRxAction", automation.Action)
ResetAction = ns.class_("ResetAction", automation.Action) ResetAction = ns.class_("ResetAction", automation.Action)
SetIdleAction = ns.class_("SetIdleAction", automation.Action) SetIdleAction = ns.class_("SetIdleAction", automation.Action)
SendPacketAction = ns.class_(
"SendPacketAction", automation.Action, cg.Parented.template(CC1101Component)
)
CC1101_ACTION_SCHEMA = cv.Schema( CC1101_ACTION_SCHEMA = cv.Schema(
maybe_simple_id({cv.GenerateID(CONF_ID): cv.use_id(CC1101Component)}) maybe_simple_id({cv.GenerateID(CONF_ID): cv.use_id(CC1101Component)})
@@ -218,3 +293,42 @@ async def cc1101_action_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg) var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID]) await cg.register_parented(var, config[CONF_ID])
return var return var
def validate_raw_data(value):
if isinstance(value, str):
return value.encode("utf-8")
if isinstance(value, list):
return cv.Schema([cv.hex_uint8_t])(value)
raise cv.Invalid(
"data must either be a string wrapped in quotes or a list of bytes"
)
SEND_PACKET_ACTION_SCHEMA = cv.maybe_simple_value(
{
cv.GenerateID(): cv.use_id(CC1101Component),
cv.Required(CONF_DATA): cv.templatable(validate_raw_data),
},
key=CONF_DATA,
)
@automation.register_action(
"cc1101.send_packet", SendPacketAction, SEND_PACKET_ACTION_SCHEMA
)
async def send_packet_action_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
data = config[CONF_DATA]
if isinstance(data, bytes):
data = list(data)
if cg.is_template(data):
templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8))
cg.add(var.set_data_template(templ))
else:
# Generate static array in flash to avoid RAM copy
arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8)
arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data))
cg.add(var.set_data_static(arr, len(data)))
return var

View File

@@ -98,25 +98,8 @@ CC1101Component::CC1101Component() {
this->state_.LENGTH_CONFIG = 2; this->state_.LENGTH_CONFIG = 2;
this->state_.FS_AUTOCAL = 1; this->state_.FS_AUTOCAL = 1;
// Default Settings
this->set_frequency(433920);
this->set_if_frequency(153);
this->set_filter_bandwidth(203);
this->set_channel(0);
this->set_channel_spacing(200);
this->set_symbol_rate(5000);
this->set_sync_mode(SyncMode::SYNC_MODE_NONE);
this->set_carrier_sense_above_threshold(true);
this->set_modulation_type(Modulation::MODULATION_ASK_OOK);
this->set_magn_target(MagnTarget::MAGN_TARGET_42DB);
this->set_max_lna_gain(MaxLnaGain::MAX_LNA_GAIN_DEFAULT);
this->set_max_dvga_gain(MaxDvgaGain::MAX_DVGA_GAIN_MINUS_3);
this->set_lna_priority(false);
this->set_wait_time(WaitTime::WAIT_TIME_32_SAMPLES);
// CRITICAL: Initialize PA Table to avoid transmitting 0 power (Silence) // CRITICAL: Initialize PA Table to avoid transmitting 0 power (Silence)
memset(this->pa_table_, 0, sizeof(this->pa_table_)); memset(this->pa_table_, 0, sizeof(this->pa_table_));
this->set_output_power(10.0f);
} }
void CC1101Component::setup() { void CC1101Component::setup() {
@@ -143,6 +126,11 @@ void CC1101Component::setup() {
return; return;
} }
// Setup GDO0 pin if configured
if (this->gdo0_pin_ != nullptr) {
this->gdo0_pin_->setup();
}
this->initialized_ = true; this->initialized_ = true;
for (uint8_t i = 0; i <= static_cast<uint8_t>(Register::TEST0); i++) { for (uint8_t i = 0; i <= static_cast<uint8_t>(Register::TEST0); i++) {
@@ -151,8 +139,70 @@ void CC1101Component::setup() {
} }
this->write_(static_cast<Register>(i)); this->write_(static_cast<Register>(i));
} }
this->write_(Register::PATABLE, this->pa_table_, sizeof(this->pa_table_)); this->set_output_power(this->output_power_requested_);
this->strobe_(Command::RX); this->strobe_(Command::RX);
// Defer pin mode setup until after all components have completed setup()
// This handles the case where remote_transmitter runs after CC1101 and changes pin mode
if (this->gdo0_pin_ != nullptr) {
this->defer([this]() { this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT); });
}
}
void CC1101Component::loop() {
if (this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO) || this->gdo0_pin_ == nullptr ||
!this->gdo0_pin_->digital_read()) {
return;
}
// Read state
this->read_(Register::RXBYTES);
uint8_t rx_bytes = this->state_.NUM_RXBYTES;
bool overflow = this->state_.RXFIFO_OVERFLOW;
if (overflow || rx_bytes == 0) {
ESP_LOGW(TAG, "RX FIFO overflow, flushing");
this->enter_idle_();
this->strobe_(Command::FRX);
this->strobe_(Command::RX);
this->wait_for_state_(State::RX);
return;
}
// Read packet
uint8_t payload_length, expected_rx;
if (this->state_.LENGTH_CONFIG == static_cast<uint8_t>(LengthConfig::LENGTH_CONFIG_VARIABLE)) {
this->read_(Register::FIFO, &payload_length, 1);
expected_rx = payload_length + 1;
} else {
payload_length = this->state_.PKTLEN;
expected_rx = payload_length;
}
if (payload_length == 0 || payload_length > 64 || rx_bytes != expected_rx) {
ESP_LOGW(TAG, "Invalid packet: rx_bytes %u, payload_length %u", rx_bytes, payload_length);
this->enter_idle_();
this->strobe_(Command::FRX);
this->strobe_(Command::RX);
this->wait_for_state_(State::RX);
return;
}
this->packet_.resize(payload_length);
this->read_(Register::FIFO, this->packet_.data(), payload_length);
// Read status from registers (more reliable than FIFO status bytes due to timing issues)
this->read_(Register::RSSI);
this->read_(Register::LQI);
float rssi = (this->state_.RSSI * RSSI_STEP) - RSSI_OFFSET;
bool crc_ok = (this->state_.LQI & STATUS_CRC_OK_MASK) != 0;
uint8_t lqi = this->state_.LQI & STATUS_LQI_MASK;
if (this->state_.CRC_EN == 0 || crc_ok) {
this->packet_trigger_->trigger(this->packet_, rssi, lqi);
}
// Return to rx
this->enter_idle_();
this->strobe_(Command::FRX);
this->strobe_(Command::RX);
this->wait_for_state_(State::RX);
} }
void CC1101Component::dump_config() { void CC1101Component::dump_config() {
@@ -177,9 +227,12 @@ void CC1101Component::dump_config() {
} }
void CC1101Component::begin_tx() { void CC1101Component::begin_tx() {
// Ensure Packet Format is 3 (Async Serial), use GDO0 as input during TX // Ensure Packet Format is 3 (Async Serial)
this->write_(Register::PKTCTRL0, 0x32); this->write_(Register::PKTCTRL0, 0x32);
ESP_LOGV(TAG, "Beginning TX sequence"); ESP_LOGV(TAG, "Beginning TX sequence");
if (this->gdo0_pin_ != nullptr) {
this->gdo0_pin_->pin_mode(gpio::FLAG_OUTPUT);
}
this->strobe_(Command::TX); this->strobe_(Command::TX);
if (!this->wait_for_state_(State::TX, 50)) { if (!this->wait_for_state_(State::TX, 50)) {
ESP_LOGW(TAG, "Timed out waiting for TX state!"); ESP_LOGW(TAG, "Timed out waiting for TX state!");
@@ -188,6 +241,9 @@ void CC1101Component::begin_tx() {
void CC1101Component::begin_rx() { void CC1101Component::begin_rx() {
ESP_LOGV(TAG, "Beginning RX sequence"); ESP_LOGV(TAG, "Beginning RX sequence");
if (this->gdo0_pin_ != nullptr) {
this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT);
}
this->strobe_(Command::RX); this->strobe_(Command::RX);
} }
@@ -201,20 +257,6 @@ void CC1101Component::set_idle() {
this->enter_idle_(); this->enter_idle_();
} }
void CC1101Component::set_gdo0_config(uint8_t value) {
this->state_.GDO0_CFG = value;
if (this->initialized_) {
this->write_(Register::IOCFG0);
}
}
void CC1101Component::set_gdo2_config(uint8_t value) {
this->state_.GDO2_CFG = value;
if (this->initialized_) {
this->write_(Register::IOCFG2);
}
}
bool CC1101Component::wait_for_state_(State target_state, uint32_t timeout_ms) { bool CC1101Component::wait_for_state_(State target_state, uint32_t timeout_ms) {
uint32_t start = millis(); uint32_t start = millis();
while (millis() - start < timeout_ms) { while (millis() - start < timeout_ms) {
@@ -282,6 +324,33 @@ void CC1101Component::read_(Register reg, uint8_t *buffer, size_t length) {
this->disable(); this->disable();
} }
CC1101Error CC1101Component::transmit_packet(const std::vector<uint8_t> &packet) {
if (this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO)) {
return CC1101Error::PARAMS;
}
// Write packet
this->enter_idle_();
this->strobe_(Command::FTX);
if (this->state_.LENGTH_CONFIG == static_cast<uint8_t>(LengthConfig::LENGTH_CONFIG_VARIABLE)) {
this->write_(Register::FIFO, static_cast<uint8_t>(packet.size()));
}
this->write_(Register::FIFO, packet.data(), packet.size());
this->strobe_(Command::TX);
if (!this->wait_for_state_(State::IDLE, 1000)) {
ESP_LOGW(TAG, "TX timeout");
this->enter_idle_();
this->strobe_(Command::RX);
this->wait_for_state_(State::RX);
return CC1101Error::TIMEOUT;
}
// Return to rx
this->strobe_(Command::RX);
this->wait_for_state_(State::RX);
return CC1101Error::NONE;
}
// Setters // Setters
void CC1101Component::set_output_power(float value) { void CC1101Component::set_output_power(float value) {
this->output_power_requested_ = value; this->output_power_requested_ = value;
@@ -428,6 +497,7 @@ void CC1101Component::set_modulation_type(Modulation value) {
this->state_.PA_POWER = value == Modulation::MODULATION_ASK_OOK ? 1 : 0; this->state_.PA_POWER = value == Modulation::MODULATION_ASK_OOK ? 1 : 0;
if (this->initialized_) { if (this->initialized_) {
this->enter_idle_(); this->enter_idle_();
this->set_output_power(this->output_power_requested_);
this->write_(Register::MDMCFG2); this->write_(Register::MDMCFG2);
this->write_(Register::FREND0); this->write_(Register::FREND0);
this->strobe_(Command::RX); this->strobe_(Command::RX);
@@ -462,13 +532,6 @@ void CC1101Component::set_sync0(uint8_t value) {
} }
} }
void CC1101Component::set_pktlen(uint8_t value) {
this->state_.PKTLEN = value;
if (this->initialized_) {
this->write_(Register::PKTLEN);
}
}
void CC1101Component::set_magn_target(MagnTarget value) { void CC1101Component::set_magn_target(MagnTarget value) {
this->state_.MAGN_TARGET = static_cast<uint8_t>(value); this->state_.MAGN_TARGET = static_cast<uint8_t>(value);
if (this->initialized_) { if (this->initialized_) {
@@ -546,4 +609,53 @@ void CC1101Component::set_hyst_level(HystLevel value) {
} }
} }
void CC1101Component::set_packet_mode(bool value) {
this->state_.PKT_FORMAT =
static_cast<uint8_t>(value ? PacketFormat::PACKET_FORMAT_FIFO : PacketFormat::PACKET_FORMAT_ASYNC_SERIAL);
if (value) {
// Configure GDO0 for FIFO status (asserts on RX FIFO threshold or end of packet)
this->state_.GDO0_CFG = 0x01;
// Set max RX FIFO threshold to ensure we only trigger on end-of-packet
this->state_.FIFO_THR = 15;
// Don't append status bytes to FIFO - we read from registers instead
this->state_.APPEND_STATUS = 0;
} else {
// Configure GDO0 for serial data (async serial mode)
this->state_.GDO0_CFG = 0x0D;
}
if (this->initialized_) {
this->write_(Register::PKTCTRL0);
this->write_(Register::PKTCTRL1);
this->write_(Register::IOCFG0);
this->write_(Register::FIFOTHR);
}
}
void CC1101Component::set_packet_length(uint8_t value) {
if (value == 0) {
this->state_.LENGTH_CONFIG = static_cast<uint8_t>(LengthConfig::LENGTH_CONFIG_VARIABLE);
} else {
this->state_.LENGTH_CONFIG = static_cast<uint8_t>(LengthConfig::LENGTH_CONFIG_FIXED);
this->state_.PKTLEN = value;
}
if (this->initialized_) {
this->write_(Register::PKTCTRL0);
this->write_(Register::PKTLEN);
}
}
void CC1101Component::set_crc_enable(bool value) {
this->state_.CRC_EN = value ? 1 : 0;
if (this->initialized_) {
this->write_(Register::PKTCTRL0);
}
}
void CC1101Component::set_whitening(bool value) {
this->state_.WHITE_DATA = value ? 1 : 0;
if (this->initialized_) {
this->write_(Register::PKTCTRL0);
}
}
} // namespace esphome::cc1101 } // namespace esphome::cc1101

View File

@@ -5,9 +5,12 @@
#include "esphome/components/spi/spi.h" #include "esphome/components/spi/spi.h"
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
#include "cc1101defs.h" #include "cc1101defs.h"
#include <vector>
namespace esphome::cc1101 { namespace esphome::cc1101 {
enum class CC1101Error { NONE = 0, TIMEOUT, PARAMS, CRC_ERROR, FIFO_OVERFLOW };
class CC1101Component : public Component, class CC1101Component : public Component,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_1MHZ> { spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_1MHZ> {
@@ -15,6 +18,7 @@ class CC1101Component : public Component,
CC1101Component(); CC1101Component();
void setup() override; void setup() override;
void loop() override;
void dump_config() override; void dump_config() override;
// Actions // Actions
@@ -24,8 +28,7 @@ class CC1101Component : public Component,
void set_idle(); void set_idle();
// GDO Pin Configuration // GDO Pin Configuration
void set_gdo0_config(uint8_t value); void set_gdo0_pin(InternalGPIOPin *pin) { this->gdo0_pin_ = pin; }
void set_gdo2_config(uint8_t value);
// Configuration Setters // Configuration Setters
void set_output_power(float value); void set_output_power(float value);
@@ -48,7 +51,6 @@ class CC1101Component : public Component,
void set_num_preamble(uint8_t value); void set_num_preamble(uint8_t value);
void set_sync1(uint8_t value); void set_sync1(uint8_t value);
void set_sync0(uint8_t value); void set_sync0(uint8_t value);
void set_pktlen(uint8_t value);
// AGC settings // AGC settings
void set_magn_target(MagnTarget value); void set_magn_target(MagnTarget value);
@@ -63,6 +65,16 @@ class CC1101Component : public Component,
void set_wait_time(WaitTime value); void set_wait_time(WaitTime value);
void set_hyst_level(HystLevel value); void set_hyst_level(HystLevel value);
// Packet mode settings
void set_packet_mode(bool value);
void set_packet_length(uint8_t value);
void set_crc_enable(bool value);
void set_whitening(bool value);
// Packet mode operations
CC1101Error transmit_packet(const std::vector<uint8_t> &packet);
Trigger<std::vector<uint8_t>, float, uint8_t> *get_packet_trigger() const { return this->packet_trigger_; }
protected: protected:
uint16_t chip_id_{0}; uint16_t chip_id_{0};
bool initialized_{false}; bool initialized_{false};
@@ -73,6 +85,13 @@ class CC1101Component : public Component,
CC1101State state_; CC1101State state_;
// GDO pin for packet reception
InternalGPIOPin *gdo0_pin_{nullptr};
// Packet handling
Trigger<std::vector<uint8_t>, float, uint8_t> *packet_trigger_{new Trigger<std::vector<uint8_t>, float, uint8_t>()};
std::vector<uint8_t> packet_;
// Low-level Helpers // Low-level Helpers
uint8_t strobe_(Command cmd); uint8_t strobe_(Command cmd);
void write_(Register reg); void write_(Register reg);
@@ -107,4 +126,28 @@ template<typename... Ts> class SetIdleAction : public Action<Ts...>, public Pare
void play(const Ts &...x) override { this->parent_->set_idle(); } void play(const Ts &...x) override { this->parent_->set_idle(); }
}; };
template<typename... Ts> class SendPacketAction : public Action<Ts...>, public Parented<CC1101Component> {
public:
void set_data_template(std::function<std::vector<uint8_t>(Ts...)> func) { this->data_func_ = func; }
void set_data_static(const uint8_t *data, size_t len) {
this->data_static_ = data;
this->data_static_len_ = len;
}
void play(const Ts &...x) override {
if (this->data_func_) {
auto data = this->data_func_(x...);
this->parent_->transmit_packet(data);
} else if (this->data_static_ != nullptr) {
std::vector<uint8_t> data(this->data_static_, this->data_static_ + this->data_static_len_);
this->parent_->transmit_packet(data);
}
}
protected:
std::function<std::vector<uint8_t>(Ts...)> data_func_{};
const uint8_t *data_static_{nullptr};
size_t data_static_len_{0};
};
} // namespace esphome::cc1101 } // namespace esphome::cc1101

View File

@@ -6,6 +6,12 @@ namespace esphome::cc1101 {
static constexpr float XTAL_FREQUENCY = 26000000; static constexpr float XTAL_FREQUENCY = 26000000;
static constexpr float RSSI_OFFSET = 74.0f;
static constexpr float RSSI_STEP = 0.5f;
static constexpr uint8_t STATUS_CRC_OK_MASK = 0x80;
static constexpr uint8_t STATUS_LQI_MASK = 0x7F;
static constexpr uint8_t BUS_BURST = 0x40; static constexpr uint8_t BUS_BURST = 0x40;
static constexpr uint8_t BUS_READ = 0x80; static constexpr uint8_t BUS_READ = 0x80;
static constexpr uint8_t BUS_WRITE = 0x00; static constexpr uint8_t BUS_WRITE = 0x00;
@@ -134,6 +140,10 @@ enum class SyncMode : uint8_t {
SYNC_MODE_15_16, SYNC_MODE_15_16,
SYNC_MODE_16_16, SYNC_MODE_16_16,
SYNC_MODE_30_32, SYNC_MODE_30_32,
SYNC_MODE_NONE_CS,
SYNC_MODE_15_16_CS,
SYNC_MODE_16_16_CS,
SYNC_MODE_30_32_CS,
}; };
enum class Modulation : uint8_t { enum class Modulation : uint8_t {
@@ -218,6 +228,19 @@ enum class HystLevel : uint8_t {
HYST_LEVEL_HIGH, HYST_LEVEL_HIGH,
}; };
enum class PacketFormat : uint8_t {
PACKET_FORMAT_FIFO,
PACKET_FORMAT_SYNC_SERIAL,
PACKET_FORMAT_RANDOM_TX,
PACKET_FORMAT_ASYNC_SERIAL,
};
enum class LengthConfig : uint8_t {
LENGTH_CONFIG_FIXED,
LENGTH_CONFIG_VARIABLE,
LENGTH_CONFIG_INFINITE,
};
struct __attribute__((packed)) CC1101State { struct __attribute__((packed)) CC1101State {
// Byte array accessors for bulk SPI transfers // Byte array accessors for bulk SPI transfers
uint8_t *regs() { return reinterpret_cast<uint8_t *>(this); } uint8_t *regs() { return reinterpret_cast<uint8_t *>(this); }

View File

@@ -117,9 +117,7 @@ CONF_MIN_HUMIDITY = "min_humidity"
CONF_MAX_HUMIDITY = "max_humidity" CONF_MAX_HUMIDITY = "max_humidity"
CONF_TARGET_HUMIDITY = "target_humidity" CONF_TARGET_HUMIDITY = "target_humidity"
visual_temperature = cv.float_with_unit( visual_temperature = cv.float_with_unit("visual_temperature", "(°|(° ?)?[CKF])?")
"visual_temperature", "(°C|° C|°|C|°K|° K|K|°F|° F|F)?"
)
VISUAL_TEMPERATURE_STEP_SCHEMA = cv.Schema( VISUAL_TEMPERATURE_STEP_SCHEMA = cv.Schema(

View File

@@ -2,6 +2,7 @@
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h" #include "esphome/core/controller_registry.h"
#include "esphome/core/macros.h" #include "esphome/core/macros.h"
#include <strings.h>
namespace esphome::climate { namespace esphome::climate {
@@ -190,24 +191,30 @@ ClimateCall &ClimateCall::set_fan_mode(ClimateFanMode fan_mode) {
} }
ClimateCall &ClimateCall::set_fan_mode(const char *custom_fan_mode) { ClimateCall &ClimateCall::set_fan_mode(const char *custom_fan_mode) {
return this->set_fan_mode(custom_fan_mode, strlen(custom_fan_mode));
}
ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) {
return this->set_fan_mode(fan_mode.data(), fan_mode.size());
}
ClimateCall &ClimateCall::set_fan_mode(const char *custom_fan_mode, size_t len) {
// Check if it's a standard enum mode first // Check if it's a standard enum mode first
for (const auto &mode_entry : CLIMATE_FAN_MODES_BY_STR) { for (const auto &mode_entry : CLIMATE_FAN_MODES_BY_STR) {
if (str_equals_case_insensitive(custom_fan_mode, mode_entry.str)) { if (strncasecmp(custom_fan_mode, mode_entry.str, len) == 0 && mode_entry.str[len] == '\0') {
return this->set_fan_mode(static_cast<ClimateFanMode>(mode_entry.value)); return this->set_fan_mode(static_cast<ClimateFanMode>(mode_entry.value));
} }
} }
// Find the matching pointer from parent climate device // Find the matching pointer from parent climate device
if (const char *mode_ptr = this->parent_->find_custom_fan_mode_(custom_fan_mode)) { if (const char *mode_ptr = this->parent_->find_custom_fan_mode_(custom_fan_mode, len)) {
this->custom_fan_mode_ = mode_ptr; this->custom_fan_mode_ = mode_ptr;
this->fan_mode_.reset(); this->fan_mode_.reset();
return *this; return *this;
} }
ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), custom_fan_mode); ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %.*s", this->parent_->get_name().c_str(), (int) len, custom_fan_mode);
return *this; return *this;
} }
ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) { return this->set_fan_mode(fan_mode.c_str()); }
ClimateCall &ClimateCall::set_fan_mode(optional<std::string> fan_mode) { ClimateCall &ClimateCall::set_fan_mode(optional<std::string> fan_mode) {
if (fan_mode.has_value()) { if (fan_mode.has_value()) {
this->set_fan_mode(fan_mode.value()); this->set_fan_mode(fan_mode.value());
@@ -222,24 +229,30 @@ ClimateCall &ClimateCall::set_preset(ClimatePreset preset) {
} }
ClimateCall &ClimateCall::set_preset(const char *custom_preset) { ClimateCall &ClimateCall::set_preset(const char *custom_preset) {
return this->set_preset(custom_preset, strlen(custom_preset));
}
ClimateCall &ClimateCall::set_preset(const std::string &preset) {
return this->set_preset(preset.data(), preset.size());
}
ClimateCall &ClimateCall::set_preset(const char *custom_preset, size_t len) {
// Check if it's a standard enum preset first // Check if it's a standard enum preset first
for (const auto &preset_entry : CLIMATE_PRESETS_BY_STR) { for (const auto &preset_entry : CLIMATE_PRESETS_BY_STR) {
if (str_equals_case_insensitive(custom_preset, preset_entry.str)) { if (strncasecmp(custom_preset, preset_entry.str, len) == 0 && preset_entry.str[len] == '\0') {
return this->set_preset(static_cast<ClimatePreset>(preset_entry.value)); return this->set_preset(static_cast<ClimatePreset>(preset_entry.value));
} }
} }
// Find the matching pointer from parent climate device // Find the matching pointer from parent climate device
if (const char *preset_ptr = this->parent_->find_custom_preset_(custom_preset)) { if (const char *preset_ptr = this->parent_->find_custom_preset_(custom_preset, len)) {
this->custom_preset_ = preset_ptr; this->custom_preset_ = preset_ptr;
this->preset_.reset(); this->preset_.reset();
return *this; return *this;
} }
ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), custom_preset); ESP_LOGW(TAG, "'%s' - Unrecognized preset %.*s", this->parent_->get_name().c_str(), (int) len, custom_preset);
return *this; return *this;
} }
ClimateCall &ClimateCall::set_preset(const std::string &preset) { return this->set_preset(preset.c_str()); }
ClimateCall &ClimateCall::set_preset(optional<std::string> preset) { ClimateCall &ClimateCall::set_preset(optional<std::string> preset) {
if (preset.has_value()) { if (preset.has_value()) {
this->set_preset(preset.value()); this->set_preset(preset.value());
@@ -356,7 +369,7 @@ optional<ClimateDeviceRestoreState> Climate::restore_state_() {
} }
void Climate::save_state_() { void Climate::save_state_() {
#if (defined(USE_ESP_IDF) || (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0))) && \ #if (defined(USE_ESP32) || (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0))) && \
!defined(CLANG_TIDY) !defined(CLANG_TIDY)
#pragma GCC diagnostic ignored "-Wclass-memaccess" #pragma GCC diagnostic ignored "-Wclass-memaccess"
#define TEMP_IGNORE_MEMACCESS #define TEMP_IGNORE_MEMACCESS
@@ -688,11 +701,19 @@ bool Climate::set_custom_preset_(const char *preset) {
void Climate::clear_custom_preset_() { this->custom_preset_ = nullptr; } void Climate::clear_custom_preset_() { this->custom_preset_ = nullptr; }
const char *Climate::find_custom_fan_mode_(const char *custom_fan_mode) { const char *Climate::find_custom_fan_mode_(const char *custom_fan_mode) {
return this->get_traits().find_custom_fan_mode_(custom_fan_mode); return this->find_custom_fan_mode_(custom_fan_mode, strlen(custom_fan_mode));
}
const char *Climate::find_custom_fan_mode_(const char *custom_fan_mode, size_t len) {
return this->get_traits().find_custom_fan_mode_(custom_fan_mode, len);
} }
const char *Climate::find_custom_preset_(const char *custom_preset) { const char *Climate::find_custom_preset_(const char *custom_preset) {
return this->get_traits().find_custom_preset_(custom_preset); return this->find_custom_preset_(custom_preset, strlen(custom_preset));
}
const char *Climate::find_custom_preset_(const char *custom_preset, size_t len) {
return this->get_traits().find_custom_preset_(custom_preset, len);
} }
void Climate::dump_traits_(const char *tag) { void Climate::dump_traits_(const char *tag) {

View File

@@ -78,6 +78,8 @@ class ClimateCall {
ClimateCall &set_fan_mode(optional<std::string> fan_mode); ClimateCall &set_fan_mode(optional<std::string> fan_mode);
/// Set the custom fan mode of the climate device. /// Set the custom fan mode of the climate device.
ClimateCall &set_fan_mode(const char *custom_fan_mode); ClimateCall &set_fan_mode(const char *custom_fan_mode);
/// Set the custom fan mode of the climate device (zero-copy API path).
ClimateCall &set_fan_mode(const char *custom_fan_mode, size_t len);
/// Set the swing mode of the climate device. /// Set the swing mode of the climate device.
ClimateCall &set_swing_mode(ClimateSwingMode swing_mode); ClimateCall &set_swing_mode(ClimateSwingMode swing_mode);
/// Set the swing mode of the climate device. /// Set the swing mode of the climate device.
@@ -94,6 +96,8 @@ class ClimateCall {
ClimateCall &set_preset(optional<std::string> preset); ClimateCall &set_preset(optional<std::string> preset);
/// Set the custom preset of the climate device. /// Set the custom preset of the climate device.
ClimateCall &set_preset(const char *custom_preset); ClimateCall &set_preset(const char *custom_preset);
/// Set the custom preset of the climate device (zero-copy API path).
ClimateCall &set_preset(const char *custom_preset, size_t len);
void perform(); void perform();
@@ -290,9 +294,11 @@ class Climate : public EntityBase {
/// Find and return the matching custom fan mode pointer from traits, or nullptr if not found. /// Find and return the matching custom fan mode pointer from traits, or nullptr if not found.
const char *find_custom_fan_mode_(const char *custom_fan_mode); const char *find_custom_fan_mode_(const char *custom_fan_mode);
const char *find_custom_fan_mode_(const char *custom_fan_mode, size_t len);
/// Find and return the matching custom preset pointer from traits, or nullptr if not found. /// Find and return the matching custom preset pointer from traits, or nullptr if not found.
const char *find_custom_preset_(const char *custom_preset); const char *find_custom_preset_(const char *custom_preset);
const char *find_custom_preset_(const char *custom_preset, size_t len);
/** Get the default traits of this climate device. /** Get the default traits of this climate device.
* *
@@ -320,8 +326,8 @@ class Climate : public EntityBase {
void dump_traits_(const char *tag); void dump_traits_(const char *tag);
CallbackManager<void(Climate &)> state_callback_{}; LazyCallbackManager<void(Climate &)> state_callback_{};
CallbackManager<void(ClimateCall &)> control_callback_{}; LazyCallbackManager<void(ClimateCall &)> control_callback_{};
ESPPreferenceObject rtc_; ESPPreferenceObject rtc_;
#ifdef USE_CLIMATE_VISUAL_OVERRIDES #ifdef USE_CLIMATE_VISUAL_OVERRIDES
float visual_min_temperature_override_{NAN}; float visual_min_temperature_override_{NAN};

View File

@@ -20,18 +20,22 @@ using ClimatePresetMask = FiniteSetMask<ClimatePreset, DefaultBitPolicy<ClimateP
// Lightweight linear search for small vectors (1-20 items) of const char* pointers // Lightweight linear search for small vectors (1-20 items) of const char* pointers
// Avoids std::find template overhead // Avoids std::find template overhead
inline bool vector_contains(const std::vector<const char *> &vec, const char *value) { inline bool vector_contains(const std::vector<const char *> &vec, const char *value, size_t len) {
for (const char *item : vec) { for (const char *item : vec) {
if (strcmp(item, value) == 0) if (strncmp(item, value, len) == 0 && item[len] == '\0')
return true; return true;
} }
return false; return false;
} }
inline bool vector_contains(const std::vector<const char *> &vec, const char *value) {
return vector_contains(vec, value, strlen(value));
}
// Find and return matching pointer from vector, or nullptr if not found // Find and return matching pointer from vector, or nullptr if not found
inline const char *vector_find(const std::vector<const char *> &vec, const char *value) { inline const char *vector_find(const std::vector<const char *> &vec, const char *value, size_t len) {
for (const char *item : vec) { for (const char *item : vec) {
if (strcmp(item, value) == 0) if (strncmp(item, value, len) == 0 && item[len] == '\0')
return item; return item;
} }
return nullptr; return nullptr;
@@ -257,13 +261,19 @@ class ClimateTraits {
/// Find and return the matching custom fan mode pointer from supported modes, or nullptr if not found /// Find and return the matching custom fan mode pointer from supported modes, or nullptr if not found
/// This is protected as it's an implementation detail - use Climate::find_custom_fan_mode_() instead /// This is protected as it's an implementation detail - use Climate::find_custom_fan_mode_() instead
const char *find_custom_fan_mode_(const char *custom_fan_mode) const { const char *find_custom_fan_mode_(const char *custom_fan_mode) const {
return vector_find(this->supported_custom_fan_modes_, custom_fan_mode); return this->find_custom_fan_mode_(custom_fan_mode, strlen(custom_fan_mode));
}
const char *find_custom_fan_mode_(const char *custom_fan_mode, size_t len) const {
return vector_find(this->supported_custom_fan_modes_, custom_fan_mode, len);
} }
/// Find and return the matching custom preset pointer from supported presets, or nullptr if not found /// Find and return the matching custom preset pointer from supported presets, or nullptr if not found
/// This is protected as it's an implementation detail - use Climate::find_custom_preset_() instead /// This is protected as it's an implementation detail - use Climate::find_custom_preset_() instead
const char *find_custom_preset_(const char *custom_preset) const { const char *find_custom_preset_(const char *custom_preset) const {
return vector_find(this->supported_custom_presets_, custom_preset); return this->find_custom_preset_(custom_preset, strlen(custom_preset));
}
const char *find_custom_preset_(const char *custom_preset, size_t len) const {
return vector_find(this->supported_custom_presets_, custom_preset, len);
} }
uint32_t feature_flags_{0}; uint32_t feature_flags_{0};

View File

@@ -7,9 +7,11 @@ BYTE_ORDER_LITTLE = "little_endian"
BYTE_ORDER_BIG = "big_endian" BYTE_ORDER_BIG = "big_endian"
CONF_COLOR_DEPTH = "color_depth" CONF_COLOR_DEPTH = "color_depth"
CONF_CRC_ENABLE = "crc_enable"
CONF_DRAW_ROUNDING = "draw_rounding" CONF_DRAW_ROUNDING = "draw_rounding"
CONF_ENABLED = "enabled" CONF_ENABLED = "enabled"
CONF_IGNORE_NOT_FOUND = "ignore_not_found" CONF_IGNORE_NOT_FOUND = "ignore_not_found"
CONF_ON_PACKET = "on_packet"
CONF_ON_RECEIVE = "on_receive" CONF_ON_RECEIVE = "on_receive"
CONF_ON_STATE_CHANGE = "on_state_change" CONF_ON_STATE_CHANGE = "on_state_change"
CONF_REQUEST_HEADERS = "request_headers" CONF_REQUEST_HEADERS = "request_headers"

View File

@@ -7,7 +7,7 @@ namespace copy {
static const char *const TAG = "copy.select"; static const char *const TAG = "copy.select";
void CopySelect::setup() { void CopySelect::setup() {
source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(index); }); source_->add_on_state_callback([this](size_t index) { this->publish_state(index); });
traits.set_options(source_->traits.get_options()); traits.set_options(source_->traits.get_options());

View File

@@ -152,7 +152,7 @@ class Cover : public EntityBase, public EntityBase_DeviceClass {
optional<CoverRestoreState> restore_state_(); optional<CoverRestoreState> restore_state_();
CallbackManager<void()> state_callback_{}; LazyCallbackManager<void()> state_callback_{};
ESPPreferenceObject rtc_; ESPPreferenceObject rtc_;
}; };

View File

@@ -51,7 +51,7 @@ void DallasTemperatureSensor::update() {
} }
float tempc = this->get_temp_c_(); float tempc = this->get_temp_c_();
ESP_LOGD(TAG, "'%s': Got Temperature=%.1f°C", this->get_name().c_str(), tempc); ESP_LOGD(TAG, "'%s': Got Temperature=%f°C", this->get_name().c_str(), tempc);
this->publish_state(tempc); this->publish_state(tempc);
}); });
} }

View File

@@ -22,7 +22,7 @@ class DateTimeBase : public EntityBase {
#endif #endif
protected: protected:
CallbackManager<void()> state_callback_; LazyCallbackManager<void()> state_callback_;
#ifdef USE_TIME #ifdef USE_TIME
time::RealTimeClock *rtc_; time::RealTimeClock *rtc_;

View File

@@ -200,7 +200,7 @@ void DebugComponent::get_device_info_(std::string &device_info) {
#ifdef USE_ARDUINO #ifdef USE_ARDUINO
ESP_LOGD(TAG, "Framework: Arduino"); ESP_LOGD(TAG, "Framework: Arduino");
device_info += "Arduino"; device_info += "Arduino";
#elif defined(USE_ESP_IDF) #elif defined(USE_ESP32)
ESP_LOGD(TAG, "Framework: ESP-IDF"); ESP_LOGD(TAG, "Framework: ESP-IDF");
device_info += "ESP-IDF"; device_info += "ESP-IDF";
#else #else

View File

@@ -1,4 +1,4 @@
from esphome import automation, pins from esphome import automation, core, pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import esp32, time from esphome.components import esp32, time
from esphome.components.esp32 import ( from esphome.components.esp32 import (
@@ -23,16 +23,20 @@ from esphome.const import (
CONF_MINUTE, CONF_MINUTE,
CONF_MODE, CONF_MODE,
CONF_NUMBER, CONF_NUMBER,
CONF_PIN,
CONF_PINS, CONF_PINS,
CONF_RUN_DURATION, CONF_RUN_DURATION,
CONF_SECOND, CONF_SECOND,
CONF_SLEEP_DURATION, CONF_SLEEP_DURATION,
CONF_TIME_ID, CONF_TIME_ID,
CONF_WAKEUP_PIN, CONF_WAKEUP_PIN,
PLATFORM_BK72XX,
PLATFORM_ESP32, PLATFORM_ESP32,
PLATFORM_ESP8266, PLATFORM_ESP8266,
PlatformFramework, PlatformFramework,
) )
from esphome.core import CORE
from esphome.types import ConfigType
WAKEUP_PINS = { WAKEUP_PINS = {
VARIANT_ESP32: [ VARIANT_ESP32: [
@@ -113,7 +117,7 @@ WAKEUP_PINS = {
} }
def validate_pin_number(value): def validate_pin_number_esp32(value: ConfigType) -> ConfigType:
valid_pins = WAKEUP_PINS.get(get_esp32_variant(), WAKEUP_PINS[VARIANT_ESP32]) valid_pins = WAKEUP_PINS.get(get_esp32_variant(), WAKEUP_PINS[VARIANT_ESP32])
if value[CONF_NUMBER] not in valid_pins: if value[CONF_NUMBER] not in valid_pins:
raise cv.Invalid( raise cv.Invalid(
@@ -122,6 +126,51 @@ def validate_pin_number(value):
return value return value
def validate_pin_number(value: ConfigType) -> ConfigType:
if not CORE.is_esp32:
return value
return validate_pin_number_esp32(value)
def validate_wakeup_pin(
value: ConfigType | list[ConfigType],
) -> list[ConfigType]:
if not isinstance(value, list):
processed_pins: list[ConfigType] = [{CONF_PIN: value}]
else:
processed_pins = list(value)
for i, pin_config in enumerate(processed_pins):
# now validate each item
validated_pin = WAKEUP_PIN_SCHEMA(pin_config)
validate_pin_number(validated_pin[CONF_PIN])
processed_pins[i] = validated_pin
return processed_pins
def validate_config(config: ConfigType) -> ConfigType:
# right now only BK72XX supports the list format for wakeup pins
if CORE.is_bk72xx:
if CONF_WAKEUP_PIN_MODE in config:
wakeup_pins = config.get(CONF_WAKEUP_PIN, [])
if len(wakeup_pins) > 1:
raise cv.Invalid(
"You need to remove the global wakeup_pin_mode and define it per pin"
)
if wakeup_pins:
wakeup_pins[0][CONF_WAKEUP_PIN_MODE] = config.pop(CONF_WAKEUP_PIN_MODE)
elif (
isinstance(config.get(CONF_WAKEUP_PIN), list)
and len(config[CONF_WAKEUP_PIN]) > 1
):
raise cv.Invalid(
"Your platform does not support providing multiple entries in wakeup_pin"
)
return config
def _validate_ex1_wakeup_mode(value): def _validate_ex1_wakeup_mode(value):
if value == "ALL_LOW": if value == "ALL_LOW":
esp32.only_on_variant(supported=[VARIANT_ESP32], msg_prefix="ALL_LOW")(value) esp32.only_on_variant(supported=[VARIANT_ESP32], msg_prefix="ALL_LOW")(value)
@@ -141,6 +190,15 @@ def _validate_ex1_wakeup_mode(value):
return value return value
def _validate_sleep_duration(value: core.TimePeriod) -> core.TimePeriod:
if not CORE.is_bk72xx:
return value
max_duration = core.TimePeriod(hours=36)
if value > max_duration:
raise cv.Invalid("sleep duration cannot be more than 36 hours on BK72XX")
return value
deep_sleep_ns = cg.esphome_ns.namespace("deep_sleep") deep_sleep_ns = cg.esphome_ns.namespace("deep_sleep")
DeepSleepComponent = deep_sleep_ns.class_("DeepSleepComponent", cg.Component) DeepSleepComponent = deep_sleep_ns.class_("DeepSleepComponent", cg.Component)
EnterDeepSleepAction = deep_sleep_ns.class_("EnterDeepSleepAction", automation.Action) EnterDeepSleepAction = deep_sleep_ns.class_("EnterDeepSleepAction", automation.Action)
@@ -186,6 +244,13 @@ WAKEUP_CAUSES_SCHEMA = cv.Schema(
} }
) )
WAKEUP_PIN_SCHEMA = cv.Schema(
{
cv.Required(CONF_PIN): pins.internal_gpio_input_pin_schema,
cv.Optional(CONF_WAKEUP_PIN_MODE): cv.enum(WAKEUP_PIN_MODES, upper=True),
}
)
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
cv.Schema( cv.Schema(
{ {
@@ -194,14 +259,15 @@ CONFIG_SCHEMA = cv.All(
cv.All(cv.only_on_esp32, WAKEUP_CAUSES_SCHEMA), cv.All(cv.only_on_esp32, WAKEUP_CAUSES_SCHEMA),
cv.positive_time_period_milliseconds, cv.positive_time_period_milliseconds,
), ),
cv.Optional(CONF_SLEEP_DURATION): cv.positive_time_period_milliseconds, cv.Optional(CONF_SLEEP_DURATION): cv.All(
cv.Optional(CONF_WAKEUP_PIN): cv.All( cv.positive_time_period_milliseconds,
cv.only_on_esp32, _validate_sleep_duration,
pins.internal_gpio_input_pin_schema,
validate_pin_number,
), ),
cv.Optional(CONF_WAKEUP_PIN): validate_wakeup_pin,
cv.Optional(CONF_WAKEUP_PIN_MODE): cv.All( cv.Optional(CONF_WAKEUP_PIN_MODE): cv.All(
cv.only_on_esp32, cv.enum(WAKEUP_PIN_MODES), upper=True cv.only_on([PLATFORM_ESP32, PLATFORM_BK72XX]),
cv.enum(WAKEUP_PIN_MODES),
upper=True,
), ),
cv.Optional(CONF_ESP32_EXT1_WAKEUP): cv.All( cv.Optional(CONF_ESP32_EXT1_WAKEUP): cv.All(
cv.only_on_esp32, cv.only_on_esp32,
@@ -212,7 +278,8 @@ CONFIG_SCHEMA = cv.All(
cv.Schema( cv.Schema(
{ {
cv.Required(CONF_PINS): cv.ensure_list( cv.Required(CONF_PINS): cv.ensure_list(
pins.internal_gpio_input_pin_schema, validate_pin_number pins.internal_gpio_input_pin_schema,
validate_pin_number_esp32,
), ),
cv.Required(CONF_MODE): cv.All( cv.Required(CONF_MODE): cv.All(
cv.enum(EXT1_WAKEUP_MODES, upper=True), cv.enum(EXT1_WAKEUP_MODES, upper=True),
@@ -238,7 +305,8 @@ CONFIG_SCHEMA = cv.All(
), ),
} }
).extend(cv.COMPONENT_SCHEMA), ).extend(cv.COMPONENT_SCHEMA),
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266]), cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX]),
validate_config,
) )
@@ -249,8 +317,21 @@ async def to_code(config):
if CONF_SLEEP_DURATION in config: if CONF_SLEEP_DURATION in config:
cg.add(var.set_sleep_duration(config[CONF_SLEEP_DURATION])) cg.add(var.set_sleep_duration(config[CONF_SLEEP_DURATION]))
if CONF_WAKEUP_PIN in config: if CONF_WAKEUP_PIN in config:
pin = await cg.gpio_pin_expression(config[CONF_WAKEUP_PIN]) pins_as_list = config.get(CONF_WAKEUP_PIN, [])
cg.add(var.set_wakeup_pin(pin)) if CORE.is_bk72xx:
cg.add(var.init_wakeup_pins_(len(pins_as_list)))
for item in pins_as_list:
cg.add(
var.add_wakeup_pin(
await cg.gpio_pin_expression(item[CONF_PIN]),
item.get(
CONF_WAKEUP_PIN_MODE, WakeupPinMode.WAKEUP_PIN_MODE_IGNORE
),
)
)
else:
pin = await cg.gpio_pin_expression(pins_as_list[0][CONF_PIN])
cg.add(var.set_wakeup_pin(pin))
if CONF_WAKEUP_PIN_MODE in config: if CONF_WAKEUP_PIN_MODE in config:
cg.add(var.set_wakeup_pin_mode(config[CONF_WAKEUP_PIN_MODE])) cg.add(var.set_wakeup_pin_mode(config[CONF_WAKEUP_PIN_MODE]))
if CONF_RUN_DURATION in config: if CONF_RUN_DURATION in config:
@@ -305,7 +386,10 @@ DEEP_SLEEP_ENTER_SCHEMA = cv.All(
cv.Schema( cv.Schema(
{ {
cv.Exclusive(CONF_SLEEP_DURATION, "time"): cv.templatable( cv.Exclusive(CONF_SLEEP_DURATION, "time"): cv.templatable(
cv.positive_time_period_milliseconds cv.All(
cv.positive_time_period_milliseconds,
_validate_sleep_duration,
)
), ),
# Only on ESP32 due to how long the RTC on ESP8266 can stay asleep # Only on ESP32 due to how long the RTC on ESP8266 can stay asleep
cv.Exclusive(CONF_UNTIL, "time"): cv.All( cv.Exclusive(CONF_UNTIL, "time"): cv.All(
@@ -363,5 +447,6 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
PlatformFramework.ESP32_IDF, PlatformFramework.ESP32_IDF,
}, },
"deep_sleep_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, "deep_sleep_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
"deep_sleep_bk72xx.cpp": {PlatformFramework.BK72XX_ARDUINO},
} }
) )

View File

@@ -0,0 +1,64 @@
#ifdef USE_BK72XX
#include "deep_sleep_component.h"
#include "esphome/core/log.h"
namespace esphome::deep_sleep {
static const char *const TAG = "deep_sleep.bk72xx";
optional<uint32_t> DeepSleepComponent::get_run_duration_() const { return this->run_duration_; }
void DeepSleepComponent::dump_config_platform_() {
for (const WakeUpPinItem &item : this->wakeup_pins_) {
LOG_PIN(" Wakeup Pin: ", item.wakeup_pin);
}
}
bool DeepSleepComponent::pin_prevents_sleep_(WakeUpPinItem &pinItem) const {
return (pinItem.wakeup_pin_mode == WAKEUP_PIN_MODE_KEEP_AWAKE && pinItem.wakeup_pin != nullptr &&
!this->sleep_duration_.has_value() && (pinItem.wakeup_level == get_real_pin_state_(*pinItem.wakeup_pin)));
}
bool DeepSleepComponent::prepare_to_sleep_() {
if (wakeup_pins_.size() > 0) {
for (WakeUpPinItem &item : this->wakeup_pins_) {
if (pin_prevents_sleep_(item)) {
// Defer deep sleep until inactive
if (!this->next_enter_deep_sleep_) {
this->status_set_warning();
ESP_LOGV(TAG, "Waiting for pin to switch state to enter deep sleep...");
}
this->next_enter_deep_sleep_ = true;
return false;
}
}
}
return true;
}
void DeepSleepComponent::deep_sleep_() {
for (WakeUpPinItem &item : this->wakeup_pins_) {
if (item.wakeup_pin_mode == WAKEUP_PIN_MODE_INVERT_WAKEUP) {
if (item.wakeup_level == get_real_pin_state_(*item.wakeup_pin)) {
item.wakeup_level = !item.wakeup_level;
}
}
ESP_LOGI(TAG, "Wake-up on P%u %s (%d)", item.wakeup_pin->get_pin(), item.wakeup_level ? "HIGH" : "LOW",
static_cast<int32_t>(item.wakeup_pin_mode));
}
if (this->sleep_duration_.has_value())
lt_deep_sleep_config_timer((*this->sleep_duration_ / 1000) & 0xFFFFFFFF);
for (WakeUpPinItem &item : this->wakeup_pins_) {
lt_deep_sleep_config_gpio(1 << item.wakeup_pin->get_pin(), item.wakeup_level);
lt_deep_sleep_keep_floating_gpio(1 << item.wakeup_pin->get_pin(), true);
}
lt_deep_sleep_enter();
}
} // namespace esphome::deep_sleep
#endif // USE_BK72XX

View File

@@ -19,7 +19,7 @@
namespace esphome { namespace esphome {
namespace deep_sleep { namespace deep_sleep {
#ifdef USE_ESP32 #if defined(USE_ESP32) || defined(USE_BK72XX)
/** The values of this enum define what should be done if deep sleep is set up with a wakeup pin on the ESP32 /** The values of this enum define what should be done if deep sleep is set up with a wakeup pin on the ESP32
* and the scenario occurs that the wakeup pin is already in the wakeup state. * and the scenario occurs that the wakeup pin is already in the wakeup state.
@@ -33,7 +33,17 @@ enum WakeupPinMode {
*/ */
WAKEUP_PIN_MODE_INVERT_WAKEUP, WAKEUP_PIN_MODE_INVERT_WAKEUP,
}; };
#endif
#if defined(USE_BK72XX)
struct WakeUpPinItem {
InternalGPIOPin *wakeup_pin;
WakeupPinMode wakeup_pin_mode;
bool wakeup_level;
};
#endif // USE_BK72XX
#ifdef USE_ESP32
#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) #if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3)
struct Ext1Wakeup { struct Ext1Wakeup {
uint64_t mask; uint64_t mask;
@@ -75,6 +85,13 @@ class DeepSleepComponent : public Component {
void set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode); void set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode);
#endif // USE_ESP32 #endif // USE_ESP32
#if defined(USE_BK72XX)
void init_wakeup_pins_(size_t capacity) { this->wakeup_pins_.init(capacity); }
void add_wakeup_pin(InternalGPIOPin *wakeup_pin, WakeupPinMode wakeup_pin_mode) {
this->wakeup_pins_.emplace_back(WakeUpPinItem{wakeup_pin, wakeup_pin_mode, !wakeup_pin->is_inverted()});
}
#endif // USE_BK72XX
#if defined(USE_ESP32) #if defined(USE_ESP32)
#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) #if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3)
void set_ext1_wakeup(Ext1Wakeup ext1_wakeup); void set_ext1_wakeup(Ext1Wakeup ext1_wakeup);
@@ -114,7 +131,17 @@ class DeepSleepComponent : public Component {
bool prepare_to_sleep_(); bool prepare_to_sleep_();
void deep_sleep_(); void deep_sleep_();
#ifdef USE_BK72XX
bool pin_prevents_sleep_(WakeUpPinItem &pinItem) const;
bool get_real_pin_state_(InternalGPIOPin &pin) const { return (pin.digital_read() ^ pin.is_inverted()); }
#endif // USE_BK72XX
optional<uint64_t> sleep_duration_; optional<uint64_t> sleep_duration_;
#ifdef USE_BK72XX
FixedVector<WakeUpPinItem> wakeup_pins_;
#endif // USE_BK72XX
#ifdef USE_ESP32 #ifdef USE_ESP32
InternalGPIOPin *wakeup_pin_; InternalGPIOPin *wakeup_pin_;
WakeupPinMode wakeup_pin_mode_{WAKEUP_PIN_MODE_IGNORE}; WakeupPinMode wakeup_pin_mode_{WAKEUP_PIN_MODE_IGNORE};
@@ -124,8 +151,10 @@ class DeepSleepComponent : public Component {
#endif #endif
optional<bool> touch_wakeup_; optional<bool> touch_wakeup_;
optional<WakeupCauseToRunDuration> wakeup_cause_to_run_duration_; optional<WakeupCauseToRunDuration> wakeup_cause_to_run_duration_;
#endif // USE_ESP32 #endif // USE_ESP32
optional<uint32_t> run_duration_; optional<uint32_t> run_duration_;
bool next_enter_deep_sleep_{false}; bool next_enter_deep_sleep_{false};
bool prevent_{false}; bool prevent_{false};

View File

@@ -8,17 +8,20 @@ namespace dht {
static const char *const TAG = "dht"; static const char *const TAG = "dht";
void DHT::setup() { void DHT::setup() {
this->pin_->digital_write(true); this->t_pin_->digital_write(true);
this->pin_->setup(); this->t_pin_->setup();
this->pin_->digital_write(true); #ifdef USE_ESP32
this->t_pin_->pin_mode(this->t_pin_->get_flags() | gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN);
#endif
this->t_pin_->digital_write(true);
} }
void DHT::dump_config() { void DHT::dump_config() {
ESP_LOGCONFIG(TAG, "DHT:"); ESP_LOGCONFIG(TAG, "DHT:");
LOG_PIN(" Pin: ", this->pin_); LOG_PIN(" Pin: ", this->t_pin_);
ESP_LOGCONFIG(TAG, " %sModel: %s", this->is_auto_detect_ ? "Auto-detected " : "", ESP_LOGCONFIG(TAG, " %sModel: %s", this->is_auto_detect_ ? "Auto-detected " : "",
this->model_ == DHT_MODEL_DHT11 ? "DHT11" : "DHT22 or equivalent"); this->model_ == DHT_MODEL_DHT11 ? "DHT11" : "DHT22 or equivalent");
ESP_LOGCONFIG(TAG, " Internal pull-up: %s", ONOFF(this->pin_->get_flags() & gpio::FLAG_PULLUP)); ESP_LOGCONFIG(TAG, " Internal pull-up: %s", ONOFF(this->t_pin_->get_flags() & gpio::FLAG_PULLUP));
LOG_UPDATE_INTERVAL(this); LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
@@ -72,21 +75,15 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
int8_t i = 0; int8_t i = 0;
uint8_t data[5] = {0, 0, 0, 0, 0}; uint8_t data[5] = {0, 0, 0, 0, 0};
this->pin_->digital_write(false); #ifndef USE_ESP32
this->pin_->pin_mode(gpio::FLAG_OUTPUT); this->pin_.pin_mode(gpio::FLAG_OUTPUT);
this->pin_->digital_write(false); #endif
this->pin_.digital_write(false);
if (this->model_ == DHT_MODEL_DHT11) { if (this->model_ == DHT_MODEL_DHT11) {
delayMicroseconds(18000); delayMicroseconds(18000);
} else if (this->model_ == DHT_MODEL_SI7021) { } else if (this->model_ == DHT_MODEL_SI7021) {
#ifdef USE_ESP8266
delayMicroseconds(500); delayMicroseconds(500);
this->pin_->digital_write(true);
delayMicroseconds(40);
#else
delayMicroseconds(400);
this->pin_->digital_write(true);
#endif
} else if (this->model_ == DHT_MODEL_DHT22_TYPE2) { } else if (this->model_ == DHT_MODEL_DHT22_TYPE2) {
delayMicroseconds(2000); delayMicroseconds(2000);
} else if (this->model_ == DHT_MODEL_AM2120 || this->model_ == DHT_MODEL_AM2302) { } else if (this->model_ == DHT_MODEL_AM2120 || this->model_ == DHT_MODEL_AM2302) {
@@ -94,7 +91,12 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
} else { } else {
delayMicroseconds(800); delayMicroseconds(800);
} }
this->pin_->pin_mode(this->pin_->get_flags());
#ifdef USE_ESP32
this->pin_.digital_write(true);
#else
this->pin_.pin_mode(this->t_pin_->get_flags());
#endif
{ {
InterruptLock lock; InterruptLock lock;
@@ -110,7 +112,7 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
uint32_t start_time = micros(); uint32_t start_time = micros();
// Wait for rising edge // Wait for rising edge
while (!this->pin_->digital_read()) { while (!this->pin_.digital_read()) {
if (micros() - start_time > 90) { if (micros() - start_time > 90) {
if (i < 0) { if (i < 0) {
error_code = 1; // line didn't clear error_code = 1; // line didn't clear
@@ -127,7 +129,7 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
uint32_t end_time = start_time; uint32_t end_time = start_time;
// Wait for falling edge // Wait for falling edge
while (this->pin_->digital_read()) { while (this->pin_.digital_read()) {
end_time = micros(); end_time = micros();
if (end_time - start_time > 90) { if (end_time - start_time > 90) {
if (i < 0) { if (i < 0) {

View File

@@ -38,7 +38,10 @@ class DHT : public PollingComponent {
*/ */
void set_dht_model(DHTModel model); void set_dht_model(DHTModel model);
void set_pin(InternalGPIOPin *pin) { pin_ = pin; } void set_pin(InternalGPIOPin *pin) {
this->t_pin_ = pin;
this->pin_ = pin->to_isr();
}
void set_model(DHTModel model) { model_ = model; } void set_model(DHTModel model) { model_ = model; }
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; }
@@ -54,7 +57,8 @@ class DHT : public PollingComponent {
protected: protected:
bool read_sensor_(float *temperature, float *humidity, bool report_errors); bool read_sensor_(float *temperature, float *humidity, bool report_errors);
InternalGPIOPin *pin_; InternalGPIOPin *t_pin_;
ISRInternalGPIOPin pin_;
DHTModel model_{DHT_MODEL_AUTO_DETECT}; DHTModel model_{DHT_MODEL_AUTO_DETECT};
bool is_auto_detect_{false}; bool is_auto_detect_{false};
sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *temperature_sensor_{nullptr};

View File

@@ -54,6 +54,7 @@ bool MenuItemSelect::select_next() {
if (this->select_var_ != nullptr) { if (this->select_var_ != nullptr) {
this->select_var_->make_call().select_next(true).perform(); this->select_var_->make_call().select_next(true).perform();
this->on_value_();
changed = true; changed = true;
} }
@@ -65,6 +66,7 @@ bool MenuItemSelect::select_prev() {
if (this->select_var_ != nullptr) { if (this->select_var_ != nullptr) {
this->select_var_->make_call().select_previous(true).perform(); this->select_var_->make_call().select_previous(true).perform();
this->on_value_();
changed = true; changed = true;
} }

View File

@@ -4,6 +4,7 @@ import itertools
import logging import logging
import os import os
from pathlib import Path from pathlib import Path
import re
from esphome import yaml_util from esphome import yaml_util
import esphome.codegen as cg import esphome.codegen as cg
@@ -12,6 +13,7 @@ from esphome.const import (
CONF_ADVANCED, CONF_ADVANCED,
CONF_BOARD, CONF_BOARD,
CONF_COMPONENTS, CONF_COMPONENTS,
CONF_DISABLED,
CONF_ESPHOME, CONF_ESPHOME,
CONF_FRAMEWORK, CONF_FRAMEWORK,
CONF_IGNORE_EFUSE_CUSTOM_MAC, CONF_IGNORE_EFUSE_CUSTOM_MAC,
@@ -23,6 +25,7 @@ from esphome.const import (
CONF_PLATFORMIO_OPTIONS, CONF_PLATFORMIO_OPTIONS,
CONF_REF, CONF_REF,
CONF_REFRESH, CONF_REFRESH,
CONF_SAFE_MODE,
CONF_SOURCE, CONF_SOURCE,
CONF_TYPE, CONF_TYPE,
CONF_VARIANT, CONF_VARIANT,
@@ -80,6 +83,7 @@ CONF_ASSERTION_LEVEL = "assertion_level"
CONF_COMPILER_OPTIMIZATION = "compiler_optimization" CONF_COMPILER_OPTIMIZATION = "compiler_optimization"
CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES = "enable_idf_experimental_features" CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES = "enable_idf_experimental_features"
CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert" CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert"
CONF_ENABLE_OTA_ROLLBACK = "enable_ota_rollback"
CONF_EXECUTE_FROM_PSRAM = "execute_from_psram" CONF_EXECUTE_FROM_PSRAM = "execute_from_psram"
CONF_RELEASE = "release" CONF_RELEASE = "release"
@@ -117,8 +121,8 @@ ARDUINO_ALLOWED_VARIANTS = [
] ]
def get_cpu_frequencies(*frequencies): def get_cpu_frequencies(*frequencies: int) -> list[str]:
return [str(x) + "MHZ" for x in frequencies] return [f"{frequency}MHZ" for frequency in frequencies]
CPU_FREQUENCIES = { CPU_FREQUENCIES = {
@@ -135,7 +139,7 @@ CPU_FREQUENCIES = {
} }
# Make sure not missed here if a new variant added. # Make sure not missed here if a new variant added.
assert all(v in CPU_FREQUENCIES for v in VARIANTS) assert all(variant in CPU_FREQUENCIES for variant in VARIANTS)
FULL_CPU_FREQUENCIES = set(itertools.chain.from_iterable(CPU_FREQUENCIES.values())) FULL_CPU_FREQUENCIES = set(itertools.chain.from_iterable(CPU_FREQUENCIES.values()))
@@ -249,10 +253,10 @@ def add_idf_sdkconfig_option(name: str, value: SdkconfigValueType):
def add_idf_component( def add_idf_component(
*, *,
name: str, name: str,
repo: str = None, repo: str | None = None,
ref: str = None, ref: str | None = None,
path: str = None, path: str | None = None,
refresh: TimePeriod = None, refresh: TimePeriod | None = None,
components: list[str] | None = None, components: list[str] | None = None,
submodules: list[str] | None = None, submodules: list[str] | None = None,
): ):
@@ -333,7 +337,7 @@ def _format_framework_espidf_version(ver: cv.Version, release: str) -> str:
return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.{ext}" return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.{ext}"
def _is_framework_url(source: str) -> str: def _is_framework_url(source: str) -> bool:
# platformio accepts many URL schemes for framework repositories and archives including http, https, git, file, and symlink # platformio accepts many URL schemes for framework repositories and archives including http, https, git, file, and symlink
import urllib.parse import urllib.parse
@@ -353,11 +357,12 @@ def _is_framework_url(source: str) -> str:
# The default/recommended arduino framework version # The default/recommended arduino framework version
# - https://github.com/espressif/arduino-esp32/releases # - https://github.com/espressif/arduino-esp32/releases
ARDUINO_FRAMEWORK_VERSION_LOOKUP = { ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
"recommended": cv.Version(3, 3, 2), "recommended": cv.Version(3, 3, 5),
"latest": cv.Version(3, 3, 4), "latest": cv.Version(3, 3, 5),
"dev": cv.Version(3, 3, 4), "dev": cv.Version(3, 3, 5),
} }
ARDUINO_PLATFORM_VERSION_LOOKUP = { ARDUINO_PLATFORM_VERSION_LOOKUP = {
cv.Version(3, 3, 5): cv.Version(55, 3, 35),
cv.Version(3, 3, 4): cv.Version(55, 3, 31, "2"), cv.Version(3, 3, 4): cv.Version(55, 3, 31, "2"),
cv.Version(3, 3, 3): cv.Version(55, 3, 31, "2"), cv.Version(3, 3, 3): cv.Version(55, 3, 31, "2"),
cv.Version(3, 3, 2): cv.Version(55, 3, 31, "2"), cv.Version(3, 3, 2): cv.Version(55, 3, 31, "2"),
@@ -370,15 +375,33 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = {
cv.Version(3, 1, 1): cv.Version(53, 3, 11), cv.Version(3, 1, 1): cv.Version(53, 3, 11),
cv.Version(3, 1, 0): cv.Version(53, 3, 10), cv.Version(3, 1, 0): cv.Version(53, 3, 10),
} }
# Maps Arduino framework versions to a compatible ESP-IDF version
# These versions correspond to pioarduino/esp-idf releases
# See: https://github.com/pioarduino/esp-idf/releases
ARDUINO_IDF_VERSION_LOOKUP = {
cv.Version(3, 3, 5): cv.Version(5, 5, 2),
cv.Version(3, 3, 4): cv.Version(5, 5, 1),
cv.Version(3, 3, 3): cv.Version(5, 5, 1),
cv.Version(3, 3, 2): cv.Version(5, 5, 1),
cv.Version(3, 3, 1): cv.Version(5, 5, 1),
cv.Version(3, 3, 0): cv.Version(5, 5, 0),
cv.Version(3, 2, 1): cv.Version(5, 4, 2),
cv.Version(3, 2, 0): cv.Version(5, 4, 2),
cv.Version(3, 1, 3): cv.Version(5, 3, 2),
cv.Version(3, 1, 2): cv.Version(5, 3, 2),
cv.Version(3, 1, 1): cv.Version(5, 3, 1),
cv.Version(3, 1, 0): cv.Version(5, 3, 0),
}
# The default/recommended esp-idf framework version # The default/recommended esp-idf framework version
# - https://github.com/espressif/esp-idf/releases # - https://github.com/espressif/esp-idf/releases
ESP_IDF_FRAMEWORK_VERSION_LOOKUP = { ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
"recommended": cv.Version(5, 5, 1), "recommended": cv.Version(5, 5, 2),
"latest": cv.Version(5, 5, 1), "latest": cv.Version(5, 5, 2),
"dev": cv.Version(5, 5, 1), "dev": cv.Version(5, 5, 2),
} }
ESP_IDF_PLATFORM_VERSION_LOOKUP = { ESP_IDF_PLATFORM_VERSION_LOOKUP = {
cv.Version(5, 5, 2): cv.Version(55, 3, 35),
cv.Version(5, 5, 1): cv.Version(55, 3, 31, "2"), cv.Version(5, 5, 1): cv.Version(55, 3, 31, "2"),
cv.Version(5, 5, 0): cv.Version(55, 3, 31, "2"), cv.Version(5, 5, 0): cv.Version(55, 3, 31, "2"),
cv.Version(5, 4, 3): cv.Version(55, 3, 32), cv.Version(5, 4, 3): cv.Version(55, 3, 32),
@@ -395,9 +418,9 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = {
# The platform-espressif32 version # The platform-espressif32 version
# - https://github.com/pioarduino/platform-espressif32/releases # - https://github.com/pioarduino/platform-espressif32/releases
PLATFORM_VERSION_LOOKUP = { PLATFORM_VERSION_LOOKUP = {
"recommended": cv.Version(55, 3, 31, "2"), "recommended": cv.Version(55, 3, 35),
"latest": cv.Version(55, 3, 31, "2"), "latest": cv.Version(55, 3, 35),
"dev": cv.Version(55, 3, 31, "2"), "dev": cv.Version(55, 3, 35),
} }
@@ -570,6 +593,13 @@ def final_validate(config):
path=[CONF_FLASH_SIZE], path=[CONF_FLASH_SIZE],
) )
) )
if advanced[CONF_ENABLE_OTA_ROLLBACK]:
safe_mode_config = full_config.get(CONF_SAFE_MODE)
if safe_mode_config is None or safe_mode_config.get(CONF_DISABLED, False):
_LOGGER.warning(
"OTA rollback requires safe_mode, disabling rollback support"
)
advanced[CONF_ENABLE_OTA_ROLLBACK] = False
if errs: if errs:
raise cv.MultipleInvalid(errs) raise cv.MultipleInvalid(errs)
@@ -595,9 +625,6 @@ CONF_LOOP_TASK_STACK_SIZE = "loop_task_stack_size"
KEY_VFS_SELECT_REQUIRED = "vfs_select_required" KEY_VFS_SELECT_REQUIRED = "vfs_select_required"
KEY_VFS_DIR_REQUIRED = "vfs_dir_required" KEY_VFS_DIR_REQUIRED = "vfs_dir_required"
# Ring buffer IRAM requirement tracking
KEY_RINGBUF_IN_IRAM = "ringbuf_in_iram"
def require_vfs_select() -> None: def require_vfs_select() -> None:
"""Mark that VFS select support is required by a component. """Mark that VFS select support is required by a component.
@@ -617,23 +644,15 @@ def require_vfs_dir() -> None:
CORE.data[KEY_VFS_DIR_REQUIRED] = True CORE.data[KEY_VFS_DIR_REQUIRED] = True
def enable_ringbuf_in_iram() -> None:
"""Keep ring buffer functions in IRAM instead of moving them to flash.
Call this from components that use esphome/core/ring_buffer.cpp and need
the ring buffer functions to remain in IRAM for performance reasons
(e.g., voice assistants, audio components).
This prevents CONFIG_RINGBUF_PLACE_FUNCTIONS_INTO_FLASH from being enabled.
"""
CORE.data[KEY_RINGBUF_IN_IRAM] = True
def _parse_idf_component(value: str) -> ConfigType: def _parse_idf_component(value: str) -> ConfigType:
"""Parse IDF component shorthand syntax like 'owner/component^version'""" """Parse IDF component shorthand syntax like 'owner/component^version'"""
if "^" not in value: # Match operator followed by version-like string (digit or *)
raise cv.Invalid(f"Invalid IDF component shorthand '{value}'") if match := re.search(r"(~=|>=|<=|==|!=|>|<|\^|~)(\d|\*)", value):
name, ref = value.split("^", 1) return {CONF_NAME: value[: match.start()], CONF_REF: value[match.start() :]}
return {CONF_NAME: name, CONF_REF: ref} raise cv.Invalid(
f"Invalid IDF component shorthand '{value}'. "
f"Expected format: 'owner/component<op>version' where <op> is one of: ^, ~, ~=, ==, !=, >=, >, <=, <"
)
def _validate_idf_component(config: ConfigType) -> ConfigType: def _validate_idf_component(config: ConfigType) -> ConfigType:
@@ -701,6 +720,7 @@ FRAMEWORK_SCHEMA = cv.Schema(
cv.Optional(CONF_LOOP_TASK_STACK_SIZE, default=8192): cv.int_range( cv.Optional(CONF_LOOP_TASK_STACK_SIZE, default=8192): cv.int_range(
min=8192, max=32768 min=8192, max=32768
), ),
cv.Optional(CONF_ENABLE_OTA_ROLLBACK, default=True): cv.boolean,
} }
), ),
cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list( cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list(
@@ -726,12 +746,14 @@ FRAMEWORK_SCHEMA = cv.Schema(
) )
# Remove this class in 2026.7.0
class _FrameworkMigrationWarning: class _FrameworkMigrationWarning:
shown = False shown = False
def _show_framework_migration_message(name: str, variant: str) -> None: def _show_framework_migration_message(name: str, variant: str) -> None:
"""Show a friendly message about framework migration when defaulting to Arduino.""" """Show a message about the framework default change and how to switch back to Arduino."""
# Remove this function in 2026.7.0
if _FrameworkMigrationWarning.shown: if _FrameworkMigrationWarning.shown:
return return
_FrameworkMigrationWarning.shown = True _FrameworkMigrationWarning.shown = True
@@ -741,41 +763,27 @@ def _show_framework_migration_message(name: str, variant: str) -> None:
message = ( message = (
color( color(
AnsiFore.BOLD_CYAN, AnsiFore.BOLD_CYAN,
f"💡 IMPORTANT: {name} doesn't have a framework specified!", f"💡 NOTICE: {name} does not have a framework specified.",
) )
+ "\n\n" + "\n\n"
+ f"Currently, {variant} defaults to the Arduino framework.\n" + f"Starting with ESPHome 2026.1.0, the default framework for {variant} is ESP-IDF.\n"
+ color(AnsiFore.YELLOW, "This will change to ESP-IDF in ESPHome 2026.1.0.\n") + "(We've been warning about this change since ESPHome 2025.8.0)\n"
+ "\n" + "\n"
+ "Note: Newer ESP32 variants (C6, H2, P4, etc.) already use ESP-IDF by default.\n" + "Why we made this change:\n"
+ "\n" + color(AnsiFore.GREEN, " ✨ Up to 40% smaller firmware binaries\n")
+ "Why change? ESP-IDF offers:\n"
+ color(AnsiFore.GREEN, " ✨ Up to 40% smaller binaries\n")
+ color(AnsiFore.GREEN, " 🚀 Better performance and optimization\n")
+ color(AnsiFore.GREEN, " ⚡ 2-3x faster compile times\n") + color(AnsiFore.GREEN, " ⚡ 2-3x faster compile times\n")
+ color(AnsiFore.GREEN, " 📦 Custom-built firmware for your exact needs\n") + color(AnsiFore.GREEN, " 🚀 Better performance and newer features\n")
+ color( + color(AnsiFore.GREEN, " 🔧 More actively maintained by ESPHome\n")
AnsiFore.GREEN,
" 🔧 Active development and testing by ESPHome developers\n",
)
+ "\n" + "\n"
+ "Trade-offs:\n" + "To continue using Arduino, add this to your YAML under 'esp32:':\n"
+ color(AnsiFore.YELLOW, " 🔄 Some components need migration\n") + color(AnsiFore.WHITE, " framework:\n")
+ color(AnsiFore.WHITE, " type: arduino\n")
+ "\n" + "\n"
+ "What should I do?\n" + "To silence this message with ESP-IDF, explicitly set:\n"
+ color(AnsiFore.CYAN, " Option 1") + color(AnsiFore.WHITE, " framework:\n")
+ ": Migrate to ESP-IDF (recommended)\n" + color(AnsiFore.WHITE, " type: esp-idf\n")
+ " Add this to your YAML under 'esp32:':\n"
+ color(AnsiFore.WHITE, " framework:\n")
+ color(AnsiFore.WHITE, " type: esp-idf\n")
+ "\n" + "\n"
+ color(AnsiFore.CYAN, " Option 2") + "Migration guide: "
+ ": Keep using Arduino (still supported)\n"
+ " Add this to your YAML under 'esp32:':\n"
+ color(AnsiFore.WHITE, " framework:\n")
+ color(AnsiFore.WHITE, " type: arduino\n")
+ "\n"
+ "Need help? Check out the migration guide:\n"
+ color( + color(
AnsiFore.BLUE, AnsiFore.BLUE,
"https://esphome.io/guides/esp32_arduino_to_idf/", "https://esphome.io/guides/esp32_arduino_to_idf/",
@@ -790,13 +798,13 @@ def _set_default_framework(config):
config[CONF_FRAMEWORK] = FRAMEWORK_SCHEMA({}) config[CONF_FRAMEWORK] = FRAMEWORK_SCHEMA({})
if CONF_TYPE not in config[CONF_FRAMEWORK]: if CONF_TYPE not in config[CONF_FRAMEWORK]:
variant = config[CONF_VARIANT] variant = config[CONF_VARIANT]
config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ESP_IDF
# Show migration message for variants that previously defaulted to Arduino
# Remove this message in 2026.7.0
if variant in ARDUINO_ALLOWED_VARIANTS: if variant in ARDUINO_ALLOWED_VARIANTS:
config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ARDUINO
_show_framework_migration_message( _show_framework_migration_message(
config.get(CONF_NAME, "This device"), variant config.get(CONF_NAME, "This device"), variant
) )
else:
config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ESP_IDF
return config return config
@@ -981,29 +989,21 @@ async def to_code(config):
cg.add_platformio_option("framework", "arduino, espidf") cg.add_platformio_option("framework", "arduino, espidf")
cg.add_build_flag("-DUSE_ARDUINO") cg.add_build_flag("-DUSE_ARDUINO")
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO") cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO")
cg.add_platformio_option(
"board_build.embed_txtfiles",
[
"managed_components/espressif__esp_insights/server_certs/https_server.crt",
"managed_components/espressif__esp_rainmaker/server_certs/rmaker_mqtt_server.crt",
"managed_components/espressif__esp_rainmaker/server_certs/rmaker_claim_service_server.crt",
"managed_components/espressif__esp_rainmaker/server_certs/rmaker_ota_server.crt",
],
)
cg.add_define( cg.add_define(
"USE_ARDUINO_VERSION_CODE", "USE_ARDUINO_VERSION_CODE",
cg.RawExpression( cg.RawExpression(
f"VERSION_CODE({framework_ver.major}, {framework_ver.minor}, {framework_ver.patch})" f"VERSION_CODE({framework_ver.major}, {framework_ver.minor}, {framework_ver.patch})"
), ),
) )
add_idf_sdkconfig_option(
"CONFIG_ARDUINO_LOOP_STACK_SIZE",
conf[CONF_ADVANCED][CONF_LOOP_TASK_STACK_SIZE],
)
add_idf_sdkconfig_option("CONFIG_AUTOSTART_ARDUINO", True)
add_idf_sdkconfig_option("CONFIG_MBEDTLS_PSK_MODES", True) add_idf_sdkconfig_option("CONFIG_MBEDTLS_PSK_MODES", True)
add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True) add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True)
add_idf_sdkconfig_option("CONFIG_ESP_PHY_REDUCE_TX_POWER", True)
# Add IDF framework source for Arduino builds to ensure it uses the same version as
# the ESP-IDF framework
if (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is not None:
cg.add_platformio_option(
"platform_packages", [_format_framework_espidf_version(idf_ver, None)]
)
# ESP32-S2 Arduino: Disable USB Serial on boot to avoid TinyUSB dependency # ESP32-S2 Arduino: Disable USB Serial on boot to avoid TinyUSB dependency
if get_esp32_variant() == VARIANT_ESP32S2: if get_esp32_variant() == VARIANT_ESP32S2:
@@ -1041,18 +1041,14 @@ async def to_code(config):
add_idf_sdkconfig_option("CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH", True) add_idf_sdkconfig_option("CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH", True)
# Place ring buffer functions into flash instead of IRAM by default # Place ring buffer functions into flash instead of IRAM by default
# This saves IRAM but may impact performance for audio/voice components. # This saves IRAM. In ESP-IDF 6.0 flash placement becomes the default.
# Components that need ring buffer in IRAM call enable_ringbuf_in_iram(). # Users can set ringbuf_in_iram: true as an escape hatch if they encounter issues.
# Users can also set ringbuf_in_iram: true to force IRAM placement. if conf[CONF_ADVANCED][CONF_RINGBUF_IN_IRAM]:
# In ESP-IDF 6.0 flash placement becomes the default. # User requests ring buffer in IRAM
if conf[CONF_ADVANCED][CONF_RINGBUF_IN_IRAM] or CORE.data.get(
KEY_RINGBUF_IN_IRAM, False
):
# User config or component requires ring buffer in IRAM for performance
# IDF 6.0+: will need CONFIG_RINGBUF_PLACE_ISR_FUNCTIONS_INTO_FLASH=n # IDF 6.0+: will need CONFIG_RINGBUF_PLACE_ISR_FUNCTIONS_INTO_FLASH=n
add_idf_sdkconfig_option("CONFIG_RINGBUF_PLACE_ISR_FUNCTIONS_INTO_FLASH", False) add_idf_sdkconfig_option("CONFIG_RINGBUF_PLACE_ISR_FUNCTIONS_INTO_FLASH", False)
else: else:
# No component needs it - place in flash to save IRAM # Place in flash to save IRAM (default)
add_idf_sdkconfig_option("CONFIG_RINGBUF_PLACE_FUNCTIONS_INTO_FLASH", True) add_idf_sdkconfig_option("CONFIG_RINGBUF_PLACE_FUNCTIONS_INTO_FLASH", True)
# Setup watchdog # Setup watchdog
@@ -1178,6 +1174,11 @@ async def to_code(config):
"CONFIG_BOOTLOADER_CACHE_32BIT_ADDR_QUAD_FLASH", True "CONFIG_BOOTLOADER_CACHE_32BIT_ADDR_QUAD_FLASH", True
) )
# Enable OTA rollback support
if advanced[CONF_ENABLE_OTA_ROLLBACK]:
add_idf_sdkconfig_option("CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE", True)
cg.add_define("USE_OTA_ROLLBACK")
cg.add_define("ESPHOME_LOOP_TASK_STACK_SIZE", advanced[CONF_LOOP_TASK_STACK_SIZE]) cg.add_define("ESPHOME_LOOP_TASK_STACK_SIZE", advanced[CONF_LOOP_TASK_STACK_SIZE])
cg.add_define( cg.add_define(
@@ -1207,7 +1208,7 @@ APP_PARTITION_SIZES = {
} }
def get_arduino_partition_csv(flash_size): def get_arduino_partition_csv(flash_size: str):
app_partition_size = APP_PARTITION_SIZES[flash_size] app_partition_size = APP_PARTITION_SIZES[flash_size]
eeprom_partition_size = 0x1000 # 4 KB eeprom_partition_size = 0x1000 # 4 KB
spiffs_partition_size = 0xF000 # 60 KB spiffs_partition_size = 0xF000 # 60 KB
@@ -1227,7 +1228,7 @@ spiffs, data, spiffs, 0x{spiffs_partition_start:X}, 0x{spiffs_partition_size:
""" """
def get_idf_partition_csv(flash_size): def get_idf_partition_csv(flash_size: str):
app_partition_size = APP_PARTITION_SIZES[flash_size] app_partition_size = APP_PARTITION_SIZES[flash_size]
return f"""\ return f"""\

View File

@@ -1488,6 +1488,10 @@ BOARDS = {
"name": "Arduino Nano ESP32", "name": "Arduino Nano ESP32",
"variant": VARIANT_ESP32S3, "variant": VARIANT_ESP32S3,
}, },
"arduino_nesso_n1": {
"name": "Arduino Nesso-N1",
"variant": VARIANT_ESP32C6,
},
"atd147_s3": { "atd147_s3": {
"name": "ArtronShop ATD1.47-S3", "name": "ArtronShop ATD1.47-S3",
"variant": VARIANT_ESP32S3, "variant": VARIANT_ESP32S3,
@@ -1656,6 +1660,10 @@ BOARDS = {
"name": "Espressif ESP32-C6-DevKitM-1", "name": "Espressif ESP32-C6-DevKitM-1",
"variant": VARIANT_ESP32C6, "variant": VARIANT_ESP32C6,
}, },
"esp32-c61-devkitc1-n8r2": {
"name": "Espressif ESP32-C61-DevKitC-1 N8R2 (8 MB Flash Quad, 2 MB PSRAM Quad)",
"variant": VARIANT_ESP32C61,
},
"esp32-devkitlipo": { "esp32-devkitlipo": {
"name": "OLIMEX ESP32-DevKit-LiPo", "name": "OLIMEX ESP32-DevKit-LiPo",
"variant": VARIANT_ESP32, "variant": VARIANT_ESP32,
@@ -1673,11 +1681,15 @@ BOARDS = {
"variant": VARIANT_ESP32H2, "variant": VARIANT_ESP32H2,
}, },
"esp32-p4": { "esp32-p4": {
"name": "Espressif ESP32-P4 generic", "name": "Espressif ESP32-P4 ES (pre rev.300) generic",
"variant": VARIANT_ESP32P4, "variant": VARIANT_ESP32P4,
}, },
"esp32-p4-evboard": { "esp32-p4-evboard": {
"name": "Espressif ESP32-P4 Function EV Board", "name": "Espressif ESP32-P4 Function EV Board (ES pre rev.300)",
"variant": VARIANT_ESP32P4,
},
"esp32-p4_r3": {
"name": "Espressif ESP32-P4 rev.300 generic",
"variant": VARIANT_ESP32P4, "variant": VARIANT_ESP32P4,
}, },
"esp32-pico-devkitm-2": { "esp32-pico-devkitm-2": {
@@ -2093,7 +2105,7 @@ BOARDS = {
"variant": VARIANT_ESP32, "variant": VARIANT_ESP32,
}, },
"m5stack-tab5-p4": { "m5stack-tab5-p4": {
"name": "M5STACK Tab5 esp32-p4 Board", "name": "M5STACK Tab5 esp32-p4 Board (ES pre rev.300)",
"variant": VARIANT_ESP32P4, "variant": VARIANT_ESP32P4,
}, },
"m5stack-timer-cam": { "m5stack-timer-cam": {

View File

@@ -4,25 +4,20 @@
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "preferences.h" #include "preferences.h"
#include <freertos/FreeRTOS.h> #include <esp_clk_tree.h>
#include <freertos/task.h> #include <esp_cpu.h>
#include <esp_idf_version.h> #include <esp_idf_version.h>
#include <esp_ota_ops.h> #include <esp_ota_ops.h>
#include <esp_task_wdt.h> #include <esp_task_wdt.h>
#include <esp_timer.h> #include <esp_timer.h>
#include <soc/rtc.h> #include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <hal/cpu_hal.h> void setup(); // NOLINT(readability-redundant-declaration)
void loop(); // NOLINT(readability-redundant-declaration)
#ifdef USE_ARDUINO // Weak stub for initArduino - overridden when the Arduino component is present
#include <Esp.h> extern "C" __attribute__((weak)) void initArduino() {}
#else
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
#include <esp_clk_tree.h>
#endif
void setup();
void loop();
#endif
namespace esphome { namespace esphome {
@@ -41,29 +36,13 @@ void arch_restart() {
void arch_init() { void arch_init() {
// Enable the task watchdog only on the loop task (from which we're currently running) // Enable the task watchdog only on the loop task (from which we're currently running)
#if defined(USE_ESP_IDF)
esp_task_wdt_add(nullptr); esp_task_wdt_add(nullptr);
// Idle task watchdog is disabled on ESP-IDF
#elif defined(USE_ARDUINO)
enableLoopWDT();
// Disable idle task watchdog on the core we're using (Arduino pins the task to a core)
#if defined(CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0) && CONFIG_ARDUINO_RUNNING_CORE == 0
disableCore0WDT();
#endif
#if defined(CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1) && CONFIG_ARDUINO_RUNNING_CORE == 1
disableCore1WDT();
#endif
#endif
// If the bootloader was compiled with CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE the current // Handle OTA rollback: mark partition valid immediately unless USE_OTA_ROLLBACK is enabled,
// partition will get rolled back unless it is marked as valid. // in which case safe_mode will mark it valid after confirming successful boot.
esp_ota_img_states_t state; #ifndef USE_OTA_ROLLBACK
const esp_partition_t *running = esp_ota_get_running_partition(); esp_ota_mark_app_valid_cancel_rollback();
if (esp_ota_get_state_partition(running, &state) == ESP_OK) { #endif
if (state == ESP_OTA_IMG_PENDING_VERIFY) {
esp_ota_mark_app_valid_cancel_rollback();
}
}
} }
void IRAM_ATTR HOT arch_feed_wdt() { esp_task_wdt_reset(); } void IRAM_ATTR HOT arch_feed_wdt() { esp_task_wdt_reset(); }
@@ -71,21 +50,10 @@ uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; }
uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); } uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); }
uint32_t arch_get_cpu_freq_hz() { uint32_t arch_get_cpu_freq_hz() {
uint32_t freq = 0; uint32_t freq = 0;
#ifdef USE_ESP_IDF
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
esp_clk_tree_src_get_freq_hz(SOC_MOD_CLK_CPU, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &freq); esp_clk_tree_src_get_freq_hz(SOC_MOD_CLK_CPU, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &freq);
#else
rtc_cpu_freq_config_t config;
rtc_clk_cpu_freq_get_config(&config);
freq = config.freq_mhz * 1000000U;
#endif
#elif defined(USE_ARDUINO)
freq = ESP.getCpuFreqMHz() * 1000000;
#endif
return freq; return freq;
} }
#ifdef USE_ESP_IDF
TaskHandle_t loop_task_handle = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) TaskHandle_t loop_task_handle = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void loop_task(void *pv_params) { void loop_task(void *pv_params) {
@@ -96,6 +64,7 @@ void loop_task(void *pv_params) {
} }
extern "C" void app_main() { extern "C" void app_main() {
initArduino();
esp32::setup_preferences(); esp32::setup_preferences();
#if CONFIG_FREERTOS_UNICORE #if CONFIG_FREERTOS_UNICORE
xTaskCreate(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, &loop_task_handle); xTaskCreate(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, &loop_task_handle);
@@ -103,11 +72,6 @@ extern "C" void app_main() {
xTaskCreatePinnedToCore(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, &loop_task_handle, 1); xTaskCreatePinnedToCore(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, &loop_task_handle, 1);
#endif #endif
} }
#endif // USE_ESP_IDF
#ifdef USE_ARDUINO
extern "C" void init() { esp32::setup_preferences(); }
#endif // USE_ARDUINO
} // namespace esphome } // namespace esphome

View File

@@ -85,6 +85,7 @@ void ESP32InternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpi
break; break;
} }
gpio_set_intr_type(this->get_pin_num(), idf_type); gpio_set_intr_type(this->get_pin_num(), idf_type);
gpio_intr_enable(this->get_pin_num());
if (!isr_service_installed) { if (!isr_service_installed) {
auto res = gpio_install_isr_service(ESP_INTR_FLAG_LEVEL3); auto res = gpio_install_isr_service(ESP_INTR_FLAG_LEVEL3);
if (res != ESP_OK) { if (res != ESP_OK) {
@@ -94,7 +95,6 @@ void ESP32InternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpi
isr_service_installed = true; isr_service_installed = true;
} }
gpio_isr_handler_add(this->get_pin_num(), func, arg); gpio_isr_handler_add(this->get_pin_num(), func, arg);
gpio_intr_enable(this->get_pin_num());
} }
std::string ESP32InternalGPIOPin::dump_summary() const { std::string ESP32InternalGPIOPin::dump_summary() const {

View File

@@ -5,6 +5,7 @@ import json # noqa: E402
import os # noqa: E402 import os # noqa: E402
import pathlib # noqa: E402 import pathlib # noqa: E402
import shutil # noqa: E402 import shutil # noqa: E402
from glob import glob # noqa: E402
def merge_factory_bin(source, target, env): def merge_factory_bin(source, target, env):
@@ -126,3 +127,14 @@ def esp32_copy_ota_bin(source, target, env):
# Run merge first, then ota copy second # Run merge first, then ota copy second
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", merge_factory_bin) # noqa: F821 env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", merge_factory_bin) # noqa: F821
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_copy_ota_bin) # noqa: F821 env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_copy_ota_bin) # noqa: F821
# Find server certificates in managed components and generate .S files.
# Workaround for PlatformIO not processing target_add_binary_data() from managed component CMakeLists.
project_dir = env.subst("$PROJECT_DIR")
managed_components = os.path.join(project_dir, "managed_components")
if os.path.isdir(managed_components):
for cert_file in glob(os.path.join(managed_components, "**/server_certs/*.crt"), recursive=True):
try:
env.FileToAsm(cert_file, FILE_TYPE="TEXT")
except Exception as e:
print(f"Error processing {os.path.basename(cert_file)}: {e}")

View File

@@ -4,26 +4,28 @@
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/preferences.h" #include "esphome/core/preferences.h"
#include <nvs_flash.h> #include <nvs_flash.h>
#include <cstring>
#include <cinttypes> #include <cinttypes>
#include <vector> #include <cstring>
#include <string>
#include <memory> #include <memory>
#include <vector>
namespace esphome { namespace esphome {
namespace esp32 { namespace esp32 {
static const char *const TAG = "esp32.preferences"; static const char *const TAG = "esp32.preferences";
// Buffer size for converting uint32_t to string: max "4294967295" (10 chars) + null terminator + 1 padding
static constexpr size_t KEY_BUFFER_SIZE = 12;
struct NVSData { struct NVSData {
std::string key; uint32_t key;
std::unique_ptr<uint8_t[]> data; std::unique_ptr<uint8_t[]> data;
size_t len; size_t len;
void set_data(const uint8_t *src, size_t size) { void set_data(const uint8_t *src, size_t size) {
data = std::make_unique<uint8_t[]>(size); this->data = std::make_unique<uint8_t[]>(size);
memcpy(data.get(), src, size); memcpy(this->data.get(), src, size);
len = size; this->len = size;
} }
}; };
@@ -31,27 +33,27 @@ static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-n
class ESP32PreferenceBackend : public ESPPreferenceBackend { class ESP32PreferenceBackend : public ESPPreferenceBackend {
public: public:
std::string key; uint32_t key;
uint32_t nvs_handle; uint32_t nvs_handle;
bool save(const uint8_t *data, size_t len) override { bool save(const uint8_t *data, size_t len) override {
// try find in pending saves and update that // try find in pending saves and update that
for (auto &obj : s_pending_save) { for (auto &obj : s_pending_save) {
if (obj.key == key) { if (obj.key == this->key) {
obj.set_data(data, len); obj.set_data(data, len);
return true; return true;
} }
} }
NVSData save{}; NVSData save{};
save.key = key; save.key = this->key;
save.set_data(data, len); save.set_data(data, len);
s_pending_save.emplace_back(std::move(save)); s_pending_save.emplace_back(std::move(save));
ESP_LOGVV(TAG, "s_pending_save: key: %s, len: %zu", key.c_str(), len); ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len);
return true; return true;
} }
bool load(uint8_t *data, size_t len) override { bool load(uint8_t *data, size_t len) override {
// try find in pending saves and load from that // try find in pending saves and load from that
for (auto &obj : s_pending_save) { for (auto &obj : s_pending_save) {
if (obj.key == key) { if (obj.key == this->key) {
if (obj.len != len) { if (obj.len != len) {
// size mismatch // size mismatch
return false; return false;
@@ -61,22 +63,24 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend {
} }
} }
char key_str[KEY_BUFFER_SIZE];
snprintf(key_str, sizeof(key_str), "%" PRIu32, this->key);
size_t actual_len; size_t actual_len;
esp_err_t err = nvs_get_blob(nvs_handle, key.c_str(), nullptr, &actual_len); esp_err_t err = nvs_get_blob(this->nvs_handle, key_str, nullptr, &actual_len);
if (err != 0) { if (err != 0) {
ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key.c_str(), esp_err_to_name(err)); ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key_str, esp_err_to_name(err));
return false; return false;
} }
if (actual_len != len) { if (actual_len != len) {
ESP_LOGVV(TAG, "NVS length does not match (%zu!=%zu)", actual_len, len); ESP_LOGVV(TAG, "NVS length does not match (%zu!=%zu)", actual_len, len);
return false; return false;
} }
err = nvs_get_blob(nvs_handle, key.c_str(), data, &len); err = nvs_get_blob(this->nvs_handle, key_str, data, &len);
if (err != 0) { if (err != 0) {
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key.c_str(), esp_err_to_name(err)); ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err));
return false; return false;
} else { } else {
ESP_LOGVV(TAG, "nvs_get_blob: key: %s, len: %zu", key.c_str(), len); ESP_LOGVV(TAG, "nvs_get_blob: key: %s, len: %zu", key_str, len);
} }
return true; return true;
} }
@@ -103,14 +107,12 @@ class ESP32Preferences : public ESPPreferences {
} }
} }
ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override { ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override {
return make_preference(length, type); return this->make_preference(length, type);
} }
ESPPreferenceObject make_preference(size_t length, uint32_t type) override { ESPPreferenceObject make_preference(size_t length, uint32_t type) override {
auto *pref = new ESP32PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory) auto *pref = new ESP32PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory)
pref->nvs_handle = nvs_handle; pref->nvs_handle = this->nvs_handle;
pref->key = type;
uint32_t keyval = type;
pref->key = str_sprintf("%" PRIu32, keyval);
return ESPPreferenceObject(pref); return ESPPreferenceObject(pref);
} }
@@ -123,17 +125,19 @@ class ESP32Preferences : public ESPPreferences {
// goal try write all pending saves even if one fails // goal try write all pending saves even if one fails
int cached = 0, written = 0, failed = 0; int cached = 0, written = 0, failed = 0;
esp_err_t last_err = ESP_OK; esp_err_t last_err = ESP_OK;
std::string last_key{}; uint32_t last_key = 0;
// go through vector from back to front (makes erase easier/more efficient) // go through vector from back to front (makes erase easier/more efficient)
for (ssize_t i = s_pending_save.size() - 1; i >= 0; i--) { for (ssize_t i = s_pending_save.size() - 1; i >= 0; i--) {
const auto &save = s_pending_save[i]; const auto &save = s_pending_save[i];
ESP_LOGVV(TAG, "Checking if NVS data %s has changed", save.key.c_str()); char key_str[KEY_BUFFER_SIZE];
if (is_changed(nvs_handle, save)) { snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
esp_err_t err = nvs_set_blob(nvs_handle, save.key.c_str(), save.data.get(), save.len); ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str);
ESP_LOGV(TAG, "sync: key: %s, len: %zu", save.key.c_str(), save.len); if (this->is_changed_(this->nvs_handle, save, key_str)) {
esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.get(), save.len);
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.len);
if (err != 0) { if (err != 0) {
ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", save.key.c_str(), save.len, esp_err_to_name(err)); ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.len, esp_err_to_name(err));
failed++; failed++;
last_err = err; last_err = err;
last_key = save.key; last_key = save.key;
@@ -141,7 +145,7 @@ class ESP32Preferences : public ESPPreferences {
} }
written++; written++;
} else { } else {
ESP_LOGV(TAG, "NVS data not changed skipping %s len=%zu", save.key.c_str(), save.len); ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.len);
cached++; cached++;
} }
s_pending_save.erase(s_pending_save.begin() + i); s_pending_save.erase(s_pending_save.begin() + i);
@@ -149,12 +153,12 @@ class ESP32Preferences : public ESPPreferences {
ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written, ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written,
failed); failed);
if (failed > 0) { if (failed > 0) {
ESP_LOGE(TAG, "Writing %d items failed. Last error=%s for key=%s", failed, esp_err_to_name(last_err), ESP_LOGE(TAG, "Writing %d items failed. Last error=%s for key=%" PRIu32, failed, esp_err_to_name(last_err),
last_key.c_str()); last_key);
} }
// note: commit on esp-idf currently is a no-op, nvs_set_blob always writes // note: commit on esp-idf currently is a no-op, nvs_set_blob always writes
esp_err_t err = nvs_commit(nvs_handle); esp_err_t err = nvs_commit(this->nvs_handle);
if (err != 0) { if (err != 0) {
ESP_LOGV(TAG, "nvs_commit() failed: %s", esp_err_to_name(err)); ESP_LOGV(TAG, "nvs_commit() failed: %s", esp_err_to_name(err));
return false; return false;
@@ -162,11 +166,13 @@ class ESP32Preferences : public ESPPreferences {
return failed == 0; return failed == 0;
} }
bool is_changed(const uint32_t nvs_handle, const NVSData &to_save) {
protected:
bool is_changed_(uint32_t nvs_handle, const NVSData &to_save, const char *key_str) {
size_t actual_len; size_t actual_len;
esp_err_t err = nvs_get_blob(nvs_handle, to_save.key.c_str(), nullptr, &actual_len); esp_err_t err = nvs_get_blob(nvs_handle, key_str, nullptr, &actual_len);
if (err != 0) { if (err != 0) {
ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", to_save.key.c_str(), esp_err_to_name(err)); ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key_str, esp_err_to_name(err));
return true; return true;
} }
// Check size first before allocating memory // Check size first before allocating memory
@@ -174,9 +180,9 @@ class ESP32Preferences : public ESPPreferences {
return true; return true;
} }
auto stored_data = std::make_unique<uint8_t[]>(actual_len); auto stored_data = std::make_unique<uint8_t[]>(actual_len);
err = nvs_get_blob(nvs_handle, to_save.key.c_str(), stored_data.get(), &actual_len); err = nvs_get_blob(nvs_handle, key_str, stored_data.get(), &actual_len);
if (err != 0) { if (err != 0) {
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", to_save.key.c_str(), esp_err_to_name(err)); ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err));
return true; return true;
} }
return memcmp(to_save.data.get(), stored_data.get(), to_save.len) != 0; return memcmp(to_save.data.get(), stored_data.get(), to_save.len) != 0;

View File

@@ -24,7 +24,9 @@ extern "C" {
#include <nvs_flash.h> #include <nvs_flash.h>
#ifdef USE_ARDUINO #ifdef USE_ARDUINO
#include <esp32-hal-bt.h> // Prevent Arduino from releasing BT memory at startup (esp32-hal-misc.c).
// Without this, esp_bt_controller_init() fails with ESP_ERR_INVALID_STATE.
extern "C" bool btInUse() { return true; } // NOLINT(readability-identifier-naming)
#endif #endif
namespace esphome::esp32_ble { namespace esphome::esp32_ble {
@@ -96,10 +98,6 @@ void ESP32BLE::advertising_set_service_data(const std::vector<uint8_t> &data) {
} }
void ESP32BLE::advertising_set_manufacturer_data(const std::vector<uint8_t> &data) { void ESP32BLE::advertising_set_manufacturer_data(const std::vector<uint8_t> &data) {
this->advertising_set_manufacturer_data(std::span<const uint8_t>(data));
}
void ESP32BLE::advertising_set_manufacturer_data(std::span<const uint8_t> data) {
this->advertising_init_(); this->advertising_init_();
this->advertising_->set_manufacturer_data(data); this->advertising_->set_manufacturer_data(data);
this->advertising_start(); this->advertising_start();
@@ -169,12 +167,6 @@ void ESP32BLE::advertising_init_() {
bool ESP32BLE::ble_setup_() { bool ESP32BLE::ble_setup_() {
esp_err_t err; esp_err_t err;
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID #ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
#ifdef USE_ARDUINO
if (!btStart()) {
ESP_LOGE(TAG, "btStart failed: %d", esp_bt_controller_get_status());
return false;
}
#else
if (esp_bt_controller_get_status() != ESP_BT_CONTROLLER_STATUS_ENABLED) { if (esp_bt_controller_get_status() != ESP_BT_CONTROLLER_STATUS_ENABLED) {
// start bt controller // start bt controller
if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_IDLE) { if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_IDLE) {
@@ -199,7 +191,6 @@ bool ESP32BLE::ble_setup_() {
return false; return false;
} }
} }
#endif
esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT); esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT);
#else #else
@@ -260,8 +251,11 @@ bool ESP32BLE::ble_setup_() {
} }
#endif #endif
// BLE device names are limited to 20 characters
// Buffer: 20 chars + null terminator
constexpr size_t ble_name_max_len = 21;
char name_buffer[ble_name_max_len];
const char *device_name; const char *device_name;
std::string name_with_suffix;
if (this->name_ != nullptr) { if (this->name_ != nullptr) {
if (App.is_name_add_mac_suffix_enabled()) { if (App.is_name_add_mac_suffix_enabled()) {
@@ -272,23 +266,28 @@ bool ESP32BLE::ble_setup_() {
char mac_addr[mac_address_len]; char mac_addr[mac_address_len];
get_mac_address_into_buffer(mac_addr); get_mac_address_into_buffer(mac_addr);
const char *mac_suffix_ptr = mac_addr + mac_address_suffix_len; const char *mac_suffix_ptr = mac_addr + mac_address_suffix_len;
name_with_suffix = make_name_with_suffix_to(name_buffer, sizeof(name_buffer), this->name_, strlen(this->name_), '-', mac_suffix_ptr,
make_name_with_suffix(this->name_, strlen(this->name_), '-', mac_suffix_ptr, mac_address_suffix_len); mac_address_suffix_len);
device_name = name_with_suffix.c_str(); device_name = name_buffer;
} else { } else {
device_name = this->name_; device_name = this->name_;
} }
} else { } else {
name_with_suffix = App.get_name(); const std::string &app_name = App.get_name();
if (name_with_suffix.length() > 20) { size_t name_len = app_name.length();
if (name_len > 20) {
if (App.is_name_add_mac_suffix_enabled()) { if (App.is_name_add_mac_suffix_enabled()) {
// Keep first 13 chars and last 7 chars (MAC suffix), remove middle // Keep first 13 chars and last 7 chars (MAC suffix), remove middle
name_with_suffix.erase(13, name_with_suffix.length() - 20); memcpy(name_buffer, app_name.c_str(), 13);
memcpy(name_buffer + 13, app_name.c_str() + name_len - 7, 7);
} else { } else {
name_with_suffix.resize(20); memcpy(name_buffer, app_name.c_str(), 20);
} }
name_buffer[20] = '\0';
} else {
memcpy(name_buffer, app_name.c_str(), name_len + 1); // Include null terminator
} }
device_name = name_with_suffix.c_str(); device_name = name_buffer;
} }
err = esp_ble_gap_set_device_name(device_name); err = esp_ble_gap_set_device_name(device_name);
@@ -312,22 +311,24 @@ bool ESP32BLE::ble_setup_() {
bool ESP32BLE::ble_dismantle_() { bool ESP32BLE::ble_dismantle_() {
esp_err_t err = esp_bluedroid_disable(); esp_err_t err = esp_bluedroid_disable();
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_bluedroid_disable failed: %d", err); // ESP_ERR_INVALID_STATE means Bluedroid is already disabled, which is fine
return false; if (err != ESP_ERR_INVALID_STATE) {
ESP_LOGE(TAG, "esp_bluedroid_disable failed: %d", err);
return false;
}
ESP_LOGD(TAG, "Already disabled");
} }
err = esp_bluedroid_deinit(); err = esp_bluedroid_deinit();
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_bluedroid_deinit failed: %d", err); // ESP_ERR_INVALID_STATE means Bluedroid is already deinitialized, which is fine
return false; if (err != ESP_ERR_INVALID_STATE) {
ESP_LOGE(TAG, "esp_bluedroid_deinit failed: %d", err);
return false;
}
ESP_LOGD(TAG, "Already deinitialized");
} }
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID #ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
#ifdef USE_ARDUINO
if (!btStop()) {
ESP_LOGE(TAG, "btStop failed: %d", esp_bt_controller_get_status());
return false;
}
#else
if (esp_bt_controller_get_status() != ESP_BT_CONTROLLER_STATUS_IDLE) { if (esp_bt_controller_get_status() != ESP_BT_CONTROLLER_STATUS_IDLE) {
// stop bt controller // stop bt controller
if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_ENABLED) { if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_ENABLED) {
@@ -351,7 +352,6 @@ bool ESP32BLE::ble_dismantle_() {
return false; return false;
} }
} }
#endif
#else #else
if (esp_hosted_bt_controller_disable() != ESP_OK) { if (esp_hosted_bt_controller_disable() != ESP_OK) {
ESP_LOGW(TAG, "esp_hosted_bt_controller_disable failed"); ESP_LOGW(TAG, "esp_hosted_bt_controller_disable failed");

View File

@@ -118,7 +118,6 @@ class ESP32BLE : public Component {
void advertising_start(); void advertising_start();
void advertising_set_service_data(const std::vector<uint8_t> &data); void advertising_set_service_data(const std::vector<uint8_t> &data);
void advertising_set_manufacturer_data(const std::vector<uint8_t> &data); void advertising_set_manufacturer_data(const std::vector<uint8_t> &data);
void advertising_set_manufacturer_data(std::span<const uint8_t> data);
void advertising_set_appearance(uint16_t appearance) { this->appearance_ = appearance; } void advertising_set_appearance(uint16_t appearance) { this->appearance_ = appearance; }
void advertising_set_service_data_and_name(std::span<const uint8_t> data, bool include_name); void advertising_set_service_data_and_name(std::span<const uint8_t> data, bool include_name);
void advertising_add_service_uuid(ESPBTUUID uuid); void advertising_add_service_uuid(ESPBTUUID uuid);
@@ -213,17 +212,23 @@ extern ESP32BLE *global_ble;
template<typename... Ts> class BLEEnabledCondition : public Condition<Ts...> { template<typename... Ts> class BLEEnabledCondition : public Condition<Ts...> {
public: public:
bool check(const Ts &...x) override { return global_ble->is_active(); } bool check(const Ts &...x) override { return global_ble != nullptr && global_ble->is_active(); }
}; };
template<typename... Ts> class BLEEnableAction : public Action<Ts...> { template<typename... Ts> class BLEEnableAction : public Action<Ts...> {
public: public:
void play(const Ts &...x) override { global_ble->enable(); } void play(const Ts &...x) override {
if (global_ble != nullptr)
global_ble->enable();
}
}; };
template<typename... Ts> class BLEDisableAction : public Action<Ts...> { template<typename... Ts> class BLEDisableAction : public Action<Ts...> {
public: public:
void play(const Ts &...x) override { global_ble->disable(); } void play(const Ts &...x) override {
if (global_ble != nullptr)
global_ble->disable();
}
}; };
} // namespace esphome::esp32_ble } // namespace esphome::esp32_ble

View File

@@ -59,10 +59,6 @@ void BLEAdvertising::set_service_data(const std::vector<uint8_t> &data) {
} }
void BLEAdvertising::set_manufacturer_data(const std::vector<uint8_t> &data) { void BLEAdvertising::set_manufacturer_data(const std::vector<uint8_t> &data) {
this->set_manufacturer_data(std::span<const uint8_t>(data));
}
void BLEAdvertising::set_manufacturer_data(std::span<const uint8_t> data) {
delete[] this->advertising_data_.p_manufacturer_data; delete[] this->advertising_data_.p_manufacturer_data;
this->advertising_data_.p_manufacturer_data = nullptr; this->advertising_data_.p_manufacturer_data = nullptr;
this->advertising_data_.manufacturer_len = data.size(); this->advertising_data_.manufacturer_len = data.size();

View File

@@ -37,7 +37,6 @@ class BLEAdvertising {
void set_scan_response(bool scan_response) { this->scan_response_ = scan_response; } void set_scan_response(bool scan_response) { this->scan_response_ = scan_response; }
void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; } void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; }
void set_manufacturer_data(const std::vector<uint8_t> &data); void set_manufacturer_data(const std::vector<uint8_t> &data);
void set_manufacturer_data(std::span<const uint8_t> data);
void set_appearance(uint16_t appearance) { this->advertising_data_.appearance = appearance; } void set_appearance(uint16_t appearance) { this->advertising_data_.appearance = appearance; }
void set_service_data(const std::vector<uint8_t> &data); void set_service_data(const std::vector<uint8_t> &data);
void set_service_data(std::span<const uint8_t> data); void set_service_data(std::span<const uint8_t> data);

View File

@@ -143,9 +143,8 @@ bool ESPBTUUID::operator==(const ESPBTUUID &uuid) const {
return this->as_128bit() == uuid.as_128bit(); return this->as_128bit() == uuid.as_128bit();
} }
esp_bt_uuid_t ESPBTUUID::get_uuid() const { return this->uuid_; } esp_bt_uuid_t ESPBTUUID::get_uuid() const { return this->uuid_; }
std::string ESPBTUUID::to_string() const { void ESPBTUUID::to_str(std::span<char, UUID_STR_LEN> output) const {
char buf[40]; // Enough for 128-bit UUID with dashes char *pos = output.data();
char *pos = buf;
switch (this->uuid_.len) { switch (this->uuid_.len) {
case ESP_UUID_LEN_16: case ESP_UUID_LEN_16:
@@ -156,7 +155,7 @@ std::string ESPBTUUID::to_string() const {
*pos++ = format_hex_pretty_char((this->uuid_.uuid.uuid16 >> 4) & 0x0F); *pos++ = format_hex_pretty_char((this->uuid_.uuid.uuid16 >> 4) & 0x0F);
*pos++ = format_hex_pretty_char(this->uuid_.uuid.uuid16 & 0x0F); *pos++ = format_hex_pretty_char(this->uuid_.uuid.uuid16 & 0x0F);
*pos = '\0'; *pos = '\0';
return std::string(buf); return;
case ESP_UUID_LEN_32: case ESP_UUID_LEN_32:
*pos++ = '0'; *pos++ = '0';
@@ -165,7 +164,7 @@ std::string ESPBTUUID::to_string() const {
*pos++ = format_hex_pretty_char((this->uuid_.uuid.uuid32 >> shift) & 0x0F); *pos++ = format_hex_pretty_char((this->uuid_.uuid.uuid32 >> shift) & 0x0F);
} }
*pos = '\0'; *pos = '\0';
return std::string(buf); return;
default: default:
case ESP_UUID_LEN_128: case ESP_UUID_LEN_128:
@@ -179,9 +178,13 @@ std::string ESPBTUUID::to_string() const {
} }
} }
*pos = '\0'; *pos = '\0';
return std::string(buf); return;
} }
return ""; }
std::string ESPBTUUID::to_string() const {
char buf[UUID_STR_LEN];
this->to_str(buf);
return std::string(buf);
} }
} // namespace esphome::esp32_ble } // namespace esphome::esp32_ble

View File

@@ -7,11 +7,15 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
#ifdef USE_ESP32_BLE_UUID #ifdef USE_ESP32_BLE_UUID
#include <span>
#include <string> #include <string>
#include <esp_bt_defs.h> #include <esp_bt_defs.h>
namespace esphome::esp32_ble { namespace esphome::esp32_ble {
/// Buffer size for UUID string: "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX\0"
static constexpr size_t UUID_STR_LEN = 37;
class ESPBTUUID { class ESPBTUUID {
public: public:
ESPBTUUID(); ESPBTUUID();
@@ -37,6 +41,7 @@ class ESPBTUUID {
esp_bt_uuid_t get_uuid() const; esp_bt_uuid_t get_uuid() const;
std::string to_string() const; std::string to_string() const;
void to_str(std::span<char, UUID_STR_LEN> output) const;
protected: protected:
esp_bt_uuid_t uuid_; esp_bt_uuid_t uuid_;

View File

@@ -1,6 +1,5 @@
#include "esp32_ble_beacon.h" #include "esp32_ble_beacon.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#ifdef USE_ESP32 #ifdef USE_ESP32

View File

@@ -50,8 +50,12 @@ void BLECharacteristic::parse_descriptors() {
desc->handle = result.handle; desc->handle = result.handle;
desc->characteristic = this; desc->characteristic = this;
this->descriptors.push_back(desc); this->descriptors.push_back(desc);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char uuid_buf[espbt::UUID_STR_LEN];
desc->uuid.to_str(uuid_buf);
ESP_LOGV(TAG, "[%d] [%s] descriptor %s, handle 0x%x", this->service->client->get_connection_index(), ESP_LOGV(TAG, "[%d] [%s] descriptor %s, handle 0x%x", this->service->client->get_connection_index(),
this->service->client->address_str(), desc->uuid.to_string().c_str(), desc->handle); this->service->client->address_str(), uuid_buf, desc->handle);
#endif
offset++; offset++;
} }
} }

View File

@@ -411,12 +411,15 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
this->update_conn_params_(MEDIUM_MIN_CONN_INTERVAL, MEDIUM_MAX_CONN_INTERVAL, 0, MEDIUM_CONN_TIMEOUT, "medium"); this->update_conn_params_(MEDIUM_MIN_CONN_INTERVAL, MEDIUM_MAX_CONN_INTERVAL, 0, MEDIUM_CONN_TIMEOUT, "medium");
} else if (this->connection_type_ != espbt::ConnectionType::V3_WITH_CACHE) { } else if (this->connection_type_ != espbt::ConnectionType::V3_WITH_CACHE) {
#ifdef USE_ESP32_BLE_DEVICE #ifdef USE_ESP32_BLE_DEVICE
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
for (auto &svc : this->services_) { for (auto &svc : this->services_) {
ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_, char uuid_buf[espbt::UUID_STR_LEN];
svc->uuid.to_string().c_str()); svc->uuid.to_str(uuid_buf);
ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_, uuid_buf);
ESP_LOGV(TAG, "[%d] [%s] start_handle: 0x%x end_handle: 0x%x", this->connection_index_, this->address_str_, ESP_LOGV(TAG, "[%d] [%s] start_handle: 0x%x end_handle: 0x%x", this->connection_index_, this->address_str_,
svc->start_handle, svc->end_handle); svc->start_handle, svc->end_handle);
} }
#endif
#endif #endif
} }
ESP_LOGI(TAG, "[%d] [%s] Service discovery complete", this->connection_index_, this->address_str_); ESP_LOGI(TAG, "[%d] [%s] Service discovery complete", this->connection_index_, this->address_str_);

View File

@@ -64,9 +64,12 @@ void BLEService::parse_characteristics() {
characteristic->handle = result.char_handle; characteristic->handle = result.char_handle;
characteristic->service = this; characteristic->service = this;
this->characteristics.push_back(characteristic); this->characteristics.push_back(characteristic);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char uuid_buf[espbt::UUID_STR_LEN];
characteristic->uuid.to_str(uuid_buf);
ESP_LOGV(TAG, "[%d] [%s] characteristic %s, handle 0x%x, properties 0x%x", this->client->get_connection_index(), ESP_LOGV(TAG, "[%d] [%s] characteristic %s, handle 0x%x, properties 0x%x", this->client->get_connection_index(),
this->client->address_str(), characteristic->uuid.to_string().c_str(), characteristic->handle, this->client->address_str(), uuid_buf, characteristic->handle, characteristic->properties);
characteristic->properties); #endif
offset++; offset++;
} }
} }

View File

@@ -109,7 +109,11 @@ void BLECharacteristic::do_create(BLEService *service) {
esp_attr_control_t control; esp_attr_control_t control;
control.auto_rsp = ESP_GATT_RSP_BY_APP; control.auto_rsp = ESP_GATT_RSP_BY_APP;
ESP_LOGV(TAG, "Creating characteristic - %s", this->uuid_.to_string().c_str()); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char uuid_buf[esp32_ble::UUID_STR_LEN];
this->uuid_.to_str(uuid_buf);
ESP_LOGV(TAG, "Creating characteristic - %s", uuid_buf);
#endif
esp_bt_uuid_t uuid = this->uuid_.get_uuid(); esp_bt_uuid_t uuid = this->uuid_.get_uuid();
esp_err_t err = esp_ble_gatts_add_char(service->get_handle(), &uuid, static_cast<esp_gatt_perm_t>(this->permissions_), esp_err_t err = esp_ble_gatts_add_char(service->get_handle(), &uuid, static_cast<esp_gatt_perm_t>(this->permissions_),

View File

@@ -34,7 +34,11 @@ void BLEDescriptor::do_create(BLECharacteristic *characteristic) {
esp_attr_control_t control; esp_attr_control_t control;
control.auto_rsp = ESP_GATT_AUTO_RSP; control.auto_rsp = ESP_GATT_AUTO_RSP;
ESP_LOGV(TAG, "Creating descriptor - %s", this->uuid_.to_string().c_str()); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char uuid_buf[esp32_ble::UUID_STR_LEN];
this->uuid_.to_str(uuid_buf);
ESP_LOGV(TAG, "Creating descriptor - %s", uuid_buf);
#endif
esp_bt_uuid_t uuid = this->uuid_.get_uuid(); esp_bt_uuid_t uuid = this->uuid_.get_uuid();
esp_err_t err = esp_ble_gatts_add_char_descr(this->characteristic_->get_service()->get_handle(), &uuid, esp_err_t err = esp_ble_gatts_add_char_descr(this->characteristic_->get_service()->get_handle(), &uuid,
this->permissions_, &this->value_, &control); this->permissions_, &this->value_, &control);

View File

@@ -106,7 +106,11 @@ void BLEServer::restart_advertising_() {
} }
BLEService *BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t num_handles) { BLEService *BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t num_handles) {
ESP_LOGV(TAG, "Creating BLE service - %s", uuid.to_string().c_str()); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char uuid_buf[esp32_ble::UUID_STR_LEN];
uuid.to_str(uuid_buf);
ESP_LOGV(TAG, "Creating BLE service - %s", uuid_buf);
#endif
// Calculate the inst_id for the service // Calculate the inst_id for the service
uint8_t inst_id = 0; uint8_t inst_id = 0;
for (; inst_id < 0xFF; inst_id++) { for (; inst_id < 0xFF; inst_id++) {
@@ -115,7 +119,9 @@ BLEService *BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t n
} }
} }
if (inst_id == 0xFF) { if (inst_id == 0xFF) {
ESP_LOGW(TAG, "Could not create BLE service %s, too many instances", uuid.to_string().c_str()); char warn_uuid_buf[esp32_ble::UUID_STR_LEN];
uuid.to_str(warn_uuid_buf);
ESP_LOGW(TAG, "Could not create BLE service %s, too many instances", warn_uuid_buf);
return nullptr; return nullptr;
} }
BLEService *service = // NOLINT(cppcoreguidelines-owning-memory) BLEService *service = // NOLINT(cppcoreguidelines-owning-memory)
@@ -128,7 +134,11 @@ BLEService *BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t n
} }
void BLEServer::remove_service(ESPBTUUID uuid, uint8_t inst_id) { void BLEServer::remove_service(ESPBTUUID uuid, uint8_t inst_id) {
ESP_LOGV(TAG, "Removing BLE service - %s %d", uuid.to_string().c_str(), inst_id); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char uuid_buf[esp32_ble::UUID_STR_LEN];
uuid.to_str(uuid_buf);
ESP_LOGV(TAG, "Removing BLE service - %s %d", uuid_buf, inst_id);
#endif
for (auto it = this->services_.begin(); it != this->services_.end(); ++it) { for (auto it = this->services_.begin(); it != this->services_.end(); ++it) {
if (it->uuid == uuid && it->inst_id == inst_id) { if (it->uuid == uuid && it->inst_id == inst_id) {
it->service->do_delete(); it->service->do_delete();
@@ -137,7 +147,9 @@ void BLEServer::remove_service(ESPBTUUID uuid, uint8_t inst_id) {
return; return;
} }
} }
ESP_LOGW(TAG, "BLE service %s %d does not exist", uuid.to_string().c_str(), inst_id); char warn_uuid_buf[esp32_ble::UUID_STR_LEN];
uuid.to_str(warn_uuid_buf);
ESP_LOGW(TAG, "BLE service %s %d does not exist", warn_uuid_buf, inst_id);
} }
BLEService *BLEServer::get_service(ESPBTUUID uuid, uint8_t inst_id) { BLEService *BLEServer::get_service(ESPBTUUID uuid, uint8_t inst_id) {

View File

@@ -15,10 +15,7 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_characteristic_on_w
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory) Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
new Trigger<std::vector<uint8_t>, uint16_t>(); new Trigger<std::vector<uint8_t>, uint16_t>();
characteristic->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) { characteristic->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
// Convert span to vector for trigger - copy is necessary because: // Convert span to vector for trigger
// 1. Trigger stores the data for use in automation actions that execute later
// 2. The span is only valid during this callback (points to temporary BLE stack data)
// 3. User lambdas in automations need persistent data they can access asynchronously
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id); on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
}); });
return on_write_trigger; return on_write_trigger;
@@ -30,10 +27,7 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_descriptor_on_write
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory) Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
new Trigger<std::vector<uint8_t>, uint16_t>(); new Trigger<std::vector<uint8_t>, uint16_t>();
descriptor->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) { descriptor->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
// Convert span to vector for trigger - copy is necessary because: // Convert span to vector for trigger
// 1. Trigger stores the data for use in automation actions that execute later
// 2. The span is only valid during this callback (points to temporary BLE stack data)
// 3. User lambdas in automations need persistent data they can access asynchronously
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id); on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
}); });
return on_write_trigger; return on_write_trigger;

View File

@@ -188,7 +188,10 @@ void ESP32BLETracker::ble_before_disabled_event_handler() { this->stop_scan_();
void ESP32BLETracker::stop_scan_() { void ESP32BLETracker::stop_scan_() {
if (this->scanner_state_ != ScannerState::RUNNING && this->scanner_state_ != ScannerState::FAILED) { if (this->scanner_state_ != ScannerState::RUNNING && this->scanner_state_ != ScannerState::FAILED) {
ESP_LOGE(TAG, "Cannot stop scan: %s", this->scanner_state_to_string_(this->scanner_state_)); // If scanner is already idle, there's nothing to stop - this is not an error
if (this->scanner_state_ != ScannerState::IDLE) {
ESP_LOGE(TAG, "Cannot stop scan: %s", this->scanner_state_to_string_(this->scanner_state_));
}
return; return;
} }
// Reset timeout state machine when stopping scan // Reset timeout state machine when stopping scan
@@ -438,24 +441,31 @@ void ESPBTDevice::parse_scan_rst(const BLEScanResult &scan_result) {
ESP_LOGVV(TAG, " Ad Flag: %u", *this->ad_flag_); ESP_LOGVV(TAG, " Ad Flag: %u", *this->ad_flag_);
} }
for (auto &uuid : this->service_uuids_) { for (auto &uuid : this->service_uuids_) {
ESP_LOGVV(TAG, " Service UUID: %s", uuid.to_string().c_str()); char uuid_buf[esp32_ble::UUID_STR_LEN];
uuid.to_str(uuid_buf);
ESP_LOGVV(TAG, " Service UUID: %s", uuid_buf);
} }
for (auto &data : this->manufacturer_datas_) { for (auto &data : this->manufacturer_datas_) {
auto ibeacon = ESPBLEiBeacon::from_manufacturer_data(data); auto ibeacon = ESPBLEiBeacon::from_manufacturer_data(data);
if (ibeacon.has_value()) { if (ibeacon.has_value()) {
ESP_LOGVV(TAG, " Manufacturer iBeacon:"); ESP_LOGVV(TAG, " Manufacturer iBeacon:");
ESP_LOGVV(TAG, " UUID: %s", ibeacon.value().get_uuid().to_string().c_str()); char uuid_buf[esp32_ble::UUID_STR_LEN];
ibeacon.value().get_uuid().to_str(uuid_buf);
ESP_LOGVV(TAG, " UUID: %s", uuid_buf);
ESP_LOGVV(TAG, " Major: %u", ibeacon.value().get_major()); ESP_LOGVV(TAG, " Major: %u", ibeacon.value().get_major());
ESP_LOGVV(TAG, " Minor: %u", ibeacon.value().get_minor()); ESP_LOGVV(TAG, " Minor: %u", ibeacon.value().get_minor());
ESP_LOGVV(TAG, " TXPower: %d", ibeacon.value().get_signal_power()); ESP_LOGVV(TAG, " TXPower: %d", ibeacon.value().get_signal_power());
} else { } else {
ESP_LOGVV(TAG, " Manufacturer ID: %s, data: %s", data.uuid.to_string().c_str(), char uuid_buf[esp32_ble::UUID_STR_LEN];
format_hex_pretty(data.data).c_str()); data.uuid.to_str(uuid_buf);
ESP_LOGVV(TAG, " Manufacturer ID: %s, data: %s", uuid_buf, format_hex_pretty(data.data).c_str());
} }
} }
for (auto &data : this->service_datas_) { for (auto &data : this->service_datas_) {
ESP_LOGVV(TAG, " Service data:"); ESP_LOGVV(TAG, " Service data:");
ESP_LOGVV(TAG, " UUID: %s", data.uuid.to_string().c_str()); char uuid_buf[esp32_ble::UUID_STR_LEN];
data.uuid.to_str(uuid_buf);
ESP_LOGVV(TAG, " UUID: %s", uuid_buf);
ESP_LOGVV(TAG, " Data: %s", format_hex_pretty(data.data).c_str()); ESP_LOGVV(TAG, " Data: %s", format_hex_pretty(data.data).c_str());
} }

View File

@@ -2,8 +2,8 @@ import logging
from esphome import automation, pins from esphome import automation, pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import i2c from esphome.components import i2c, socket
from esphome.components.esp32 import add_idf_component from esphome.components.esp32 import add_idf_component, add_idf_sdkconfig_option
from esphome.components.psram import DOMAIN as psram_domain from esphome.components.psram import DOMAIN as psram_domain
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
@@ -27,7 +27,7 @@ import esphome.final_validate as fv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
AUTO_LOAD = ["camera"] AUTO_LOAD = ["camera", "socket"]
DEPENDENCIES = ["esp32"] DEPENDENCIES = ["esp32"]
esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera") esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera")
@@ -186,7 +186,7 @@ CONFIG_SCHEMA = cv.All(
{ {
cv.Required(CONF_PIN): pins.internal_gpio_input_pin_number, cv.Required(CONF_PIN): pins.internal_gpio_input_pin_number,
cv.Optional(CONF_FREQUENCY, default="20MHz"): cv.All( cv.Optional(CONF_FREQUENCY, default="20MHz"): cv.All(
cv.frequency, cv.Range(min=8e6, max=20e6) cv.frequency, cv.float_range(min=8e6, max=20e6)
), ),
} }
), ),
@@ -324,6 +324,7 @@ SETTERS = {
async def to_code(config): async def to_code(config):
cg.add_define("USE_CAMERA") cg.add_define("USE_CAMERA")
socket.require_wake_loop_threadsafe()
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await setup_entity(var, config, "camera") await setup_entity(var, config, "camera")
await cg.register_component(var, config) await cg.register_component(var, config)
@@ -352,6 +353,8 @@ async def to_code(config):
cg.add_define("USE_CAMERA") cg.add_define("USE_CAMERA")
add_idf_component(name="espressif/esp32-camera", ref="2.1.1") add_idf_component(name="espressif/esp32-camera", ref="2.1.1")
add_idf_sdkconfig_option("CONFIG_SCCB_HARDWARE_I2C_DRIVER_NEW", True)
add_idf_sdkconfig_option("CONFIG_SCCB_HARDWARE_I2C_DRIVER_LEGACY", False)
for conf in config.get(CONF_ON_STREAM_START, []): for conf in config.get(CONF_ON_STREAM_START, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)

View File

@@ -11,6 +11,10 @@ namespace esphome {
namespace esp32_camera { namespace esp32_camera {
static const char *const TAG = "esp32_camera"; static const char *const TAG = "esp32_camera";
static constexpr size_t FRAMEBUFFER_TASK_STACK_SIZE = 1792;
#if ESPHOME_LOG_LEVEL < ESPHOME_LOG_LEVEL_VERBOSE
static constexpr uint32_t FRAME_LOG_INTERVAL_MS = 60000;
#endif
/* ---------------- public API (derivated) ---------------- */ /* ---------------- public API (derivated) ---------------- */
void ESP32Camera::setup() { void ESP32Camera::setup() {
@@ -39,12 +43,12 @@ void ESP32Camera::setup() {
this->framebuffer_get_queue_ = xQueueCreate(1, sizeof(camera_fb_t *)); this->framebuffer_get_queue_ = xQueueCreate(1, sizeof(camera_fb_t *));
this->framebuffer_return_queue_ = xQueueCreate(1, sizeof(camera_fb_t *)); this->framebuffer_return_queue_ = xQueueCreate(1, sizeof(camera_fb_t *));
xTaskCreatePinnedToCore(&ESP32Camera::framebuffer_task, xTaskCreatePinnedToCore(&ESP32Camera::framebuffer_task,
"framebuffer_task", // name "framebuffer_task", // name
1024, // stack size FRAMEBUFFER_TASK_STACK_SIZE, // stack size
this, // task pv params this, // task pv params
1, // priority 1, // priority
nullptr, // handle nullptr, // handle
1 // core 1 // core
); );
} }
@@ -164,6 +168,19 @@ void ESP32Camera::dump_config() {
} }
void ESP32Camera::loop() { void ESP32Camera::loop() {
// Fast path: skip all work when truly idle
// (no current image, no pending requests, and not time for idle request yet)
const uint32_t now = App.get_loop_component_start_time();
if (!this->current_image_ && !this->has_requested_image_()) {
// Only check idle interval when we're otherwise idle
if (this->idle_update_interval_ != 0 && now - this->last_idle_request_ > this->idle_update_interval_) {
this->last_idle_request_ = now;
this->request_image(camera::IDLE);
} else {
return;
}
}
// check if we can return the image // check if we can return the image
if (this->can_return_image_()) { if (this->can_return_image_()) {
// return image // return image
@@ -172,13 +189,6 @@ void ESP32Camera::loop() {
this->current_image_.reset(); this->current_image_.reset();
} }
// request idle image every idle_update_interval
const uint32_t now = App.get_loop_component_start_time();
if (this->idle_update_interval_ != 0 && now - this->last_idle_request_ > this->idle_update_interval_) {
this->last_idle_request_ = now;
this->request_image(camera::IDLE);
}
// Check if we should fetch a new image // Check if we should fetch a new image
if (!this->has_requested_image_()) if (!this->has_requested_image_())
return; return;
@@ -204,7 +214,20 @@ void ESP32Camera::loop() {
} }
this->current_image_ = std::make_shared<ESP32CameraImage>(fb, this->single_requesters_ | this->stream_requesters_); this->current_image_ = std::make_shared<ESP32CameraImage>(fb, this->single_requesters_ | this->stream_requesters_);
ESP_LOGD(TAG, "Got Image: len=%u", fb->len); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
ESP_LOGV(TAG, "Got Image: len=%u", fb->len);
#else
// Initialize log time on first frame to ensure accurate interval measurement
if (this->frame_count_ == 0) {
this->last_log_time_ = now;
}
this->frame_count_++;
if (now - this->last_log_time_ >= FRAME_LOG_INTERVAL_MS) {
ESP_LOGD(TAG, "Received %u images in last %us", this->frame_count_, FRAME_LOG_INTERVAL_MS / 1000);
this->last_log_time_ = now;
this->frame_count_ = 0;
}
#endif
for (auto *listener : this->listeners_) { for (auto *listener : this->listeners_) {
listener->on_camera_image(this->current_image_); listener->on_camera_image(this->current_image_);
} }
@@ -405,6 +428,10 @@ void ESP32Camera::framebuffer_task(void *pv) {
while (true) { while (true) {
camera_fb_t *framebuffer = esp_camera_fb_get(); camera_fb_t *framebuffer = esp_camera_fb_get();
xQueueSend(that->framebuffer_get_queue_, &framebuffer, portMAX_DELAY); xQueueSend(that->framebuffer_get_queue_, &framebuffer, portMAX_DELAY);
// Only wake the main loop if there's a pending request to consume the frame
if (that->has_requested_image_()) {
App.wake_loop_threadsafe();
}
// return is no-op for config with 1 fb // return is no-op for config with 1 fb
xQueueReceive(that->framebuffer_return_queue_, &framebuffer, portMAX_DELAY); xQueueReceive(that->framebuffer_return_queue_, &framebuffer, portMAX_DELAY);
esp_camera_fb_return(framebuffer); esp_camera_fb_return(framebuffer);

View File

@@ -2,6 +2,7 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
#include <atomic>
#include <esp_camera.h> #include <esp_camera.h>
#include <freertos/FreeRTOS.h> #include <freertos/FreeRTOS.h>
#include <freertos/queue.h> #include <freertos/queue.h>
@@ -205,14 +206,18 @@ class ESP32Camera : public camera::Camera {
esp_err_t init_error_{ESP_OK}; esp_err_t init_error_{ESP_OK};
std::shared_ptr<ESP32CameraImage> current_image_; std::shared_ptr<ESP32CameraImage> current_image_;
uint8_t single_requesters_{0}; std::atomic<uint8_t> single_requesters_{0};
uint8_t stream_requesters_{0}; std::atomic<uint8_t> stream_requesters_{0};
QueueHandle_t framebuffer_get_queue_; QueueHandle_t framebuffer_get_queue_;
QueueHandle_t framebuffer_return_queue_; QueueHandle_t framebuffer_return_queue_;
std::vector<camera::CameraListener *> listeners_; std::vector<camera::CameraListener *> listeners_;
uint32_t last_idle_request_{0}; uint32_t last_idle_request_{0};
uint32_t last_update_{0}; uint32_t last_update_{0};
#if ESPHOME_LOG_LEVEL < ESPHOME_LOG_LEVEL_VERBOSE
uint32_t last_log_time_{0};
uint16_t frame_count_{0};
#endif
#ifdef USE_I2C #ifdef USE_I2C
i2c::InternalI2CBus *i2c_bus_{nullptr}; i2c::InternalI2CBus *i2c_bus_{nullptr};
#endif // USE_I2C #endif // USE_I2C

View File

@@ -191,7 +191,8 @@ async def to_code(config):
cg.add_define(ThreadModel.SINGLE) cg.add_define(ThreadModel.SINGLE)
cg.add_platformio_option( cg.add_platformio_option(
"extra_scripts", ["pre:testing_mode.py", "post:post_build.py"] "extra_scripts",
["pre:testing_mode.py", "pre:exclude_updater.py", "post:post_build.py"],
) )
conf = config[CONF_FRAMEWORK] conf = config[CONF_FRAMEWORK]
@@ -278,3 +279,8 @@ def copy_files():
testing_mode_file, testing_mode_file,
CORE.relative_build_path("testing_mode.py"), CORE.relative_build_path("testing_mode.py"),
) )
exclude_updater_file = dir / "exclude_updater.py.script"
copy_file_if_changed(
exclude_updater_file,
CORE.relative_build_path("exclude_updater.py"),
)

View File

@@ -0,0 +1,21 @@
# pylint: disable=E0602
Import("env") # noqa
import os
# Filter out Updater.cpp from the Arduino core build
# This saves 228 bytes of .bss by not instantiating the global Update object
# ESPHome uses its own native OTA backend instead
def filter_updater_from_core(env, node):
"""Filter callback to exclude Updater.cpp from framework build."""
path = node.get_path()
if path.endswith("Updater.cpp"):
print(f"ESPHome: Excluding {os.path.basename(path)} from build (using native OTA backend)")
return None
return node
# Apply the filter to framework sources
env.AddBuildMiddleware(filter_updater_from_core, "**/cores/esp8266/Updater.cpp")

View File

@@ -16,7 +16,7 @@ def valid_pwm_pin(value):
esp8266_pwm_ns = cg.esphome_ns.namespace("esp8266_pwm") esp8266_pwm_ns = cg.esphome_ns.namespace("esp8266_pwm")
ESP8266PWM = esp8266_pwm_ns.class_("ESP8266PWM", output.FloatOutput, cg.Component) ESP8266PWM = esp8266_pwm_ns.class_("ESP8266PWM", output.FloatOutput, cg.Component)
SetFrequencyAction = esp8266_pwm_ns.class_("SetFrequencyAction", automation.Action) SetFrequencyAction = esp8266_pwm_ns.class_("SetFrequencyAction", automation.Action)
validate_frequency = cv.All(cv.frequency, cv.Range(min=1.0e-6)) validate_frequency = cv.All(cv.frequency, cv.float_range(min=1.0e-6))
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
output.FLOAT_OUTPUT_SCHEMA.extend( output.FLOAT_OUTPUT_SCHEMA.extend(

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