Compare commits

..

890 Commits

Author SHA1 Message Date
J. Nick Koston
e7ba19b3ea Merge branch 'integration' into memory_api 2025-10-19 12:53:45 -10:00
J. Nick Koston
0dc08941f4 Merge branch 'dev' into integration 2025-10-19 12:53:31 -10:00
Jesse Hills
020cea80b2 [nextion] Clean up deprecated code from 1.20 (#11393) 2025-10-19 22:16:50 +00:00
Jesse Hills
9c146a7070 [climate] Clean up deprecated functions from 1.20 (#11388) 2025-10-19 22:11:35 +00:00
Jesse Hills
afbd3f77af [light] Clean up deprecated functions from 1.21 (#11389) 2025-10-19 22:08:30 +00:00
Javier Peletier
1e1fefbd0a [substitutions] !extend and !remove now support substitutions and jinja (#11203) 2025-10-20 10:31:25 +13:00
Juan Antonio Aldea
1a2057df30 Migrate from hexencode() to format_hex_pretty() in Kuntze component (#11372) 2025-10-20 10:15:17 +13:00
J. Nick Koston
70cb1793f3 [wifi] Optimize WiFi scan results with in-place construction (#11330) 2025-10-19 19:53:05 +00:00
J. Nick Koston
3bdd351d49 [wifi] Convert fast_connect to compile-time define, save 156-1024 bytes flash (#11328) 2025-10-19 19:52:33 +00:00
Jonathan Swoboda
b0ea3f57de [esp32] Fix OTA rollback (#11300)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-10-19 09:49:05 -10:00
J. Nick Koston
c9312d5c27 [script] Fix unbounded queue growth, optimize queued mode (default max_runs=5) (#11308)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-10-19 09:42:17 -10:00
J. Nick Koston
82f7b7f0d5 debug 2025-10-19 09:41:04 -10:00
J. Nick Koston
db5c78acb9 preen 2025-10-19 09:36:55 -10:00
J. Nick Koston
0b45e1d608 Merge branch 'integration' into memory_api 2025-10-19 09:36:02 -10:00
J. Nick Koston
aa6b7664f4 Merge branch 'dev' into integration 2025-10-19 09:35:53 -10:00
J. Nick Koston
33fea90c19 [wifi] Optimize WiFi scanning to reduce copies and heap allocations (#11323) 2025-10-19 19:26:18 +00:00
J. Nick Koston
25f3b6a959 [mqtt] Reduce flash usage by optimizing ArduinoJson assignments (#11340) 2025-10-19 19:17:33 +00:00
J. Nick Koston
e993312640 [core] Fix IndexError when OTA devices cannot be resolved (#11311) 2025-10-20 08:15:47 +13:00
J. Nick Koston
85babe85e4 [sensor] Optimize sliding window filters to eliminate heap fragmentation (#11282) 2025-10-20 07:59:47 +13:00
J. Nick Koston
0266c897c9 [mdns] Use std::unique_ptr for TXT records to reduce ESP32 flash usage (#11362) 2025-10-20 07:53:00 +13:00
J. Nick Koston
bda7676e3a [bluetooth_proxy] Merge duplicate loops in get_connection_() (#11359) 2025-10-20 07:51:41 +13:00
J. Nick Koston
57e98ec3fc [wifi] Replace std::vector with std::unique_ptr for WiFi scan buffer (#11364) 2025-10-20 07:49:58 +13:00
J. Nick Koston
09b2ad071b [esp32_ble_client] Remove duplicate MAC address extraction in set_address() (#11358) 2025-10-20 07:49:13 +13:00
J. Nick Koston
fdecda3d65 [light] Use bitmask instead of std::set for color modes (#11348) 2025-10-20 07:48:14 +13:00
J. Nick Koston
453a668cfb Merge branch 'integration' into memory_api 2025-10-19 08:46:39 -10:00
J. Nick Koston
4e234c354b Merge branch 'max_socket_listen' into integration 2025-10-19 08:46:32 -10:00
J. Nick Koston
a0922bc8b0 [ci] Add automated memory impact analysis for pull requests (#11242)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-10-20 07:43:38 +13:00
J. Nick Koston
4fa908d0b8 preen 2025-10-19 08:43:30 -10:00
J. Nick Koston
148a78aa01 preen 2025-10-19 08:41:21 -10:00
J. Nick Koston
7107f5d984 preen 2025-10-19 08:40:01 -10:00
J. Nick Koston
55473991a9 preen 2025-10-19 08:37:43 -10:00
J. Nick Koston
1586a185a0 [esp32] Automatic CONFIG_LWIP_MAX_SOCKETS configuration based on component needs 2025-10-19 08:34:38 -10:00
J. Nick Koston
f25af18655 [scheduler] Replace defer queue deque with vector to avoid 512-byte upfront allocation (#11305) 2025-10-20 07:34:34 +13:00
J. Nick Koston
5db07c2d70 [api][time] Refactor timezone update logic for cleaner code (#11327) 2025-10-20 07:31:40 +13:00
J. Nick Koston
ae0c3875fc Merge branch 'integration' into memory_api 2025-10-19 08:02:39 -10:00
J. Nick Koston
a0bd7d100b Merge remote-tracking branch 'upstream/dev' into integration 2025-10-19 08:02:33 -10:00
Juan Antonio Aldea
40823df7bc make types sensors_t and sensor_type_t internal to StatsdComponent. (#11345) 2025-10-19 07:47:31 -10:00
tomaszduda23
5e1019a6fa [nrf52, ble_nus] add logging over BLE (#9846) 2025-10-19 07:41:19 -10:00
tomaszduda23
f3cdbd0a05 [nrf52] fix task names in logs (#11367) 2025-10-19 07:39:48 -10:00
J. Nick Koston
c3b652b977 Merge branch 'integration' into memory_api 2025-10-18 22:43:58 -10:00
J. Nick Koston
4de6bf9d3d Merge branch 'unbound_queued_script_fix' into integration 2025-10-18 22:43:36 -10:00
J. Nick Koston
428240d538 Merge branch 'min_filter_ring_buffer' into integration 2025-10-18 22:43:27 -10:00
J. Nick Koston
3da9139db4 Merge branch 'ci_impact_analysis' into integration 2025-10-18 22:43:24 -10:00
J. Nick Koston
504c2b9dfd Merge branch 'ci_impact_analysis' into memory_api 2025-10-18 22:43:09 -10:00
J. Nick Koston
314fe77b8d Merge branch 'min_filter_ring_buffer' into memory_api 2025-10-18 22:42:55 -10:00
J. Nick Koston
8686d05d05 Merge branch 'unbound_queued_script_fix' into memory_api 2025-10-18 22:42:50 -10:00
J. Nick Koston
be3dacfe64 Merge branch 'mqtt_reduce_json_assign' into memory_api 2025-10-18 22:42:45 -10:00
J. Nick Koston
3f7e22e271 Merge branch 'integration' into memory_api 2025-10-18 22:24:32 -10:00
J. Nick Koston
714fafff4c Merge remote-tracking branch 'upstream/dev' into integration 2025-10-18 22:24:24 -10:00
J. Nick Koston
1010006b5e Merge branch 'dev' into mqtt_reduce_json_assign 2025-10-18 22:16:33 -10:00
J. Nick Koston
9fc3ad1fa5 bot 2025-10-18 22:16:09 -10:00
Keith Burzinski
ddf1b67e49 [prometheus] Update to use new climate API (#11361) 2025-10-18 22:11:44 -10:00
Keith Burzinski
b4d9fddd07 [mqtt] Update to use new climate API (#11360) 2025-10-18 22:11:10 -10:00
Keith Burzinski
25f03074ab [web_server] Update to use new climate API (#11363) 2025-10-18 22:10:07 -10:00
J. Nick Koston
70479dec0d suggestions 2025-10-18 21:57:19 -10:00
J. Nick Koston
acdecafeef suggestions 2025-10-18 21:55:25 -10:00
J. Nick Koston
bf40bc3b25 Merge remote-tracking branch 'upstream/unbound_queued_script_fix' into unbound_queued_script_fix 2025-10-18 21:54:27 -10:00
J. Nick Koston
32a1e45842 suggestions 2025-10-18 21:54:20 -10:00
J. Nick Koston
498dece382 suggestions 2025-10-18 21:54:05 -10:00
pre-commit-ci-lite[bot]
e0477e3bb1 [pre-commit.ci lite] apply automatic fixes 2025-10-19 07:53:21 +00:00
J. Nick Koston
7bb222a574 Update esphome/components/script/script.h
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-18 21:51:51 -10:00
J. Nick Koston
9a3b5ec090 Merge remote-tracking branch 'upstream/dev' into min_filter_ring_buffer 2025-10-18 21:49:13 -10:00
J. Nick Koston
f5e5f4ef06 preen 2025-10-18 21:47:03 -10:00
J. Nick Koston
8d338610a9 Merge remote-tracking branch 'upstream/unbound_queued_script_fix' into unbound_queued_script_fix 2025-10-18 21:45:16 -10:00
J. Nick Koston
0e513b41e4 preen 2025-10-18 21:45:02 -10:00
J. Nick Koston
f7ebef83f2 Merge branch 'dev' into unbound_queued_script_fix 2025-10-18 21:34:51 -10:00
J. Nick Koston
f387d9ec50 unique ptr 2025-10-18 21:33:38 -10:00
J. Nick Koston
fc2c55b642 Merge branch 'integration' into memory_api 2025-10-18 21:26:22 -10:00
J. Nick Koston
fd0384eec4 Merge branch 'esp32_mdns_fixed_vector' into integration 2025-10-18 21:26:12 -10:00
J. Nick Koston
13292809a2 Merge branch 'dev' into esp32_mdns_fixed_vector 2025-10-18 21:22:50 -10:00
J. Nick Koston
f036e894c8 adjust 2025-10-18 21:18:11 -10:00
J. Nick Koston
0a17893686 Merge remote-tracking branch 'upstream/ci_impact_analysis' into ci_impact_analysis 2025-10-18 21:08:39 -10:00
J. Nick Koston
7dd7a23977 Merge remote-tracking branch 'upstream/dev' into ci_impact_analysis 2025-10-18 21:08:26 -10:00
J. Nick Koston
cfcc6e22e0 Merge branch 'integration' into memory_api 2025-10-18 21:01:21 -10:00
J. Nick Koston
4ea4359553 Merge branch 'wifi_no_vector_for_simple_array' into integration 2025-10-18 21:01:12 -10:00
J. Nick Koston
53d7b4f433 [wifi] Replace std::vector with std::unique_ptr for WiFi scan buffer 2025-10-18 21:00:45 -10:00
J. Nick Koston
e52eeb528d Merge branch 'integration' into memory_api 2025-10-18 20:45:19 -10:00
J. Nick Koston
61adb2545e Merge branch 'esp32_mdns_fixed_vector' into integration 2025-10-18 20:45:01 -10:00
J. Nick Koston
6d1288c806 [mdns] Use FixedVector for TXT records to reduce ESP32 flash usage 2025-10-18 20:44:36 -10:00
J. Nick Koston
c8b2cf1ceb Merge branch 'integration' into memory_api 2025-10-18 20:21:22 -10:00
J. Nick Koston
4b6384c63b Merge branch 'get_connection_single_loop' into integration 2025-10-18 20:21:16 -10:00
J. Nick Koston
071bdfa67f [bluetooth_proxy] Merge duplicate loops in get_connection_() 2025-10-18 20:20:51 -10:00
Keith Burzinski
590f6ff70b [api] Update to use new climate API (#11357) 2025-10-19 06:20:11 +00:00
J. Nick Koston
a236f1c6d1 Merge branch 'integration' into memory_api 2025-10-18 20:05:56 -10:00
J. Nick Koston
d600cbf42a Merge branch 'esp32_ble_client_dupe_mac_build' into integration 2025-10-18 20:05:49 -10:00
J. Nick Koston
b378038253 [esp32_ble_client] Remove duplicate MAC address extraction in set_address() 2025-10-18 20:05:22 -10:00
J. Nick Koston
d695c99dbe Merge branch 'integration' into memory_api 2025-10-18 19:50:34 -10:00
J. Nick Koston
b230551bb3 Merge remote-tracking branch 'upstream/dev' into integration 2025-10-18 19:50:26 -10:00
Keith Burzinski
a33ed5e47b [thermostat] Add humidity support (#11286) 2025-10-18 17:25:53 -10:00
J. Nick Koston
48718ffe06 Merge remote-tracking branch 'upstream/dev' into integration 2025-10-18 16:56:40 -10:00
Spectre5
c11a9bb97f Change all temperature offsets to temperature_delta (#11347) 2025-10-18 21:13:57 -04:00
J. Nick Koston
84b69a5766 Merge branch 'dev' into ci_impact_analysis 2025-10-18 15:12:46 -10:00
J. Nick Koston
eb45427d07 Merge branch 'integration' into memory_api 2025-10-18 14:58:13 -10:00
J. Nick Koston
d817461f11 Merge branch 'light_bitmask' into integration 2025-10-18 14:58:06 -10:00
J. Nick Koston
f7d52a342b review comments 2025-10-18 14:42:12 -10:00
J. Nick Koston
76ad649bf9 review comments 2025-10-18 14:41:59 -10:00
J. Nick Koston
35f8dc528d Merge branch 'integration' into memory_api 2025-10-18 14:26:11 -10:00
J. Nick Koston
d0fa64e419 Merge branch 'light_bitmask' into integration 2025-10-18 14:26:06 -10:00
J. Nick Koston
437dd503ca more cover 2025-10-18 14:21:52 -10:00
J. Nick Koston
1381db37ad preen 2025-10-18 14:18:17 -10:00
J. Nick Koston
32eb43fd02 preen 2025-10-18 14:14:48 -10:00
J. Nick Koston
764428870d reduce diff 2025-10-18 14:11:23 -10:00
J. Nick Koston
f2d01ecd6c dry 2025-10-18 13:58:52 -10:00
J. Nick Koston
f1086b13af Merge branch 'dev' into light_bitmask 2025-10-18 13:53:38 -10:00
J. Nick Koston
2cdfd04204 dry 2025-10-18 13:53:05 -10:00
J. Nick Koston
a249c9c282 preen 2025-10-18 13:46:49 -10:00
J. Nick Koston
8545b5231b preen 2025-10-18 13:38:45 -10:00
J. Nick Koston
1c8b60891c simplify 2025-10-18 13:32:48 -10:00
J. Nick Koston
44d3f355a5 overkill 2025-10-18 13:16:52 -10:00
J. Nick Koston
cc6b798f2b overkill 2025-10-18 13:15:47 -10:00
J. Nick Koston
80fd51e198 preen 2025-10-18 13:14:05 -10:00
J. Nick Koston
ec8d8538f6 preen 2025-10-18 13:12:48 -10:00
dependabot[bot]
acef2085d9 Bump aioesphomeapi from 42.1.0 to 42.2.0 (#11352)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-18 23:11:36 +00:00
J. Nick Koston
89c719d71d preen 2025-10-18 13:10:41 -10:00
J. Nick Koston
89903929f3 preen 2025-10-18 13:05:59 -10:00
J. Nick Koston
3ef402ef64 cover 2025-10-18 12:38:02 -10:00
J. Nick Koston
e7bc2b0a44 Merge branch 'light_bitmask' into memory_api 2025-10-18 12:35:33 -10:00
J. Nick Koston
e27472b87d fixes 2025-10-18 12:35:13 -10:00
J. Nick Koston
f3c1733662 Merge branch 'integration' into memory_api 2025-10-18 12:07:32 -10:00
J. Nick Koston
f3777b6171 Merge branch 'light_bitmask' into integration 2025-10-18 12:07:19 -10:00
J. Nick Koston
753bebdde8 fix 2025-10-18 12:02:52 -10:00
J. Nick Koston
f88cc33cfc fix 2025-10-18 12:01:57 -10:00
J. Nick Koston
02b626ae1a fix 2025-10-18 12:00:29 -10:00
J. Nick Koston
94414e767c Merge remote-tracking branch 'origin/light_bitmask' into light_bitmask 2025-10-18 11:56:57 -10:00
J. Nick Koston
ef52ce4d76 [api_protobuf] Address copilot review: add bounds checking and clarify 32-bit loop intent
- Add bounds checking in decode_varint_content to prevent undefined behavior if decoded enum value exceeds 31
- Add clarifying comments that 32-bit loops in encode_content and get_size_calculation are intentional to support the full range of enum_as_bitmask (enums with up to 32 values)
- The uint32_t storage type supports general-purpose enum_as_bitmask, not just ColorMode's 10 values
2025-10-18 11:56:40 -10:00
J. Nick Koston
018e28a137 Merge branch 'dev' into light_bitmask 2025-10-18 11:48:38 -10:00
dependabot[bot]
865663ce5f Bump aioesphomeapi from 42.0.0 to 42.1.0 (#11350)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-18 11:48:25 -10:00
J. Nick Koston
efc98e9a2c Merge branch 'integration' into memory_api 2025-10-18 11:10:35 -10:00
J. Nick Koston
6632cfc177 Merge branch 'light_bitmask' into integration 2025-10-18 11:10:10 -10:00
J. Nick Koston
27b876df93 preen 2025-10-18 10:52:42 -10:00
J. Nick Koston
596ce59991 dead code 2025-10-18 10:42:51 -10:00
J. Nick Koston
13e9d0c851 fix 2025-10-18 10:41:52 -10:00
J. Nick Koston
a0008d6f44 fix 2025-10-18 10:41:37 -10:00
J. Nick Koston
bb03d73106 Merge branch 'integration' into memory_api 2025-10-18 10:36:13 -10:00
J. Nick Koston
654dd64850 Merge branch 'light_bitmask' into integration 2025-10-18 10:36:08 -10:00
J. Nick Koston
98df9fd2ff preen 2025-10-18 10:32:20 -10:00
J. Nick Koston
cfb061abc4 preen 2025-10-18 10:29:08 -10:00
J. Nick Koston
957b5e98a7 comment 2025-10-18 10:18:34 -10:00
J. Nick Koston
599e636468 comment 2025-10-18 10:17:52 -10:00
J. Nick Koston
2dc6c56edc align 2025-10-18 10:15:32 -10:00
J. Nick Koston
c0c30ba22d tweak 2025-10-18 10:02:45 -10:00
J. Nick Koston
b01ab914f3 tweak 2025-10-18 10:01:39 -10:00
J. Nick Koston
c76e386a79 no vector 2025-10-18 09:59:24 -10:00
J. Nick Koston
6a96e0ee90 [light] Use bitmask instead of std::set for color modes 2025-10-18 09:38:37 -10:00
tomaszduda23
ae010fd6f1 [dashboard] fix migration to Path (#11342)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-10-18 17:32:12 +00:00
J. Nick Koston
455e8bb059 Merge branch 'dev' into min_filter_ring_buffer 2025-10-18 06:47:44 -10:00
J. Nick Koston
91a10d0e36 [total_daily_energy] Fix ID conflicts in component test configuration (#11337) 2025-10-18 08:36:30 -04:00
J. Nick Koston
5ad22620c9 [mqtt] Reduce flash usage by optimizing ArduinoJson assignments 2025-10-17 23:35:52 -10:00
J. Nick Koston
e200f82d7a fixes 2025-10-17 21:48:03 -10:00
J. Nick Koston
d18a0888e9 Merge branch 'integration' into memory_api 2025-10-17 21:44:48 -10:00
J. Nick Koston
f2a63a7a56 Merge branch 'ci_impact_analysis' into integration
Resolved conflict in esphome/platformio_api.py:
- Kept refactored objdump_path and readelf_path from ci_impact_analysis
- Preserved analyze_memory_usage function from integration branch
2025-10-17 21:44:21 -10:00
J. Nick Koston
4dd1f51246 Merge branch 'integration' into memory_api 2025-10-17 21:41:47 -10:00
J. Nick Koston
74c9d0586a Merge branch 'min_filter_ring_buffer' into integration 2025-10-17 21:41:40 -10:00
Juan Antonio Aldea
d5c36eaf2a [tests] Remove superfluous else-blocks from lambdas (#11322)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-10-17 21:40:54 -10:00
J. Nick Koston
fd32f6930e Merge remote-tracking branch 'upstream/dev' into min_filter_ring_buffer 2025-10-17 21:04:24 -10:00
J. Nick Koston
b4ae85cf0f cleanup sorting 2025-10-17 21:03:51 -10:00
J. Nick Koston
7f38987c60 Merge branch 'integration' into memory_api 2025-10-17 19:24:31 -10:00
J. Nick Koston
d13ca46a30 Merge remote-tracking branch 'upstream/dev' into integration 2025-10-17 19:24:23 -10:00
J. Nick Koston
e70cb098ae whitespace 2025-10-17 18:50:07 -10:00
J. Nick Koston
7f2d8a2c11 whitespace 2025-10-17 18:46:41 -10:00
J. Nick Koston
4f4da1de22 preen 2025-10-17 18:41:12 -10:00
J. Nick Koston
f9807db08a preen 2025-10-17 18:37:24 -10:00
J. Nick Koston
541fb8b27c update test 2025-10-17 18:32:22 -10:00
J. Nick Koston
85e0a4fbf9 update test 2025-10-17 18:29:36 -10:00
J. Nick Koston
7e54803ede update test 2025-10-17 18:25:41 -10:00
J. Nick Koston
a078486a87 update test 2025-10-17 18:21:28 -10:00
J. Nick Koston
ba18bb6a4f template all the things 2025-10-17 18:18:15 -10:00
J. Nick Koston
07ad32968e template all the things 2025-10-17 18:15:46 -10:00
J. Nick Koston
0b077bdfc6 preen 2025-10-17 18:08:52 -10:00
J. Nick Koston
1f00617738 Merge remote-tracking branch 'upstream/ci_impact_analysis' into ci_impact_analysis 2025-10-17 18:06:44 -10:00
J. Nick Koston
9cf1fd24fd preen 2025-10-17 18:06:13 -10:00
pre-commit-ci-lite[bot]
bbd636a8cc [pre-commit.ci lite] apply automatic fixes 2025-10-18 03:59:23 +00:00
J. Nick Koston
322dc530a9 Merge remote-tracking branch 'origin/ci_impact_analysis' into ci_impact_analysis 2025-10-17 17:58:05 -10:00
J. Nick Koston
0b09e50685 preen 2025-10-17 17:57:42 -10:00
J. Nick Koston
a96cc5e6f2 Update esphome/analyze_memory/__init__.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-17 17:57:33 -10:00
J. Nick Koston
9a4288d81a Update script/determine-jobs.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-17 17:56:41 -10:00
J. Nick Koston
b95999aca7 Update esphome/analyze_memory/__init__.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-17 17:55:37 -10:00
J. Nick Koston
c70937ed01 dry 2025-10-17 17:55:05 -10:00
J. Nick Koston
3151606d50 Merge branch 'dev' into ci_impact_analysis 2025-10-17 17:47:36 -10:00
J. Nick Koston
5080698c3a no memory when tatget branch does not have 2025-10-17 17:34:16 -10:00
J. Nick Koston
931e3f80f0 no memory when tatget branch does not have 2025-10-17 17:25:14 -10:00
J. Nick Koston
85f1019d90 [tests] Migrate remote_transmitter/receiver to common bus definitions (#11325) 2025-10-17 17:21:38 -10:00
J. Nick Koston
cd93f7f55a tweak 2025-10-17 17:13:24 -10:00
J. Nick Koston
d98b00f56d tweak 2025-10-17 17:10:28 -10:00
J. Nick Koston
8fd43f1d96 tweak 2025-10-17 17:09:05 -10:00
J. Nick Koston
0475ec5533 preen 2025-10-17 17:01:20 -10:00
J. Nick Koston
6fe5a0c736 preen 2025-10-17 16:44:38 -10:00
J. Nick Koston
1ec9383abe preen 2025-10-17 16:39:10 -10:00
J. Nick Koston
5e1ee92754 add tests 2025-10-17 16:34:25 -10:00
J. Nick Koston
558d4eb9dd preen 2025-10-17 16:19:50 -10:00
J. Nick Koston
c6ecfd0c55 esp32 only platforms 2025-10-17 16:15:46 -10:00
J. Nick Koston
3b8b2c0754 esp32 only platforms 2025-10-17 16:13:30 -10:00
J. Nick Koston
f5d69a2539 esp32 only platforms 2025-10-17 16:11:28 -10:00
J. Nick Koston
29b9073d62 esp32 only platforms 2025-10-17 16:08:16 -10:00
J. Nick Koston
a45e94cd06 preen 2025-10-17 16:02:08 -10:00
J. Nick Koston
71f2fb8353 preen 2025-10-17 15:56:13 -10:00
J. Nick Koston
0fcae15c25 preen 2025-10-17 15:53:03 -10:00
J. Nick Koston
a1d6bac21a preen 2025-10-17 15:44:36 -10:00
J. Nick Koston
db69ce24ae fix 2025-10-17 15:41:20 -10:00
J. Nick Koston
293400ee14 fix 2025-10-17 15:35:51 -10:00
J. Nick Koston
57bf3f968f fix 2025-10-17 15:34:17 -10:00
J. Nick Koston
922c2bcd5a fix 2025-10-17 15:26:55 -10:00
J. Nick Koston
5e9b972831 fix 2025-10-17 15:24:49 -10:00
J. Nick Koston
3bc0041b94 fix 2025-10-17 15:22:06 -10:00
J. Nick Koston
daa03e5b3c fix 2025-10-17 15:17:28 -10:00
J. Nick Koston
62ce39e430 fix 2025-10-17 15:17:15 -10:00
J. Nick Koston
a9e5e4d6d2 tweak 2025-10-17 15:14:00 -10:00
Leonardo Rivera
bfeade1e2b [remote_base] Add Symphony IR protocol (encode/decode) with command_repeats support (#10777) 2025-10-17 21:13:33 -04:00
J. Nick Koston
95a0c9594f tweak 2025-10-17 15:12:36 -10:00
J. Nick Koston
8762d7cf0e Merge remote-tracking branch 'upstream/dev' into ci_impact_analysis 2025-10-17 15:06:15 -10:00
J. Nick Koston
84316d62f9 tweak 2025-10-17 15:04:19 -10:00
J. Nick Koston
e1e047c53f tweak 2025-10-17 15:02:09 -10:00
J. Nick Koston
b0ada914bc tweak 2025-10-17 14:57:45 -10:00
J. Nick Koston
e2101f5a20 tweak 2025-10-17 14:52:07 -10:00
Niall Douglas
b134d42e3b [xgzp68xx] Add oversampling config and tidy up implementation. (#10306)
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-10-18 00:47:18 +00:00
J. Nick Koston
f87c969b43 tweak 2025-10-17 14:40:45 -10:00
J. Nick Koston
f011c44130 merge 2025-10-17 14:26:44 -10:00
J. Nick Koston
843f590db4 fix 2025-10-17 14:13:25 -10:00
J. Nick Koston
2c86ebaf7f merge 2025-10-17 14:10:23 -10:00
J. Nick Koston
25fe4a1476 merge 2025-10-17 14:09:08 -10:00
J. Nick Koston
86c12079b4 merge 2025-10-17 14:05:24 -10:00
J. Nick Koston
79aafe2cd5 merge 2025-10-17 14:01:21 -10:00
J. Nick Koston
a5d6e39b2f merge 2025-10-17 14:01:07 -10:00
J. Nick Koston
a78a7dfa4e merge 2025-10-17 13:58:59 -10:00
J. Nick Koston
7879df4dd1 merge 2025-10-17 13:57:57 -10:00
J. Nick Koston
43c62297e8 merge 2025-10-17 13:56:31 -10:00
J. Nick Koston
5049c7227d reduce 2025-10-17 13:50:15 -10:00
J. Nick Koston
256d3b119b relo 2025-10-17 13:44:30 -10:00
J. Nick Koston
6d2c700c43 relo 2025-10-17 13:43:05 -10:00
J. Nick Koston
9d081795e8 relo 2025-10-17 13:41:55 -10:00
J. Nick Koston
59848a2c8a tweak 2025-10-17 13:31:04 -10:00
J. Nick Koston
c7c408e667 tweak 2025-10-17 13:28:13 -10:00
J. Nick Koston
acfa325f23 merge 2025-10-17 13:22:01 -10:00
J. Nick Koston
cb97271704 Merge remote-tracking branch 'upstream/dev' into ci_impact_analysis 2025-10-17 13:19:47 -10:00
J. Nick Koston
b3b65316f0 [ci] Fix test_build_components missing test files with hyphen naming pattern (#11329) 2025-10-17 13:05:44 -10:00
J. Nick Koston
54163bb520 Merge branch 'integration' into memory_api 2025-10-17 12:39:47 -10:00
J. Nick Koston
fe2e598cfb Merge branch 'wifi_in_place' into integration 2025-10-17 12:39:39 -10:00
J. Nick Koston
ce1d10eff0 [wifi] Optimize WiFi scan results with in-place construction 2025-10-17 12:39:14 -10:00
J. Nick Koston
879ba5e090 Merge branch 'integration' into memory_api 2025-10-17 11:48:09 -10:00
J. Nick Koston
a0d9b4fdf9 Merge branch 'ci_missed_files' into integration 2025-10-17 11:48:04 -10:00
J. Nick Koston
de5894ca1a [ci] Fix test_build_components missing test files with hyphen naming pattern 2025-10-17 11:29:17 -10:00
J. Nick Koston
c26125c732 Merge branch 'integration' into memory_api 2025-10-17 11:20:53 -10:00
J. Nick Koston
1867831ff5 Merge branch 'fast_connect_cond_compile' into integration 2025-10-17 11:20:46 -10:00
J. Nick Koston
00dd48d1f8 tweak 2025-10-17 11:17:31 -10:00
J. Nick Koston
35bcc6ff8a missing guard 2025-10-17 10:54:50 -10:00
J. Nick Koston
63f9e1fde8 missing guard 2025-10-17 10:54:39 -10:00
J. Nick Koston
16a8645833 Merge branch 'integration' into memory_api 2025-10-17 10:41:20 -10:00
J. Nick Koston
320df90b6b Merge branch 'fast_connect_cond_compile' into integration 2025-10-17 10:41:15 -10:00
J. Nick Koston
3fce283053 [wifi] Convert fast_connect to compile-time define, save 608-1024 bytes flash 2025-10-17 10:40:28 -10:00
J. Nick Koston
2eeb9d097f Merge branch 'integration' into memory_api 2025-10-17 09:40:31 -10:00
J. Nick Koston
4f4714cec2 Merge branch 'set_timezone_cleanup' into integration 2025-10-17 09:40:26 -10:00
J. Nick Koston
6dd0020bf6 [api][time] Refactor timezone update logic for cleaner code 2025-10-17 09:39:51 -10:00
J. Nick Koston
2ad80d2208 tweak 2025-10-17 09:29:58 -10:00
dependabot[bot]
b61cec8e77 Bump github/codeql-action from 4.30.8 to 4.30.9 (#11326)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-17 09:14:45 -10:00
J. Nick Koston
7220d25a4d Merge branch 'integration' into memory_api 2025-10-17 09:07:34 -10:00
J. Nick Koston
c769b6fdf7 Merge branch 'ota_fix' into integration 2025-10-17 09:07:25 -10:00
J. Nick Koston
5918db6012 Merge branch 'integration' into memory_api 2025-10-17 09:01:59 -10:00
J. Nick Koston
f65235be3a Merge branch 'wifi_scans_less_copies' into integration 2025-10-17 09:01:53 -10:00
Jonathan Swoboda
24243fb22c [tests] Add i2c_id to mcp47a1 & mcp4725 and remove from isolation (#11324) 2025-10-17 08:23:49 -10:00
J. Nick Koston
ba6c8c87c2 [dashboard] Fix binary download with packages using secrets after Path migration (#11313) 2025-10-17 08:20:55 -10:00
J. Nick Koston
f5774cc138 [debug] Replace std::map with struct array for ESP32 chip features (#11307) 2025-10-17 08:20:31 -10:00
J. Nick Koston
6722e5c8d8 [wifi] Optimize WiFi scanning to reduce copies and heap allocations 2025-10-17 07:43:24 -10:00
B48D81EFCC
6d09e68b2e [bh1900nux] Add bh1900nux temperature Sensor (#8631)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Andreas Riehl <andreas.riehl@acp.de>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-10-17 11:11:51 -04:00
tomaszduda23
fe9db75c27 [nrf52] add xiao_ble board (#10698)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-10-17 09:02:37 -04:00
mrtoy-me
2b832e9ee8 [cap1188] remove delays in setup (#11317)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-10-17 08:55:07 -04:00
exotime
661e9f9991 [toshiba] Add support for RAS-2819T air conditioner (#9490)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
2025-10-17 03:33:50 -05:00
esphomebot
39e23c323d Synchronise Device Classes from Home Assistant (#11285) 2025-10-17 07:49:10 +00:00
J. Nick Koston
bdfbac0301 [tests] Fix ESP32-C3 component test binary size by using larger partition table (#11319) 2025-10-17 20:20:00 +13:00
J. Nick Koston
6e3cc21c34 Merge branch 'integration' into memory_api 2025-10-16 21:18:51 -10:00
J. Nick Koston
6a5abb0a9d Merge remote-tracking branch 'upstream/dev' into integration 2025-10-16 21:18:39 -10:00
Daniel Stiner
9646653e57 [const] Add CONF_OPENTHREAD (#11318) 2025-10-16 21:02:28 -10:00
J. Nick Koston
62e5b5cd2b Merge branch 'integration' into memory_api 2025-10-16 16:31:27 -10:00
Daniel Stiner
f9e53453f2 [openthread] Backport address resolution support to prevent OTA crash
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-10-16 16:31:20 -10:00
J. Nick Koston
05844afe97 Merge branch 'unbound_queued_script_fix' into integration 2025-10-16 16:31:03 -10:00
Joshua M. Boniface
c6c202e4f7 [ina2xx_base] add device reset-on-boot disablement option (#10787)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-10-16 21:57:19 -04:00
J. Nick Koston
dd25080550 Merge remote-tracking branch 'upstream/dev' into unbound_queued_script_fix 2025-10-16 15:52:26 -10:00
J. Nick Koston
2e30a4953a address review comments 2025-10-16 15:51:01 -10:00
J. Nick Koston
75b6fd4b43 Merge branch 'integration' into memory_api 2025-10-16 14:13:58 -10:00
J. Nick Koston
fbca7bca04 Merge branch 'choose_upload_no_devices_fix' into integration 2025-10-16 14:13:53 -10:00
J. Nick Koston
8b5509328e adjust tesdts 2025-10-16 14:12:55 -10:00
J. Nick Koston
076313b850 [core] Fix IndexError when OTA devices cannot be resolved 2025-10-16 14:09:09 -10:00
J. Nick Koston
237504f4e8 Merge branch 'integration' into memory_api 2025-10-16 13:24:18 -10:00
J. Nick Koston
b933154d6a Merge branch 'unbound_queued_script_fix' into integration 2025-10-16 13:24:12 -10:00
J. Nick Koston
f2ec2c3fbf max_runs was actually correct after re-testing dev 2025-10-16 13:20:42 -10:00
J. Nick Koston
353d8b8fb2 update var name to specify what it really is 2025-10-16 13:11:17 -10:00
J. Nick Koston
9de34901f9 tidy up 2025-10-16 13:06:38 -10:00
J. Nick Koston
532e6acbed fix assumptions 2025-10-16 13:00:16 -10:00
J. Nick Koston
8340bb8566 test 2025-10-16 12:53:08 -10:00
J. Nick Koston
283c9a208f max_runs for queued 2025-10-16 12:23:01 -10:00
J. Nick Koston
e96b66a9d7 [script] BREAKING: Fix unbounded queue growth, optimize queued mode (default max_runs=5) 2025-10-16 12:15:31 -10:00
Jonathan Swoboda
62f73c768e [esp32] Reduce tx power on IDF in the event of a brownout (#11306) 2025-10-16 17:43:15 -04:00
J. Nick Koston
e48371a36d Merge branch 'integration' into memory_api 2025-10-16 11:18:40 -10:00
J. Nick Koston
b53b2cbf41 Merge branch 'debug_esp32_map' into integration 2025-10-16 11:18:33 -10:00
J. Nick Koston
4ae737fc7b [debug] Replace std::map with struct array for ESP32 chip features 2025-10-16 11:08:28 -10:00
Jonathan Swoboda
cd1215347e [esp32] Reduce tx power on Arduino (#11304) 2025-10-16 16:55:30 -04:00
dependabot[bot]
b8353b3117 Bump ruff from 0.14.0 to 0.14.1 (#11303)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-10-16 20:52:22 +00:00
J. Nick Koston
b0cc53fe7d Merge branch 'integration' into memory_api 2025-10-16 10:29:58 -10:00
J. Nick Koston
4514876953 Merge branch 'scheduler_defer_drains_each_loop' into integration 2025-10-16 10:29:05 -10:00
J. Nick Koston
fed833cd27 cleanup 2025-10-16 10:23:40 -10:00
J. Nick Koston
90c9cb98c6 nullptr 2025-10-16 10:23:01 -10:00
J. Nick Koston
b0cefbe507 nullptr 2025-10-16 10:20:28 -10:00
J. Nick Koston
819e155939 Merge branch 'integration' into memory_api 2025-10-16 10:19:27 -10:00
J. Nick Koston
0ba56171f9 Merge branch 'scheduler_defer_drains_each_loop' into integration 2025-10-16 10:19:13 -10:00
J. Nick Koston
9baa5fc47c nullptr 2025-10-16 10:19:04 -10:00
J. Nick Koston
0430fea572 nullptr 2025-10-16 10:16:32 -10:00
J. Nick Koston
a1b366b699 Merge branch 'integration' into memory_api 2025-10-16 09:58:42 -10:00
J. Nick Koston
e0c39fcc4c Merge branch 'scheduler_defer_drains_each_loop' into integration 2025-10-16 09:58:34 -10:00
J. Nick Koston
da551a9257 Merge branch 'integration' into memory_api 2025-10-16 09:53:43 -10:00
J. Nick Koston
de0c1c6d0c Merge remote-tracking branch 'upstream/dev' into integration 2025-10-16 09:53:28 -10:00
J. Nick Koston
94704f5bd1 vector for defer 2025-10-16 09:40:15 -10:00
Jonathan Swoboda
ea33d7db2d Mark build as valid 2025-10-16 13:50:31 -04:00
J. Nick Koston
1118ef32c3 preen 2025-10-16 06:16:37 -10:00
J. Nick Koston
0cff6acdf4 fix flakey 2025-10-16 06:09:44 -10:00
J. Nick Koston
7be04916ac fix flakey 2025-10-16 06:09:38 -10:00
J. Nick Koston
b5c4dc13e0 fix flakey 2025-10-16 06:07:41 -10:00
J. Nick Koston
0200d7c358 fix flakey 2025-10-16 06:05:39 -10:00
J. Nick Koston
44ad787cb3 fix flakey 2025-10-16 06:04:42 -10:00
J. Nick Koston
699da1adc1 Merge remote-tracking branch 'upstream/min_filter_ring_buffer' into min_filter_ring_buffer 2025-10-16 06:02:03 -10:00
J. Nick Koston
4d7e8ffd0a Merge remote-tracking branch 'upstream/dev' into min_filter_ring_buffer 2025-10-16 06:01:57 -10:00
J. Nick Koston
3ba2212cfc fix flakey 2025-10-16 06:01:32 -10:00
Jonathan Swoboda
5d3574c81f [htu21d] Revert register address change (#11291) 2025-10-16 11:29:05 -04:00
Jonathan Swoboda
364e5ffd79 [core] Add ESP32 ROM functions to reserved ids (#11293) 2025-10-16 11:28:52 -04:00
Jesse Hills
c38c2a1daf Merge branch 'release' into dev 2025-10-16 23:00:45 +13:00
Jesse Hills
070b0882b8 Merge pull request #11284 from esphome/bump-2025.10.1
2025.10.1
2025-10-16 23:00:00 +13:00
J. Nick Koston
7c6351b5d7 Merge branch 'integration' into memory_api 2025-10-15 23:51:56 -10:00
J. Nick Koston
dde5382b51 Merge branch 'min_filter_ring_buffer' into integration 2025-10-15 23:51:44 -10:00
J. Nick Koston
bb2be9869d Merge branch 'dev' into min_filter_ring_buffer 2025-10-15 23:45:55 -10:00
J. Nick Koston
7e2ccb7bc3 [datetime] Fix DateTimeStateTrigger compilation when time component is not used (#11287) 2025-10-15 23:45:42 -10:00
J. Nick Koston
b4ba2aff30 remove dead unreachable code 2025-10-15 23:30:45 -10:00
J. Nick Koston
febe075bb2 helper 2025-10-15 23:17:08 -10:00
J. Nick Koston
453d56fd13 Merge branch 'integration' into memory_api 2025-10-15 22:13:38 -10:00
J. Nick Koston
32ac20bc99 Merge branch 'min_filter_ring_buffer' into integration 2025-10-15 22:13:26 -10:00
J. Nick Koston
baf117b411 fix flakey test 2025-10-15 22:03:22 -10:00
J. Nick Koston
55e03036e2 preen 2025-10-15 21:46:00 -10:00
J. Nick Koston
af45dc206f Merge remote-tracking branch 'origin/datetime_guard_not_needed' into min_filter_ring_buffer 2025-10-15 21:45:13 -10:00
J. Nick Koston
7027ae9833 race 2025-10-15 21:44:38 -10:00
J. Nick Koston
784183ca8d [datetime] Fix DateTimeStateTrigger compilation when time component is not used 2025-10-15 21:38:02 -10:00
J. Nick Koston
855df423ee add 2025-10-15 19:58:18 -10:00
J. Nick Koston
f75f11b550 add 2025-10-15 19:57:29 -10:00
J. Nick Koston
a999349fa5 tweak 2025-10-15 19:29:55 -10:00
J. Nick Koston
92d54ffb09 tweak 2025-10-15 19:28:51 -10:00
J. Nick Koston
589c25e65a tweak 2025-10-15 19:24:44 -10:00
J. Nick Koston
5a8558e1c5 tweak 2025-10-15 19:23:35 -10:00
J. Nick Koston
a72c494b75 tweak 2025-10-15 19:23:01 -10:00
J. Nick Koston
e3089ff0f6 tweak 2025-10-15 19:21:33 -10:00
J. Nick Koston
a4b14902db perf 2025-10-15 18:44:37 -10:00
J. Nick Koston
4ff39ee82c Merge remote-tracking branch 'origin/min_filter_ring_buffer' into min_filter_ring_buffer 2025-10-15 18:27:33 -10:00
J. Nick Koston
447ee3da39 tests 2025-10-15 18:26:23 -10:00
J. Nick Koston
9b6707c1c0 tests 2025-10-15 18:25:42 -10:00
Jesse Hills
7f1173fcba Bump version to 2025.10.1 2025-10-16 17:17:07 +13:00
J. Nick Koston
a75ccf841c [substitutions] Fix AttributeError when using packages with substitutions (#11274)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-16 17:17:07 +13:00
Jonathan Swoboda
56eb605ec9 [wifi] Fix enterprise wifi (#11276)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-10-16 17:17:07 +13:00
J. Nick Koston
2c4818de00 [tests] Fix OTA password test assertions after merge collision (#11275) 2025-10-16 17:17:06 +13:00
Jesse Hills
2b94de8732 [ota.esphome] Handle blank password the same as no password defined (#11271) 2025-10-16 17:17:06 +13:00
J. Nick Koston
f71aed3a5c [ota] Fix MQTT resolution when static IP appears first in device list (#11272) 2025-10-16 17:17:06 +13:00
Clyde Stubbs
353e097085 [mipi_spi] Rotation fixes (#11226) 2025-10-16 17:17:06 +13:00
J. Nick Koston
0c18dd872b Merge branch 'dev' into min_filter_ring_buffer 2025-10-15 18:05:02 -10:00
J. Nick Koston
b074ca8a1e fix 2025-10-15 18:00:33 -10:00
J. Nick Koston
4c24545b82 fix 2025-10-15 17:51:08 -10:00
J. Nick Koston
cd252a33f9 fix 2025-10-15 17:51:03 -10:00
J. Nick Koston
36f8511309 fix 2025-10-15 17:50:32 -10:00
J. Nick Koston
12874187dd fix 2025-10-15 17:50:27 -10:00
J. Nick Koston
d7832c44bc [sensor] Fix sliding window filter memory fragmentation with FixedVector ring buffer 2025-10-15 17:45:37 -10:00
J. Nick Koston
14d76e9e4e [ci] Merge components with different buses to reduce CI time (#11251)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-10-16 16:36:03 +13:00
J. Nick Koston
f2e0a412db [substitutions] Fix AttributeError when using packages with substitutions (#11274)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-16 16:23:20 +13:00
J. Nick Koston
6943b1d985 [api] Use FixedVector for ExecuteServiceRequest/Argument arrays to eliminate reallocations (#11270) 2025-10-16 16:22:08 +13:00
J. Nick Koston
18062d154f [esp32_ble_tracker] Refactor to use CORE.data instead of module-level globals (#11220) 2025-10-16 16:18:30 +13:00
J. Nick Koston
2b0b82b2fb [esp32_ble] Refactor to use CORE.data instead of module-level globals (#11222) 2025-10-16 16:17:16 +13:00
J. Nick Koston
3e1c8f37c5 [i2s_audio] Refactor to use CORE.data instead of module-level globals (#11223) 2025-10-16 16:16:28 +13:00
Keith Burzinski
236ca12d3e [api, climate, thermostat] Implement feature_flags for climate (#10987)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-10-15 21:59:55 -05:00
J. Nick Koston
42f1b61e31 [git] Automatically recover from broken git repositories in external_components (#11246) 2025-10-16 15:58:58 +13:00
J. Nick Koston
708f8a95e5 [api] Use FixedVector for HomeAssistantServiceCallAction to reduce flash usage and avoid realloc (#11277) 2025-10-16 02:48:38 +00:00
J. Nick Koston
10ca86ae8d [api] Use std::unique_ptr for fixed-size byte buffers in Noise protocol (#11278) 2025-10-16 02:41:25 +00:00
J. Nick Koston
a49aed2dcb Merge branch 'integration' into memory_api 2025-10-15 16:19:52 -10:00
J. Nick Koston
322b141e51 Merge branch 'fixed_vector_HomeAssistantServiceCallAction' into integration 2025-10-15 16:19:47 -10:00
J. Nick Koston
295ac4b1b8 protect 2025-10-15 16:19:36 -10:00
J. Nick Koston
0a7a3bae8b protect 2025-10-15 16:19:22 -10:00
J. Nick Koston
a5c955f9a5 bot comments 2025-10-15 16:16:27 -10:00
J. Nick Koston
b7fbc728f8 Merge branch 'integration' into memory_api 2025-10-15 16:08:38 -10:00
J. Nick Koston
052b4b44e8 Merge branch 'noise_no_vec_uint8t' into integration 2025-10-15 16:08:32 -10:00
J. Nick Koston
628d781fe8 [api] Use std::unique_ptr for fixed-size byte buffers in Noise protocol 2025-10-15 16:08:07 -10:00
J. Nick Koston
8156c8ccf5 Merge branch 'integration' into memory_api 2025-10-15 15:51:19 -10:00
J. Nick Koston
f4d228b314 Merge branch 'execute_fixed_vector' into integration 2025-10-15 15:51:07 -10:00
J. Nick Koston
b39976ce35 no more magic 3 2025-10-15 15:47:26 -10:00
J. Nick Koston
5e5620fb49 bot comments 2025-10-15 15:43:34 -10:00
J. Nick Koston
98c1c2d9af Merge branch 'integration' into memory_api 2025-10-15 15:40:17 -10:00
J. Nick Koston
f0ba401c74 Merge branch 'fixed_vector_HomeAssistantServiceCallAction' into integration 2025-10-15 15:40:09 -10:00
J. Nick Koston
e3d5ca1375 [api] Use FixedVector for HomeAssistantServiceCallAction to reduce flash and avoid reallocations 2025-10-15 15:38:32 -10:00
Jonathan Swoboda
22056e0809 [wifi] Fix enterprise wifi (#11276)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-10-16 01:24:56 +00:00
J. Nick Koston
ec31f0b13b Merge branch 'memory_api' of https://github.com/esphome/esphome into memory_api 2025-10-15 15:21:05 -10:00
J. Nick Koston
d443d3037b Merge branch 'integration' into memory_api 2025-10-15 15:20:48 -10:00
J. Nick Koston
2aa2f4bb17 Merge branch 'execute_fixed_vector' into integration 2025-10-15 15:20:39 -10:00
J. Nick Koston
8de406c633 Merge remote-tracking branch 'upstream/dev' into integration 2025-10-15 15:20:28 -10:00
J. Nick Koston
fe4857fabb [tests] Fix OTA password test assertions after merge collision (#11275) 2025-10-16 13:28:19 +13:00
Jesse Hills
3054c2bc29 [ota.esphome] Handle blank password the same as no password defined (#11271) 2025-10-16 13:07:37 +13:00
J. Nick Koston
b190f37ae7 [ota] Fix MQTT resolution when static IP appears first in device list (#11272) 2025-10-16 13:06:02 +13:00
dependabot[bot]
28454b8219 Bump aioesphomeapi from 41.18.0 to 42.0.0 (#11273)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-15 14:00:27 -10:00
J. Nick Koston
922b0bb324 Merge remote-tracking branch 'upstream/dev' into execute_fixed_vector 2025-10-15 12:29:41 -10:00
J. Nick Koston
332f52e149 [api] Use FixedVector for ListEntitiesServicesResponse args (#11230) 2025-10-15 12:28:56 -10:00
J. Nick Koston
ce6718eeaa [api] Use FixedVector for ExecuteServiceRequest/Argument arrays to eliminate reallocations 2025-10-15 10:29:53 -10:00
Clyde Stubbs
ae1f54d398 [mipi_spi] Rotation fixes (#11226) 2025-10-16 07:09:52 +11:00
J. Nick Koston
95f5b18fe2 Merge branch 'dev' into ListEntitiesServicesArgument_FixedVector 2025-10-15 10:00:15 -10:00
J. Nick Koston
7a0ea74ec6 Merge branch 'integration' into memory_api 2025-10-15 09:45:21 -10:00
J. Nick Koston
2001ae1d41 Merge remote-tracking branch 'origin/integration' into memory_api 2025-10-15 09:45:15 -10:00
J. Nick Koston
42d53b301c Merge branch 'integration' of https://github.com/esphome/esphome into integration 2025-10-15 09:44:42 -10:00
J. Nick Koston
9355a3592c Merge remote-tracking branch 'upstream/dev' into integration 2025-10-15 09:43:58 -10:00
J. Nick Koston
2b3e7f38d2 [esp32] Add option to disable libc locks in IRAM, saving ~1.3KB RAM (#10930)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-15 09:34:14 -10:00
dependabot[bot]
5510ece6ac Bump pylint from 4.0.0 to 4.0.1 (#11267)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-15 09:34:01 -10:00
Jonathan Swoboda
e19a85b523 [esp32_ble] Add support for hosted BLE (#11167) 2025-10-15 15:19:08 -04:00
Jonathan Swoboda
cf02a08209 [esp32] Bump IDF version to 5.5.1 and Arduino version to 3.3.2 (#9839) 2025-10-15 08:45:33 -10:00
Jonathan Swoboda
90e8c12df1 [ci] Isolate openthread (#11259) 2025-10-15 06:45:06 -10:00
Jonathan Swoboda
42bf5840c9 [esp32_rmt_led_strip] Don't send reset if duration is zero (#11235) 2025-10-15 07:49:28 -04:00
Thane Gill
47817485e7 [esp32] Remove kconfiglib from requirements.txt (#11210) 2025-10-15 07:48:26 -04:00
Jonathan Swoboda
ded98ff705 [esp32_hosted] Bump hosted components (#11170)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-10-15 07:48:05 -04:00
J. Nick Koston
7f8ca5ddef [ci] Add Python 3.14 testing and streamline version matrix (#11238) 2025-10-14 19:49:48 -10:00
J. Nick Koston
1b0ca3360e [ci] Group all PR builds, isolate direct changes for full validation on dev (#11193) 2025-10-14 19:49:14 -10:00
Jesse Hills
66263b40e1 Merge branch 'release' into dev 2025-10-15 17:31:16 +13:00
Jesse Hills
9a29dec6d9 Merge pull request #11245 from esphome/bump-2025.10.0
2025.10.0
2025-10-15 17:30:30 +13:00
Stuart Parmenter
6d5e41ef7f [const] Add CONF_ROWS (#11249) 2025-10-15 17:29:41 +13:00
Stuart Parmenter
fedfda6c29 [core] Fix regression from #10654 (#11248) 2025-10-15 16:57:47 +13:00
J. Nick Koston
9ce3847a56 Merge branch 'dev' into ListEntitiesServicesArgument_FixedVector 2025-10-14 17:29:41 -10:00
dependabot[bot]
6e2088f836 Bump aioesphomeapi from 41.17.0 to 41.18.0 (#11247)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-15 03:12:34 +00:00
J. Nick Koston
62774bfd70 Merge branch 'dev' into ListEntitiesServicesArgument_FixedVector 2025-10-14 16:25:06 -10:00
J. Nick Koston
9b00ab5fe4 Merge branch 'integration' into memory_api 2025-10-14 16:24:24 -10:00
J. Nick Koston
5bfc2b95d1 Merge branch 'recover_broken_git_repo' into integration 2025-10-14 16:24:19 -10:00
J. Nick Koston
b927cea0d6 [git] Automatically recover from broken git repositories in external_components 2025-10-14 16:23:06 -10:00
J. Nick Koston
7a82379c88 [mdns] Use FixedVector for txt_records to reduce flash usage (#11228) 2025-10-15 02:16:59 +00:00
J. Nick Koston
c983581b6c [api] Convert HomeassistantActionRequest vectors to FixedVector for flash savings (#11229) 2025-10-14 21:10:04 -05:00
J. Nick Koston
0a738b23f1 Merge branch 'integration' into memory_api 2025-10-14 16:06:16 -10:00
J. Nick Koston
1edda6e632 Merge remote-tracking branch 'upstream/dev' into integration 2025-10-14 16:06:04 -10:00
J. Nick Koston
f0ac61f247 [light] Use FixedVector for LightState effects list (#11232)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-14 16:00:22 -10:00
Jesse Hills
63b113d823 Bump version to 2025.10.0 2025-10-15 14:01:02 +13:00
J. Nick Koston
85420b0606 [web_server_idf] Use std::vector instead of std::set for SSE sessions (#11233) 2025-10-14 19:50:40 -05:00
J. Nick Koston
00230f7cc6 [wifi] Use FixedVector for scan results to reduce flash usage (#11216) 2025-10-15 00:45:28 +00:00
J. Nick Koston
63a87a5ef3 [core] Use FixedVector for automation condition vectors to save 384 bytes flash (#11237) 2025-10-14 14:27:10 -10:00
J. Nick Koston
d75ae357c2 [wifi] Free scan results memory after connection (saves up to 1.2KB RAM) (#11205) 2025-10-14 14:25:31 -10:00
Jesse Hills
88d223d03a Merge branch 'beta' into dev 2025-10-15 12:44:49 +13:00
Jesse Hills
0381644605 Merge pull request #11241 from esphome/bump-2025.10.0b4
2025.10.0b4
2025-10-15 12:44:15 +13:00
J. Nick Koston
8e6ee2bed1 debug 2025-10-14 13:43:58 -10:00
J. Nick Koston
354f46f7c0 debug 2025-10-14 13:38:41 -10:00
Jesse Hills
48a557b005 [netlify] Pin python version (#11244) 2025-10-15 12:37:55 +13:00
Jesse Hills
b927b29a0a [netlify] Pin python version (#11244) 2025-10-15 12:37:27 +13:00
J. Nick Koston
7b6acd3c00 tidy 2025-10-14 13:33:31 -10:00
J. Nick Koston
11f5f7683c tidy 2025-10-14 13:32:21 -10:00
J. Nick Koston
5da589abd0 fix 2025-10-14 13:27:13 -10:00
J. Nick Koston
daa39a489d fix tests 2025-10-14 13:20:31 -10:00
J. Nick Koston
3bb95a190d fix 2025-10-14 13:15:44 -10:00
J. Nick Koston
25a6202bb9 [ci] Automatic Flash/RAM impact analysis 2025-10-14 13:09:01 -10:00
J. Nick Koston
c4eeed7f7e [ci] Automatic Flash/RAM impact analysis 2025-10-14 13:05:02 -10:00
Jesse Hills
780ece73ff Bump version to 2025.10.0b4 2025-10-15 11:35:52 +13:00
J. Nick Koston
d7fcf8d57b [pzemac, pzemdc, sdm_meter] Fix pin conflicts in ESP32-IDF tests (#11240) 2025-10-15 11:35:52 +13:00
TJQ
82a3ca575f [mipi_dsi] Update waveshare P4-86 display parameters (#10562) 2025-10-15 11:35:52 +13:00
dependabot[bot]
5913da5a89 Bump aioesphomeapi from 41.16.0 to 41.16.1 (#11221)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-15 11:35:52 +13:00
dependabot[bot]
8c13105ce1 Bump aioesphomeapi from 41.14.0 to 41.16.0 (#11215)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-15 11:35:52 +13:00
J. Nick Koston
72ec9b672e [pzemac, pzemdc, sdm_meter] Fix pin conflicts in ESP32-IDF tests (#11240) 2025-10-15 11:33:19 +13:00
J. Nick Koston
e2327b46e6 Merge branch 'integration' into memory_api 2025-10-14 12:30:06 -10:00
J. Nick Koston
cd836e7594 Merge branch 'pin_conflits_fix' into integration 2025-10-14 12:30:00 -10:00
J. Nick Koston
837a0bf6df [pzemac, pzemdc, sdm_meter] Fix pin conflicts in ESP32-IDF tests 2025-10-14 12:18:12 -10:00
J. Nick Koston
8f49b1da54 Bump pillow to 11.3.0 (#11239) 2025-10-14 11:49:39 -10:00
dependabot[bot]
9ff6f344ab Bump ruamel-yaml-clib from 0.2.12 to 0.2.14 (#10842)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-14 10:49:30 -10:00
J. Nick Koston
7598900cc2 Merge branch 'integration' into memory_api 2025-10-14 09:39:00 -10:00
J. Nick Koston
07fd35a7a0 Merge branch 'fixed_vectors_for_automations' into integration 2025-10-14 09:38:54 -10:00
J. Nick Koston
793e75a093 [core] Use FixedVector for automation condition vectors to save 384 bytes flash 2025-10-14 09:28:56 -10:00
J. Nick Koston
138c0fdf91 Merge branch 'integration' into memory_api 2025-10-14 09:17:14 -10:00
J. Nick Koston
577f9a39f4 Merge remote-tracking branch 'upstream/dev' into integration 2025-10-14 09:17:03 -10:00
J. Nick Koston
0e703ddbba [docs] Add embedded systems optimization best practices to AI instructions (#11225) 2025-10-15 07:54:16 +13:00
TJQ
2175c2909b [mipi_dsi] Update waveshare P4-86 display parameters (#10562) 2025-10-14 22:28:06 +11:00
J. Nick Koston
41d5122a57 Merge branch 'integration' into memory_api 2025-10-13 22:51:25 -10:00
J. Nick Koston
b1c7cc6b06 Merge branch 'web_server_idf_vector_sessions' into integration 2025-10-13 22:51:19 -10:00
J. Nick Koston
3cf24a259c [web_server_idf] Use std::vector instead of std::set for SSE sessions 2025-10-13 22:46:45 -10:00
J. Nick Koston
5c3379120f Merge branch 'integration' into memory_api 2025-10-13 22:31:55 -10:00
J. Nick Koston
7291502ff3 Merge branch 'light_effects_fixed_vector' into integration 2025-10-13 22:31:49 -10:00
J. Nick Koston
87ae07e7be [light] Use FixedVector for LightState effects list 2025-10-13 22:27:41 -10:00
dependabot[bot]
cbdb9d4a56 Bump aioesphomeapi from 41.16.1 to 41.17.0 (#11231)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-14 08:06:18 +00:00
J. Nick Koston
3dff1d02d2 Merge branch 'integration' into memory_api 2025-10-13 21:59:26 -10:00
J. Nick Koston
a2d555c54d Merge branch 'ListEntitiesServicesArgument_FixedVector' into integration 2025-10-13 21:59:18 -10:00
J. Nick Koston
7572951bc5 Merge branch 'integration' into memory_api 2025-10-13 21:58:53 -10:00
J. Nick Koston
00760ff183 Merge branch 'fixed_vector_mdns_txt_records' into integration 2025-10-13 21:58:43 -10:00
J. Nick Koston
1acd7d4672 Update esphome/core/helpers.h
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-13 21:56:11 -10:00
J. Nick Koston
ce3bd55a38 [api] Use FixedVector for ListEntitiesServicesResponse args 2025-10-13 21:52:24 -10:00
J. Nick Koston
a8b6a56467 Merge branch 'integration' into memory_api 2025-10-13 21:46:14 -10:00
J. Nick Koston
c808998ba7 Merge branch 'fixed_vector_HomeassistantServiceMap' into integration 2025-10-13 21:46:09 -10:00
J. Nick Koston
5ebb68b719 fixed 2025-10-13 21:45:52 -10:00
J. Nick Koston
66d859bfae Merge branch 'integration' into memory_api 2025-10-13 21:42:24 -10:00
J. Nick Koston
dcde6b1cdf Merge branch 'fixed_vector_HomeassistantServiceMap' into integration 2025-10-13 21:42:18 -10:00
J. Nick Koston
95aab61e58 Merge branch 'ai_instructions_embedded_systems' into integration 2025-10-13 21:42:14 -10:00
J. Nick Koston
0fca842afe preen 2025-10-13 21:41:57 -10:00
J. Nick Koston
e241e43064 preen 2025-10-13 21:41:49 -10:00
J. Nick Koston
92a6aade17 fixes 2025-10-13 21:35:26 -10:00
J. Nick Koston
ef766f81e9 Merge upstream/dev and address Copilot review comments
- Resolved merge conflict between embedded systems optimization and state management sections
- Added StaticVector (compile-time max size) and FixedVector (runtime size) documentation
- Clarified std::unique_ptr<uint8_t[]> lacks bounds checking and iterator support
- Added nuance to linear search vs hashing tradeoffs for small datasets
- Updated detection patterns to include _M_realloc_insert and _M_default_append
2025-10-13 21:31:47 -10:00
J. Nick Koston
17c44504da Merge branch 'dev' into fixed_vector_mdns_txt_records 2025-10-13 21:17:37 -10:00
J. Nick Koston
2316f3a5e3 Merge branch 'integration' into memory_api 2025-10-13 21:14:52 -10:00
J. Nick Koston
19c8a638f4 Merge branch 'fixed_vector_mdns_txt_records' into integration 2025-10-13 21:14:46 -10:00
J. Nick Koston
05efb6e925 refactor to avoid move 2025-10-13 21:14:32 -10:00
J. Nick Koston
baa010583e [docs] Add state management best practices to CLAUDE.md (#11224) 2025-10-14 20:09:48 +13:00
J. Nick Koston
1479f48636 Merge branch 'fixed_vector_HomeassistantServiceMap' into memory_api 2025-10-13 19:31:38 -10:00
J. Nick Koston
240fe97d20 Merge branch 'fixed_vector_HomeassistantServiceMap' into integration 2025-10-13 19:31:29 -10:00
J. Nick Koston
43d8386c4a tidy 2025-10-13 19:31:13 -10:00
J. Nick Koston
adc0e986a2 Merge branch 'integration' into memory_api 2025-10-13 19:28:36 -10:00
J. Nick Koston
6c5ba7d394 Merge branch 'fixed_vector_HomeassistantServiceMap' into integration 2025-10-13 19:28:21 -10:00
J. Nick Koston
7492d7a437 [api] Convert HomeassistantActionRequest vectors to FixedVector for flash savings 2025-10-13 19:27:33 -10:00
J. Nick Koston
1c6dd52e9f Merge branch 'integration' into memory_api 2025-10-13 19:15:55 -10:00
J. Nick Koston
62497a43fb Merge branch 'fixed_vector_mdns_txt_records' into integration 2025-10-13 19:15:49 -10:00
J. Nick Koston
24a7426a2a rename to fix shadow 2025-10-13 19:15:39 -10:00
J. Nick Koston
b7af2d7f4c Merge branch 'integration' into memory_api 2025-10-13 19:07:08 -10:00
J. Nick Koston
2132427fe7 Merge branch 'fixed_vector_mdns_txt_records' into integration 2025-10-13 19:07:01 -10:00
J. Nick Koston
fc30326e60 preen 2025-10-13 19:06:02 -10:00
J. Nick Koston
45014db027 preen 2025-10-13 19:05:26 -10:00
J. Nick Koston
ac35c97a44 we need copy now 2025-10-13 18:59:46 -10:00
J. Nick Koston
541c697a42 [mdns] Use FixedVector for txt_records to reduce flash usage 2025-10-13 18:52:49 -10:00
J. Nick Koston
96dd348f9a Merge branch 'integration' into memory_api 2025-10-13 18:32:24 -10:00
J. Nick Koston
6d5dcf109e Merge remote-tracking branch 'upstream/dev' into integration 2025-10-13 18:32:01 -10:00
J. Nick Koston
01c41fc57c Merge branch 'i2s_core_data' into integration 2025-10-13 18:29:11 -10:00
J. Nick Koston
97d3cae81e Merge branch 'esp32_ble_core_data' into integration 2025-10-13 18:29:07 -10:00
J. Nick Koston
60e59b98d5 Merge branch 'esp32_ble_tracker_core_data' into integration 2025-10-13 18:29:03 -10:00
J. Nick Koston
0f43f4cbbf [docs] Add embedded systems optimization and state management best practices to CLAUDE.md 2025-10-13 18:26:45 -10:00
dependabot[bot]
8e9a68a107 Bump aioesphomeapi from 41.16.0 to 41.16.1 (#11221)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-13 18:12:52 -10:00
J. Nick Koston
18d5fd160a [i2s_audio] Refactor to use CORE.data instead of module-level globals 2025-10-13 18:11:34 -10:00
J. Nick Koston
dd0699305e [esp32_ble] Refactor to use CORE.data instead of module-level globals 2025-10-13 18:08:52 -10:00
J. Nick Koston
5bdd6dac97 [esp32_ble_tracker] Refactor to use CORE.data instead of module-level globals 2025-10-13 18:02:28 -10:00
J. Nick Koston
4c688a4b00 [network] Optimize get_use_address() to return const reference instead of a copy (#11218)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-14 03:54:33 +00:00
J. Nick Koston
98132cb950 Merge branch 'integration' into memory_api 2025-10-13 17:50:00 -10:00
J. Nick Koston
7629309e76 Merge branch 'clear_scan_result_done' into integration 2025-10-13 17:49:47 -10:00
J. Nick Koston
5c30c1b691 core.data 2025-10-13 17:49:07 -10:00
Jesse Hills
9eef281895 Merge branch 'beta' into dev 2025-10-14 16:35:50 +13:00
Jesse Hills
c3fd07f8bc Merge pull request #11219 from esphome/bump-2025.10.0b3
2025.10.0b3
2025-10-14 16:35:13 +13:00
J. Nick Koston
8a15c18066 [bluetooth_proxy] Use FixedVector for GATT characteristics and descriptors (#11214)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-14 16:05:13 +13:00
J. Nick Koston
a674727fa6 Merge branch 'integration' into memory_api 2025-10-13 16:51:04 -10:00
J. Nick Koston
f54db515bc Merge branch 'get_use_address' into integration 2025-10-13 16:50:58 -10:00
Jesse Hills
d02ed41eb4 Bump version to 2025.10.0b3 2025-10-14 15:38:15 +13:00
J. Nick Koston
07504c8208 Fix log retrieval with FQDN when mDNS is disabled (#11202) 2025-10-14 15:38:15 +13:00
Jonathan Swoboda
b666b8e261 [core] Properly clean the build dir in the HA addon (#11208) 2025-10-14 15:38:15 +13:00
dependabot[bot]
8627b56e36 Bump esphome-dashboard from 20251009.0 to 20251013.0 (#11212)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-14 15:38:15 +13:00
Jesse Hills
69df07ddcf [media_player.speaker] Dynamic auto load (#11084)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-10-14 15:38:15 +13:00
J. Nick Koston
5bb69a968c [esp32_ble] Replace handler vectors with StaticVector for 560B-2KB memory savings (#11200)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-14 15:33:33 +13:00
J. Nick Koston
c5076e69f0 host platform 2025-10-13 15:52:00 -10:00
J. Nick Koston
9945c4b350 Merge branch 'integration' into memory_api 2025-10-13 15:41:57 -10:00
J. Nick Koston
9f9abd2259 Merge branch 'get_use_address' into integration 2025-10-13 15:41:47 -10:00
J. Nick Koston
6b8d5be528 Update esphome/components/network/util.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-13 15:41:17 -10:00
J. Nick Koston
044b3c07ef Merge branch 'integration' into memory_api 2025-10-13 15:30:38 -10:00
J. Nick Koston
3439f38ebf Merge branch 'fixed_vector_wifi' into integration 2025-10-13 15:30:29 -10:00
J. Nick Koston
2626a851fb cleanup 2025-10-13 15:30:18 -10:00
J. Nick Koston
9775274007 preen 2025-10-13 15:25:47 -10:00
J. Nick Koston
d5ba16f13a merge 2025-10-13 15:22:52 -10:00
J. Nick Koston
e17cdffc78 merge 2025-10-13 15:04:40 -10:00
J. Nick Koston
7c02f2f10a [socket] Split LWIP socket classes to reduce memory overhead on ESP8266/RP2040 (#11172) 2025-10-14 14:00:49 +13:00
J. Nick Koston
910aff6589 Merge branch 'integration' into memory_api 2025-10-13 14:51:27 -10:00
J. Nick Koston
84d42aeeaf Merge branch 'fixed_vector_wifi' into integration 2025-10-13 14:51:18 -10:00
J. Nick Koston
fa830cfd39 fix 2025-10-13 14:50:55 -10:00
J. Nick Koston
d82bcea106 Merge branch 'integration' into memory_api 2025-10-13 14:39:08 -10:00
J. Nick Koston
e9766603b7 Merge branch 'get_use_address' into integration 2025-10-13 14:39:02 -10:00
J. Nick Koston
2881f32b08 [network] Optimize get_use_address() to return const reference instead of copy 2025-10-13 14:37:10 -10:00
J. Nick Koston
10724f411b [network] Optimize get_use_address() to return const reference instead of copy 2025-10-13 14:32:17 -10:00
J. Nick Koston
99a5a6f3a6 Merge branch 'integration' into memory_api 2025-10-13 14:20:29 -10:00
J. Nick Koston
9d04878a0c Merge branch 'fixed_vector_wifi' into integration 2025-10-13 14:20:17 -10:00
J. Nick Koston
5a11a2e5b2 Merge branch 'fixed_vector_bluetooth_services' into integration 2025-10-13 14:20:12 -10:00
J. Nick Koston
7b5a86e4df fixes 2025-10-13 14:15:37 -10:00
J. Nick Koston
453ab0adb8 backmerge 2025-10-13 14:10:56 -10:00
J. Nick Koston
de10d78125 dry 2025-10-13 14:10:41 -10:00
J. Nick Koston
b878aa0270 fix 2025-10-13 14:09:44 -10:00
J. Nick Koston
c9a1664398 merge 2025-10-13 14:08:27 -10:00
J. Nick Koston
bb2f568f3d merge 2025-10-13 14:07:52 -10:00
J. Nick Koston
7792a115c2 merge 2025-10-13 14:05:31 -10:00
J. Nick Koston
ce46f16308 merge 2025-10-13 14:05:19 -10:00
J. Nick Koston
d5234e3357 merge 2025-10-13 14:04:39 -10:00
J. Nick Koston
ddf6e0a7b6 revert 2025-10-13 14:04:15 -10:00
J. Nick Koston
fbef9b1264 revert 2025-10-13 14:03:59 -10:00
J. Nick Koston
eb545127c0 Merge branch 'fixed_vector_bluetooth_services' into fixed_vector_wifi 2025-10-13 14:03:34 -10:00
J. Nick Koston
8aa832ab08 Merge remote-tracking branch 'upstream/fixed_vector_bluetooth_services' into fixed_vector_bluetooth_services 2025-10-13 14:03:21 -10:00
J. Nick Koston
22370c0ad1 merge 2025-10-13 14:03:08 -10:00
J. Nick Koston
3fd9c42d82 Merge branch 'wifi_fixed_vector' into fixed_vector_wifi 2025-10-13 14:02:41 -10:00
J. Nick Koston
e9448d7126 Merge branch 'dev' into fixed_vector_bluetooth_services 2025-10-13 14:02:06 -10:00
dependabot[bot]
fe07c34246 Bump aioesphomeapi from 41.14.0 to 41.16.0 (#11215)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-14 00:00:45 +00:00
J. Nick Koston
3847e8aa73 Merge remote-tracking branch 'upstream/dev' into fixed_vector_bluetooth_services 2025-10-13 13:25:58 -10:00
dependabot[bot]
c652aa375a Bump pylint from 3.3.9 to 4.0.0 (#11211)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-13 13:10:46 -10:00
J. Nick Koston
7231b0cb5c Merge branch 'integration' into memory_api 2025-10-13 11:41:49 -10:00
J. Nick Koston
494ffb8c48 Merge remote-tracking branch 'upstream/dev' into integration 2025-10-13 11:41:35 -10:00
J. Nick Koston
9fb254fdc2 Fix log retrieval with FQDN when mDNS is disabled (#11202) 2025-10-14 10:23:44 +13:00
Jonathan Swoboda
3df4dbd3a6 [core] Properly clean the build dir in the HA addon (#11208) 2025-10-13 17:12:45 -04:00
J. Nick Koston
6372099df3 [http_request] Pass parameters by const reference to reduce flash usage (#11184) 2025-10-14 09:53:11 +13:00
J. Nick Koston
8d8fcfeda2 [core] Add make_name_with_suffix helper to optimize string concatenation (#11176)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-14 09:39:38 +13:00
J. Nick Koston
0f356fcc79 [core] Optimize looping_components_ with FixedVector to save flash (#11183) 2025-10-14 09:20:43 +13:00
dependabot[bot]
aec60d122b Bump esphome-dashboard from 20251009.0 to 20251013.0 (#11212)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-13 09:34:12 -10:00
J. Nick Koston
c10f68ef0c [mdns] Conditionally store services to reduce RAM usage by 200-464 bytes (#11180) 2025-10-14 07:24:57 +13:00
J. Nick Koston
bcc424afed [web_server] Reduce code duplication in JSON generation with helper functions (#11117) 2025-10-14 07:21:19 +13:00
J. Nick Koston
be2c859df3 [web_server] Consolidate duplicate client connection checks (saves 288 bytes of flash) (#11116) 2025-10-14 07:01:47 +13:00
J. Nick Koston
dd09897a1d Update esphome/components/wifi_info/text_sensor.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-12 20:46:36 -10:00
J. Nick Koston
e28599b403 Merge branch 'integration' into memory_api 2025-10-12 20:22:31 -10:00
J. Nick Koston
71bf274214 Merge branch 'clear_scan_result_done' into integration 2025-10-12 20:22:23 -10:00
J. Nick Koston
d191d1e99a preen 2025-10-12 20:21:56 -10:00
J. Nick Koston
987d616846 Merge branch 'integration' into memory_api 2025-10-12 20:20:49 -10:00
J. Nick Koston
299cbdd411 Merge branch 'clear_scan_result_done' into integration 2025-10-12 20:20:42 -10:00
J. Nick Koston
4d55c8f309 preen 2025-10-12 20:20:17 -10:00
J. Nick Koston
ba408a10bb Merge branch 'integration' into memory_api 2025-10-12 20:15:20 -10:00
J. Nick Koston
9435a3a1fc Merge branch 'clear_scan_result_done' into integration 2025-10-12 20:15:11 -10:00
J. Nick Koston
6f3a996698 [wifi] Free scan results memory after successful connection 2025-10-12 20:12:34 -10:00
J. Nick Koston
347501d895 wifi fixed vector 2025-10-12 19:39:55 -10:00
J. Nick Koston
a9fd0a3b26 fixed_vector, bluetooth services 2025-10-12 18:21:14 -10:00
Jesse Hills
59f728488e [media_player.speaker] Dynamic auto load (#11084)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-10-13 12:58:30 +13:00
J. Nick Koston
767b5a11ef Merge branch 'integration' into memory_api 2025-10-12 12:55:42 -10:00
J. Nick Koston
b1eb65da7d Merge branch 'logs_fqdn_fix' into integration 2025-10-12 12:55:31 -10:00
J. Nick Koston
7c8f8e282d Fix log retrieval with FQDN when mDNS is disabled 2025-10-12 12:52:38 -10:00
Jesse Hills
04a0de556d Merge branch 'beta' into dev 2025-10-13 10:56:08 +13:00
Jesse Hills
13cfa30c67 Merge pull request #11199 from esphome/bump-2025.10.0b2
2025.10.0b2
2025-10-13 10:55:34 +13:00
J. Nick Koston
a2254a6d55 Merge branch 'integration' into memory_api 2025-10-12 11:29:04 -10:00
J. Nick Koston
d3806d790d Merge branch 'ble_handlers_static' into integration 2025-10-12 11:28:54 -10:00
J. Nick Koston
26ebfa4906 cleaner 2025-10-12 11:19:58 -10:00
J. Nick Koston
2c6828eb84 Merge branch 'integration' into memory_api 2025-10-12 11:16:36 -10:00
J. Nick Koston
7070204b00 Merge branch 'ble_handlers_static' into integration 2025-10-12 11:16:27 -10:00
J. Nick Koston
6f2c7c0e5d fixes 2025-10-12 11:13:14 -10:00
J. Nick Koston
2a94463ac1 [esp32_ble] Replace handler vectors with StaticVector for 2KB memory savings 2025-10-12 11:07:01 -10:00
J. Nick Koston
a635db726d Merge branch 'integration' into memory_api 2025-10-12 10:32:11 -10:00
J. Nick Koston
41860c312e Merge remote-tracking branch 'upstream/dev' into integration 2025-10-12 10:31:59 -10:00
Jesse Hills
da1959ab5d Bump version to 2025.10.0b2 2025-10-13 08:49:29 +13:00
J. Nick Koston
2b42903e9c [usb_host] Fix transfer slot exhaustion at high data rates and add configurable max_transfer_requests (#11174)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-10-13 08:49:29 +13:00
J. Nick Koston
742c9cbb53 [esp32_improv] Fix state not transitioning to PROVISIONED when WiFi configured via captive portal (#11181) 2025-10-13 08:49:29 +13:00
J. Nick Koston
e4bc465a3d [ota] Increase handshake timeout to 20s now that auth is non-blocking (#11186) 2025-10-13 08:49:29 +13:00
J. Nick Koston
5cec0941f8 [wifi] Fix missed string literal in flash on ESP8266 (#11187) 2025-10-13 08:49:29 +13:00
J. Nick Koston
72a7aeb430 [ci] Dynamic runner allocation: 8 for releases, 4 for dev (#11191) 2025-10-13 08:49:29 +13:00
J. Nick Koston
53e6b28092 [mipi_rgb] Fix pin conflicts introduced by shared SPI bus in #11134 (#11185) 2025-10-13 08:49:28 +13:00
dependabot[bot]
7f3c7bb5c6 Bump aioesphomeapi from 41.13.0 to 41.14.0 (#11188)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-13 08:49:28 +13:00
Jonathan Swoboda
c02c0b2a96 [esp32] Change Arduino dev & latest to 3.3.2 (#11169) 2025-10-13 08:49:28 +13:00
J. Nick Koston
5f5092e29f [ci] Filter out components without tests from CI test jobs (#11134 followup) (#11178) 2025-10-13 08:49:28 +13:00
J. Nick Koston
2864bf1674 Group component tests to reduce CI time (#11134) 2025-10-13 08:49:28 +13:00
J. Nick Koston
132e949927 [mdns] Restore mdns_txt_record() public API for external components (#11158) 2025-10-13 08:49:28 +13:00
J. Nick Koston
8fa44e471d [esp32_ble] Partial revert of #10862 - Fix GATT client notifications (#11171) 2025-10-13 08:49:28 +13:00
J. Nick Koston
ccedcfb600 [json] Fix PSRAM allocator dangling pointer crash (#11165) 2025-10-13 08:49:28 +13:00
J. Nick Koston
8b0ec0afe3 [ci][tests] Remove all redundant ESP32-C3 Arduino tests (#11154) 2025-10-13 08:49:28 +13:00
J. Nick Koston
dca29ed89b [datetime][ci][tests] Replace test.all.yaml with minimal platform cover (#11151) 2025-10-13 08:49:28 +13:00
J. Nick Koston
728726e29e [ci][tests] Remove redundant ESP32-C3 Arduino tests for non-variant-specific components (#11152) 2025-10-13 08:49:28 +13:00
J. Nick Koston
79f4ca20b8 [opentherm][ci][tests] Remove redundant ESP32 Arduino tests and simplify conditionals (#11149) 2025-10-13 08:49:28 +13:00
J. Nick Koston
3eca72e0b8 [ci][logger][tests] Remove redundant ESP32 Arduino test files (#11144) 2025-10-13 08:49:28 +13:00
J. Nick Koston
22c0f55cef [ci][debug][tests] Remove redundant ESP32 variant Arduino test files (#11146) 2025-10-13 08:49:28 +13:00
J. Nick Koston
fd8ecc9608 [ci][time][tests] Remove redundant ESP32 Arduino test files (#11147) 2025-10-13 08:49:27 +13:00
J. Nick Koston
ac96a59d58 [network][ci][tests] Remove redundant ESP32 Arduino test files (#11148) 2025-10-13 08:49:27 +13:00
J. Nick Koston
dceed992d8 [esp32_ble_beacon, esp32_ble_tracker] Remove unused Arduino includes and redundant tests (#11140) 2025-10-13 08:49:27 +13:00
J. Nick Koston
b0c66c1c09 [ci][mdns][tests] Remove redundant ESP32 Arduino test files (#11143) 2025-10-13 08:49:27 +13:00
J. Nick Koston
8f04a5b944 [esp32] Update migration warning for Arduino-as-IDF-component transition (#11142) 2025-10-13 08:49:27 +13:00
Jonathan Swoboda
e6c21df30b [esp32] Update IDF 5.5 and Arduino 3.3 to use 55.03.31-1 (#11120) 2025-10-13 08:49:27 +13:00
J. Nick Koston
842cb9033a [mdns] Store TXT record values in flash to reduce heap usage (#11114) 2025-10-13 08:49:27 +13:00
J. Nick Koston
a2cb415dfa [ci][improv_serial][tests] Remove redundant ESP32 Arduino test files (#11138) 2025-10-13 08:49:27 +13:00
J. Nick Koston
1fac193535 [ci][ethernet][tests] Remove redundant Arduino tests for ethernet PHYs (#11137) 2025-10-13 08:49:27 +13:00
J. Nick Koston
34632f78cf [ci][tests] Remove redundant ESP32 Arduino test files (#11136) 2025-10-13 08:49:27 +13:00
J. Nick Koston
b93c60e85a [canbus][mcp23xxx_base] Mark virtual methods as pure virtual to fix linker errors (#11133) 2025-10-13 08:49:27 +13:00
dependabot[bot]
60dc055509 Bump esphome-dashboard from 20250904.0 to 20251009.0 (#11123)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-13 08:49:27 +13:00
J. Nick Koston
1f13d44c1b [usb_host] Fix transfer slot exhaustion at high data rates and add configurable max_transfer_requests (#11174)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-10-13 08:04:30 +13:00
J. Nick Koston
9ebfa9aaa8 [esp32_improv] Fix state not transitioning to PROVISIONED when WiFi configured via captive portal (#11181) 2025-10-13 07:30:58 +13:00
J. Nick Koston
6bc9ed0810 [ota] Increase handshake timeout to 20s now that auth is non-blocking (#11186) 2025-10-13 07:27:43 +13:00
J. Nick Koston
9b6e8b4b41 [wifi] Fix missed string literal in flash on ESP8266 (#11187) 2025-10-13 07:26:28 +13:00
J. Nick Koston
cad747c672 [ci] Dynamic runner allocation: 8 for releases, 4 for dev (#11191) 2025-10-13 07:25:35 +13:00
J. Nick Koston
660adccda3 [mipi_rgb] Fix pin conflicts introduced by shared SPI bus in #11134 (#11185) 2025-10-12 08:58:56 -04:00
dependabot[bot]
51fbc4f7a3 Bump aioesphomeapi from 41.13.0 to 41.14.0 (#11188)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-12 08:12:52 +00:00
J. Nick Koston
dc53831b27 Merge branch 'integration' into memory_api 2025-10-11 22:03:51 -10:00
J. Nick Koston
e8397704fb Merge branch 'wifi_missed_string_lit' into integration 2025-10-11 22:03:40 -10:00
J. Nick Koston
ddc7a15302 [wifi] Fix missed string literal in flash on ESP8266 2025-10-11 22:02:59 -10:00
J. Nick Koston
6a0bcdf4f6 Merge branch 'integration' into memory_api 2025-10-11 21:26:37 -10:00
J. Nick Koston
cc63edcf7a Merge branch 'flakey_ota_first_connect' into integration 2025-10-11 21:26:29 -10:00
J. Nick Koston
072662c395 timeout 2025-10-11 21:26:13 -10:00
J. Nick Koston
cebc8a3867 Merge branch 'integration' into memory_api 2025-10-11 21:23:51 -10:00
J. Nick Koston
2795d67787 Merge branch 'flakey_ota_first_connect' into integration 2025-10-11 21:23:37 -10:00
J. Nick Koston
66c8c045f2 [ota] Increase handshake timeout to 20s now that auth is non-blocking 2025-10-11 21:23:02 -10:00
J. Nick Koston
91dbdffea5 [mipi_rgb] Fix pin conflicts introduced by shared SPI bus in #11134 2025-10-11 19:56:05 -10:00
J. Nick Koston
2fc5afc79e Merge branch 'integration' into memory_api 2025-10-11 17:36:14 -10:00
J. Nick Koston
e0933e0094 Merge branch 'http_request_const' into integration 2025-10-11 17:36:02 -10:00
J. Nick Koston
0c0ed8c4fd Merge branch 'loop_fix_vec' into integration 2025-10-11 17:35:57 -10:00
J. Nick Koston
4c00861760 add comments for bot 2025-10-11 17:35:31 -10:00
J. Nick Koston
2ff3e7fb2b add comments for bot 2025-10-11 17:34:51 -10:00
J. Nick Koston
fdc9ea285d [http_request] Pass parameters by const reference to reduce flash usage 2025-10-11 17:30:30 -10:00
J. Nick Koston
34d891761a Merge branch 'integration' into memory_api 2025-10-11 17:00:46 -10:00
J. Nick Koston
e64111345c Merge branch 'loop_fix_vec' into integration 2025-10-11 17:00:34 -10:00
J. Nick Koston
d6239398ed Merge branch 'loop_fix_vec' into memory_api 2025-10-11 17:00:25 -10:00
J. Nick Koston
b0c20d7adb [core] Optimize looping_components_ with FixedVector to save flash 2025-10-11 16:54:40 -10:00
J. Nick Koston
d2a31b95c4 preen 2025-10-11 16:08:47 -10:00
J. Nick Koston
0d3489df3f Merge branch 'integration' into memory_api 2025-10-11 15:34:42 -10:00
J. Nick Koston
6b2ef78787 preen 2025-10-11 15:34:31 -10:00
J. Nick Koston
153f01ef77 preen 2025-10-11 15:34:15 -10:00
J. Nick Koston
e69013317d Update esphome/core/helpers.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-11 15:33:46 -10:00
J. Nick Koston
3f65f261ab Merge branch 'helper_for_name_suffix' of https://github.com/esphome/esphome into helper_for_name_suffix 2025-10-11 15:33:28 -10:00
J. Nick Koston
5fe319fcc5 preen 2025-10-11 15:33:22 -10:00
J. Nick Koston
21c2c6e782 Update esphome/config_validation.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-11 15:33:19 -10:00
J. Nick Koston
6ecdb395fd Merge branch 'dev' into helper_for_name_suffix 2025-10-11 15:27:41 -10:00
J. Nick Koston
3d328d7233 Merge remote-tracking branch 'origin/helper_for_name_suffix' into helper_for_name_suffix 2025-10-11 15:24:11 -10:00
J. Nick Koston
9f20c48a24 adjust 2025-10-11 15:23:51 -10:00
Jonathan Swoboda
2cc5e24b38 [esp32] Change Arduino dev & latest to 3.3.2 (#11169) 2025-10-11 20:44:44 -04:00
J. Nick Koston
069893abb9 Merge branch 'integration' into memory_api 2025-10-11 13:46:07 -10:00
J. Nick Koston
edd73ed192 Merge branch 'improv_cap_portal_fix' into integration 2025-10-11 13:45:59 -10:00
J. Nick Koston
10c231e872 Merge remote-tracking branch 'upstream/improv_cap_portal_fix' into improv_cap_portal_fix 2025-10-11 13:45:36 -10:00
J. Nick Koston
3758b4c801 preen 2025-10-11 13:45:22 -10:00
J. Nick Koston
5bd87906af Merge branch 'dev' into improv_cap_portal_fix 2025-10-11 13:42:41 -10:00
J. Nick Koston
c8b2a74a7e Merge branch 'dev' into helper_for_name_suffix 2025-10-11 13:37:43 -10:00
J. Nick Koston
3afa73b449 [ci] Filter out components without tests from CI test jobs (#11134 followup) (#11178) 2025-10-11 18:27:18 -05:00
J. Nick Koston
678a93cc56 fix 2025-10-11 13:08:10 -10:00
J. Nick Koston
5a0184cb35 [esp32_improv] Fix state not transitioning to PROVISIONED when WiFi configured via captive portal 2025-10-11 13:01:19 -10:00
J. Nick Koston
c63902781b [esp32_improv] Fix state not transitioning to PROVISIONED when WiFi configured via captive portal 2025-10-11 12:57:13 -10:00
J. Nick Koston
a193d5b40e [esp32_improv] Fix state not transitioning to PROVISIONED when WiFi configured via captive portal 2025-10-11 12:56:28 -10:00
J. Nick Koston
ff6191cfd4 [esp32_improv] Fix state not transitioning to PROVISIONED when WiFi configured via captive portal 2025-10-11 12:55:03 -10:00
J. Nick Koston
b7b2b296a0 Merge branch 'integration' into memory_api 2025-10-11 12:39:33 -10:00
J. Nick Koston
b032ba9bd4 Merge branch 'mdns_store' into integration 2025-10-11 12:39:27 -10:00
J. Nick Koston
0975dbfb01 cleanup 2025-10-11 12:38:12 -10:00
J. Nick Koston
0c8c99dbf8 [mdns] Conditionally store services to reduce RAM usage by 200-464 bytes 2025-10-11 12:27:39 -10:00
J. Nick Koston
c241258dfe Merge branch 'integration' into memory_api 2025-10-11 11:26:19 -10:00
J. Nick Koston
417f574cff Merge branch 'helper_for_name_suffix' into integration 2025-10-11 11:26:09 -10:00
J. Nick Koston
5e1848854e tweak for bot 2025-10-11 11:25:19 -10:00
J. Nick Koston
19c541f1e6 Merge branch 'integration' into memory_api 2025-10-11 11:11:46 -10:00
J. Nick Koston
4ad3f9d962 Merge branch 'helper_for_name_suffix' into integration 2025-10-11 11:11:39 -10:00
J. Nick Koston
81b7f41dd5 Merge branch 'fix_ci_only_test_comp_with_tests' into helper_for_name_suffix 2025-10-11 11:09:41 -10:00
J. Nick Koston
1acbb007dd [ci] Filter out components without tests from CI test jobs (#11134 followup) 2025-10-11 11:08:47 -10:00
J. Nick Koston
245ccb02fa Merge branch 'integration' into memory_api 2025-10-11 10:52:51 -10:00
J. Nick Koston
ce6b51e27d Merge branch 'helper_for_name_suffix' into integration 2025-10-11 10:52:36 -10:00
J. Nick Koston
6273380407 [core] Add make_name_with_suffix helper to optimize string concatenation 2025-10-11 10:51:17 -10:00
J. Nick Koston
69888af408 Merge branch 'integration' into memory_api 2025-10-11 09:32:00 -10:00
J. Nick Koston
2572157fc3 Merge remote-tracking branch 'upstream/integration' into integration 2025-10-11 09:31:48 -10:00
J. Nick Koston
a012557911 Merge branch 'integration' into memory_api 2025-10-11 09:31:08 -10:00
J. Nick Koston
3187e045d2 Merge remote-tracking branch 'upstream/dev' into integration 2025-10-11 09:30:48 -10:00
J. Nick Koston
dcf2697a2a Group component tests to reduce CI time (#11134) 2025-10-12 07:21:45 +13:00
J. Nick Koston
6a11700a6b [mdns] Restore mdns_txt_record() public API for external components (#11158) 2025-10-12 07:21:37 +13:00
J. Nick Koston
d63af64282 Merge branch 'integration' into memory_api 2025-10-11 06:27:06 -10:00
J. Nick Koston
4a7a0bbc93 Merge branch 'usb_host_keep_up' into integration 2025-10-11 06:26:59 -10:00
J. Nick Koston
fa69b74e6c tweak comments 2025-10-11 06:25:44 -10:00
J. Nick Koston
ec71669bff tweak comments 2025-10-11 06:24:35 -10:00
J. Nick Koston
2796cac972 compile tests 2025-10-11 06:17:36 -10:00
J. Nick Koston
442a60766d missing defines 2025-10-11 06:02:49 -10:00
J. Nick Koston
dd6085456a tweak 2025-10-11 06:00:57 -10:00
J. Nick Koston
460c41d9b8 [usb_host] Fix transfer slot exhaustion at high data rates and add configurable max_transfer_requests 2025-10-11 05:53:14 -10:00
J. Nick Koston
9bd9b043c8 [esp32_ble_tracker] Replace std::vector with StaticVector for listeners and clients (#11173) 2025-10-11 05:47:42 -10:00
J. Nick Koston
cb602c9b1a [esp32_ble] Partial revert of #10862 - Fix GATT client notifications (#11171) 2025-10-11 05:47:23 -10:00
J. Nick Koston
c524e6c2b3 Merge branch 'integration' into memory_api 2025-10-10 21:00:54 -10:00
J. Nick Koston
5d7731b39d Merge branch 'static_vector_esp32_ble_tracker' into integration 2025-10-10 21:00:48 -10:00
J. Nick Koston
dacead836f [esp32_ble_tracker] Replace std::vector with StaticVector for listeners and clients 2025-10-10 20:59:34 -10:00
J. Nick Koston
2184c1fde6 Merge branch 'integration' into memory_api 2025-10-10 20:04:45 -10:00
J. Nick Koston
1df2896796 Merge branch 'raw_tcp_mem' into integration 2025-10-10 20:04:39 -10:00
J. Nick Koston
3f49a61b03 tweak 2025-10-10 20:01:16 -10:00
J. Nick Koston
ec44856537 Merge branch 'integration' into memory_api 2025-10-10 19:44:02 -10:00
J. Nick Koston
a00cda32c7 Merge branch 'raw_tcp_mem' into integration 2025-10-10 19:43:53 -10:00
J. Nick Koston
8a4bd0f21c [socket] Split LWIP socket classes to reduce memory overhead on ESP8266/RP2040 2025-10-10 19:42:41 -10:00
J. Nick Koston
ee3af3904f Merge remote-tracking branch 'origin/memory_api' into memory_api 2025-10-10 17:39:56 -10:00
J. Nick Koston
02de8f9f80 merge 2025-10-10 17:39:37 -10:00
J. Nick Koston
9722c8eb60 Merge remote-tracking branch 'origin/integration' into memory_api 2025-10-10 17:39:14 -10:00
J. Nick Koston
29fb40a89f Merge branch 'integration' into memory_api 2025-10-10 17:39:01 -10:00
J. Nick Koston
1c7ff84e6a Merge branch 'partial_revert_10862' into integration 2025-10-10 17:38:53 -10:00
J. Nick Koston
632cd929ac adj 2025-10-10 17:38:26 -10:00
J. Nick Koston
3ea929eeb2 adj 2025-10-10 17:37:36 -10:00
J. Nick Koston
36ab68c1ea [esp32_ble] Partial revert of #10862 - Fix GATT client notifications 2025-10-10 17:31:13 -10:00
dependabot[bot]
b54beb357a Bump github/codeql-action from 4.30.7 to 4.30.8 (#11163)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-10 13:46:27 -10:00
J. Nick Koston
6abc2efd96 [json] Fix PSRAM allocator dangling pointer crash (#11165) 2025-10-10 21:18:57 +00:00
J. Nick Koston
e972767a11 Merge branch 'integration' into memory_api 2025-10-09 22:48:13 -10:00
J. Nick Koston
4890720c0e Merge branch 'mdns_back_compat' into integration 2025-10-09 22:48:05 -10:00
J. Nick Koston
cdc87a4445 [mdns] Restore mdns_txt_record() public API for external components 2025-10-09 22:46:45 -10:00
J. Nick Koston
be51093a7e [ci][tests] Remove all redundant ESP32-C3 Arduino tests (#11154) 2025-10-10 16:02:18 +13:00
J. Nick Koston
06a0ab6839 Merge branch 'integration' into memory_api 2025-10-09 16:04:24 -10:00
J. Nick Koston
6cc5b7c3af Merge remote-tracking branch 'upstream/dev' into integration 2025-10-09 16:04:16 -10:00
J. Nick Koston
52219c4dcc [datetime][ci][tests] Replace test.all.yaml with minimal platform cover (#11151) 2025-10-09 13:45:59 -10:00
J. Nick Koston
590cae13c0 [ci][tests] Remove redundant ESP32-C3 Arduino tests for non-variant-specific components (#11152) 2025-10-09 18:41:50 -05:00
J. Nick Koston
e15429b0f5 [opentherm][ci][tests] Remove redundant ESP32 Arduino tests and simplify conditionals (#11149) 2025-10-09 23:38:34 +00:00
J. Nick Koston
b5cc668a45 [ci][logger][tests] Remove redundant ESP32 Arduino test files (#11144) 2025-10-09 13:30:05 -10:00
Jonathan Swoboda
a1b0ae78e0 [stale] Increase operations-per-run (#11135)
CI passed, stuck on status
2025-10-09 19:10:09 -04:00
J. Nick Koston
88082911e9 [opentherm][ci][tests] Remove redundant ESP32 Arduino tests and fix conditionals 2025-10-09 12:24:37 -10:00
J. Nick Koston
fcc8a809e6 [ci][debug][tests] Remove redundant ESP32 variant Arduino test files (#11146) 2025-10-09 16:57:40 -05:00
J. Nick Koston
48474c0f8c [ci][time][tests] Remove redundant ESP32 Arduino test files (#11147) 2025-10-09 16:57:11 -05:00
J. Nick Koston
9f9c95dd09 [network][ci][tests] Remove redundant ESP32 Arduino test files (#11148) 2025-10-09 16:56:53 -05:00
J. Nick Koston
a74fcbc8b6 [esp32_ble_beacon, esp32_ble_tracker] Remove unused Arduino includes and redundant tests (#11140) 2025-10-09 11:42:25 -10:00
J. Nick Koston
c8b898f9c5 [ci][mdns][tests] Remove redundant ESP32 Arduino test files (#11143) 2025-10-09 11:40:47 -10:00
J. Nick Koston
81bf2688b4 [esp32] Update migration warning for Arduino-as-IDF-component transition (#11142) 2025-10-09 11:36:31 -10:00
Jonathan Swoboda
87d2c9868f [esp32] Update IDF 5.5 and Arduino 3.3 to use 55.03.31-1 (#11120) 2025-10-09 21:27:36 +00:00
J. Nick Koston
968d1e2647 Merge branch 'integration' into memory_api 2025-10-09 10:41:47 -10:00
J. Nick Koston
5a4f1dd2da Merge branch 'esp32_remove_esp32_ard_apis' into integration 2025-10-09 10:41:40 -10:00
J. Nick Koston
d8af6e0c75 fix 2025-10-09 10:40:02 -10:00
J. Nick Koston
36bcd8c204 fix 2025-10-09 10:39:51 -10:00
J. Nick Koston
5b146e1f12 fix 2025-10-09 10:39:41 -10:00
J. Nick Koston
de8a4ff6b0 Merge branch 'integration' into memory_api 2025-10-09 10:33:08 -10:00
J. Nick Koston
d837a001db Merge branch 'esp32_remove_esp32_ard_apis' into integration 2025-10-09 10:33:01 -10:00
J. Nick Koston
df71198a24 Merge remote-tracking branch 'upstream/dev' into integration 2025-10-09 10:32:58 -10:00
J. Nick Koston
5a5bebe71e Merge branch 'integration' of https://github.com/esphome/esphome into integration 2025-10-09 10:32:52 -10:00
J. Nick Koston
8853593a7b [esp32_ble*] Remove Arduino BLE wrapper dependencies 2025-10-09 10:32:04 -10:00
J. Nick Koston
5ca407e27c [mdns] Store TXT record values in flash to reduce heap usage (#11114) 2025-10-10 09:01:58 +13:00
dependabot[bot]
5bbc2ab482 Bump pyupgrade from 3.20.0 to 3.21.0 (#11139)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-09 19:40:40 +00:00
J. Nick Koston
309e8b4c92 [ci][improv_serial][tests] Remove redundant ESP32 Arduino test files (#11138) 2025-10-09 19:17:04 +00:00
Jesse Hills
eee2987c99 Merge branch 'beta' into dev 2025-10-10 07:53:53 +13:00
J. Nick Koston
061e55f8c5 [ci][ethernet][tests] Remove redundant Arduino tests for ethernet PHYs (#11137) 2025-10-09 08:45:45 -10:00
Jesse Hills
9ad462d8c6 Merge pull request #11115 from esphome/bump-2025.10.0b1
2025.10.0b1
2025-10-10 07:28:02 +13:00
J. Nick Koston
56334b7832 [ci][tests] Remove redundant ESP32 Arduino test files (#11136) 2025-10-10 07:26:41 +13:00
J. Nick Koston
a4b7e0c700 [canbus][mcp23xxx_base] Mark virtual methods as pure virtual to fix linker errors (#11133) 2025-10-09 07:41:49 -10:00
Jeff Brown
84ad7ee0e4 [esp32] Accept more framework URL schemes as sources (#11125) 2025-10-09 13:10:48 -04:00
dependabot[bot]
d006008539 Bump esphome-dashboard from 20250904.0 to 20251009.0 (#11123)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-09 09:26:38 -04:00
J. Nick Koston
f1af9d978c [ci] Reduce component test group size to 10 to prevent runner disk exhaustion (#11122) 2025-10-09 10:36:13 +13:00
J. Nick Koston
6bb1e4c9c0 [ci] Reduce component test group size to 10 to prevent runner disk exhaustion (#11122) 2025-10-09 10:35:52 +13:00
J. Nick Koston
c756e132a7 Merge branch 'integration' into memory_api 2025-10-08 09:28:07 -10:00
J. Nick Koston
e5a0a1d143 Merge remote-tracking branch 'upstream/dev' into integration 2025-10-08 09:27:34 -10:00
J. Nick Koston
785df05631 [ci] Reduce component test group size to prevent runner disk exhaustion (#11121) 2025-10-09 07:53:49 +13:00
J. Nick Koston
82bdb08884 [ci] Reduce component test group size to prevent runner disk exhaustion (#11121) 2025-10-08 14:24:26 -04:00
J. Nick Koston
98e68c32ee Merge branch 'integration' into memory_api 2025-10-08 05:50:03 -10:00
J. Nick Koston
b33b68b885 Merge branch 'webserver_helpers' into integration 2025-10-08 05:49:55 -10:00
J. Nick Koston
9ac48b162b tweak 2025-10-08 05:48:56 -10:00
J. Nick Koston
41d07701ee tweak 2025-10-08 05:46:20 -10:00
J. Nick Koston
fed252d1d3 wip 2025-10-08 05:40:31 -10:00
J. Nick Koston
2b8fdfb6a6 [web_server] Reduce code duplication in JSON generation with helper functions 2025-10-08 05:22:15 -10:00
J. Nick Koston
2ea32635c9 Merge branch 'integration' into memory_api 2025-10-08 05:12:32 -10:00
J. Nick Koston
8c876ec07d Merge branch 'webserver_dupe_checks' into integration 2025-10-08 05:12:22 -10:00
J. Nick Koston
576cf8ed6d [web_server] Consolidate duplicate client connection checks (saves 288 bytes flash) 2025-10-08 05:11:34 -10:00
J. Nick Koston
48799517eb Merge branch 'integration' into memory_api 2025-10-07 22:55:15 -10:00
J. Nick Koston
3e8672f351 Merge branch 'mdns_value_flash' into integration 2025-10-07 22:55:08 -10:00
J. Nick Koston
16f7de29eb Merge remote-tracking branch 'upstream/dev' into mdns_value_flash 2025-10-07 22:51:06 -10:00
J. Nick Koston
b1e950e785 better cond 2025-10-07 22:45:54 -10:00
J. Nick Koston
a0d9098f41 Merge branch 'integration' into memory_api 2025-10-07 22:34:07 -10:00
J. Nick Koston
e1852bdd59 Merge branch 'mdns_value_flash' into integration 2025-10-07 22:34:00 -10:00
J. Nick Koston
6eef594110 Merge remote-tracking branch 'origin/mdns_value_flash' into mdns_value_flash 2025-10-07 22:33:48 -10:00
J. Nick Koston
b22e154284 just remove it 2025-10-07 22:33:37 -10:00
J. Nick Koston
a793690795 Merge branch 'dev' into mdns_value_flash 2025-10-07 22:23:27 -10:00
J. Nick Koston
fc0afa1793 Merge branch 'integration' into memory_api 2025-10-07 22:22:31 -10:00
J. Nick Koston
d80e7a5ab6 Merge branch 'mdns_value_flash' into integration 2025-10-07 22:22:26 -10:00
J. Nick Koston
f33d9a77f3 bot comments 2025-10-07 22:22:12 -10:00
Jesse Hills
b709ff84c3 Bump version to 2025.11.0-dev 2025-10-08 21:14:45 +13:00
Jesse Hills
93266ad08f Bump version to 2025.10.0b1 2025-10-08 21:14:44 +13:00
Jesse Hills
2fac813f18 [epaper_spi] New epaper component (#10462)
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
Co-authored-by: Tudor Sandu <tm.sandu@gmail.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-10-08 21:11:06 +13:00
J. Nick Koston
734a0f3998 static analysis 2025-10-07 22:01:22 -10:00
J. Nick Koston
21d4e090bf Merge branch 'integration' into memory_api 2025-10-07 21:52:06 -10:00
J. Nick Koston
fe8af38f62 Merge branch 'mdns_value_flash' into integration 2025-10-07 21:51:56 -10:00
J. Nick Koston
d7964c4068 Merge branch 'dev' into integration 2025-10-07 21:51:53 -10:00
J. Nick Koston
72087bf6ba store mdns values in flash 2025-10-07 21:48:18 -10:00
Jesse Hills
a62c7a03dd [api] Add support for getting action responses from home-assistant (#10948)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-10-08 20:47:36 +13:00
J. Nick Koston
f5bb79cbc4 goodbye strdup 2025-10-07 21:46:08 -10:00
J. Nick Koston
d9c3213ef6 goodbye strdup 2025-10-07 21:43:35 -10:00
J. Nick Koston
328c1a8469 goodbye strdup 2025-10-07 21:39:04 -10:00
J. Nick Koston
6c0a0334a8 preen 2025-10-07 21:26:56 -10:00
J. Nick Koston
1476dcf5c8 preen 2025-10-07 21:24:10 -10:00
J. Nick Koston
ac7bd4137f preen 2025-10-07 21:22:34 -10:00
J. Nick Koston
52f2826d38 preen 2025-10-07 21:21:22 -10:00
J. Nick Koston
55888b9bee store mdns values in flash 2025-10-07 21:19:35 -10:00
J. Nick Koston
ec63247ae0 [mdns] Fix delete/malloc bug and store string constants in flash (#11105) 2025-10-08 04:19:29 +00:00
carlessolegrau
0fe6e7169c [modbus_controller] courtesy response (#10027)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-10-08 16:40:49 +13:00
dependabot[bot]
a0f4de1bfb Bump aioesphomeapi from 41.12.0 to 41.13.0 (#11113)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-08 03:35:17 +00:00
Jesse Hills
a541549d23 [core] Fix dynamic auto load priority (#11112) 2025-10-07 17:05:09 -10:00
Jonathan Swoboda
b74715fe14 [esp32] Fix issue when framework source is set (#11106) 2025-10-07 22:55:59 -04:00
J. Nick Koston
181f360176 Merge branch 'integration' into memory_api 2025-10-07 16:21:40 -10:00
J. Nick Koston
4acbf03f4e Merge branch 'jesserockz-2025-457' into integration 2025-10-07 16:21:32 -10:00
J. Nick Koston
5e16d84e0c Merge branch 'dev' into jesserockz-2025-457 2025-10-07 21:12:50 -05:00
J. Nick Koston
58796141e9 Merge branch 'integration' into memory_api 2025-10-07 16:12:17 -10:00
J. Nick Koston
a554d8b122 Merge remote-tracking branch 'upstream/dev' into integration 2025-10-07 16:11:56 -10:00
J. Nick Koston
5aff20a624 [api] Add message size limits to prevent memory exhaustion (#10936) 2025-10-08 00:47:31 +00:00
J. Nick Koston
76c8da03fe Merge branch 'integration' into memory_api 2025-10-07 14:10:20 -10:00
J. Nick Koston
b5ef87a1b8 Merge branch 'api_size_limits' into integration 2025-10-07 14:10:14 -10:00
J. Nick Koston
7f13080478 Merge branch 'mdns_esp32_cleanup' into integration 2025-10-07 14:10:10 -10:00
J. Nick Koston
2c408b7d78 Merge remote-tracking branch 'upstream/dev' into integration 2025-10-07 14:10:03 -10:00
J. Nick Koston
43c7ebcab4 missed python 2025-10-07 14:06:28 -10:00
J. Nick Koston
e3fadb1858 missed python 2025-10-07 14:05:22 -10:00
J. Nick Koston
a991768772 missed python 2025-10-07 14:02:39 -10:00
Kevin Ahrendt
7682b4e9a3 [audio] Update esp-audio-libs 2.0.1 to use new FLAC decoder (#10974)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-10-07 23:35:42 +00:00
J. Nick Koston
b9e2a30a38 Update test_oversized_payloads.py 2025-10-07 18:17:17 -05:00
J. Nick Koston
cb578c2198 Update test_oversized_payloads.py 2025-10-07 18:16:20 -05:00
J. Nick Koston
3b06b3386f Merge branch 'integration' into memory_api 2025-10-07 17:47:36 -05:00
J. Nick Koston
3a68268f39 Merge branch 'api_size_limits' into integration 2025-10-07 17:47:04 -05:00
J. Nick Koston
ef1c12c21f adjust 2025-10-07 17:37:50 -05:00
J. Nick Koston
6107802d69 Merge remote-tracking branch 'upstream/dev' into memory_api 2025-10-07 17:17:47 -05:00
J. Nick Koston
f59d2d5aca Merge branch 'integration' into memory_api 2025-10-07 17:16:24 -05:00
J. Nick Koston
453eecb240 Merge branch 'mdns_esp32_cleanup' into integration 2025-10-07 17:16:15 -05:00
J. Nick Koston
fa66b3235d tidy 2025-10-07 16:58:59 -05:00
J. Nick Koston
7446c87267 tidy 2025-10-07 16:58:19 -05:00
J. Nick Koston
57bd6ec68c tidy 2025-10-07 16:46:26 -05:00
J. Nick Koston
95ecacc5f7 tidy 2025-10-07 16:39:40 -05:00
J. Nick Koston
2e1d5662ea tidy 2025-10-07 16:34:51 -05:00
J. Nick Koston
87a1040285 keep all 8266 in flash 2025-10-07 16:29:10 -05:00
Jonathan Swoboda
6eabf709c6 [esp32] Hide build warnings (#11102) 2025-10-08 10:27:56 +13:00
J. Nick Koston
71765f01e6 Merge branch 'dev' into api_size_limits 2025-10-07 16:18:23 -05:00
J. Nick Koston
6209d4b493 [api] Optimize frame helpers to eliminate double-move overhead (#11092) 2025-10-08 10:16:44 +13:00
J. Nick Koston
1a6aaedbb7 preen 2025-10-07 16:16:36 -05:00
J. Nick Koston
b49f60569e tidy 2025-10-07 15:50:51 -05:00
J. Nick Koston
63a94df74f tidy 2025-10-07 15:47:19 -05:00
J. Nick Koston
15968cd8be Merge branch 'integration' into memory_api 2025-10-07 15:42:36 -05:00
J. Nick Koston
7693545d86 Merge branch 'mdns_esp32_cleanup' into integration 2025-10-07 15:42:10 -05:00
J. Nick Koston
f10c361454 [esp32_ble] Refactor ESPBTUUID comparison with direct returns and memcmp (#11074) 2025-10-08 09:34:08 +13:00
J. Nick Koston
f0a7c6b0bb simplify 2025-10-07 15:32:59 -05:00
J. Nick Koston
27456c1370 [esp32_ble] Refactor ESPBTUUID::from_raw to use parse_hex helpers (#11073) 2025-10-08 09:32:47 +13:00
J. Nick Koston
711532465e simplify 2025-10-07 15:27:49 -05:00
J. Nick Koston
2e4722104e simplify 2025-10-07 15:25:13 -05:00
J. Nick Koston
c9a709675a simplify 2025-10-07 15:25:13 -05:00
J. Nick Koston
65b8148f2e simplify 2025-10-07 15:24:48 -05:00
J. Nick Koston
93d493004c simplify 2025-10-07 15:24:42 -05:00
J. Nick Koston
1aeefbe547 [light] Reduce flash usage by eliminating duplicate validation code (#11030)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-08 09:23:57 +13:00
J. Nick Koston
94eab93110 Merge branch 'integration' into memory_api 2025-10-07 14:59:21 -05:00
J. Nick Koston
762c141d93 Merge branch 'mdns_esp32_cleanup' into integration 2025-10-07 14:59:15 -05:00
J. Nick Koston
cf1ba30e90 just store key in flash 2025-10-07 14:54:28 -05:00
J. Nick Koston
7bc1f23d6c Merge branch 'dev' into jesserockz-2025-457 2025-10-07 14:52:47 -05:00
J. Nick Koston
9cecbee33a revise 2025-10-07 14:44:38 -05:00
J. Nick Koston
03884d05b4 fix test 2025-10-07 14:34:03 -05:00
dependabot[bot]
3f3bce7ef4 Bump ruff from 0.13.3 to 0.14.0 (#11107)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-10-07 19:27:21 +00:00
J. Nick Koston
0fa47e3bf5 Merge branch 'dev' into jesserockz-2025-457 2025-10-07 14:25:18 -05:00
Jesse Hills
0acc58d5a1 [core] Update helpers for new auto load functionality (#11097) 2025-10-07 14:24:28 -05:00
dependabot[bot]
0b4ef0fea2 Bump github/codeql-action from 3.30.6 to 4.30.7 (#11109)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-07 14:22:36 -05:00
dependabot[bot]
a067bdb769 Bump aioesphomeapi from 41.11.0 to 41.12.0 (#11108)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-07 14:21:35 -05:00
J. Nick Koston
a159e4762a Merge branch 'dev' into api_size_limits 2025-10-05 22:29:00 -05:00
3270 changed files with 23621 additions and 14901 deletions

View File

@@ -186,6 +186,11 @@ This document provides essential context for AI models interacting with this pro
└── components/[component]/ # Component-specific tests
```
Run them using `script/test_build_components`. Use `-c <component>` to test specific components and `-t <target>` for specific platforms.
* **Testing All Components Together:** To verify that all components can be tested together without ID conflicts or configuration issues, use:
```bash
./script/test_component_grouping.py -e config --all
```
This tests all components in a single build to catch conflicts that might not appear when testing components individually. Use `-e config` for fast configuration validation, or `-e compile` for full compilation testing.
* **Debugging and Troubleshooting:**
* **Debug Tools:**
- `esphome config <file>.yaml` to validate configuration.
@@ -216,6 +221,146 @@ This document provides essential context for AI models interacting with this pro
* **Component Development:** Keep dependencies minimal, provide clear error messages, and write comprehensive docstrings and tests.
* **Code Generation:** Generate minimal and efficient C++ code. Validate all user inputs thoroughly. Support multiple platform variations.
* **Configuration Design:** Aim for simplicity with sensible defaults, while allowing for advanced customization.
* **Embedded Systems Optimization:** ESPHome targets resource-constrained microcontrollers. Be mindful of flash size and RAM usage.
**STL Container Guidelines:**
ESPHome runs on embedded systems with limited resources. Choose containers carefully:
1. **Compile-time-known sizes:** Use `std::array` instead of `std::vector` when size is known at compile time.
```cpp
// Bad - generates STL realloc code
std::vector<int> values;
// Good - no dynamic allocation
std::array<int, MAX_VALUES> values;
```
Use `cg.add_define("MAX_VALUES", count)` to set the size from Python configuration.
**For byte buffers:** Avoid `std::vector<uint8_t>` unless the buffer needs to grow. Use `std::unique_ptr<uint8_t[]>` instead.
> **Note:** `std::unique_ptr<uint8_t[]>` does **not** provide bounds checking or iterator support like `std::vector<uint8_t>`. Use it only when you do not need these features and want minimal overhead.
```cpp
// Bad - STL overhead for simple byte buffer
std::vector<uint8_t> buffer;
buffer.resize(256);
// Good - minimal overhead, single allocation
std::unique_ptr<uint8_t[]> buffer = std::make_unique<uint8_t[]>(256);
// Or if size is constant:
std::array<uint8_t, 256> buffer;
```
2. **Compile-time-known fixed sizes with vector-like API:** Use `StaticVector` from `esphome/core/helpers.h` for fixed-size stack allocation with `push_back()` interface.
```cpp
// Bad - generates STL realloc code (_M_realloc_insert)
std::vector<ServiceRecord> services;
services.reserve(5); // Still includes reallocation machinery
// Good - compile-time fixed size, stack allocated, no reallocation machinery
StaticVector<ServiceRecord, MAX_SERVICES> services; // Allocates all MAX_SERVICES on stack
services.push_back(record1); // Tracks count but all slots allocated
```
Use `cg.add_define("MAX_SERVICES", count)` to set the size from Python configuration.
Like `std::array` but with vector-like API (`push_back()`, `size()`) and no STL reallocation code.
3. **Runtime-known sizes:** Use `FixedVector` from `esphome/core/helpers.h` when the size is only known at runtime initialization.
```cpp
// Bad - generates STL realloc code (_M_realloc_insert)
std::vector<TxtRecord> txt_records;
txt_records.reserve(5); // Still includes reallocation machinery
// Good - runtime size, single allocation, no reallocation machinery
FixedVector<TxtRecord> txt_records;
txt_records.init(record_count); // Initialize with exact size at runtime
```
**Benefits:**
- Eliminates `_M_realloc_insert`, `_M_default_append` template instantiations (saves 200-500 bytes per instance)
- Single allocation, no upper bound needed
- No reallocation overhead
- Compatible with protobuf code generation when using `[(fixed_vector) = true]` option
4. **Small datasets (1-16 elements):** Use `std::vector` or `std::array` with simple structs instead of `std::map`/`std::set`/`std::unordered_map`.
```cpp
// Bad - 2KB+ overhead for red-black tree/hash table
std::map<std::string, int> small_lookup;
std::unordered_map<int, std::string> tiny_map;
// Good - simple struct with linear search (std::vector is fine)
struct LookupEntry {
const char *key;
int value;
};
std::vector<LookupEntry> small_lookup = {
{"key1", 10},
{"key2", 20},
{"key3", 30},
};
// Or std::array if size is compile-time constant:
// std::array<LookupEntry, 3> small_lookup = {{ ... }};
```
Linear search on small datasets (1-16 elements) is often faster than hashing/tree overhead, but this depends on lookup frequency and access patterns. For frequent lookups in hot code paths, the O(1) vs O(n) complexity difference may still matter even for small datasets. `std::vector` with simple structs is usually fine—it's the heavy containers (`map`, `set`, `unordered_map`) that should be avoided for small datasets unless profiling shows otherwise.
5. **Detection:** Look for these patterns in compiler output:
- Large code sections with STL symbols (vector, map, set)
- `alloc`, `realloc`, `dealloc` in symbol names
- `_M_realloc_insert`, `_M_default_append` (vector reallocation)
- Red-black tree code (`rb_tree`, `_Rb_tree`)
- Hash table infrastructure (`unordered_map`, `hash`)
**When to optimize:**
- Core components (API, network, logger)
- Widely-used components (mdns, wifi, ble)
- Components causing flash size complaints
**When not to optimize:**
- Single-use niche components
- Code where readability matters more than bytes
- Already using appropriate containers
* **State Management:** Use `CORE.data` for component state that needs to persist during configuration generation. Avoid module-level mutable globals.
**Bad Pattern (Module-Level Globals):**
```python
# Don't do this - state persists between compilation runs
_component_state = []
_use_feature = None
def enable_feature():
global _use_feature
_use_feature = True
```
**Good Pattern (CORE.data with Helpers):**
```python
from esphome.core import CORE
# Keys for CORE.data storage
COMPONENT_STATE_KEY = "my_component_state"
USE_FEATURE_KEY = "my_component_use_feature"
def _get_component_state() -> list:
"""Get component state from CORE.data."""
return CORE.data.setdefault(COMPONENT_STATE_KEY, [])
def _get_use_feature() -> bool | None:
"""Get feature flag from CORE.data."""
return CORE.data.get(USE_FEATURE_KEY)
def _set_use_feature(value: bool) -> None:
"""Set feature flag in CORE.data."""
CORE.data[USE_FEATURE_KEY] = value
def enable_feature():
_set_use_feature(True)
```
**Why this matters:**
- Module-level globals persist between compilation runs if the dashboard doesn't fork/exec
- `CORE.data` automatically clears between runs
- Typed helper functions provide better IDE support and maintainability
- Encapsulation makes state management explicit and testable
* **Security:** Be mindful of security when making changes to the API, web server, or any other network-related code. Do not hardcode secrets or keys.

View File

@@ -1 +1 @@
ab49c22900dd39c004623e450a1076b111d6741f31967a637ab6e0e3dd2e753e
d7693a1e996cacd4a3d1c9a16336799c2a8cc3db02e4e74084151ce964581248

View File

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

View File

@@ -114,8 +114,7 @@ jobs:
matrix:
python-version:
- "3.11"
- "3.12"
- "3.13"
- "3.14"
os:
- ubuntu-latest
- macOS-latest
@@ -124,13 +123,9 @@ jobs:
# Minimize CI resource usage
# by only running the Python version
# version used for docker images on Windows and macOS
- python-version: "3.13"
- python-version: "3.14"
os: windows-latest
- python-version: "3.12"
os: windows-latest
- python-version: "3.13"
os: macOS-latest
- python-version: "3.12"
- python-version: "3.14"
os: macOS-latest
runs-on: ${{ matrix.os }}
needs:
@@ -177,7 +172,10 @@ jobs:
clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
python-linters: ${{ steps.determine.outputs.python-linters }}
changed-components: ${{ steps.determine.outputs.changed-components }}
changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }}
directly-changed-components-with-tests: ${{ steps.determine.outputs.directly-changed-components-with-tests }}
component-test-count: ${{ steps.determine.outputs.component-test-count }}
memory_impact: ${{ steps.determine.outputs.memory-impact }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -204,7 +202,10 @@ jobs:
echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT
echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT
echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT
echo "changed-components-with-tests=$(echo "$output" | jq -c '.changed_components_with_tests')" >> $GITHUB_OUTPUT
echo "directly-changed-components-with-tests=$(echo "$output" | jq -c '.directly_changed_components_with_tests')" >> $GITHUB_OUTPUT
echo "component-test-count=$(echo "$output" | jq -r '.component_test_count')" >> $GITHUB_OUTPUT
echo "memory-impact=$(echo "$output" | jq -c '.memory_impact')" >> $GITHUB_OUTPUT
integration-tests:
name: Run integration tests
@@ -356,79 +357,73 @@ jobs:
# yamllint disable-line rule:line-length
if: always()
test-build-components:
name: Component test ${{ matrix.file }}
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0 && fromJSON(needs.determine-jobs.outputs.component-test-count) < 100
strategy:
fail-fast: false
max-parallel: 2
matrix:
file: ${{ fromJson(needs.determine-jobs.outputs.changed-components) }}
steps:
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install libsdl2-dev
- name: Check out code from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: test_build_components -e config -c ${{ matrix.file }}
run: |
. venv/bin/activate
./script/test_build_components -e config -c ${{ matrix.file }}
- name: test_build_components -e compile -c ${{ matrix.file }}
run: |
. venv/bin/activate
./script/test_build_components -e compile -c ${{ matrix.file }}
test-build-components-splitter:
name: Split components for testing into 20 groups maximum
name: Split components for intelligent grouping (40 weighted per batch)
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) >= 100
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0
outputs:
matrix: ${{ steps.split.outputs.components }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Split components into 20 groups
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Split components intelligently based on bus configurations
id: split
run: |
components=$(echo '${{ needs.determine-jobs.outputs.changed-components }}' | jq -c '.[]' | shuf | jq -s -c '[_nwise(20) | join(" ")]')
echo "components=$components" >> $GITHUB_OUTPUT
. venv/bin/activate
# Use intelligent splitter that groups components with same bus configs
components='${{ needs.determine-jobs.outputs.changed-components-with-tests }}'
# Only isolate directly changed components when targeting dev branch
# For beta/release branches, group everything for faster CI
if [[ "${{ github.base_ref }}" == beta* ]] || [[ "${{ github.base_ref }}" == release* ]]; then
directly_changed='[]'
echo "Target branch: ${{ github.base_ref }} - grouping all components"
else
directly_changed='${{ needs.determine-jobs.outputs.directly-changed-components-with-tests }}'
echo "Target branch: ${{ github.base_ref }} - isolating directly changed components"
fi
echo "Splitting components intelligently..."
output=$(python3 script/split_components_for_ci.py --components "$components" --directly-changed "$directly_changed" --batch-size 40 --output github)
echo "$output" >> $GITHUB_OUTPUT
test-build-components-split:
name: Test split components
name: Test components batch (${{ matrix.components }})
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
- test-build-components-splitter
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) >= 100
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0
strategy:
fail-fast: false
max-parallel: 4
max-parallel: ${{ (startsWith(github.base_ref, 'beta') || startsWith(github.base_ref, 'release')) && 8 || 4 }}
matrix:
components: ${{ fromJson(needs.test-build-components-splitter.outputs.matrix) }}
steps:
- name: Show disk space
run: |
echo "Available disk space:"
df -h
- name: List components
run: echo ${{ matrix.components }}
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install libsdl2-dev
- name: Cache apt packages
uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.5.3
with:
packages: libsdl2-dev
version: 1.0
- name: Check out code from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -437,27 +432,83 @@ jobs:
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Validate config
- name: Validate and compile components with intelligent grouping
run: |
. venv/bin/activate
for component in ${{ matrix.components }}; do
./script/test_build_components -e config -c $component
done
- name: Compile config
run: |
. venv/bin/activate
mkdir build_cache
export PLATFORMIO_BUILD_CACHE_DIR=$PWD/build_cache
for component in ${{ matrix.components }}; do
./script/test_build_components -e compile -c $component
done
# Check if /mnt has more free space than / before bind mounting
# Extract available space in KB for comparison
root_avail=$(df -k / | awk 'NR==2 {print $4}')
mnt_avail=$(df -k /mnt 2>/dev/null | awk 'NR==2 {print $4}')
echo "Available space: / has ${root_avail}KB, /mnt has ${mnt_avail}KB"
# Only use /mnt if it has more space than /
if [ -n "$mnt_avail" ] && [ "$mnt_avail" -gt "$root_avail" ]; then
echo "Using /mnt for build files (more space available)"
# Bind mount PlatformIO directory to /mnt (tools, packages, build cache all go there)
sudo mkdir -p /mnt/platformio
sudo chown $USER:$USER /mnt/platformio
mkdir -p ~/.platformio
sudo mount --bind /mnt/platformio ~/.platformio
# Bind mount test build directory to /mnt
sudo mkdir -p /mnt/test_build_components_build
sudo chown $USER:$USER /mnt/test_build_components_build
mkdir -p tests/test_build_components/build
sudo mount --bind /mnt/test_build_components_build tests/test_build_components/build
else
echo "Using / for build files (more space available than /mnt or /mnt unavailable)"
fi
# Convert space-separated components to comma-separated for Python script
components_csv=$(echo "${{ matrix.components }}" | tr ' ' ',')
# Only isolate directly changed components when targeting dev branch
# For beta/release branches, group everything for faster CI
#
# WHY ISOLATE DIRECTLY CHANGED COMPONENTS?
# - Isolated tests run WITHOUT --testing-mode, enabling full validation
# - This catches pin conflicts and other issues in directly changed code
# - Grouped tests use --testing-mode to allow config merging (disables some checks)
# - Dependencies are safe to group since they weren't modified in this PR
if [[ "${{ github.base_ref }}" == beta* ]] || [[ "${{ github.base_ref }}" == release* ]]; then
directly_changed_csv=""
echo "Testing components: $components_csv"
echo "Target branch: ${{ github.base_ref }} - grouping all components"
else
directly_changed_csv=$(echo '${{ needs.determine-jobs.outputs.directly-changed-components-with-tests }}' | jq -r 'join(",")')
echo "Testing components: $components_csv"
echo "Target branch: ${{ github.base_ref }} - isolating directly changed components: $directly_changed_csv"
fi
echo ""
# Show disk space before validation (after bind mounts setup)
echo "Disk space before config validation:"
df -h
echo ""
# Run config validation with grouping and isolation
python3 script/test_build_components.py -e config -c "$components_csv" -f --isolate "$directly_changed_csv"
echo ""
echo "Config validation passed! Starting compilation..."
echo ""
# Show disk space before compilation
echo "Disk space before compilation:"
df -h
echo ""
# Run compilation with grouping and isolation
python3 script/test_build_components.py -e compile -c "$components_csv" -f --isolate "$directly_changed_csv"
pre-commit-ci-lite:
name: pre-commit.ci lite
runs-on: ubuntu-latest
needs:
- common
if: github.event_name == 'pull_request' && github.base_ref != 'beta' && github.base_ref != 'release'
if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release')
steps:
- name: Check out code from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -472,6 +523,292 @@ jobs:
- uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0
if: always()
memory-impact-target-branch:
name: Build target branch for memory impact
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.memory_impact).should_run == 'true'
outputs:
ram_usage: ${{ steps.extract.outputs.ram_usage }}
flash_usage: ${{ steps.extract.outputs.flash_usage }}
cache_hit: ${{ steps.cache-memory-analysis.outputs.cache-hit }}
skip: ${{ steps.check-script.outputs.skip }}
steps:
- name: Check out target branch
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
ref: ${{ github.base_ref }}
# Check if memory impact extraction script exists on target branch
# If not, skip the analysis (this handles older branches that don't have the feature)
- name: Check for memory impact script
id: check-script
run: |
if [ -f "script/ci_memory_impact_extract.py" ]; then
echo "skip=false" >> $GITHUB_OUTPUT
else
echo "skip=true" >> $GITHUB_OUTPUT
echo "::warning::ci_memory_impact_extract.py not found on target branch, skipping memory impact analysis"
fi
# All remaining steps only run if script exists
- name: Generate cache key
id: cache-key
if: steps.check-script.outputs.skip != 'true'
run: |
# Get the commit SHA of the target branch
target_sha=$(git rev-parse HEAD)
# Hash the build infrastructure files (all files that affect build/analysis)
infra_hash=$(cat \
script/test_build_components.py \
script/ci_memory_impact_extract.py \
script/analyze_component_buses.py \
script/merge_component_configs.py \
script/ci_helpers.py \
.github/workflows/ci.yml \
| sha256sum | cut -d' ' -f1)
# Get platform and components from job inputs
platform="${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}"
components='${{ toJSON(fromJSON(needs.determine-jobs.outputs.memory_impact).components) }}'
components_hash=$(echo "$components" | sha256sum | cut -d' ' -f1)
# Combine into cache key
cache_key="memory-analysis-target-${target_sha}-${infra_hash}-${platform}-${components_hash}"
echo "cache-key=${cache_key}" >> $GITHUB_OUTPUT
echo "Cache key: ${cache_key}"
- name: Restore cached memory analysis
id: cache-memory-analysis
if: steps.check-script.outputs.skip != 'true'
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: memory-analysis-target.json
key: ${{ steps.cache-key.outputs.cache-key }}
- name: Cache status
if: steps.check-script.outputs.skip != 'true'
run: |
if [ "${{ steps.cache-memory-analysis.outputs.cache-hit }}" == "true" ]; then
echo "✓ Cache hit! Using cached memory analysis results."
echo " Skipping build step to save time."
else
echo "✗ Cache miss. Will build and analyze memory usage."
fi
- name: Restore Python
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache platformio
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.platformio
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
- name: Build, compile, and analyze memory
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
id: build
run: |
. venv/bin/activate
components='${{ toJSON(fromJSON(needs.determine-jobs.outputs.memory_impact).components) }}'
platform="${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}"
echo "Building with test_build_components.py for $platform with components:"
echo "$components" | jq -r '.[]' | sed 's/^/ - /'
# Use test_build_components.py which handles grouping automatically
# Pass components as comma-separated list
component_list=$(echo "$components" | jq -r 'join(",")')
echo "Compiling with test_build_components.py..."
# Run build and extract memory with auto-detection of build directory for detailed analysis
# Use tee to show output in CI while also piping to extraction script
python script/test_build_components.py \
-e compile \
-c "$component_list" \
-t "$platform" 2>&1 | \
tee /dev/stderr | \
python script/ci_memory_impact_extract.py \
--output-env \
--output-json memory-analysis-target.json
- name: Save memory analysis to cache
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success'
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: memory-analysis-target.json
key: ${{ steps.cache-key.outputs.cache-key }}
- name: Extract memory usage for outputs
id: extract
if: steps.check-script.outputs.skip != 'true'
run: |
if [ -f memory-analysis-target.json ]; then
ram=$(jq -r '.ram_bytes' memory-analysis-target.json)
flash=$(jq -r '.flash_bytes' memory-analysis-target.json)
echo "ram_usage=${ram}" >> $GITHUB_OUTPUT
echo "flash_usage=${flash}" >> $GITHUB_OUTPUT
echo "RAM: ${ram} bytes, Flash: ${flash} bytes"
else
echo "Error: memory-analysis-target.json not found"
exit 1
fi
- name: Upload memory analysis JSON
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: memory-analysis-target
path: memory-analysis-target.json
if-no-files-found: warn
retention-days: 1
memory-impact-pr-branch:
name: Build PR branch for memory impact
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.memory_impact).should_run == 'true'
outputs:
ram_usage: ${{ steps.extract.outputs.ram_usage }}
flash_usage: ${{ steps.extract.outputs.flash_usage }}
steps:
- name: Check out PR branch
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache platformio
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.platformio
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
- name: Build, compile, and analyze memory
id: extract
run: |
. venv/bin/activate
components='${{ toJSON(fromJSON(needs.determine-jobs.outputs.memory_impact).components) }}'
platform="${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}"
echo "Building with test_build_components.py for $platform with components:"
echo "$components" | jq -r '.[]' | sed 's/^/ - /'
# Use test_build_components.py which handles grouping automatically
# Pass components as comma-separated list
component_list=$(echo "$components" | jq -r 'join(",")')
echo "Compiling with test_build_components.py..."
# Run build and extract memory with auto-detection of build directory for detailed analysis
# Use tee to show output in CI while also piping to extraction script
python script/test_build_components.py \
-e compile \
-c "$component_list" \
-t "$platform" 2>&1 | \
tee /dev/stderr | \
python script/ci_memory_impact_extract.py \
--output-env \
--output-json memory-analysis-pr.json
- name: Upload memory analysis JSON
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: memory-analysis-pr
path: memory-analysis-pr.json
if-no-files-found: warn
retention-days: 1
memory-impact-comment:
name: Comment memory impact
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
- memory-impact-target-branch
- memory-impact-pr-branch
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.memory_impact).should_run == 'true' && needs.memory-impact-target-branch.outputs.skip != 'true'
permissions:
contents: read
pull-requests: write
steps:
- name: Check out code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Download target analysis JSON
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: memory-analysis-target
path: ./memory-analysis
continue-on-error: true
- name: Download PR analysis JSON
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: memory-analysis-pr
path: ./memory-analysis
continue-on-error: true
- name: Post or update PR comment
env:
GH_TOKEN: ${{ github.token }}
COMPONENTS: ${{ toJSON(fromJSON(needs.determine-jobs.outputs.memory_impact).components) }}
PLATFORM: ${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}
TARGET_RAM: ${{ needs.memory-impact-target-branch.outputs.ram_usage }}
TARGET_FLASH: ${{ needs.memory-impact-target-branch.outputs.flash_usage }}
PR_RAM: ${{ needs.memory-impact-pr-branch.outputs.ram_usage }}
PR_FLASH: ${{ needs.memory-impact-pr-branch.outputs.flash_usage }}
TARGET_CACHE_HIT: ${{ needs.memory-impact-target-branch.outputs.cache_hit }}
run: |
. venv/bin/activate
# Check if analysis JSON files exist
target_json_arg=""
pr_json_arg=""
if [ -f ./memory-analysis/memory-analysis-target.json ]; then
echo "Found target analysis JSON"
target_json_arg="--target-json ./memory-analysis/memory-analysis-target.json"
else
echo "No target analysis JSON found"
fi
if [ -f ./memory-analysis/memory-analysis-pr.json ]; then
echo "Found PR analysis JSON"
pr_json_arg="--pr-json ./memory-analysis/memory-analysis-pr.json"
else
echo "No PR analysis JSON found"
fi
# Add cache flag if target was cached
cache_flag=""
if [ "$TARGET_CACHE_HIT" == "true" ]; then
cache_flag="--target-cache-hit"
fi
python script/ci_memory_impact_comment.py \
--pr-number "${{ github.event.pull_request.number }}" \
--components "$COMPONENTS" \
--platform "$PLATFORM" \
--target-ram "$TARGET_RAM" \
--target-flash "$TARGET_FLASH" \
--pr-ram "$PR_RAM" \
--pr-flash "$PR_FLASH" \
$target_json_arg \
$pr_json_arg \
$cache_flag
ci-status:
name: CI Status
runs-on: ubuntu-24.04
@@ -483,10 +820,12 @@ jobs:
- integration-tests
- clang-tidy
- determine-jobs
- test-build-components
- test-build-components-splitter
- test-build-components-split
- pre-commit-ci-lite
- memory-impact-target-branch
- memory-impact-pr-branch
- memory-impact-comment
if: always()
steps:
- name: Success

View File

@@ -58,7 +58,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6
uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -86,6 +86,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6
uses: github/codeql-action/analyze@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
with:
category: "/language:${{matrix.language}}"

View File

@@ -23,7 +23,7 @@ jobs:
with:
debug-only: ${{ github.ref != 'refs/heads/dev' }} # Dry-run when not run on dev branch
remove-stale-when-updated: true
operations-per-run: 150
operations-per-run: 400
# The 90 day stale policy for PRs
# - PRs

View File

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

View File

@@ -62,6 +62,7 @@ esphome/components/bedjet/fan/* @jhansche
esphome/components/bedjet/sensor/* @javawizard @jhansche
esphome/components/beken_spi_led_strip/* @Mat931
esphome/components/bh1750/* @OttoWinter
esphome/components/bh1900nux/* @B48D81EFCC
esphome/components/binary_sensor/* @esphome/core
esphome/components/bk72xx/* @kuba2k2
esphome/components/bl0906/* @athom-tech @jesserockz @tarontop
@@ -69,6 +70,7 @@ esphome/components/bl0939/* @ziceva
esphome/components/bl0940/* @dan-s-github @tobias-
esphome/components/bl0942/* @dbuezas @dwmw2
esphome/components/ble_client/* @buxtronix @clydebarrow
esphome/components/ble_nus/* @tomaszduda23
esphome/components/bluetooth_proxy/* @bdraco @jesserockz
esphome/components/bme280_base/* @esphome/core
esphome/components/bme280_spi/* @apbodrov
@@ -139,6 +141,7 @@ esphome/components/ens160_base/* @latonita @vincentscode
esphome/components/ens160_i2c/* @latonita
esphome/components/ens160_spi/* @latonita
esphome/components/ens210/* @itn3rd77
esphome/components/epaper_spi/* @esphome/core
esphome/components/es7210/* @kahrendt
esphome/components/es7243e/* @kbx81
esphome/components/es8156/* @kbx81
@@ -429,6 +432,7 @@ esphome/components/speaker/media_player/* @kahrendt @synesthesiam
esphome/components/spi/* @clydebarrow @esphome/core
esphome/components/spi_device/* @clydebarrow
esphome/components/spi_led_strip/* @clydebarrow
esphome/components/split_buffer/* @jesserockz
esphome/components/sprinkler/* @kbx81
esphome/components/sps30/* @martgras
esphome/components/ssd1322_base/* @kbx81

View File

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

View File

@@ -117,6 +117,17 @@ class Purpose(StrEnum):
LOGGING = "logging"
class PortType(StrEnum):
SERIAL = "SERIAL"
NETWORK = "NETWORK"
MQTT = "MQTT"
MQTTIP = "MQTTIP"
# Magic MQTT port types that require special handling
_MQTT_PORT_TYPES = frozenset({PortType.MQTT, PortType.MQTTIP})
def _resolve_with_cache(address: str, purpose: Purpose) -> list[str]:
"""Resolve an address using cache if available, otherwise return the address itself."""
if CORE.address_cache and (cached := CORE.address_cache.get_addresses(address)):
@@ -174,7 +185,9 @@ def choose_upload_log_host(
else:
resolved.append(device)
if not resolved:
_LOGGER.error("All specified devices: %s could not be resolved.", defaults)
raise EsphomeError(
f"All specified devices {defaults} could not be resolved. Is the device connected to the network?"
)
return resolved
# No devices specified, show interactive chooser
@@ -268,8 +281,10 @@ def has_ip_address() -> bool:
def has_resolvable_address() -> bool:
"""Check if CORE.address is resolvable (via mDNS or is an IP address)."""
return has_mdns() or has_ip_address()
"""Check if CORE.address is resolvable (via mDNS, DNS, or is an IP address)."""
# Any address (IP, mDNS hostname, or regular DNS hostname) is resolvable
# The resolve_ip_address() function in helpers.py handles all types via AsyncResolver
return CORE.address is not None
def mqtt_get_ip(config: ConfigType, username: str, password: str, client_id: str):
@@ -278,16 +293,67 @@ def mqtt_get_ip(config: ConfigType, username: str, password: str, client_id: str
return mqtt.get_esphome_device_ip(config, username, password, client_id)
_PORT_TO_PORT_TYPE = {
"MQTT": "MQTT",
"MQTTIP": "MQTTIP",
}
def _resolve_network_devices(
devices: list[str], config: ConfigType, args: ArgsProtocol
) -> list[str]:
"""Resolve device list, converting MQTT magic strings to actual IP addresses.
This function filters the devices list to:
- Replace MQTT/MQTTIP magic strings with actual IP addresses via MQTT lookup
- Deduplicate addresses while preserving order
- Only resolve MQTT once even if multiple MQTT strings are present
- If MQTT resolution fails, log a warning and continue with other devices
Args:
devices: List of device identifiers (IPs, hostnames, or magic strings)
config: ESPHome configuration
args: Command-line arguments containing MQTT credentials
Returns:
List of network addresses suitable for connection attempts
"""
network_devices: list[str] = []
mqtt_resolved: bool = False
for device in devices:
port_type = get_port_type(device)
if port_type in _MQTT_PORT_TYPES:
# Only resolve MQTT once, even if multiple MQTT entries
if not mqtt_resolved:
try:
mqtt_ips = mqtt_get_ip(
config, args.username, args.password, args.client_id
)
network_devices.extend(mqtt_ips)
except EsphomeError as err:
_LOGGER.warning(
"MQTT IP discovery failed (%s), will try other devices if available",
err,
)
mqtt_resolved = True
elif device not in network_devices:
# Regular network address or IP - add if not already present
network_devices.append(device)
return network_devices
def get_port_type(port: str) -> str:
def get_port_type(port: str) -> PortType:
"""Determine the type of port/device identifier.
Returns:
PortType.SERIAL for serial ports (/dev/ttyUSB0, COM1, etc.)
PortType.MQTT for MQTT logging
PortType.MQTTIP for MQTT IP lookup
PortType.NETWORK for IP addresses, hostnames, or mDNS names
"""
if port.startswith("/") or port.startswith("COM"):
return "SERIAL"
return _PORT_TO_PORT_TYPE.get(port, "NETWORK")
return PortType.SERIAL
if port == "MQTT":
return PortType.MQTT
if port == "MQTTIP":
return PortType.MQTTIP
return PortType.NETWORK
def run_miniterm(config: ConfigType, port: str, args) -> int:
@@ -402,7 +468,9 @@ def write_cpp_file() -> int:
def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
from esphome import platformio_api
_LOGGER.info("Compiling app...")
# NOTE: "Build path:" format is parsed by script/ci_memory_impact_extract.py
# If you change this format, update the regex in that script as well
_LOGGER.info("Compiling app... Build path: %s", CORE.build_path)
rc = platformio_api.run_compile(config, CORE.verbose)
if rc != 0:
return rc
@@ -487,7 +555,7 @@ def upload_using_platformio(config: ConfigType, port: str):
def check_permissions(port: str):
if os.name == "posix" and get_port_type(port) == "SERIAL":
if os.name == "posix" and get_port_type(port) == PortType.SERIAL:
# Check if we can open selected serial port
if not os.access(port, os.F_OK):
raise EsphomeError(
@@ -515,7 +583,7 @@ def upload_program(
except AttributeError:
pass
if get_port_type(host) == "SERIAL":
if get_port_type(host) == PortType.SERIAL:
check_permissions(host)
exit_code = 1
@@ -542,17 +610,16 @@ def upload_program(
from esphome import espota2
remote_port = int(ota_conf[CONF_PORT])
password = ota_conf.get(CONF_PASSWORD, "")
password = ota_conf.get(CONF_PASSWORD)
if getattr(args, "file", None) is not None:
binary = Path(args.file)
else:
binary = CORE.firmware_bin
# MQTT address resolution
if get_port_type(host) in ("MQTT", "MQTTIP"):
devices = mqtt_get_ip(config, args.username, args.password, args.client_id)
# Resolve MQTT magic strings to actual IP addresses
network_devices = _resolve_network_devices(devices, config, args)
return espota2.run_ota(devices, remote_port, password, binary)
return espota2.run_ota(network_devices, remote_port, password, binary)
def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int | None:
@@ -567,32 +634,22 @@ def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int
raise EsphomeError("Logger is not configured!")
port = devices[0]
port_type = get_port_type(port)
if get_port_type(port) == "SERIAL":
if port_type == PortType.SERIAL:
check_permissions(port)
return run_miniterm(config, port, args)
port_type = get_port_type(port)
# Check if we should use API for logging
if has_api():
addresses_to_use: list[str] | None = None
# Resolve MQTT magic strings to actual IP addresses
if has_api() and (
network_devices := _resolve_network_devices(devices, config, args)
):
from esphome.components.api.client import run_logs
if port_type == "NETWORK" and (has_mdns() or is_ip_address(port)):
addresses_to_use = devices
elif port_type in ("NETWORK", "MQTT", "MQTTIP") and has_mqtt_ip_lookup():
# Only use MQTT IP lookup if the first condition didn't match
# (for MQTT/MQTTIP types, or for NETWORK when mdns/ip check fails)
addresses_to_use = mqtt_get_ip(
config, args.username, args.password, args.client_id
)
return run_logs(config, network_devices)
if addresses_to_use is not None:
from esphome.components.api.client import run_logs
return run_logs(config, addresses_to_use)
if port_type in ("NETWORK", "MQTT") and has_mqtt_logging():
if port_type in (PortType.NETWORK, PortType.MQTT) and has_mqtt_logging():
from esphome import mqtt
return mqtt.show_logs(
@@ -1009,6 +1066,12 @@ def parse_args(argv):
action="append",
default=[],
)
options_parser.add_argument(
"--testing-mode",
help="Enable testing mode (disables validation checks for grouped component testing)",
action="store_true",
default=False,
)
parser = argparse.ArgumentParser(
description=f"ESPHome {const.__version__}", parents=[options_parser]
@@ -1278,6 +1341,7 @@ def run_esphome(argv):
args = parse_args(argv)
CORE.dashboard = args.dashboard
CORE.testing_mode = args.testing_mode
# Create address cache from command-line arguments
CORE.address_cache = AddressCache.from_cli_args(

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,502 @@
"""Memory usage analyzer for ESPHome compiled binaries."""
from collections import defaultdict
from dataclasses import dataclass, field
import logging
from pathlib import Path
import re
import subprocess
from typing import TYPE_CHECKING
from .const import (
CORE_SUBCATEGORY_PATTERNS,
DEMANGLED_PATTERNS,
ESPHOME_COMPONENT_PATTERN,
SECTION_TO_ATTR,
SYMBOL_PATTERNS,
)
from .helpers import (
get_component_class_patterns,
get_esphome_components,
map_section_name,
parse_symbol_line,
)
if TYPE_CHECKING:
from esphome.platformio_api import IDEData
_LOGGER = logging.getLogger(__name__)
# GCC global constructor/destructor prefix annotations
_GCC_PREFIX_ANNOTATIONS = {
"_GLOBAL__sub_I_": "global constructor for",
"_GLOBAL__sub_D_": "global destructor for",
}
# GCC optimization suffix pattern (e.g., $isra$0, $part$1, $constprop$2)
_GCC_OPTIMIZATION_SUFFIX_PATTERN = re.compile(r"(\$(?:isra|part|constprop)\$\d+)")
# C++ runtime patterns for categorization
_CPP_RUNTIME_PATTERNS = frozenset(["vtable", "typeinfo", "thunk"])
# libc printf/scanf family base names (used to detect variants like _printf_r, vfprintf, etc.)
_LIBC_PRINTF_SCANF_FAMILY = frozenset(["printf", "fprintf", "sprintf", "scanf"])
# Regex pattern for parsing readelf section headers
# Format: [ #] name type addr off size
_READELF_SECTION_PATTERN = re.compile(
r"\s*\[\s*\d+\]\s+([\.\w]+)\s+\w+\s+[\da-fA-F]+\s+[\da-fA-F]+\s+([\da-fA-F]+)"
)
# Component category prefixes
_COMPONENT_PREFIX_ESPHOME = "[esphome]"
_COMPONENT_PREFIX_EXTERNAL = "[external]"
_COMPONENT_CORE = f"{_COMPONENT_PREFIX_ESPHOME}core"
_COMPONENT_API = f"{_COMPONENT_PREFIX_ESPHOME}api"
# C++ namespace prefixes
_NAMESPACE_ESPHOME = "esphome::"
_NAMESPACE_STD = "std::"
# Type alias for symbol information: (symbol_name, size, component)
SymbolInfoType = tuple[str, int, str]
@dataclass
class MemorySection:
"""Represents a memory section with its symbols."""
name: str
symbols: list[SymbolInfoType] = field(default_factory=list)
total_size: int = 0
@dataclass
class ComponentMemory:
"""Tracks memory usage for a component."""
name: str
text_size: int = 0 # Code in flash
rodata_size: int = 0 # Read-only data in flash
data_size: int = 0 # Initialized data (flash + ram)
bss_size: int = 0 # Uninitialized data (ram only)
symbol_count: int = 0
@property
def flash_total(self) -> int:
"""Total flash usage (text + rodata + data)."""
return self.text_size + self.rodata_size + self.data_size
@property
def ram_total(self) -> int:
"""Total RAM usage (data + bss)."""
return self.data_size + self.bss_size
class MemoryAnalyzer:
"""Analyzes memory usage from ELF files."""
def __init__(
self,
elf_path: str,
objdump_path: str | None = None,
readelf_path: str | None = None,
external_components: set[str] | None = None,
idedata: "IDEData | None" = None,
) -> None:
"""Initialize memory analyzer.
Args:
elf_path: Path to ELF file to analyze
objdump_path: Path to objdump binary (auto-detected from idedata if not provided)
readelf_path: Path to readelf binary (auto-detected from idedata if not provided)
external_components: Set of external component names
idedata: Optional PlatformIO IDEData object to auto-detect toolchain paths
"""
self.elf_path = Path(elf_path)
if not self.elf_path.exists():
raise FileNotFoundError(f"ELF file not found: {elf_path}")
# Auto-detect toolchain paths from idedata if not provided
if idedata is not None and (objdump_path is None or readelf_path is None):
objdump_path = objdump_path or idedata.objdump_path
readelf_path = readelf_path or idedata.readelf_path
_LOGGER.debug("Using toolchain paths from PlatformIO idedata")
self.objdump_path = objdump_path or "objdump"
self.readelf_path = readelf_path or "readelf"
self.external_components = external_components or set()
self.sections: dict[str, MemorySection] = {}
self.components: dict[str, ComponentMemory] = defaultdict(
lambda: ComponentMemory("")
)
self._demangle_cache: dict[str, str] = {}
self._uncategorized_symbols: list[tuple[str, str, int]] = []
self._esphome_core_symbols: list[
tuple[str, str, int]
] = [] # Track core symbols
self._component_symbols: dict[str, list[tuple[str, str, int]]] = defaultdict(
list
) # Track symbols for all components
def analyze(self) -> dict[str, ComponentMemory]:
"""Analyze the ELF file and return component memory usage."""
self._parse_sections()
self._parse_symbols()
self._categorize_symbols()
return dict(self.components)
def _parse_sections(self) -> None:
"""Parse section headers from ELF file."""
result = subprocess.run(
[self.readelf_path, "-S", str(self.elf_path)],
capture_output=True,
text=True,
check=True,
)
# Parse section headers
for line in result.stdout.splitlines():
# Look for section entries
if not (match := _READELF_SECTION_PATTERN.match(line)):
continue
section_name = match.group(1)
size_hex = match.group(2)
size = int(size_hex, 16)
# Map to standard section name
mapped_section = map_section_name(section_name)
if not mapped_section:
continue
if mapped_section not in self.sections:
self.sections[mapped_section] = MemorySection(mapped_section)
self.sections[mapped_section].total_size += size
def _parse_symbols(self) -> None:
"""Parse symbols from ELF file."""
result = subprocess.run(
[self.objdump_path, "-t", str(self.elf_path)],
capture_output=True,
text=True,
check=True,
)
# Track seen addresses to avoid duplicates
seen_addresses: set[str] = set()
for line in result.stdout.splitlines():
if not (symbol_info := parse_symbol_line(line)):
continue
section, name, size, address = symbol_info
# Skip duplicate symbols at the same address (e.g., C1/C2 constructors)
if address in seen_addresses or section not in self.sections:
continue
self.sections[section].symbols.append((name, size, ""))
seen_addresses.add(address)
def _categorize_symbols(self) -> None:
"""Categorize symbols by component."""
# First, collect all unique symbol names for batch demangling
all_symbols = {
symbol_name
for section in self.sections.values()
for symbol_name, _, _ in section.symbols
}
# Batch demangle all symbols at once
self._batch_demangle_symbols(list(all_symbols))
# Now categorize with cached demangled names
for section_name, section in self.sections.items():
for symbol_name, size, _ in section.symbols:
component = self._identify_component(symbol_name)
if component not in self.components:
self.components[component] = ComponentMemory(component)
comp_mem = self.components[component]
comp_mem.symbol_count += 1
# Update the appropriate size attribute based on section
if attr_name := SECTION_TO_ATTR.get(section_name):
setattr(comp_mem, attr_name, getattr(comp_mem, attr_name) + size)
# Track uncategorized symbols
if component == "other" and size > 0:
demangled = self._demangle_symbol(symbol_name)
self._uncategorized_symbols.append((symbol_name, demangled, size))
# Track ESPHome core symbols for detailed analysis
if component == _COMPONENT_CORE and size > 0:
demangled = self._demangle_symbol(symbol_name)
self._esphome_core_symbols.append((symbol_name, demangled, size))
# Track all component symbols for detailed analysis
if size > 0:
demangled = self._demangle_symbol(symbol_name)
self._component_symbols[component].append(
(symbol_name, demangled, size)
)
def _identify_component(self, symbol_name: str) -> str:
"""Identify which component a symbol belongs to."""
# Demangle C++ names if needed
demangled = self._demangle_symbol(symbol_name)
# Check for special component classes first (before namespace pattern)
# This handles cases like esphome::ESPHomeOTAComponent which should map to ota
if _NAMESPACE_ESPHOME in demangled:
# Check for special component classes that include component name in the class
# For example: esphome::ESPHomeOTAComponent -> ota component
for component_name in get_esphome_components():
patterns = get_component_class_patterns(component_name)
if any(pattern in demangled for pattern in patterns):
return f"{_COMPONENT_PREFIX_ESPHOME}{component_name}"
# Check for ESPHome component namespaces
match = ESPHOME_COMPONENT_PATTERN.search(demangled)
if match:
component_name = match.group(1)
# Strip trailing underscore if present (e.g., switch_ -> switch)
component_name = component_name.rstrip("_")
# Check if this is an actual component in the components directory
if component_name in get_esphome_components():
return f"{_COMPONENT_PREFIX_ESPHOME}{component_name}"
# Check if this is a known external component from the config
if component_name in self.external_components:
return f"{_COMPONENT_PREFIX_EXTERNAL}{component_name}"
# Everything else in esphome:: namespace is core
return _COMPONENT_CORE
# Check for esphome core namespace (no component namespace)
if _NAMESPACE_ESPHOME in demangled:
# If no component match found, it's core
return _COMPONENT_CORE
# Check against symbol patterns
for component, patterns in SYMBOL_PATTERNS.items():
if any(pattern in symbol_name for pattern in patterns):
return component
# Check against demangled patterns
for component, patterns in DEMANGLED_PATTERNS.items():
if any(pattern in demangled for pattern in patterns):
return component
# Special cases that need more complex logic
# Check if spi_flash vs spi_driver
if "spi_" in symbol_name or "SPI" in symbol_name:
return "spi_flash" if "spi_flash" in symbol_name else "spi_driver"
# libc special printf variants
if (
symbol_name.startswith("_")
and symbol_name[1:].replace("_r", "").replace("v", "").replace("s", "")
in _LIBC_PRINTF_SCANF_FAMILY
):
return "libc"
# Track uncategorized symbols for analysis
return "other"
def _batch_demangle_symbols(self, symbols: list[str]) -> None:
"""Batch demangle C++ symbol names for efficiency."""
if not symbols:
return
# Try to find the appropriate c++filt for the platform
cppfilt_cmd = "c++filt"
_LOGGER.info("Demangling %d symbols", len(symbols))
_LOGGER.debug("objdump_path = %s", self.objdump_path)
# Check if we have a toolchain-specific c++filt
if self.objdump_path and self.objdump_path != "objdump":
# Replace objdump with c++filt in the path
potential_cppfilt = self.objdump_path.replace("objdump", "c++filt")
_LOGGER.info("Checking for toolchain c++filt at: %s", potential_cppfilt)
if Path(potential_cppfilt).exists():
cppfilt_cmd = potential_cppfilt
_LOGGER.info("✓ Using toolchain c++filt: %s", cppfilt_cmd)
else:
_LOGGER.info(
"✗ Toolchain c++filt not found at %s, using system c++filt",
potential_cppfilt,
)
else:
_LOGGER.info("✗ Using system c++filt (objdump_path=%s)", self.objdump_path)
# Strip GCC optimization suffixes and prefixes before demangling
# Suffixes like $isra$0, $part$0, $constprop$0 confuse c++filt
# Prefixes like _GLOBAL__sub_I_ need to be removed and tracked
symbols_stripped: list[str] = []
symbols_prefixes: list[str] = [] # Track removed prefixes
for symbol in symbols:
# Remove GCC optimization markers
stripped = _GCC_OPTIMIZATION_SUFFIX_PATTERN.sub("", symbol)
# Handle GCC global constructor/initializer prefixes
# _GLOBAL__sub_I_<mangled> -> extract <mangled> for demangling
prefix = ""
for gcc_prefix in _GCC_PREFIX_ANNOTATIONS:
if stripped.startswith(gcc_prefix):
prefix = gcc_prefix
stripped = stripped[len(prefix) :]
break
symbols_stripped.append(stripped)
symbols_prefixes.append(prefix)
try:
# Send all symbols to c++filt at once
result = subprocess.run(
[cppfilt_cmd],
input="\n".join(symbols_stripped),
capture_output=True,
text=True,
check=False,
)
except (subprocess.SubprocessError, OSError, UnicodeDecodeError) as e:
# On error, cache originals
_LOGGER.warning("Failed to batch demangle symbols: %s", e)
for symbol in symbols:
self._demangle_cache[symbol] = symbol
return
if result.returncode != 0:
_LOGGER.warning(
"c++filt exited with code %d: %s",
result.returncode,
result.stderr[:200] if result.stderr else "(no error output)",
)
# Cache originals on failure
for symbol in symbols:
self._demangle_cache[symbol] = symbol
return
# Process demangled output
self._process_demangled_output(
symbols, symbols_stripped, symbols_prefixes, result.stdout, cppfilt_cmd
)
def _process_demangled_output(
self,
symbols: list[str],
symbols_stripped: list[str],
symbols_prefixes: list[str],
demangled_output: str,
cppfilt_cmd: str,
) -> None:
"""Process demangled symbol output and populate cache.
Args:
symbols: Original symbol names
symbols_stripped: Stripped symbol names sent to c++filt
symbols_prefixes: Removed prefixes to restore
demangled_output: Output from c++filt
cppfilt_cmd: Path to c++filt command (for logging)
"""
demangled_lines = demangled_output.strip().split("\n")
failed_count = 0
for original, stripped, prefix, demangled in zip(
symbols, symbols_stripped, symbols_prefixes, demangled_lines
):
# Add back any prefix that was removed
demangled = self._restore_symbol_prefix(prefix, stripped, demangled)
# If we stripped a suffix, add it back to the demangled name for clarity
if original != stripped and not prefix:
demangled = self._restore_symbol_suffix(original, demangled)
self._demangle_cache[original] = demangled
# Log symbols that failed to demangle (stayed the same as stripped version)
if stripped == demangled and stripped.startswith("_Z"):
failed_count += 1
if failed_count <= 5: # Only log first 5 failures
_LOGGER.warning("Failed to demangle: %s", original)
if failed_count == 0:
_LOGGER.info("Successfully demangled all %d symbols", len(symbols))
return
_LOGGER.warning(
"Failed to demangle %d/%d symbols using %s",
failed_count,
len(symbols),
cppfilt_cmd,
)
@staticmethod
def _restore_symbol_prefix(prefix: str, stripped: str, demangled: str) -> str:
"""Restore prefix that was removed before demangling.
Args:
prefix: Prefix that was removed (e.g., "_GLOBAL__sub_I_")
stripped: Stripped symbol name
demangled: Demangled symbol name
Returns:
Demangled name with prefix restored/annotated
"""
if not prefix:
return demangled
# Successfully demangled - add descriptive prefix
if demangled != stripped and (
annotation := _GCC_PREFIX_ANNOTATIONS.get(prefix)
):
return f"[{annotation}: {demangled}]"
# Failed to demangle - restore original prefix
return prefix + demangled
@staticmethod
def _restore_symbol_suffix(original: str, demangled: str) -> str:
"""Restore GCC optimization suffix that was removed before demangling.
Args:
original: Original symbol name with suffix
demangled: Demangled symbol name without suffix
Returns:
Demangled name with suffix annotation
"""
if suffix_match := _GCC_OPTIMIZATION_SUFFIX_PATTERN.search(original):
return f"{demangled} [{suffix_match.group(1)}]"
return demangled
def _demangle_symbol(self, symbol: str) -> str:
"""Get demangled C++ symbol name from cache."""
return self._demangle_cache.get(symbol, symbol)
def _categorize_esphome_core_symbol(self, demangled: str) -> str:
"""Categorize ESPHome core symbols into subcategories."""
# Special patterns that need to be checked separately
if any(pattern in demangled for pattern in _CPP_RUNTIME_PATTERNS):
return "C++ Runtime (vtables/RTTI)"
if demangled.startswith(_NAMESPACE_STD):
return "C++ STL"
# Check against patterns from const.py
for category, patterns in CORE_SUBCATEGORY_PATTERNS.items():
if any(pattern in demangled for pattern in patterns):
return category
return "Other Core"
if __name__ == "__main__":
from .cli import main
main()

View File

@@ -0,0 +1,6 @@
"""Main entry point for running the memory analyzer as a module."""
from .cli import main
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,431 @@
"""CLI interface for memory analysis with report generation."""
from collections import defaultdict
import json
import sys
from . import (
_COMPONENT_API,
_COMPONENT_CORE,
_COMPONENT_PREFIX_ESPHOME,
_COMPONENT_PREFIX_EXTERNAL,
MemoryAnalyzer,
)
class MemoryAnalyzerCLI(MemoryAnalyzer):
"""Memory analyzer with CLI-specific report generation."""
# Column width constants
COL_COMPONENT: int = 29
COL_FLASH_TEXT: int = 14
COL_FLASH_DATA: int = 14
COL_RAM_DATA: int = 12
COL_RAM_BSS: int = 12
COL_TOTAL_FLASH: int = 15
COL_TOTAL_RAM: int = 12
COL_SEPARATOR: int = 3 # " | "
# Core analysis column widths
COL_CORE_SUBCATEGORY: int = 30
COL_CORE_SIZE: int = 12
COL_CORE_COUNT: int = 6
COL_CORE_PERCENT: int = 10
# Calculate table width once at class level
TABLE_WIDTH: int = (
COL_COMPONENT
+ COL_SEPARATOR
+ COL_FLASH_TEXT
+ COL_SEPARATOR
+ COL_FLASH_DATA
+ COL_SEPARATOR
+ COL_RAM_DATA
+ COL_SEPARATOR
+ COL_RAM_BSS
+ COL_SEPARATOR
+ COL_TOTAL_FLASH
+ COL_SEPARATOR
+ COL_TOTAL_RAM
)
@staticmethod
def _make_separator_line(*widths: int) -> str:
"""Create a separator line with given column widths.
Args:
widths: Column widths to create separators for
Returns:
Separator line like "----+---------+-----"
"""
return "-+-".join("-" * width for width in widths)
# Pre-computed separator lines
MAIN_TABLE_SEPARATOR: str = _make_separator_line(
COL_COMPONENT,
COL_FLASH_TEXT,
COL_FLASH_DATA,
COL_RAM_DATA,
COL_RAM_BSS,
COL_TOTAL_FLASH,
COL_TOTAL_RAM,
)
CORE_TABLE_SEPARATOR: str = _make_separator_line(
COL_CORE_SUBCATEGORY,
COL_CORE_SIZE,
COL_CORE_COUNT,
COL_CORE_PERCENT,
)
def generate_report(self, detailed: bool = False) -> str:
"""Generate a formatted memory report."""
components = sorted(
self.components.items(), key=lambda x: x[1].flash_total, reverse=True
)
# Calculate totals
total_flash = sum(c.flash_total for _, c in components)
total_ram = sum(c.ram_total for _, c in components)
# Build report
lines: list[str] = []
lines.append("=" * self.TABLE_WIDTH)
lines.append("Component Memory Analysis".center(self.TABLE_WIDTH))
lines.append("=" * self.TABLE_WIDTH)
lines.append("")
# Main table - fixed column widths
lines.append(
f"{'Component':<{self.COL_COMPONENT}} | {'Flash (text)':>{self.COL_FLASH_TEXT}} | {'Flash (data)':>{self.COL_FLASH_DATA}} | {'RAM (data)':>{self.COL_RAM_DATA}} | {'RAM (bss)':>{self.COL_RAM_BSS}} | {'Total Flash':>{self.COL_TOTAL_FLASH}} | {'Total RAM':>{self.COL_TOTAL_RAM}}"
)
lines.append(self.MAIN_TABLE_SEPARATOR)
for name, mem in components:
if mem.flash_total > 0 or mem.ram_total > 0:
flash_rodata = mem.rodata_size + mem.data_size
lines.append(
f"{name:<{self.COL_COMPONENT}} | {mem.text_size:>{self.COL_FLASH_TEXT - 2},} B | {flash_rodata:>{self.COL_FLASH_DATA - 2},} B | "
f"{mem.data_size:>{self.COL_RAM_DATA - 2},} B | {mem.bss_size:>{self.COL_RAM_BSS - 2},} B | "
f"{mem.flash_total:>{self.COL_TOTAL_FLASH - 2},} B | {mem.ram_total:>{self.COL_TOTAL_RAM - 2},} B"
)
lines.append(self.MAIN_TABLE_SEPARATOR)
lines.append(
f"{'TOTAL':<{self.COL_COMPONENT}} | {' ':>{self.COL_FLASH_TEXT}} | {' ':>{self.COL_FLASH_DATA}} | "
f"{' ':>{self.COL_RAM_DATA}} | {' ':>{self.COL_RAM_BSS}} | "
f"{total_flash:>{self.COL_TOTAL_FLASH - 2},} B | {total_ram:>{self.COL_TOTAL_RAM - 2},} B"
)
# Top consumers
lines.append("")
lines.append("Top Flash Consumers:")
for i, (name, mem) in enumerate(components[:25]):
if mem.flash_total > 0:
percentage = (
(mem.flash_total / total_flash * 100) if total_flash > 0 else 0
)
lines.append(
f"{i + 1}. {name} ({mem.flash_total:,} B) - {percentage:.1f}% of analyzed flash"
)
lines.append("")
lines.append("Top RAM Consumers:")
ram_components = sorted(components, key=lambda x: x[1].ram_total, reverse=True)
for i, (name, mem) in enumerate(ram_components[:25]):
if mem.ram_total > 0:
percentage = (mem.ram_total / total_ram * 100) if total_ram > 0 else 0
lines.append(
f"{i + 1}. {name} ({mem.ram_total:,} B) - {percentage:.1f}% of analyzed RAM"
)
lines.append("")
lines.append(
"Note: This analysis covers symbols in the ELF file. Some runtime allocations may not be included."
)
lines.append("=" * self.TABLE_WIDTH)
# Add ESPHome core detailed analysis if there are core symbols
if self._esphome_core_symbols:
lines.append("")
lines.append("=" * self.TABLE_WIDTH)
lines.append(
f"{_COMPONENT_CORE} Detailed Analysis".center(self.TABLE_WIDTH)
)
lines.append("=" * self.TABLE_WIDTH)
lines.append("")
# Group core symbols by subcategory
core_subcategories: dict[str, list[tuple[str, str, int]]] = defaultdict(
list
)
for symbol, demangled, size in self._esphome_core_symbols:
# Categorize based on demangled name patterns
subcategory = self._categorize_esphome_core_symbol(demangled)
core_subcategories[subcategory].append((symbol, demangled, size))
# Sort subcategories by total size
sorted_subcategories = sorted(
[
(name, symbols, sum(s[2] for s in symbols))
for name, symbols in core_subcategories.items()
],
key=lambda x: x[2],
reverse=True,
)
lines.append(
f"{'Subcategory':<{self.COL_CORE_SUBCATEGORY}} | {'Size':>{self.COL_CORE_SIZE}} | "
f"{'Count':>{self.COL_CORE_COUNT}} | {'% of Core':>{self.COL_CORE_PERCENT}}"
)
lines.append(self.CORE_TABLE_SEPARATOR)
core_total = sum(size for _, _, size in self._esphome_core_symbols)
for subcategory, symbols, total_size in sorted_subcategories:
percentage = (total_size / core_total * 100) if core_total > 0 else 0
lines.append(
f"{subcategory:<{self.COL_CORE_SUBCATEGORY}} | {total_size:>{self.COL_CORE_SIZE - 2},} B | "
f"{len(symbols):>{self.COL_CORE_COUNT}} | {percentage:>{self.COL_CORE_PERCENT - 1}.1f}%"
)
# Top 15 largest core symbols
lines.append("")
lines.append(f"Top 15 Largest {_COMPONENT_CORE} Symbols:")
sorted_core_symbols = sorted(
self._esphome_core_symbols, key=lambda x: x[2], reverse=True
)
for i, (symbol, demangled, size) in enumerate(sorted_core_symbols[:15]):
lines.append(f"{i + 1}. {demangled} ({size:,} B)")
lines.append("=" * self.TABLE_WIDTH)
# Add detailed analysis for top ESPHome and external components
esphome_components = [
(name, mem)
for name, mem in components
if name.startswith(_COMPONENT_PREFIX_ESPHOME) and name != _COMPONENT_CORE
]
external_components = [
(name, mem)
for name, mem in components
if name.startswith(_COMPONENT_PREFIX_EXTERNAL)
]
top_esphome_components = sorted(
esphome_components, key=lambda x: x[1].flash_total, reverse=True
)[:30]
# Include all external components (they're usually important)
top_external_components = sorted(
external_components, key=lambda x: x[1].flash_total, reverse=True
)
# Check if API component exists and ensure it's included
api_component = None
for name, mem in components:
if name == _COMPONENT_API:
api_component = (name, mem)
break
# Combine all components to analyze: top ESPHome + all external + API if not already included
components_to_analyze = list(top_esphome_components) + list(
top_external_components
)
if api_component and api_component not in components_to_analyze:
components_to_analyze.append(api_component)
if components_to_analyze:
for comp_name, comp_mem in components_to_analyze:
if not (comp_symbols := self._component_symbols.get(comp_name, [])):
continue
lines.append("")
lines.append("=" * self.TABLE_WIDTH)
lines.append(f"{comp_name} Detailed Analysis".center(self.TABLE_WIDTH))
lines.append("=" * self.TABLE_WIDTH)
lines.append("")
# Sort symbols by size
sorted_symbols = sorted(comp_symbols, key=lambda x: x[2], reverse=True)
lines.append(f"Total symbols: {len(sorted_symbols)}")
lines.append(f"Total size: {comp_mem.flash_total:,} B")
lines.append("")
# Show all symbols > 100 bytes for better visibility
large_symbols = [
(sym, dem, size) for sym, dem, size in sorted_symbols if size > 100
]
lines.append(
f"{comp_name} Symbols > 100 B ({len(large_symbols)} symbols):"
)
for i, (symbol, demangled, size) in enumerate(large_symbols):
lines.append(f"{i + 1}. {demangled} ({size:,} B)")
lines.append("=" * self.TABLE_WIDTH)
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:
"""Dump uncategorized symbols for analysis."""
# Sort by size descending
sorted_symbols = sorted(
self._uncategorized_symbols, key=lambda x: x[2], reverse=True
)
lines = ["Uncategorized Symbols Analysis", "=" * 80]
lines.append(f"Total uncategorized symbols: {len(sorted_symbols)}")
lines.append(
f"Total uncategorized size: {sum(s[2] for s in sorted_symbols):,} bytes"
)
lines.append("")
lines.append(f"{'Size':>10} | {'Symbol':<60} | Demangled")
lines.append("-" * 10 + "-+-" + "-" * 60 + "-+-" + "-" * 40)
for symbol, demangled, size in sorted_symbols[:100]: # Top 100
demangled_display = (
demangled[:100] if symbol != demangled else "[not demangled]"
)
lines.append(f"{size:>10,} | {symbol[:60]:<60} | {demangled_display}")
if len(sorted_symbols) > 100:
lines.append(f"\n... and {len(sorted_symbols) - 100} more symbols")
content = "\n".join(lines)
if output_file:
with open(output_file, "w", encoding="utf-8") as f:
f.write(content)
else:
print(content)
def analyze_elf(
elf_path: str,
objdump_path: str | None = None,
readelf_path: str | None = None,
detailed: bool = False,
external_components: set[str] | None = None,
) -> str:
"""Analyze an ELF file and return a memory report."""
analyzer = MemoryAnalyzerCLI(
elf_path, objdump_path, readelf_path, external_components
)
analyzer.analyze()
return analyzer.generate_report(detailed)
def main():
"""CLI entrypoint for memory analysis."""
if len(sys.argv) < 2:
print("Usage: python -m esphome.analyze_memory <build_directory>")
print("\nAnalyze memory usage from an ESPHome build directory.")
print("The build directory should contain firmware.elf and idedata will be")
print("loaded from ~/.esphome/.internal/idedata/<device>.json")
print("\nExamples:")
print(" python -m esphome.analyze_memory ~/.esphome/build/my-device")
print(" python -m esphome.analyze_memory .esphome/build/my-device")
print(" python -m esphome.analyze_memory my-device # Short form")
sys.exit(1)
build_dir = sys.argv[1]
# Load build directory
import json
from pathlib import Path
from esphome.platformio_api import IDEData
build_path = Path(build_dir)
# If no path separator in name, assume it's a device name
if "/" not in build_dir and not build_path.is_dir():
# Try current directory first
cwd_path = Path.cwd() / ".esphome" / "build" / build_dir
if cwd_path.is_dir():
build_path = cwd_path
print(f"Using build directory: {build_path}", file=sys.stderr)
else:
# Fall back to home directory
build_path = Path.home() / ".esphome" / "build" / build_dir
print(f"Using build directory: {build_path}", file=sys.stderr)
if not build_path.is_dir():
print(f"Error: {build_path} is not a directory", file=sys.stderr)
sys.exit(1)
# Find firmware.elf
elf_file = None
for elf_candidate in [
build_path / "firmware.elf",
build_path / ".pioenvs" / build_path.name / "firmware.elf",
]:
if elf_candidate.exists():
elf_file = str(elf_candidate)
break
if not elf_file:
print(f"Error: firmware.elf not found in {build_dir}", file=sys.stderr)
sys.exit(1)
# Find idedata.json - check current directory first, then home
device_name = build_path.name
idedata_candidates = [
Path.cwd() / ".esphome" / "idedata" / f"{device_name}.json",
Path.home() / ".esphome" / "idedata" / f"{device_name}.json",
]
idedata = None
for idedata_path in idedata_candidates:
if not idedata_path.exists():
continue
try:
with open(idedata_path, encoding="utf-8") as f:
raw_data = json.load(f)
idedata = IDEData(raw_data)
print(f"Loaded idedata from: {idedata_path}", file=sys.stderr)
break
except (json.JSONDecodeError, OSError) as e:
print(f"Warning: Failed to load idedata: {e}", file=sys.stderr)
if not idedata:
print(
f"Warning: idedata not found (searched {idedata_candidates[0]} and {idedata_candidates[1]})",
file=sys.stderr,
)
analyzer = MemoryAnalyzerCLI(elf_file, idedata=idedata)
analyzer.analyze()
report = analyzer.generate_report()
print(report)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,903 @@
"""Constants for memory analysis symbol pattern matching."""
import re
# Pattern to extract ESPHome component namespaces dynamically
ESPHOME_COMPONENT_PATTERN = re.compile(r"esphome::([a-zA-Z0-9_]+)::")
# Section mapping for ELF file sections
# Maps standard section names to their various platform-specific variants
SECTION_MAPPING = {
".text": frozenset([".text", ".iram"]),
".rodata": frozenset([".rodata"]),
".data": frozenset([".data", ".dram"]),
".bss": frozenset([".bss"]),
}
# Section to ComponentMemory attribute mapping
# Maps section names to the attribute name in ComponentMemory dataclass
SECTION_TO_ATTR = {
".text": "text_size",
".rodata": "rodata_size",
".data": "data_size",
".bss": "bss_size",
}
# Component identification rules
# Symbol patterns: patterns found in raw symbol names
SYMBOL_PATTERNS = {
"freertos": [
"vTask",
"xTask",
"xQueue",
"pvPort",
"vPort",
"uxTask",
"pcTask",
"prvTimerTask",
"prvAddNewTaskToReadyList",
"pxReadyTasksLists",
"prvAddCurrentTaskToDelayedList",
"xEventGroupWaitBits",
"xRingbufferSendFromISR",
"prvSendItemDoneNoSplit",
"prvReceiveGeneric",
"prvSendAcquireGeneric",
"prvCopyItemAllowSplit",
"xEventGroup",
"xRingbuffer",
"prvSend",
"prvReceive",
"prvCopy",
"xPort",
"ulTaskGenericNotifyTake",
"prvIdleTask",
"prvInitialiseNewTask",
"prvIsYieldRequiredSMP",
"prvGetItemByteBuf",
"prvInitializeNewRingbuffer",
"prvAcquireItemNoSplit",
"prvNotifyQueueSetContainer",
"ucStaticTimerQueueStorage",
"eTaskGetState",
"main_task",
"do_system_init_fn",
"xSemaphoreCreateGenericWithCaps",
"vListInsert",
"uxListRemove",
"vRingbufferReturnItem",
"vRingbufferReturnItemFromISR",
"prvCheckItemFitsByteBuffer",
"prvGetCurMaxSizeAllowSplit",
"tick_hook",
"sys_sem_new",
"sys_arch_mbox_fetch",
"sys_arch_sem_wait",
"prvDeleteTCB",
"vQueueDeleteWithCaps",
"vRingbufferDeleteWithCaps",
"vSemaphoreDeleteWithCaps",
"prvCheckItemAvail",
"prvCheckTaskCanBeScheduledSMP",
"prvGetCurMaxSizeNoSplit",
"prvResetNextTaskUnblockTime",
"prvReturnItemByteBuf",
"vApplicationStackOverflowHook",
"vApplicationGetIdleTaskMemory",
"sys_init",
"sys_mbox_new",
"sys_arch_mbox_tryfetch",
],
"xtensa": ["xt_", "_xt_", "xPortEnterCriticalTimeout"],
"heap": ["heap_", "multi_heap"],
"spi_flash": ["spi_flash"],
"rtc": ["rtc_", "rtcio_ll_"],
"gpio_driver": ["gpio_", "pins"],
"uart_driver": ["uart", "_uart", "UART"],
"timer": ["timer_", "esp_timer"],
"peripherals": ["periph_", "periman"],
"network_stack": [
"vj_compress",
"raw_sendto",
"raw_input",
"etharp_",
"icmp_input",
"socket_ipv6",
"ip_napt",
"socket_ipv4_multicast",
"socket_ipv6_multicast",
"netconn_",
"recv_raw",
"accept_function",
"netconn_recv_data",
"netconn_accept",
"netconn_write_vectors_partly",
"netconn_drain",
"raw_connect",
"raw_bind",
"icmp_send_response",
"sockets",
"icmp_dest_unreach",
"inet_chksum_pseudo",
"alloc_socket",
"done_socket",
"set_global_fd_sets",
"inet_chksum_pbuf",
"tryget_socket_unconn_locked",
"tryget_socket_unconn",
"cs_create_ctrl_sock",
"netbuf_alloc",
],
"ipv6_stack": ["nd6_", "ip6_", "mld6_", "icmp6_", "icmp6_input"],
"wifi_stack": [
"ieee80211",
"hostap",
"sta_",
"ap_",
"scan_",
"wifi_",
"wpa_",
"wps_",
"esp_wifi",
"cnx_",
"wpa3_",
"sae_",
"wDev_",
"ic_",
"mac_",
"esf_buf",
"gWpaSm",
"sm_WPA",
"eapol_",
"owe_",
"wifiLowLevelInit",
"s_do_mapping",
"gScanStruct",
"ppSearchTxframe",
"ppMapWaitTxq",
"ppFillAMPDUBar",
"ppCheckTxConnTrafficIdle",
"ppCalTkipMic",
],
"bluetooth": ["bt_", "ble_", "l2c_", "gatt_", "gap_", "hci_", "BT_init"],
"wifi_bt_coex": ["coex"],
"bluetooth_rom": ["r_ble", "r_lld", "r_llc", "r_llm"],
"bluedroid_bt": [
"bluedroid",
"btc_",
"bta_",
"btm_",
"btu_",
"BTM_",
"GATT",
"L2CA_",
"smp_",
"gatts_",
"attp_",
"l2cu_",
"l2cb",
"smp_cb",
"BTA_GATTC_",
"SMP_",
"BTU_",
"BTA_Dm",
"GAP_Ble",
"BT_tx_if",
"host_recv_pkt_cb",
"saved_local_oob_data",
"string_to_bdaddr",
"string_is_bdaddr",
"CalConnectParamTimeout",
"transmit_fragment",
"transmit_data",
"event_command_ready",
"read_command_complete_header",
"parse_read_local_extended_features_response",
"parse_read_local_version_info_response",
"should_request_high",
"btdm_wakeup_request",
"BTA_SetAttributeValue",
"BTA_EnableBluetooth",
"transmit_command_futured",
"transmit_command",
"get_waiting_command",
"make_command",
"transmit_downward",
"host_recv_adv_packet",
"copy_extra_byte_in_db",
"parse_read_local_supported_commands_response",
],
"crypto_math": [
"ecp_",
"bignum_",
"mpi_",
"sswu",
"modp",
"dragonfly_",
"gcm_mult",
"__multiply",
"quorem",
"__mdiff",
"__lshift",
"__mprec_tens",
"ECC_",
"multiprecision_",
"mix_sub_columns",
"sbox",
"gfm2_sbox",
"gfm3_sbox",
"curve_p256",
"curve",
"p_256_init_curve",
"shift_sub_rows",
"rshift",
],
"hw_crypto": ["esp_aes", "esp_sha", "esp_rsa", "esp_bignum", "esp_mpi"],
"libc": [
"printf",
"scanf",
"malloc",
"free",
"memcpy",
"memset",
"strcpy",
"strlen",
"_dtoa",
"_fopen",
"__sfvwrite_r",
"qsort",
"__sf",
"__sflush_r",
"__srefill_r",
"_impure_data",
"_reclaim_reent",
"_open_r",
"strncpy",
"_strtod_l",
"__gethex",
"__hexnan",
"_setenv_r",
"_tzset_unlocked_r",
"__tzcalc_limits",
"select",
"scalbnf",
"strtof",
"strtof_l",
"__d2b",
"__b2d",
"__s2b",
"_Balloc",
"__multadd",
"__lo0bits",
"__atexit0",
"__smakebuf_r",
"__swhatbuf_r",
"_sungetc_r",
"_close_r",
"_link_r",
"_unsetenv_r",
"_rename_r",
"__month_lengths",
"tzinfo",
"__ratio",
"__hi0bits",
"__ulp",
"__any_on",
"__copybits",
"L_shift",
"_fcntl_r",
"_lseek_r",
"_read_r",
"_write_r",
"_unlink_r",
"_fstat_r",
"access",
"fsync",
"tcsetattr",
"tcgetattr",
"tcflush",
"tcdrain",
"__ssrefill_r",
"_stat_r",
"__hexdig_fun",
"__mcmp",
"_fwalk_sglue",
"__fpclassifyf",
"_setlocale_r",
"_mbrtowc_r",
"fcntl",
"__match",
"_lock_close",
"__c$",
"__func__$",
"__FUNCTION__$",
"DAYS_IN_MONTH",
"_DAYS_BEFORE_MONTH",
"CSWTCH$",
"dst$",
"sulp",
],
"string_ops": ["strcmp", "strncmp", "strchr", "strstr", "strtok", "strdup"],
"memory_alloc": ["malloc", "calloc", "realloc", "free", "_sbrk"],
"file_io": [
"fread",
"fwrite",
"fopen",
"fclose",
"fseek",
"ftell",
"fflush",
"s_fd_table",
],
"string_formatting": [
"snprintf",
"vsnprintf",
"sprintf",
"vsprintf",
"sscanf",
"vsscanf",
],
"cpp_anonymous": ["_GLOBAL__N_", "n$"],
"cpp_runtime": ["__cxx", "_ZN", "_ZL", "_ZSt", "__gxx_personality", "_Z16"],
"exception_handling": ["__cxa_", "_Unwind_", "__gcc_personality", "uw_frame_state"],
"static_init": ["_GLOBAL__sub_I_"],
"mdns_lib": ["mdns"],
"phy_radio": [
"phy_",
"rf_",
"chip_",
"register_chipv7",
"pbus_",
"bb_",
"fe_",
"rfcal_",
"ram_rfcal",
"tx_pwctrl",
"rx_chan",
"set_rx_gain",
"set_chan",
"agc_reg",
"ram_txiq",
"ram_txdc",
"ram_gen_rx_gain",
"rx_11b_opt",
"set_rx_sense",
"set_rx_gain_cal",
"set_chan_dig_gain",
"tx_pwctrl_init_cal",
"rfcal_txiq",
"set_tx_gain_table",
"correct_rfpll_offset",
"pll_correct_dcap",
"txiq_cal_init",
"pwdet_sar",
"pwdet_sar2_init",
"ram_iq_est_enable",
"ram_rfpll_set_freq",
"ant_wifirx_cfg",
"ant_btrx_cfg",
"force_txrxoff",
"force_txrx_off",
"tx_paon_set",
"opt_11b_resart",
"rfpll_1p2_opt",
"ram_dc_iq_est",
"ram_start_tx_tone",
"ram_en_pwdet",
"ram_cbw2040_cfg",
"rxdc_est_min",
"i2cmst_reg_init",
"temprature_sens_read",
"ram_restart_cal",
"ram_write_gain_mem",
"ram_wait_rfpll_cal_end",
"txcal_debuge_mode",
"ant_wifitx_cfg",
"reg_init_begin",
],
"wifi_phy_pp": ["pp_", "ppT", "ppR", "ppP", "ppInstall", "ppCalTxAMPDULength"],
"wifi_lmac": ["lmac"],
"wifi_device": ["wdev", "wDev_"],
"power_mgmt": [
"pm_",
"sleep",
"rtc_sleep",
"light_sleep",
"deep_sleep",
"power_down",
"g_pm",
],
"memory_mgmt": [
"mem_",
"memory_",
"tlsf_",
"memp_",
"pbuf_",
"pbuf_alloc",
"pbuf_copy_partial_pbuf",
],
"hal_layer": ["hal_"],
"clock_mgmt": [
"clk_",
"clock_",
"rtc_clk",
"apb_",
"cpu_freq",
"setCpuFrequencyMhz",
],
"cache_mgmt": ["cache"],
"flash_ops": ["flash", "image_load"],
"interrupt_handlers": [
"isr",
"interrupt",
"intr_",
"exc_",
"exception",
"port_IntStack",
],
"wrapper_functions": ["_wrapper"],
"error_handling": ["panic", "abort", "assert", "error_", "fault"],
"authentication": ["auth"],
"ppp_protocol": ["ppp", "ipcp_", "lcp_", "chap_", "LcpEchoCheck"],
"dhcp": ["dhcp", "handle_dhcp"],
"ethernet_phy": [
"emac_",
"eth_phy_",
"phy_tlk110",
"phy_lan87",
"phy_ip101",
"phy_rtl",
"phy_dp83",
"phy_ksz",
"lan87xx_",
"rtl8201_",
"ip101_",
"ksz80xx_",
"jl1101_",
"dp83848_",
"eth_on_state_changed",
],
"threading": ["pthread_", "thread_", "_task_"],
"pthread": ["pthread"],
"synchronization": ["mutex", "semaphore", "spinlock", "portMUX"],
"math_lib": [
"sin",
"cos",
"tan",
"sqrt",
"pow",
"exp",
"log",
"atan",
"asin",
"acos",
"floor",
"ceil",
"fabs",
"round",
],
"random": ["rand", "random", "rng_", "prng"],
"time_lib": [
"time",
"clock",
"gettimeofday",
"settimeofday",
"localtime",
"gmtime",
"mktime",
"strftime",
],
"console_io": ["console_", "uart_tx", "uart_rx", "puts", "putchar", "getchar"],
"rom_functions": ["r_", "rom_"],
"compiler_runtime": [
"__divdi3",
"__udivdi3",
"__moddi3",
"__muldi3",
"__ashldi3",
"__ashrdi3",
"__lshrdi3",
"__cmpdi2",
"__fixdfdi",
"__floatdidf",
],
"libgcc": ["libgcc", "_divdi3", "_udivdi3"],
"boot_startup": ["boot", "start_cpu", "call_start", "startup", "bootloader"],
"bootloader": ["bootloader_", "esp_bootloader"],
"app_framework": ["app_", "initArduino", "setup", "loop", "Update"],
"weak_symbols": ["__weak_"],
"compiler_builtins": ["__builtin_"],
"vfs": ["vfs_", "VFS"],
"esp32_sdk": ["esp32_", "esp32c", "esp32s"],
"usb": ["usb_", "USB", "cdc_", "CDC"],
"i2c_driver": ["i2c_", "I2C"],
"i2s_driver": ["i2s_", "I2S"],
"spi_driver": ["spi_", "SPI"],
"adc_driver": ["adc_", "ADC"],
"dac_driver": ["dac_", "DAC"],
"touch_driver": ["touch_", "TOUCH"],
"pwm_driver": ["pwm_", "PWM", "ledc_", "LEDC"],
"rmt_driver": ["rmt_", "RMT"],
"pcnt_driver": ["pcnt_", "PCNT"],
"can_driver": ["can_", "CAN", "twai_", "TWAI"],
"sdmmc_driver": ["sdmmc_", "SDMMC", "sdcard", "sd_card"],
"temp_sensor": ["temp_sensor", "tsens_"],
"watchdog": ["wdt_", "WDT", "watchdog"],
"brownout": ["brownout", "bod_"],
"ulp": ["ulp_", "ULP"],
"psram": ["psram", "PSRAM", "spiram", "SPIRAM"],
"efuse": ["efuse", "EFUSE"],
"partition": ["partition", "esp_partition"],
"esp_event": ["esp_event", "event_loop", "event_callback"],
"esp_console": ["esp_console", "console_"],
"chip_specific": ["chip_", "esp_chip"],
"esp_system_utils": ["esp_system", "esp_hw", "esp_clk", "esp_sleep"],
"ipc": ["esp_ipc", "ipc_"],
"wifi_config": [
"g_cnxMgr",
"gChmCxt",
"g_ic",
"TxRxCxt",
"s_dp",
"s_ni",
"s_reg_dump",
"packet$",
"d_mult_table",
"K",
"fcstab",
],
"smartconfig": ["sc_ack_send"],
"rc_calibration": ["rc_cal", "rcUpdate"],
"noise_floor": ["noise_check"],
"rf_calibration": [
"set_rx_sense",
"set_rx_gain_cal",
"set_chan_dig_gain",
"tx_pwctrl_init_cal",
"rfcal_txiq",
"set_tx_gain_table",
"correct_rfpll_offset",
"pll_correct_dcap",
"txiq_cal_init",
"pwdet_sar",
"rx_11b_opt",
],
"wifi_crypto": [
"pk_use_ecparams",
"process_segments",
"ccmp_",
"rc4_",
"aria_",
"mgf_mask",
"dh_group",
"ccmp_aad_nonce",
"ccmp_encrypt",
"rc4_skip",
"aria_sb1",
"aria_sb2",
"aria_is1",
"aria_is2",
"aria_sl",
"aria_a",
],
"radio_control": ["fsm_input", "fsm_sconfreq"],
"pbuf": [
"pbuf_",
],
"event_group": ["xEventGroup"],
"ringbuffer": ["xRingbuffer", "prvSend", "prvReceive", "prvCopy"],
"provisioning": ["prov_", "prov_stop_and_notify"],
"scan": ["gScanStruct"],
"port": ["xPort"],
"elf_loader": [
"elf_add",
"elf_add_note",
"elf_add_segment",
"process_image",
"read_encoded",
"read_encoded_value",
"read_encoded_value_with_base",
"process_image_header",
],
"socket_api": [
"sockets",
"netconn_",
"accept_function",
"recv_raw",
"socket_ipv4_multicast",
"socket_ipv6_multicast",
],
"igmp": ["igmp_", "igmp_send", "igmp_input"],
"icmp6": ["icmp6_"],
"arp": ["arp_table"],
"ampdu": [
"ampdu_",
"rcAmpdu",
"trc_onAmpduOp",
"rcAmpduLowerRate",
"ampdu_dispatch_upto",
],
"ieee802_11": ["ieee802_11_", "ieee802_11_parse_elems"],
"rate_control": ["rssi_margin", "rcGetSched", "get_rate_fcc_index"],
"nan": ["nan_dp_", "nan_dp_post_tx", "nan_dp_delete_peer"],
"channel_mgmt": ["chm_init", "chm_set_current_channel"],
"trace": ["trc_init", "trc_onAmpduOp"],
"country_code": ["country_info", "country_info_24ghz"],
"multicore": ["do_multicore_settings"],
"Update_lib": ["Update"],
"stdio": [
"__sf",
"__sflush_r",
"__srefill_r",
"_impure_data",
"_reclaim_reent",
"_open_r",
],
"strncpy_ops": ["strncpy"],
"math_internal": ["__mdiff", "__lshift", "__mprec_tens", "quorem"],
"character_class": ["__chclass"],
"camellia": ["camellia_", "camellia_feistel"],
"crypto_tables": ["FSb", "FSb2", "FSb3", "FSb4"],
"event_buffer": ["g_eb_list_desc", "eb_space"],
"base_node": ["base_node_", "base_node_add_handler"],
"file_descriptor": ["s_fd_table"],
"tx_delay": ["tx_delay_cfg"],
"deinit": ["deinit_functions"],
"lcp_echo": ["LcpEchoCheck"],
"raw_api": ["raw_bind", "raw_connect"],
"checksum": ["process_checksum"],
"entry_management": ["add_entry"],
"esp_ota": ["esp_ota", "ota_", "read_otadata"],
"http_server": [
"httpd_",
"parse_url_char",
"cb_headers_complete",
"delete_entry",
"validate_structure",
"config_save",
"config_new",
"verify_url",
"cb_url",
],
"misc_system": [
"alarm_cbs",
"start_up",
"tokens",
"unhex",
"osi_funcs_ro",
"enum_function",
"fragment_and_dispatch",
"alarm_set",
"osi_alarm_new",
"config_set_string",
"config_update_newest_section",
"config_remove_key",
"method_strings",
"interop_match",
"interop_database",
"__state_table",
"__action_table",
"s_stub_table",
"s_context",
"s_mmu_ctx",
"s_get_bus_mask",
"hli_queue_put",
"list_remove",
"list_delete",
"lock_acquire_generic",
"is_vect_desc_usable",
"io_mode_str",
"__c$20233",
"interface",
"read_id_core",
"subscribe_idle",
"unsubscribe_idle",
"s_clkout_handle",
"lock_release_generic",
"config_set_int",
"config_get_int",
"config_get_string",
"config_has_key",
"config_remove_section",
"osi_alarm_init",
"osi_alarm_deinit",
"fixed_queue_enqueue",
"fixed_queue_dequeue",
"fixed_queue_new",
"fixed_pkt_queue_enqueue",
"fixed_pkt_queue_new",
"list_append",
"list_prepend",
"list_insert_after",
"list_contains",
"list_get_node",
"hash_function_blob",
"cb_no_body",
"cb_on_body",
"profile_tab",
"get_arg",
"trim",
"buf$",
"process_appended_hash_and_sig$constprop$0",
"uuidType",
"allocate_svc_db_buf",
"_hostname_is_ours",
"s_hli_handlers",
"tick_cb",
"idle_cb",
"input",
"entry_find",
"section_find",
"find_bucket_entry_",
"config_has_section",
"hli_queue_create",
"hli_queue_get",
"hli_c_handler",
"future_ready",
"future_await",
"future_new",
"pkt_queue_enqueue",
"pkt_queue_dequeue",
"pkt_queue_cleanup",
"pkt_queue_create",
"pkt_queue_destroy",
"fixed_pkt_queue_dequeue",
"osi_alarm_cancel",
"osi_alarm_is_active",
"osi_sem_take",
"osi_event_create",
"osi_event_bind",
"alarm_cb_handler",
"list_foreach",
"list_back",
"list_front",
"list_clear",
"fixed_queue_try_peek_first",
"translate_path",
"get_idx",
"find_key",
"init",
"end",
"start",
"set_read_value",
"copy_address_list",
"copy_and_key",
"sdk_cfg_opts",
"leftshift_onebit",
"config_section_end",
"config_section_begin",
"find_entry_and_check_all_reset",
"image_validate",
"xPendingReadyList",
"vListInitialise",
"lock_init_generic",
"ant_bttx_cfg",
"ant_dft_cfg",
"cs_send_to_ctrl_sock",
"config_llc_util_funcs_reset",
"make_set_adv_report_flow_control",
"make_set_event_mask",
"raw_new",
"raw_remove",
"BTE_InitStack",
"parse_read_local_supported_features_response",
"__math_invalidf",
"tinytens",
"__mprec_tinytens",
"__mprec_bigtens",
"vRingbufferDelete",
"vRingbufferDeleteWithCaps",
"vRingbufferReturnItem",
"vRingbufferReturnItemFromISR",
"get_acl_data_size_ble",
"get_features_ble",
"get_features_classic",
"get_acl_packet_size_ble",
"get_acl_packet_size_classic",
"supports_extended_inquiry_response",
"supports_rssi_with_inquiry_results",
"supports_interlaced_inquiry_scan",
"supports_reading_remote_extended_features",
],
"bluetooth_ll": [
"lld_pdu_",
"ld_acl_",
"lld_stop_ind_handler",
"lld_evt_winsize_change",
"config_lld_evt_funcs_reset",
"config_lld_funcs_reset",
"config_llm_funcs_reset",
"llm_set_long_adv_data",
"lld_retry_tx_prog",
"llc_link_sup_to_ind_handler",
"config_llc_funcs_reset",
"lld_evt_rxwin_compute",
"config_btdm_funcs_reset",
"config_ea_funcs_reset",
"llc_defalut_state_tab_reset",
"config_rwip_funcs_reset",
"ke_lmp_rx_flooding_detect",
],
}
# Demangled patterns: patterns found in demangled C++ names
DEMANGLED_PATTERNS = {
"gpio_driver": ["GPIO"],
"uart_driver": ["UART"],
"network_stack": [
"lwip",
"tcp",
"udp",
"ip4",
"ip6",
"dhcp",
"dns",
"netif",
"ethernet",
"ppp",
"slip",
],
"wifi_stack": ["NetworkInterface"],
"nimble_bt": [
"nimble",
"NimBLE",
"ble_hs",
"ble_gap",
"ble_gatt",
"ble_att",
"ble_l2cap",
"ble_sm",
],
"crypto": ["mbedtls", "crypto", "sha", "aes", "rsa", "ecc", "tls", "ssl"],
"cpp_stdlib": ["std::", "__gnu_cxx::", "__cxxabiv"],
"static_init": ["__static_initialization"],
"rtti": ["__type_info", "__class_type_info"],
"web_server_lib": ["AsyncWebServer", "AsyncWebHandler", "WebServer"],
"async_tcp": ["AsyncClient", "AsyncServer"],
"mdns_lib": ["mdns"],
"json_lib": [
"ArduinoJson",
"JsonDocument",
"JsonArray",
"JsonObject",
"deserialize",
"serialize",
],
"http_lib": ["HTTP", "http_", "Request", "Response", "Uri", "WebSocket"],
"logging": ["log", "Log", "print", "Print", "diag_"],
"authentication": ["checkDigestAuthentication"],
"libgcc": ["libgcc"],
"esp_system": ["esp_", "ESP"],
"arduino": ["arduino"],
"nvs": ["nvs_", "_ZTVN3nvs", "nvs::"],
"filesystem": ["spiffs", "vfs"],
"libc": ["newlib"],
}
# Patterns for categorizing ESPHome core symbols into subcategories
CORE_SUBCATEGORY_PATTERNS = {
"Component Framework": ["Component"],
"Application Core": ["Application"],
"Scheduler": ["Scheduler"],
"Component Iterator": ["ComponentIterator"],
"Helper Functions": ["Helpers", "helpers"],
"Preferences/Storage": ["Preferences", "ESPPreferences"],
"I/O Utilities": ["HighFrequencyLoopRequester"],
"String Utilities": ["str_"],
"Bit Utilities": ["reverse_bits"],
"Data Conversion": ["convert_"],
"Network Utilities": ["network", "IPAddress"],
"API Protocol": ["api::"],
"WiFi Manager": ["wifi::"],
"MQTT Client": ["mqtt::"],
"Logger": ["logger::"],
"OTA Updates": ["ota::"],
"Web Server": ["web_server::"],
"Time Management": ["time::"],
"Sensor Framework": ["sensor::"],
"Binary Sensor": ["binary_sensor::"],
"Switch Framework": ["switch_::"],
"Light Framework": ["light::"],
"Climate Framework": ["climate::"],
"Cover Framework": ["cover::"],
}

View File

@@ -0,0 +1,121 @@
"""Helper functions for memory analysis."""
from functools import cache
from pathlib import Path
from .const import SECTION_MAPPING
# Import namespace constant from parent module
# Note: This would create a circular import if done at module level,
# so we'll define it locally here as well
_NAMESPACE_ESPHOME = "esphome::"
# Get the list of actual ESPHome components by scanning the components directory
@cache
def get_esphome_components():
"""Get set of actual ESPHome components from the components directory."""
# Find the components directory relative to this file
# Go up two levels from analyze_memory/helpers.py to esphome/
current_dir = Path(__file__).parent.parent
components_dir = current_dir / "components"
if not components_dir.exists() or not components_dir.is_dir():
return frozenset()
return frozenset(
item.name
for item in components_dir.iterdir()
if item.is_dir()
and not item.name.startswith(".")
and not item.name.startswith("__")
)
@cache
def get_component_class_patterns(component_name: str) -> list[str]:
"""Generate component class name patterns for symbol matching.
Args:
component_name: The component name (e.g., "ota", "wifi", "api")
Returns:
List of pattern strings to match against demangled symbols
"""
component_upper = component_name.upper()
component_camel = component_name.replace("_", "").title()
return [
f"{_NAMESPACE_ESPHOME}{component_upper}Component", # e.g., esphome::OTAComponent
f"{_NAMESPACE_ESPHOME}ESPHome{component_upper}Component", # e.g., esphome::ESPHomeOTAComponent
f"{_NAMESPACE_ESPHOME}{component_camel}Component", # e.g., esphome::OtaComponent
f"{_NAMESPACE_ESPHOME}ESPHome{component_camel}Component", # e.g., esphome::ESPHomeOtaComponent
]
def map_section_name(raw_section: str) -> str | None:
"""Map raw section name to standard section.
Args:
raw_section: Raw section name from ELF file (e.g., ".iram0.text", ".rodata.str1.1")
Returns:
Standard section name (".text", ".rodata", ".data", ".bss") or None
"""
for standard_section, patterns in SECTION_MAPPING.items():
if any(pattern in raw_section for pattern in patterns):
return standard_section
return None
def parse_symbol_line(line: str) -> tuple[str, str, int, str] | None:
"""Parse a single symbol line from objdump output.
Args:
line: Line from objdump -t output
Returns:
Tuple of (section, name, size, address) or None if not a valid symbol.
Format: address l/g w/d F/O section size name
Example: 40084870 l F .iram0.text 00000000 _xt_user_exc
"""
parts = line.split()
if len(parts) < 5:
return None
try:
# Validate and extract address
address = parts[0]
int(address, 16)
except ValueError:
return None
# Look for F (function) or O (object) flag
if "F" not in parts and "O" not in parts:
return None
# Find section, size, and name
for i, part in enumerate(parts):
if not part.startswith("."):
continue
section = map_section_name(part)
if not section:
break
# Need at least size field after section
if i + 1 >= len(parts):
break
try:
size = int(parts[i + 1], 16)
except ValueError:
break
# Need symbol name and non-zero size
if i + 2 >= len(parts) or size == 0:
break
name = " ".join(parts[i + 2 :])
return (section, name, size, address)
return None

View File

@@ -155,6 +155,17 @@ def _validate_api_config(config: ConfigType) -> ConfigType:
return config
def _consume_api_sockets(config: ConfigType) -> ConfigType:
"""Register socket needs for API component."""
from esphome.components import socket
# API needs 1 listening socket + typically 3 concurrent client connections
# (not max_connections, which is the upper limit rarely reached)
sockets_needed = 1 + 3
socket.consume_sockets(sockets_needed, "api")(config)
return config
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
@@ -222,6 +233,7 @@ CONFIG_SCHEMA = cv.All(
).extend(cv.COMPONENT_SCHEMA),
cv.rename_key(CONF_SERVICES, CONF_ACTIONS),
_validate_api_config,
_consume_api_sockets,
)
@@ -380,12 +392,19 @@ async def homeassistant_service_to_code(
var = cg.new_Pvariable(action_id, template_arg, serv, False)
templ = await cg.templatable(config[CONF_ACTION], args, None)
cg.add(var.set_service(templ))
# Initialize FixedVectors with exact sizes from config
cg.add(var.init_data(len(config[CONF_DATA])))
for key, value in config[CONF_DATA].items():
templ = await cg.templatable(value, args, None)
cg.add(var.add_data(key, templ))
cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE])))
for key, value in config[CONF_DATA_TEMPLATE].items():
templ = await cg.templatable(value, args, None)
cg.add(var.add_data_template(key, templ))
cg.add(var.init_variables(len(config[CONF_VARIABLES])))
for key, value in config[CONF_VARIABLES].items():
templ = await cg.templatable(value, args, None)
cg.add(var.add_variable(key, templ))
@@ -458,15 +477,23 @@ async def homeassistant_event_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg, serv, True)
templ = await cg.templatable(config[CONF_EVENT], args, None)
cg.add(var.set_service(templ))
# Initialize FixedVectors with exact sizes from config
cg.add(var.init_data(len(config[CONF_DATA])))
for key, value in config[CONF_DATA].items():
templ = await cg.templatable(value, args, None)
cg.add(var.add_data(key, templ))
cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE])))
for key, value in config[CONF_DATA_TEMPLATE].items():
templ = await cg.templatable(value, args, None)
cg.add(var.add_data_template(key, templ))
cg.add(var.init_variables(len(config[CONF_VARIABLES])))
for key, value in config[CONF_VARIABLES].items():
templ = await cg.templatable(value, args, None)
cg.add(var.add_variable(key, templ))
return var
@@ -489,6 +516,8 @@ async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, arg
serv = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, serv, True)
cg.add(var.set_service("esphome.tag_scanned"))
# Initialize FixedVector with exact size (1 data field)
cg.add(var.init_data(1))
templ = await cg.templatable(config[CONF_TAG], args, cg.std_string)
cg.add(var.add_data("tag_id", templ))
return var

View File

@@ -506,7 +506,7 @@ message ListEntitiesLightResponse {
string name = 3;
reserved 4; // Deprecated: was string unique_id
repeated ColorMode supported_color_modes = 12 [(container_pointer) = "std::set<light::ColorMode>"];
repeated ColorMode supported_color_modes = 12 [(container_pointer_no_template) = "light::ColorModeMask"];
// next four supports_* are for legacy clients, newer clients should use color modes
// Deprecated in API version 1.6
bool legacy_supports_brightness = 5 [deprecated=true];
@@ -776,9 +776,9 @@ message HomeassistantActionRequest {
option (ifdef) = "USE_API_HOMEASSISTANT_SERVICES";
string service = 1;
repeated HomeassistantServiceMap data = 2;
repeated HomeassistantServiceMap data_template = 3;
repeated HomeassistantServiceMap variables = 4;
repeated HomeassistantServiceMap data = 2 [(fixed_vector) = true];
repeated HomeassistantServiceMap data_template = 3 [(fixed_vector) = true];
repeated HomeassistantServiceMap variables = 4 [(fixed_vector) = true];
bool is_event = 5;
uint32 call_id = 6 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES"];
bool wants_response = 7 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
@@ -866,7 +866,7 @@ message ListEntitiesServicesResponse {
string name = 1;
fixed32 key = 2;
repeated ListEntitiesServicesArgument args = 3;
repeated ListEntitiesServicesArgument args = 3 [(fixed_vector) = true];
}
message ExecuteServiceArgument {
option (ifdef) = "USE_API_SERVICES";
@@ -876,10 +876,10 @@ message ExecuteServiceArgument {
string string_ = 4;
// ESPHome 1.14 (api v1.3) make int a signed value
sint32 int_ = 5;
repeated bool bool_array = 6 [packed=false];
repeated sint32 int_array = 7 [packed=false];
repeated float float_array = 8 [packed=false];
repeated string string_array = 9;
repeated bool bool_array = 6 [packed=false, (fixed_vector) = true];
repeated sint32 int_array = 7 [packed=false, (fixed_vector) = true];
repeated float float_array = 8 [packed=false, (fixed_vector) = true];
repeated string string_array = 9 [(fixed_vector) = true];
}
message ExecuteServiceRequest {
option (id) = 42;
@@ -888,7 +888,7 @@ message ExecuteServiceRequest {
option (ifdef) = "USE_API_SERVICES";
fixed32 key = 1;
repeated ExecuteServiceArgument args = 2;
repeated ExecuteServiceArgument args = 2 [(fixed_vector) = true];
}
// ==================== CAMERA ====================
@@ -987,8 +987,8 @@ message ListEntitiesClimateResponse {
string name = 3;
reserved 4; // Deprecated: was string unique_id
bool supports_current_temperature = 5;
bool supports_two_point_target_temperature = 6;
bool supports_current_temperature = 5; // Deprecated: use feature_flags
bool supports_two_point_target_temperature = 6; // Deprecated: use feature_flags
repeated ClimateMode supported_modes = 7 [(container_pointer) = "std::set<climate::ClimateMode>"];
float visual_min_temperature = 8;
float visual_max_temperature = 9;
@@ -997,7 +997,7 @@ message ListEntitiesClimateResponse {
// is if CLIMATE_PRESET_AWAY exists is supported_presets
// Deprecated in API version 1.5
bool legacy_supports_away = 11 [deprecated=true];
bool supports_action = 12;
bool supports_action = 12; // Deprecated: use feature_flags
repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer) = "std::set<climate::ClimateFanMode>"];
repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer) = "std::set<climate::ClimateSwingMode>"];
repeated string supported_custom_fan_modes = 15 [(container_pointer) = "std::set"];
@@ -1007,11 +1007,12 @@ message ListEntitiesClimateResponse {
string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"];
EntityCategory entity_category = 20;
float visual_current_temperature_step = 21;
bool supports_current_humidity = 22;
bool supports_target_humidity = 23;
bool supports_current_humidity = 22; // Deprecated: use feature_flags
bool supports_target_humidity = 23; // Deprecated: use feature_flags
float visual_min_humidity = 24;
float visual_max_humidity = 25;
uint32 device_id = 26 [(field_ifdef) = "USE_DEVICES"];
uint32 feature_flags = 27;
}
message ClimateStateResponse {
option (id) = 47;
@@ -1519,7 +1520,7 @@ message BluetoothGATTCharacteristic {
repeated uint64 uuid = 1 [(fixed_array_size) = 2, (fixed_array_skip_zero) = true];
uint32 handle = 2;
uint32 properties = 3;
repeated BluetoothGATTDescriptor descriptors = 4;
repeated BluetoothGATTDescriptor descriptors = 4 [(fixed_vector) = true];
// New field for efficient UUID (v1.12+)
// Only one of uuid or short_uuid will be set.
@@ -1531,7 +1532,7 @@ message BluetoothGATTCharacteristic {
message BluetoothGATTService {
repeated uint64 uuid = 1 [(fixed_array_size) = 2, (fixed_array_skip_zero) = true];
uint32 handle = 2;
repeated BluetoothGATTCharacteristic characteristics = 3;
repeated BluetoothGATTCharacteristic characteristics = 3 [(fixed_vector) = true];
// New field for efficient UUID (v1.12+)
// Only one of uuid or short_uuid will be set.

View File

@@ -27,6 +27,9 @@
#ifdef USE_BLUETOOTH_PROXY
#include "esphome/components/bluetooth_proxy/bluetooth_proxy.h"
#endif
#ifdef USE_CLIMATE
#include "esphome/components/climate/climate_mode.h"
#endif
#ifdef USE_VOICE_ASSISTANT
#include "esphome/components/voice_assistant/voice_assistant.h"
#endif
@@ -450,7 +453,6 @@ uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *
bool is_single) {
auto *light = static_cast<light::LightState *>(entity);
LightStateResponse resp;
auto traits = light->get_traits();
auto values = light->remote_values;
auto color_mode = values.get_color_mode();
resp.state = values.is_on();
@@ -474,7 +476,8 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c
auto *light = static_cast<light::LightState *>(entity);
ListEntitiesLightResponse msg;
auto traits = light->get_traits();
msg.supported_color_modes = &traits.get_supported_color_modes_for_api_();
// Pass pointer to ColorModeMask so the iterator can encode actual ColorMode enum values
msg.supported_color_modes = &traits.get_supported_color_modes();
if (traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) ||
traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)) {
msg.min_mireds = traits.get_min_mireds();
@@ -623,9 +626,10 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection
auto traits = climate->get_traits();
resp.mode = static_cast<enums::ClimateMode>(climate->mode);
resp.action = static_cast<enums::ClimateAction>(climate->action);
if (traits.get_supports_current_temperature())
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE))
resp.current_temperature = climate->current_temperature;
if (traits.get_supports_two_point_target_temperature()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
resp.target_temperature_low = climate->target_temperature_low;
resp.target_temperature_high = climate->target_temperature_high;
} else {
@@ -644,9 +648,9 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection
}
if (traits.get_supports_swing_modes())
resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode);
if (traits.get_supports_current_humidity())
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY))
resp.current_humidity = climate->current_humidity;
if (traits.get_supports_target_humidity())
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY))
resp.target_humidity = climate->target_humidity;
return fill_and_encode_entity_state(climate, resp, ClimateStateResponse::MESSAGE_TYPE, conn, remaining_size,
is_single);
@@ -656,10 +660,15 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection
auto *climate = static_cast<climate::Climate *>(entity);
ListEntitiesClimateResponse msg;
auto traits = climate->get_traits();
msg.supports_current_temperature = traits.get_supports_current_temperature();
msg.supports_current_humidity = traits.get_supports_current_humidity();
msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature();
msg.supports_target_humidity = traits.get_supports_target_humidity();
// Flags set for backward compatibility, deprecated in 2025.11.0
msg.supports_current_temperature = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
msg.supports_current_humidity = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY);
msg.supports_two_point_target_temperature = traits.has_feature_flags(
climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE);
msg.supports_target_humidity = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY);
msg.supports_action = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION);
// Current feature flags and other supported parameters
msg.feature_flags = traits.get_feature_flags();
msg.supported_modes = &traits.get_supported_modes_for_api_();
msg.visual_min_temperature = traits.get_visual_min_temperature();
msg.visual_max_temperature = traits.get_visual_max_temperature();
@@ -667,7 +676,6 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection
msg.visual_current_temperature_step = traits.get_visual_current_temperature_step();
msg.visual_min_humidity = traits.get_visual_min_humidity();
msg.visual_max_humidity = traits.get_visual_max_humidity();
msg.supports_action = traits.get_supports_action();
msg.supported_fan_modes = &traits.get_supported_fan_modes_for_api_();
msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes_for_api_();
msg.supported_presets = &traits.get_supported_presets_for_api_();
@@ -1074,13 +1082,8 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) {
homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds);
#ifdef USE_TIME_TIMEZONE
if (value.timezone_len > 0) {
const std::string &current_tz = homeassistant::global_homeassistant_time->get_timezone();
// Compare without allocating a string
if (current_tz.length() != value.timezone_len ||
memcmp(current_tz.c_str(), value.timezone, value.timezone_len) != 0) {
homeassistant::global_homeassistant_time->set_timezone(
std::string(reinterpret_cast<const char *>(value.timezone), value.timezone_len));
}
homeassistant::global_homeassistant_time->set_timezone(reinterpret_cast<const char *>(value.timezone),
value.timezone_len);
}
#endif
}
@@ -1406,7 +1409,7 @@ bool APIConnection::send_hello_response(const HelloRequest &msg) {
HelloResponse resp;
resp.api_version_major = 1;
resp.api_version_minor = 12;
resp.api_version_minor = 13;
// Send only the version string - the client only logs this for debugging and doesn't use it otherwise
resp.set_server_info(ESPHOME_VERSION_REF);
resp.set_name(StringRef(App.get_name()));

View File

@@ -19,13 +19,14 @@ namespace esphome::api {
//#define HELPER_LOG_PACKETS
// Maximum message size limits to prevent OOM on constrained devices
// Voice Assistant is our largest user at 1024 bytes per audio chunk
// Using 2048 + 256 bytes overhead = 2304 bytes total to support voice and future needs
// ESP8266 has very limited RAM and cannot support voice assistant
// Handshake messages are limited to a small size for security
static constexpr uint16_t MAX_HANDSHAKE_SIZE = 128;
// Data message limits vary by platform based on available memory
#ifdef USE_ESP8266
static constexpr uint16_t MAX_MESSAGE_SIZE = 512; // Keep small for memory constrained ESP8266
static constexpr uint16_t MAX_MESSAGE_SIZE = 8192; // 8 KiB for ESP8266
#else
static constexpr uint16_t MAX_MESSAGE_SIZE = 2304; // Support voice (1024) + headroom for larger messages
static constexpr uint16_t MAX_MESSAGE_SIZE = 32768; // 32 KiB for ESP32 and other platforms
#endif
// Forward declaration

View File

@@ -133,9 +133,6 @@ APIError APINoiseFrameHelper::loop() {
}
/** Read a packet into the rx_buf_.
*
* On success, rx_buf_ contains the frame data and state variables are cleared for the next read.
* Caller is responsible for consuming rx_buf_ (e.g., via std::move).
*
* @return APIError::OK if a full packet is in rx_buf_
*
@@ -176,18 +173,12 @@ APIError APINoiseFrameHelper::try_read_frame_() {
// read body
uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2];
if (state_ != State::DATA && msg_size > 128) {
// for handshake message only permit up to 128 bytes
// Check against size limits to prevent OOM: MAX_HANDSHAKE_SIZE for handshake, MAX_MESSAGE_SIZE for data
uint16_t limit = (state_ == State::DATA) ? MAX_MESSAGE_SIZE : MAX_HANDSHAKE_SIZE;
if (msg_size > limit) {
state_ = State::FAILED;
HELPER_LOG("Bad packet len for handshake: %d", msg_size);
return APIError::BAD_HANDSHAKE_PACKET_LEN;
}
// Check against maximum message size to prevent OOM
if (msg_size > MAX_MESSAGE_SIZE) {
state_ = State::FAILED;
HELPER_LOG("Bad packet: message size %u exceeds maximum %u", msg_size, MAX_MESSAGE_SIZE);
return APIError::BAD_DATA_PACKET;
HELPER_LOG("Bad packet: message size %u exceeds maximum %u", msg_size, limit);
return (state_ == State::DATA) ? APIError::BAD_DATA_PACKET : APIError::BAD_HANDSHAKE_PACKET_LEN;
}
// Reserve space for body
@@ -256,7 +247,6 @@ APIError APINoiseFrameHelper::state_action_() {
const std::string &name = App.get_name();
const std::string &mac = get_mac_address();
std::vector<uint8_t> msg;
// Calculate positions and sizes
size_t name_len = name.size() + 1; // including null terminator
size_t mac_len = mac.size() + 1; // including null terminator
@@ -264,17 +254,17 @@ APIError APINoiseFrameHelper::state_action_() {
size_t mac_offset = name_offset + name_len;
size_t total_size = 1 + name_len + mac_len;
msg.resize(total_size);
auto msg = std::make_unique<uint8_t[]>(total_size);
// chosen proto
msg[0] = 0x01;
// node name, terminated by null byte
std::memcpy(msg.data() + name_offset, name.c_str(), name_len);
std::memcpy(msg.get() + name_offset, name.c_str(), name_len);
// node mac, terminated by null byte
std::memcpy(msg.data() + mac_offset, mac.c_str(), mac_len);
std::memcpy(msg.get() + mac_offset, mac.c_str(), mac_len);
aerr = write_frame_(msg.data(), msg.size());
aerr = write_frame_(msg.get(), total_size);
if (aerr != APIError::OK)
return aerr;
@@ -353,32 +343,32 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reaso
#ifdef USE_STORE_LOG_STR_IN_FLASH
// On ESP8266 with flash strings, we need to use PROGMEM-aware functions
size_t reason_len = strlen_P(reinterpret_cast<PGM_P>(reason));
std::vector<uint8_t> data;
data.resize(reason_len + 1);
size_t data_size = reason_len + 1;
auto data = std::make_unique<uint8_t[]>(data_size);
data[0] = 0x01; // failure
// Copy error message from PROGMEM
if (reason_len > 0) {
memcpy_P(data.data() + 1, reinterpret_cast<PGM_P>(reason), reason_len);
memcpy_P(data.get() + 1, reinterpret_cast<PGM_P>(reason), reason_len);
}
#else
// Normal memory access
const char *reason_str = LOG_STR_ARG(reason);
size_t reason_len = strlen(reason_str);
std::vector<uint8_t> data;
data.resize(reason_len + 1);
size_t data_size = reason_len + 1;
auto data = std::make_unique<uint8_t[]>(data_size);
data[0] = 0x01; // failure
// Copy error message in bulk
if (reason_len > 0) {
std::memcpy(data.data() + 1, reason_str, reason_len);
std::memcpy(data.get() + 1, reason_str, reason_len);
}
#endif
// temporarily remove failed state
auto orig_state = state_;
state_ = State::EXPLICIT_REJECT;
write_frame_(data.data(), data.size());
write_frame_(data.get(), data_size);
state_ = orig_state;
}
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {

View File

@@ -48,9 +48,6 @@ APIError APIPlaintextFrameHelper::loop() {
}
/** Read a packet into the rx_buf_.
*
* On success, rx_buf_ contains the frame data and state variables are cleared for the next read.
* Caller is responsible for consuming rx_buf_ (e.g., via std::move).
*
* @return See APIError
*

View File

@@ -64,4 +64,20 @@ extend google.protobuf.FieldOptions {
// This is typically done through methods returning const T& or special accessor
// methods like get_options() or supported_modes_for_api_().
optional string container_pointer = 50001;
// fixed_vector: Use FixedVector instead of std::vector for repeated fields
// When set, the repeated field will use FixedVector<T> which requires calling
// init(size) before adding elements. This eliminates std::vector template overhead
// and is ideal when the exact size is known before populating the array.
optional bool fixed_vector = 50013 [default=false];
// container_pointer_no_template: Use a non-template container type for repeated fields
// Similar to container_pointer, but for containers that don't take template parameters.
// The container type is used as-is without appending element type.
// The container must have:
// - begin() and end() methods returning iterators
// - empty() method
// Example: [(container_pointer_no_template) = "light::ColorModeMask"]
// generates: const light::ColorModeMask *supported_color_modes{};
optional string container_pointer_no_template = 50014;
}

View File

@@ -1064,6 +1064,17 @@ bool ExecuteServiceArgument::decode_32bit(uint32_t field_id, Proto32Bit value) {
}
return true;
}
void ExecuteServiceArgument::decode(const uint8_t *buffer, size_t length) {
uint32_t count_bool_array = ProtoDecodableMessage::count_repeated_field(buffer, length, 6);
this->bool_array.init(count_bool_array);
uint32_t count_int_array = ProtoDecodableMessage::count_repeated_field(buffer, length, 7);
this->int_array.init(count_int_array);
uint32_t count_float_array = ProtoDecodableMessage::count_repeated_field(buffer, length, 8);
this->float_array.init(count_float_array);
uint32_t count_string_array = ProtoDecodableMessage::count_repeated_field(buffer, length, 9);
this->string_array.init(count_string_array);
ProtoDecodableMessage::decode(buffer, length);
}
bool ExecuteServiceRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 2:
@@ -1085,6 +1096,11 @@ bool ExecuteServiceRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
}
return true;
}
void ExecuteServiceRequest::decode(const uint8_t *buffer, size_t length) {
uint32_t count_args = ProtoDecodableMessage::count_repeated_field(buffer, length, 2);
this->args.init(count_args);
ProtoDecodableMessage::decode(buffer, length);
}
#endif
#ifdef USE_CAMERA
void ListEntitiesCameraResponse::encode(ProtoWriteBuffer buffer) const {
@@ -1185,6 +1201,7 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const {
#ifdef USE_DEVICES
buffer.encode_uint32(26, this->device_id);
#endif
buffer.encode_uint32(27, this->feature_flags);
}
void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const {
size.add_length(1, this->object_id_ref_.size());
@@ -1239,6 +1256,7 @@ void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const {
#ifdef USE_DEVICES
size.add_uint32(2, this->device_id);
#endif
size.add_uint32(2, this->feature_flags);
}
void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_fixed32(1, this->key);

View File

@@ -790,7 +790,7 @@ class ListEntitiesLightResponse final : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_light_response"; }
#endif
const std::set<light::ColorMode> *supported_color_modes{};
const light::ColorModeMask *supported_color_modes{};
float min_mireds{0.0f};
float max_mireds{0.0f};
std::vector<std::string> effects{};
@@ -1110,9 +1110,9 @@ class HomeassistantActionRequest final : public ProtoMessage {
#endif
StringRef service_ref_{};
void set_service(const StringRef &ref) { this->service_ref_ = ref; }
std::vector<HomeassistantServiceMap> data{};
std::vector<HomeassistantServiceMap> data_template{};
std::vector<HomeassistantServiceMap> variables{};
FixedVector<HomeassistantServiceMap> data{};
FixedVector<HomeassistantServiceMap> data_template{};
FixedVector<HomeassistantServiceMap> variables{};
bool is_event{false};
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
uint32_t call_id{0};
@@ -1263,7 +1263,7 @@ class ListEntitiesServicesResponse final : public ProtoMessage {
StringRef name_ref_{};
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
uint32_t key{0};
std::vector<ListEntitiesServicesArgument> args{};
FixedVector<ListEntitiesServicesArgument> args{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -1279,10 +1279,11 @@ class ExecuteServiceArgument final : public ProtoDecodableMessage {
float float_{0.0f};
std::string string_{};
int32_t int_{0};
std::vector<bool> bool_array{};
std::vector<int32_t> int_array{};
std::vector<float> float_array{};
std::vector<std::string> string_array{};
FixedVector<bool> bool_array{};
FixedVector<int32_t> int_array{};
FixedVector<float> float_array{};
FixedVector<std::string> string_array{};
void decode(const uint8_t *buffer, size_t length) override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1300,7 +1301,8 @@ class ExecuteServiceRequest final : public ProtoDecodableMessage {
const char *message_name() const override { return "execute_service_request"; }
#endif
uint32_t key{0};
std::vector<ExecuteServiceArgument> args{};
FixedVector<ExecuteServiceArgument> args{};
void decode(const uint8_t *buffer, size_t length) override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1369,7 +1371,7 @@ class CameraImageRequest final : public ProtoDecodableMessage {
class ListEntitiesClimateResponse final : public InfoResponseProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 46;
static constexpr uint8_t ESTIMATED_SIZE = 145;
static constexpr uint8_t ESTIMATED_SIZE = 150;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_climate_response"; }
#endif
@@ -1390,6 +1392,7 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage {
bool supports_target_humidity{false};
float visual_min_humidity{0.0f};
float visual_max_humidity{0.0f};
uint32_t feature_flags{0};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -1923,7 +1926,7 @@ class BluetoothGATTCharacteristic final : public ProtoMessage {
std::array<uint64_t, 2> uuid{};
uint32_t handle{0};
uint32_t properties{0};
std::vector<BluetoothGATTDescriptor> descriptors{};
FixedVector<BluetoothGATTDescriptor> descriptors{};
uint32_t short_uuid{0};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
@@ -1937,7 +1940,7 @@ class BluetoothGATTService final : public ProtoMessage {
public:
std::array<uint64_t, 2> uuid{};
uint32_t handle{0};
std::vector<BluetoothGATTCharacteristic> characteristics{};
FixedVector<BluetoothGATTCharacteristic> characteristics{};
uint32_t short_uuid{0};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;

View File

@@ -1292,6 +1292,7 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const {
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
#endif
dump_field(out, "feature_flags", this->feature_flags);
}
void ClimateStateResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ClimateStateResponse");

View File

@@ -201,9 +201,9 @@ class CustomAPIDevice {
void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) {
HomeassistantActionRequest resp;
resp.set_service(StringRef(service_name));
resp.data.init(data.size());
for (auto &it : data) {
resp.data.emplace_back();
auto &kv = resp.data.back();
auto &kv = resp.data.emplace_back();
kv.set_key(StringRef(it.first));
kv.value = it.second;
}
@@ -244,9 +244,9 @@ class CustomAPIDevice {
HomeassistantActionRequest resp;
resp.set_service(StringRef(service_name));
resp.is_event = true;
resp.data.init(data.size());
for (auto &it : data) {
resp.data.emplace_back();
auto &kv = resp.data.back();
auto &kv = resp.data.emplace_back();
kv.set_key(StringRef(it.first));
kv.value = it.second;
}

View File

@@ -41,10 +41,14 @@ template<typename... X> class TemplatableStringValue : public TemplatableValue<s
template<typename... Ts> class TemplatableKeyValuePair {
public:
// Default constructor needed for FixedVector::emplace_back()
TemplatableKeyValuePair() = default;
// Keys are always string literals from YAML dictionary keys (e.g., "code", "event")
// and never templatable values or lambdas. Only the value parameter can be a lambda/template.
// Using pass-by-value with std::move allows optimal performance for both lvalues and rvalues.
template<typename T> TemplatableKeyValuePair(std::string key, T value) : key(std::move(key)), value(value) {}
std::string key;
TemplatableStringValue<Ts...> value;
};
@@ -93,15 +97,22 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
template<typename T> void set_service(T service) { this->service_ = service; }
// Initialize FixedVector members - called from Python codegen with compile-time known sizes.
// Must be called before any add_* methods; capacity must match the number of subsequent add_* calls.
void init_data(size_t count) { this->data_.init(count); }
void init_data_template(size_t count) { this->data_template_.init(count); }
void init_variables(size_t count) { this->variables_.init(count); }
// Keys are always string literals from the Python code generation (e.g., cg.add(var.add_data("tag_id", templ))).
// The value parameter can be a lambda/template, but keys are never templatable.
// Using pass-by-value allows the compiler to optimize for both lvalues and rvalues.
template<typename T> void add_data(std::string key, T value) { this->data_.emplace_back(std::move(key), value); }
template<typename T> void add_data_template(std::string key, T value) {
this->data_template_.emplace_back(std::move(key), value);
template<typename K, typename V> void add_data(K &&key, V &&value) {
this->add_kv_(this->data_, std::forward<K>(key), std::forward<V>(value));
}
template<typename T> void add_variable(std::string key, T value) {
this->variables_.emplace_back(std::move(key), value);
template<typename K, typename V> void add_data_template(K &&key, V &&value) {
this->add_kv_(this->data_template_, std::forward<K>(key), std::forward<V>(value));
}
template<typename K, typename V> void add_variable(K &&key, V &&value) {
this->add_kv_(this->variables_, std::forward<K>(key), std::forward<V>(value));
}
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
@@ -127,24 +138,9 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
std::string service_value = this->service_.value(x...);
resp.set_service(StringRef(service_value));
resp.is_event = this->flags_.is_event;
for (auto &it : this->data_) {
resp.data.emplace_back();
auto &kv = resp.data.back();
kv.set_key(StringRef(it.key));
kv.value = it.value.value(x...);
}
for (auto &it : this->data_template_) {
resp.data_template.emplace_back();
auto &kv = resp.data_template.back();
kv.set_key(StringRef(it.key));
kv.value = it.value.value(x...);
}
for (auto &it : this->variables_) {
resp.variables.emplace_back();
auto &kv = resp.variables.back();
kv.set_key(StringRef(it.key));
kv.value = it.value.value(x...);
}
this->populate_service_map(resp.data, this->data_, x...);
this->populate_service_map(resp.data_template, this->data_template_, x...);
this->populate_service_map(resp.variables, this->variables_, x...);
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
if (this->flags_.wants_status) {
@@ -189,11 +185,28 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
}
protected:
// Helper to add key-value pairs to FixedVectors with perfect forwarding to avoid copies
template<typename K, typename V> void add_kv_(FixedVector<TemplatableKeyValuePair<Ts...>> &vec, K &&key, V &&value) {
auto &kv = vec.emplace_back();
kv.key = std::forward<K>(key);
kv.value = std::forward<V>(value);
}
template<typename VectorType, typename SourceType>
static void populate_service_map(VectorType &dest, SourceType &source, Ts... x) {
dest.init(source.size());
for (auto &it : source) {
auto &kv = dest.emplace_back();
kv.set_key(StringRef(it.key));
kv.value = it.value.value(x...);
}
}
APIServer *parent_;
TemplatableStringValue<Ts...> service_{};
std::vector<TemplatableKeyValuePair<Ts...>> data_;
std::vector<TemplatableKeyValuePair<Ts...>> data_template_;
std::vector<TemplatableKeyValuePair<Ts...>> variables_;
FixedVector<TemplatableKeyValuePair<Ts...>> data_;
FixedVector<TemplatableKeyValuePair<Ts...>> data_template_;
FixedVector<TemplatableKeyValuePair<Ts...>> variables_;
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
TemplatableStringValue<Ts...> response_template_{""};

View File

@@ -7,6 +7,69 @@ namespace esphome::api {
static const char *const TAG = "api.proto";
uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size_t length, uint32_t target_field_id) {
uint32_t count = 0;
const uint8_t *ptr = buffer;
const uint8_t *end = buffer + length;
while (ptr < end) {
uint32_t consumed;
// Parse field header (tag)
auto res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
if (!res.has_value()) {
break; // Invalid data, stop counting
}
uint32_t tag = res->as_uint32();
uint32_t field_type = tag & WIRE_TYPE_MASK;
uint32_t field_id = tag >> 3;
ptr += consumed;
// Count if this is the target field
if (field_id == target_field_id) {
count++;
}
// Skip field data based on wire type
switch (field_type) {
case WIRE_TYPE_VARINT: { // VarInt - parse and skip
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
if (!res.has_value()) {
return count; // Invalid data, return what we have
}
ptr += consumed;
break;
}
case WIRE_TYPE_LENGTH_DELIMITED: { // Length-delimited - parse length and skip data
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
if (!res.has_value()) {
return count;
}
uint32_t field_length = res->as_uint32();
ptr += consumed;
if (ptr + field_length > end) {
return count; // Out of bounds
}
ptr += field_length;
break;
}
case WIRE_TYPE_FIXED32: { // 32-bit - skip 4 bytes
if (ptr + 4 > end) {
return count;
}
ptr += 4;
break;
}
default:
// Unknown wire type, can't continue
return count;
}
}
return count;
}
void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
const uint8_t *ptr = buffer;
const uint8_t *end = buffer + length;
@@ -22,12 +85,12 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
}
uint32_t tag = res->as_uint32();
uint32_t field_type = tag & 0b111;
uint32_t field_type = tag & WIRE_TYPE_MASK;
uint32_t field_id = tag >> 3;
ptr += consumed;
switch (field_type) {
case 0: { // VarInt
case WIRE_TYPE_VARINT: { // VarInt
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
if (!res.has_value()) {
ESP_LOGV(TAG, "Invalid VarInt at offset %ld", (long) (ptr - buffer));
@@ -39,7 +102,7 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
ptr += consumed;
break;
}
case 2: { // Length-delimited
case WIRE_TYPE_LENGTH_DELIMITED: { // Length-delimited
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
if (!res.has_value()) {
ESP_LOGV(TAG, "Invalid Length Delimited at offset %ld", (long) (ptr - buffer));
@@ -57,7 +120,7 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
ptr += field_length;
break;
}
case 5: { // 32-bit
case WIRE_TYPE_FIXED32: { // 32-bit
if (ptr + 4 > end) {
ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer));
return;

View File

@@ -15,6 +15,13 @@
namespace esphome::api {
// Protocol Buffer wire type constants
// See https://protobuf.dev/programming-guides/encoding/#structure
constexpr uint8_t WIRE_TYPE_VARINT = 0; // int32, int64, uint32, uint64, sint32, sint64, bool, enum
constexpr uint8_t WIRE_TYPE_LENGTH_DELIMITED = 2; // string, bytes, embedded messages, packed repeated fields
constexpr uint8_t WIRE_TYPE_FIXED32 = 5; // fixed32, sfixed32, float
constexpr uint8_t WIRE_TYPE_MASK = 0b111; // Mask to extract wire type from tag
// Helper functions for ZigZag encoding/decoding
inline constexpr uint32_t encode_zigzag32(int32_t value) {
return (static_cast<uint32_t>(value) << 1) ^ (static_cast<uint32_t>(value >> 31));
@@ -241,7 +248,7 @@ class ProtoWriteBuffer {
* Following https://protobuf.dev/programming-guides/encoding/#structure
*/
void encode_field_raw(uint32_t field_id, uint32_t type) {
uint32_t val = (field_id << 3) | (type & 0b111);
uint32_t val = (field_id << 3) | (type & WIRE_TYPE_MASK);
this->encode_varint_raw(val);
}
void encode_string(uint32_t field_id, const char *string, size_t len, bool force = false) {
@@ -354,7 +361,18 @@ class ProtoMessage {
// Base class for messages that support decoding
class ProtoDecodableMessage : public ProtoMessage {
public:
void decode(const uint8_t *buffer, size_t length);
virtual void decode(const uint8_t *buffer, size_t length);
/**
* Count occurrences of a repeated field in a protobuf buffer.
* This is a lightweight scan that only parses tags and skips field data.
*
* @param buffer Pointer to the protobuf buffer
* @param length Length of the buffer in bytes
* @param target_field_id The field ID to count
* @return Number of times the field appears in the buffer
*/
static uint32_t count_repeated_field(const uint8_t *buffer, size_t length, uint32_t target_field_id);
protected:
virtual bool decode_varint(uint32_t field_id, ProtoVarInt value) { return false; }
@@ -482,7 +500,7 @@ class ProtoSize {
* @return The number of bytes needed to encode the field ID and wire type
*/
static constexpr uint32_t field(uint32_t field_id, uint32_t type) {
uint32_t tag = (field_id << 3) | (type & 0b111);
uint32_t tag = (field_id << 3) | (type & WIRE_TYPE_MASK);
return varint(tag);
}
@@ -749,13 +767,29 @@ class ProtoSize {
template<typename MessageType>
inline void add_repeated_message(uint32_t field_id_size, const std::vector<MessageType> &messages) {
// Skip if the vector is empty
if (messages.empty()) {
return;
if (!messages.empty()) {
// Use the force version for all messages in the repeated field
for (const auto &message : messages) {
add_message_object_force(field_id_size, message);
}
}
}
// Use the force version for all messages in the repeated field
for (const auto &message : messages) {
add_message_object_force(field_id_size, message);
/**
* @brief Calculates and adds the sizes of all messages in a repeated field to the total message size (FixedVector
* version)
*
* @tparam MessageType The type of the nested messages in the FixedVector
* @param messages FixedVector of message objects
*/
template<typename MessageType>
inline void add_repeated_message(uint32_t field_id_size, const FixedVector<MessageType> &messages) {
// Skip if the fixed vector is empty
if (!messages.empty()) {
// Use the force version for all messages in the repeated field
for (const auto &message : messages) {
add_message_object_force(field_id_size, message);
}
}
}
};

View File

@@ -12,16 +12,16 @@ template<> int32_t get_execute_arg_value<int32_t>(const ExecuteServiceArgument &
template<> float get_execute_arg_value<float>(const ExecuteServiceArgument &arg) { return arg.float_; }
template<> std::string get_execute_arg_value<std::string>(const ExecuteServiceArgument &arg) { return arg.string_; }
template<> std::vector<bool> get_execute_arg_value<std::vector<bool>>(const ExecuteServiceArgument &arg) {
return arg.bool_array;
return std::vector<bool>(arg.bool_array.begin(), arg.bool_array.end());
}
template<> std::vector<int32_t> get_execute_arg_value<std::vector<int32_t>>(const ExecuteServiceArgument &arg) {
return arg.int_array;
return std::vector<int32_t>(arg.int_array.begin(), arg.int_array.end());
}
template<> std::vector<float> get_execute_arg_value<std::vector<float>>(const ExecuteServiceArgument &arg) {
return arg.float_array;
return std::vector<float>(arg.float_array.begin(), arg.float_array.end());
}
template<> std::vector<std::string> get_execute_arg_value<std::vector<std::string>>(const ExecuteServiceArgument &arg) {
return arg.string_array;
return std::vector<std::string>(arg.string_array.begin(), arg.string_array.end());
}
template<> enums::ServiceArgType to_service_arg_type<bool>() { return enums::SERVICE_ARG_TYPE_BOOL; }

View File

@@ -35,9 +35,9 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
msg.set_name(StringRef(this->name_));
msg.key = this->key_;
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
msg.args.init(sizeof...(Ts));
for (size_t i = 0; i < sizeof...(Ts); i++) {
msg.args.emplace_back();
auto &arg = msg.args.back();
auto &arg = msg.args.emplace_back();
arg.type = arg_types[i];
arg.set_name(StringRef(this->arg_names_[i]));
}
@@ -55,7 +55,7 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
protected:
virtual void execute(Ts... x) = 0;
template<int... S> void execute_(const std::vector<ExecuteServiceArgument> &args, seq<S...> type) {
template<typename ArgsContainer, int... S> void execute_(const ArgsContainer &args, seq<S...> type) {
this->execute((get_execute_arg_value<Ts>(args[S]))...);
}

View File

@@ -165,4 +165,4 @@ def final_validate_audio_schema(
async def to_code(config):
cg.add_library("esphome/esp-audio-libs", "1.1.4")
cg.add_library("esphome/esp-audio-libs", "2.0.1")

View File

@@ -229,18 +229,18 @@ FileDecoderState AudioDecoder::decode_flac_() {
auto result = this->flac_decoder_->read_header(this->input_transfer_buffer_->get_buffer_start(),
this->input_transfer_buffer_->available());
if (result == esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) {
return FileDecoderState::POTENTIALLY_FAILED;
}
if (result != esp_audio_libs::flac::FLAC_DECODER_SUCCESS) {
// Couldn't read FLAC header
if (result > esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) {
// Serrious error reading FLAC header, there is no recovery
return FileDecoderState::FAILED;
}
size_t bytes_consumed = this->flac_decoder_->get_bytes_index();
this->input_transfer_buffer_->decrease_buffer_length(bytes_consumed);
if (result == esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) {
return FileDecoderState::MORE_TO_PROCESS;
}
// Reallocate the output transfer buffer to the smallest necessary size
this->free_buffer_required_ = flac_decoder_->get_output_buffer_size_bytes();
if (!this->output_transfer_buffer_->reallocate(this->free_buffer_required_)) {
@@ -256,9 +256,9 @@ FileDecoderState AudioDecoder::decode_flac_() {
}
uint32_t output_samples = 0;
auto result = this->flac_decoder_->decode_frame(
this->input_transfer_buffer_->get_buffer_start(), this->input_transfer_buffer_->available(),
reinterpret_cast<int16_t *>(this->output_transfer_buffer_->get_buffer_end()), &output_samples);
auto result = this->flac_decoder_->decode_frame(this->input_transfer_buffer_->get_buffer_start(),
this->input_transfer_buffer_->available(),
this->output_transfer_buffer_->get_buffer_end(), &output_samples);
if (result == esp_audio_libs::flac::FLAC_DECODER_ERROR_OUT_OF_DATA) {
// Not an issue, just needs more data that we'll get next time.

View File

View File

@@ -0,0 +1,54 @@
#include "esphome/core/log.h"
#include "bh1900nux.h"
namespace esphome {
namespace bh1900nux {
static const char *const TAG = "bh1900nux.sensor";
// I2C Registers
static const uint8_t TEMPERATURE_REG = 0x00;
static const uint8_t CONFIG_REG = 0x01; // Not used and supported yet
static const uint8_t TEMPERATURE_LOW_REG = 0x02; // Not used and supported yet
static const uint8_t TEMPERATURE_HIGH_REG = 0x03; // Not used and supported yet
static const uint8_t SOFT_RESET_REG = 0x04;
// I2C Command payloads
static const uint8_t SOFT_RESET_PAYLOAD = 0x01; // Soft Reset value
static const float SENSOR_RESOLUTION = 0.0625f; // Sensor resolution per bit in degrees celsius
void BH1900NUXSensor::setup() {
// Initialize I2C device
i2c::ErrorCode result_code =
this->write_register(SOFT_RESET_REG, &SOFT_RESET_PAYLOAD, 1); // Software Reset to check communication
if (result_code != i2c::ERROR_OK) {
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
return;
}
}
void BH1900NUXSensor::update() {
uint8_t temperature_raw[2];
if (this->read_register(TEMPERATURE_REG, temperature_raw, 2) != i2c::ERROR_OK) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
return;
}
// Combined raw value, unsigned and unaligned 16 bit
// Temperature is represented in just 12 bits, shift needed
int16_t raw_temperature_register_value = encode_uint16(temperature_raw[0], temperature_raw[1]);
raw_temperature_register_value >>= 4;
float temperature_value = raw_temperature_register_value * SENSOR_RESOLUTION; // Apply sensor resolution
this->publish_state(temperature_value);
}
void BH1900NUXSensor::dump_config() {
LOG_SENSOR("", "BH1900NUX", this);
LOG_I2C_DEVICE(this);
LOG_UPDATE_INTERVAL(this);
}
} // namespace bh1900nux
} // namespace esphome

View File

@@ -0,0 +1,18 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome {
namespace bh1900nux {
class BH1900NUXSensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice {
public:
void setup() override;
void update() override;
void dump_config() override;
};
} // namespace bh1900nux
} // namespace esphome

View File

@@ -0,0 +1,34 @@
import esphome.codegen as cg
from esphome.components import i2c, sensor
import esphome.config_validation as cv
from esphome.const import (
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
)
DEPENDENCIES = ["i2c"]
CODEOWNERS = ["@B48D81EFCC"]
sensor_ns = cg.esphome_ns.namespace("bh1900nux")
BH1900NUXSensor = sensor_ns.class_(
"BH1900NUXSensor", cg.PollingComponent, i2c.I2CDevice
)
CONFIG_SCHEMA = (
sensor.sensor_schema(
BH1900NUXSensor,
accuracy_decimals=1,
unit_of_measurement=UNIT_CELSIUS,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x48))
)
async def to_code(config):
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)

View File

@@ -0,0 +1,29 @@
import esphome.codegen as cg
from esphome.components.zephyr import zephyr_add_prj_conf
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_LOGS, CONF_TYPE
AUTO_LOAD = ["zephyr_ble_server"]
CODEOWNERS = ["@tomaszduda23"]
ble_nus_ns = cg.esphome_ns.namespace("ble_nus")
BLENUS = ble_nus_ns.class_("BLENUS", cg.Component)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(BLENUS),
cv.Optional(CONF_TYPE, default=CONF_LOGS): cv.one_of(
*[CONF_LOGS], lower=True
),
}
).extend(cv.COMPONENT_SCHEMA),
cv.only_with_framework("zephyr"),
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
zephyr_add_prj_conf("BT_NUS", True)
cg.add(var.set_expose_log(config[CONF_TYPE] == CONF_LOGS))
await cg.register_component(var, config)

View File

@@ -0,0 +1,157 @@
#ifdef USE_ZEPHYR
#include "ble_nus.h"
#include <zephyr/kernel.h>
#include <bluetooth/services/nus.h>
#include "esphome/core/log.h"
#ifdef USE_LOGGER
#include "esphome/components/logger/logger.h"
#include "esphome/core/application.h"
#endif
#include <zephyr/sys/ring_buffer.h>
namespace esphome::ble_nus {
constexpr size_t BLE_TX_BUF_SIZE = 2048;
// NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables)
BLENUS *global_ble_nus;
RING_BUF_DECLARE(global_ble_tx_ring_buf, BLE_TX_BUF_SIZE);
// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables)
static const char *const TAG = "ble_nus";
size_t BLENUS::write_array(const uint8_t *data, size_t len) {
if (atomic_get(&this->tx_status_) == TX_DISABLED) {
return 0;
}
return ring_buf_put(&global_ble_tx_ring_buf, data, len);
}
void BLENUS::connected(bt_conn *conn, uint8_t err) {
if (err == 0) {
global_ble_nus->conn_.store(bt_conn_ref(conn));
}
}
void BLENUS::disconnected(bt_conn *conn, uint8_t reason) {
if (global_ble_nus->conn_) {
bt_conn_unref(global_ble_nus->conn_.load());
// Connection array is global static.
// Reference can be kept even if disconnected.
}
}
void BLENUS::tx_callback(bt_conn *conn) {
atomic_cas(&global_ble_nus->tx_status_, TX_BUSY, TX_ENABLED);
ESP_LOGVV(TAG, "Sent operation completed");
}
void BLENUS::send_enabled_callback(bt_nus_send_status status) {
switch (status) {
case BT_NUS_SEND_STATUS_ENABLED:
atomic_set(&global_ble_nus->tx_status_, TX_ENABLED);
#ifdef USE_LOGGER
if (global_ble_nus->expose_log_) {
App.schedule_dump_config();
}
#endif
ESP_LOGD(TAG, "NUS notification has been enabled");
break;
case BT_NUS_SEND_STATUS_DISABLED:
atomic_set(&global_ble_nus->tx_status_, TX_DISABLED);
ESP_LOGD(TAG, "NUS notification has been disabled");
break;
}
}
void BLENUS::rx_callback(bt_conn *conn, const uint8_t *const data, uint16_t len) {
ESP_LOGD(TAG, "Received %d bytes.", len);
}
void BLENUS::setup() {
bt_nus_cb callbacks = {
.received = rx_callback,
.sent = tx_callback,
.send_enabled = send_enabled_callback,
};
bt_nus_init(&callbacks);
static bt_conn_cb conn_callbacks = {
.connected = BLENUS::connected,
.disconnected = BLENUS::disconnected,
};
bt_conn_cb_register(&conn_callbacks);
global_ble_nus = this;
#ifdef USE_LOGGER
if (logger::global_logger != nullptr && this->expose_log_) {
logger::global_logger->add_on_log_callback(
[this](int level, const char *tag, const char *message, size_t message_len) {
this->write_array(reinterpret_cast<const uint8_t *>(message), message_len);
const char c = '\n';
this->write_array(reinterpret_cast<const uint8_t *>(&c), 1);
});
}
#endif
}
void BLENUS::dump_config() {
ESP_LOGCONFIG(TAG, "ble nus:");
ESP_LOGCONFIG(TAG, " log: %s", YESNO(this->expose_log_));
uint32_t mtu = 0;
bt_conn *conn = this->conn_.load();
if (conn) {
mtu = bt_nus_get_mtu(conn);
}
ESP_LOGCONFIG(TAG, " MTU: %u", mtu);
}
void BLENUS::loop() {
if (ring_buf_is_empty(&global_ble_tx_ring_buf)) {
return;
}
if (!atomic_cas(&this->tx_status_, TX_ENABLED, TX_BUSY)) {
if (atomic_get(&this->tx_status_) == TX_DISABLED) {
ring_buf_reset(&global_ble_tx_ring_buf);
}
return;
}
bt_conn *conn = this->conn_.load();
if (conn) {
conn = bt_conn_ref(conn);
}
if (nullptr == conn) {
atomic_cas(&this->tx_status_, TX_BUSY, TX_ENABLED);
return;
}
uint32_t req_len = bt_nus_get_mtu(conn);
uint8_t *buf;
uint32_t size = ring_buf_get_claim(&global_ble_tx_ring_buf, &buf, req_len);
int err, err2;
err = bt_nus_send(conn, buf, size);
err2 = ring_buf_get_finish(&global_ble_tx_ring_buf, size);
if (err2) {
// It should no happen.
ESP_LOGE(TAG, "Size %u exceeds valid bytes in the ring buffer (%d error)", size, err2);
}
if (err == 0) {
ESP_LOGVV(TAG, "Sent %d bytes", size);
} else {
ESP_LOGE(TAG, "Failed to send %d bytes (%d error)", size, err);
atomic_cas(&this->tx_status_, TX_BUSY, TX_ENABLED);
}
bt_conn_unref(conn);
}
} // namespace esphome::ble_nus
#endif

View File

@@ -0,0 +1,37 @@
#pragma once
#ifdef USE_ZEPHYR
#include "esphome/core/defines.h"
#include "esphome/core/component.h"
#include <shell/shell_bt_nus.h>
#include <atomic>
namespace esphome::ble_nus {
class BLENUS : public Component {
enum TxStatus {
TX_DISABLED,
TX_ENABLED,
TX_BUSY,
};
public:
void setup() override;
void dump_config() override;
void loop() override;
size_t write_array(const uint8_t *data, size_t len);
void set_expose_log(bool expose_log) { this->expose_log_ = expose_log; }
protected:
static void send_enabled_callback(bt_nus_send_status status);
static void tx_callback(bt_conn *conn);
static void rx_callback(bt_conn *conn, const uint8_t *data, uint16_t len);
static void connected(bt_conn *conn, uint8_t err);
static void disconnected(bt_conn *conn, uint8_t reason);
std::atomic<bt_conn *> conn_ = nullptr;
bool expose_log_ = false;
atomic_t tx_status_ = ATOMIC_INIT(TX_DISABLED);
};
} // namespace esphome::ble_nus
#endif

View File

@@ -230,8 +230,8 @@ void BluetoothConnection::send_service_for_discovery_() {
service_resp.handle = service_result.start_handle;
if (total_char_count > 0) {
// Reserve space and process characteristics
service_resp.characteristics.reserve(total_char_count);
// Initialize FixedVector with exact count and process characteristics
service_resp.characteristics.init(total_char_count);
uint16_t char_offset = 0;
esp_gattc_char_elem_t char_result;
while (true) { // characteristics
@@ -253,9 +253,7 @@ void BluetoothConnection::send_service_for_discovery_() {
service_resp.characteristics.emplace_back();
auto &characteristic_resp = service_resp.characteristics.back();
fill_gatt_uuid(characteristic_resp.uuid, characteristic_resp.short_uuid, char_result.uuid, use_efficient_uuids);
characteristic_resp.handle = char_result.char_handle;
characteristic_resp.properties = char_result.properties;
char_offset++;
@@ -271,12 +269,11 @@ void BluetoothConnection::send_service_for_discovery_() {
return;
}
if (total_desc_count == 0) {
// No descriptors, continue to next characteristic
continue;
}
// Reserve space and process descriptors
characteristic_resp.descriptors.reserve(total_desc_count);
// Initialize FixedVector with exact count and process descriptors
characteristic_resp.descriptors.init(total_desc_count);
uint16_t desc_offset = 0;
esp_gattc_descr_elem_t desc_result;
while (true) { // descriptors
@@ -297,9 +294,7 @@ void BluetoothConnection::send_service_for_discovery_() {
characteristic_resp.descriptors.emplace_back();
auto &descriptor_resp = characteristic_resp.descriptors.back();
fill_gatt_uuid(descriptor_resp.uuid, descriptor_resp.short_uuid, desc_result.uuid, use_efficient_uuids);
descriptor_resp.handle = desc_result.handle;
desc_offset++;
}

View File

@@ -155,16 +155,12 @@ esp32_ble_tracker::AdvertisementParserType BluetoothProxy::get_advertisement_par
BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool reserve) {
for (uint8_t i = 0; i < this->connection_count_; i++) {
auto *connection = this->connections_[i];
if (connection->get_address() == address)
uint64_t conn_addr = connection->get_address();
if (conn_addr == address)
return connection;
}
if (!reserve)
return nullptr;
for (uint8_t i = 0; i < this->connection_count_; i++) {
auto *connection = this->connections_[i];
if (connection->get_address() == 0) {
if (reserve && conn_addr == 0) {
connection->send_service_ = INIT_SENDING_SERVICES;
connection->set_address(address);
// All connections must start at INIT
@@ -175,7 +171,6 @@ BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool rese
return connection;
}
}
return nullptr;
}

View File

@@ -16,7 +16,9 @@
#include "bluetooth_connection.h"
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
#include <esp_bt.h>
#endif
#include <esp_bt_device.h>
namespace esphome::bluetooth_proxy {

View File

@@ -41,7 +41,7 @@ CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(BME680BSECComponent),
cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature,
cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature_delta,
cv.Optional(CONF_IAQ_MODE, default="STATIC"): cv.enum(
IAQ_MODE_OPTIONS, upper=True
),

View File

@@ -139,7 +139,7 @@ CONFIG_SCHEMA_BASE = (
cv.Optional(CONF_SUPPLY_VOLTAGE, default="3.3V"): cv.enum(
VOLTAGE_OPTIONS, upper=True
),
cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature,
cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature_delta,
cv.Optional(
CONF_STATE_SAVE_INTERVAL, default="6hours"
): cv.positive_time_period_minutes,

View File

@@ -105,9 +105,9 @@ class Canbus : public Component {
CallbackManager<void(uint32_t can_id, bool extended_id, bool rtr, const std::vector<uint8_t> &data)>
callback_manager_{};
virtual bool setup_internal();
virtual Error send_message(struct CanFrame *frame);
virtual Error read_message(struct CanFrame *frame);
virtual bool setup_internal() = 0;
virtual Error send_message(struct CanFrame *frame) = 0;
virtual Error read_message(struct CanFrame *frame) = 0;
};
template<typename... Ts> class CanbusSendAction : public Action<Ts...>, public Parented<Canbus> {

View File

@@ -8,17 +8,30 @@ namespace cap1188 {
static const char *const TAG = "cap1188";
void CAP1188Component::setup() {
// Reset device using the reset pin
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup();
this->reset_pin_->digital_write(false);
delay(100); // NOLINT
this->reset_pin_->digital_write(true);
delay(100); // NOLINT
this->reset_pin_->digital_write(false);
delay(100); // NOLINT
this->disable_loop();
// no reset pin
if (this->reset_pin_ == nullptr) {
this->finish_setup_();
return;
}
// reset pin configured so reset before finishing setup
this->reset_pin_->setup();
this->reset_pin_->digital_write(false);
// delay after reset pin write
this->set_timeout(100, [this]() {
this->reset_pin_->digital_write(true);
// delay after reset pin write
this->set_timeout(100, [this]() {
this->reset_pin_->digital_write(false);
// delay after reset pin write
this->set_timeout(100, [this]() { this->finish_setup_(); });
});
});
}
void CAP1188Component::finish_setup_() {
// Check if CAP1188 is actually connected
this->read_byte(CAP1188_PRODUCT_ID, &this->cap1188_product_id_);
this->read_byte(CAP1188_MANUFACTURE_ID, &this->cap1188_manufacture_id_);
@@ -44,6 +57,9 @@ void CAP1188Component::setup() {
// Speed up a bit
this->write_byte(CAP1188_STAND_BY_CONFIGURATION, 0x30);
// Setup successful, so enable loop
this->enable_loop();
}
void CAP1188Component::dump_config() {

View File

@@ -49,6 +49,8 @@ class CAP1188Component : public Component, public i2c::I2CDevice {
void loop() override;
protected:
void finish_setup_();
std::vector<CAP1188Channel *> channels_{};
uint8_t touch_threshold_{0x20};
uint8_t allow_multiple_touches_{0x80};

View File

@@ -96,7 +96,8 @@ void ClimateCall::validate_() {
}
if (this->target_temperature_.has_value()) {
auto target = *this->target_temperature_;
if (traits.get_supports_two_point_target_temperature()) {
if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
ESP_LOGW(TAG, " Cannot set target temperature for climate device "
"with two-point target temperature!");
this->target_temperature_.reset();
@@ -106,7 +107,8 @@ void ClimateCall::validate_() {
}
}
if (this->target_temperature_low_.has_value() || this->target_temperature_high_.has_value()) {
if (!traits.get_supports_two_point_target_temperature()) {
if (!traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
ESP_LOGW(TAG, " Cannot set low/high target temperature for this device!");
this->target_temperature_low_.reset();
this->target_temperature_high_.reset();
@@ -350,13 +352,14 @@ void Climate::save_state_() {
state.mode = this->mode;
auto traits = this->get_traits();
if (traits.get_supports_two_point_target_temperature()) {
if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
state.target_temperature_low = this->target_temperature_low;
state.target_temperature_high = this->target_temperature_high;
} else {
state.target_temperature = this->target_temperature;
}
if (traits.get_supports_target_humidity()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
state.target_humidity = this->target_humidity;
}
if (traits.get_supports_fan_modes() && fan_mode.has_value()) {
@@ -400,7 +403,7 @@ void Climate::publish_state() {
auto traits = this->get_traits();
ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(climate_mode_to_string(this->mode)));
if (traits.get_supports_action()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) {
ESP_LOGD(TAG, " Action: %s", LOG_STR_ARG(climate_action_to_string(this->action)));
}
if (traits.get_supports_fan_modes() && this->fan_mode.has_value()) {
@@ -418,19 +421,20 @@ void Climate::publish_state() {
if (traits.get_supports_swing_modes()) {
ESP_LOGD(TAG, " Swing Mode: %s", LOG_STR_ARG(climate_swing_mode_to_string(this->swing_mode)));
}
if (traits.get_supports_current_temperature()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
ESP_LOGD(TAG, " Current Temperature: %.2f°C", this->current_temperature);
}
if (traits.get_supports_two_point_target_temperature()) {
if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
ESP_LOGD(TAG, " Target Temperature: Low: %.2f°C High: %.2f°C", this->target_temperature_low,
this->target_temperature_high);
} else {
ESP_LOGD(TAG, " Target Temperature: %.2f°C", this->target_temperature);
}
if (traits.get_supports_current_humidity()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) {
ESP_LOGD(TAG, " Current Humidity: %.0f%%", this->current_humidity);
}
if (traits.get_supports_target_humidity()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
ESP_LOGD(TAG, " Target Humidity: %.0f%%", this->target_humidity);
}
@@ -485,13 +489,14 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) {
auto call = climate->make_call();
auto traits = climate->get_traits();
call.set_mode(this->mode);
if (traits.get_supports_two_point_target_temperature()) {
if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
call.set_target_temperature_low(this->target_temperature_low);
call.set_target_temperature_high(this->target_temperature_high);
} else {
call.set_target_temperature(this->target_temperature);
}
if (traits.get_supports_target_humidity()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
call.set_target_humidity(this->target_humidity);
}
if (traits.get_supports_fan_modes() || !traits.get_supported_custom_fan_modes().empty()) {
@@ -508,13 +513,14 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) {
void ClimateDeviceRestoreState::apply(Climate *climate) {
auto traits = climate->get_traits();
climate->mode = this->mode;
if (traits.get_supports_two_point_target_temperature()) {
if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
climate->target_temperature_low = this->target_temperature_low;
climate->target_temperature_high = this->target_temperature_high;
} else {
climate->target_temperature = this->target_temperature;
}
if (traits.get_supports_target_humidity()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
climate->target_humidity = this->target_humidity;
}
if (traits.get_supports_fan_modes() && !this->uses_custom_fan_mode) {
@@ -580,28 +586,30 @@ void Climate::dump_traits_(const char *tag) {
" Target: %.1f",
traits.get_visual_min_temperature(), traits.get_visual_max_temperature(),
traits.get_visual_target_temperature_step());
if (traits.get_supports_current_temperature()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
ESP_LOGCONFIG(tag, " Current: %.1f", traits.get_visual_current_temperature_step());
}
if (traits.get_supports_target_humidity() || traits.get_supports_current_humidity()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY |
climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) {
ESP_LOGCONFIG(tag,
" - Min humidity: %.0f\n"
" - Max humidity: %.0f",
traits.get_visual_min_humidity(), traits.get_visual_max_humidity());
}
if (traits.get_supports_two_point_target_temperature()) {
if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
ESP_LOGCONFIG(tag, " [x] Supports two-point target temperature");
}
if (traits.get_supports_current_temperature()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
ESP_LOGCONFIG(tag, " [x] Supports current temperature");
}
if (traits.get_supports_target_humidity()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
ESP_LOGCONFIG(tag, " [x] Supports target humidity");
}
if (traits.get_supports_current_humidity()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) {
ESP_LOGCONFIG(tag, " [x] Supports current humidity");
}
if (traits.get_supports_action()) {
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) {
ESP_LOGCONFIG(tag, " [x] Supports action");
}
if (!traits.get_supported_modes().empty()) {

View File

@@ -98,6 +98,21 @@ enum ClimatePreset : uint8_t {
CLIMATE_PRESET_ACTIVITY = 7,
};
enum ClimateFeature : uint32_t {
// Reporting current temperature is supported
CLIMATE_SUPPORTS_CURRENT_TEMPERATURE = 1 << 0,
// Setting two target temperatures is supported (used in conjunction with CLIMATE_MODE_HEAT_COOL)
CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE = 1 << 1,
// Single-point mode is NOT supported (UI always displays two handles, setting 'target_temperature' is not supported)
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE = 1 << 2,
// Reporting current humidity is supported
CLIMATE_SUPPORTS_CURRENT_HUMIDITY = 1 << 3,
// Setting a target humidity is supported
CLIMATE_SUPPORTS_TARGET_HUMIDITY = 1 << 4,
// Reporting current climate action is supported
CLIMATE_SUPPORTS_ACTION = 1 << 5,
};
/// Convert the given ClimateMode to a human-readable string.
const LogString *climate_mode_to_string(ClimateMode mode);

View File

@@ -1,8 +1,8 @@
#pragma once
#include "esphome/core/helpers.h"
#include "climate_mode.h"
#include <set>
#include "climate_mode.h"
#include "esphome/core/helpers.h"
namespace esphome {
@@ -21,91 +21,100 @@ namespace climate {
* - Target Temperature
*
* All other properties and modes are optional and the integration must mark
* each of them as supported by setting the appropriate flag here.
* each of them as supported by setting the appropriate flag(s) here.
*
* - supports current temperature - if the climate device supports reporting a current temperature
* - supports two point target temperature - if the climate device's target temperature should be
* split in target_temperature_low and target_temperature_high instead of just the single target_temperature
* - feature flags: see ClimateFeatures enum in climate_mode.h
* - supports modes:
* - auto mode (automatic control)
* - cool mode (lowers current temperature)
* - heat mode (increases current temperature)
* - dry mode (removes humidity from air)
* - fan mode (only turns on fan)
* - supports action - if the climate device supports reporting the active
* current action of the device with the action property.
* - supports fan modes - optionally, if it has a fan which can be configured in different ways:
* - on, off, auto, high, medium, low, middle, focus, diffuse, quiet
* - supports swing modes - optionally, if it has a swing which can be configured in different ways:
* - off, both, vertical, horizontal
*
* This class also contains static data for the climate device display:
* - visual min/max temperature - tells the frontend what range of temperatures the climate device
* should display (gauge min/max values)
* - visual min/max temperature/humidity - tells the frontend what range of temperature/humidity the
* climate device should display (gauge min/max values)
* - temperature step - the step with which to increase/decrease target temperature.
* This also affects with how many decimal places the temperature is shown
*/
class ClimateTraits {
public:
bool get_supports_current_temperature() const { return this->supports_current_temperature_; }
/// Get/set feature flags (see ClimateFeatures enum in climate_mode.h)
uint32_t get_feature_flags() const { return this->feature_flags_; }
void add_feature_flags(uint32_t feature_flags) { this->feature_flags_ |= feature_flags; }
void clear_feature_flags(uint32_t feature_flags) { this->feature_flags_ &= ~feature_flags; }
bool has_feature_flags(uint32_t feature_flags) const { return this->feature_flags_ & feature_flags; }
void set_feature_flags(uint32_t feature_flags) { this->feature_flags_ = feature_flags; }
ESPDEPRECATED("This method is deprecated, use get_feature_flags() instead", "2025.11.0")
bool get_supports_current_temperature() const {
return this->has_feature_flags(CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
}
ESPDEPRECATED("This method is deprecated, use add_feature_flags() instead", "2025.11.0")
void set_supports_current_temperature(bool supports_current_temperature) {
this->supports_current_temperature_ = supports_current_temperature;
if (supports_current_temperature) {
this->add_feature_flags(CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
} else {
this->clear_feature_flags(CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
}
}
bool get_supports_current_humidity() const { return this->supports_current_humidity_; }
ESPDEPRECATED("This method is deprecated, use get_feature_flags() instead", "2025.11.0")
bool get_supports_current_humidity() const { return this->has_feature_flags(CLIMATE_SUPPORTS_CURRENT_HUMIDITY); }
ESPDEPRECATED("This method is deprecated, use add_feature_flags() instead", "2025.11.0")
void set_supports_current_humidity(bool supports_current_humidity) {
this->supports_current_humidity_ = supports_current_humidity;
if (supports_current_humidity) {
this->add_feature_flags(CLIMATE_SUPPORTS_CURRENT_HUMIDITY);
} else {
this->clear_feature_flags(CLIMATE_SUPPORTS_CURRENT_HUMIDITY);
}
}
bool get_supports_two_point_target_temperature() const { return this->supports_two_point_target_temperature_; }
ESPDEPRECATED("This method is deprecated, use get_feature_flags() instead", "2025.11.0")
bool get_supports_two_point_target_temperature() const {
return this->has_feature_flags(CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE);
}
ESPDEPRECATED("This method is deprecated, use add_feature_flags() instead", "2025.11.0")
void set_supports_two_point_target_temperature(bool supports_two_point_target_temperature) {
this->supports_two_point_target_temperature_ = supports_two_point_target_temperature;
if (supports_two_point_target_temperature)
// Use CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE to mimic previous behavior
{
this->add_feature_flags(CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE);
} else {
this->clear_feature_flags(CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE);
}
}
bool get_supports_target_humidity() const { return this->supports_target_humidity_; }
ESPDEPRECATED("This method is deprecated, use get_feature_flags() instead", "2025.11.0")
bool get_supports_target_humidity() const { return this->has_feature_flags(CLIMATE_SUPPORTS_TARGET_HUMIDITY); }
ESPDEPRECATED("This method is deprecated, use add_feature_flags() instead", "2025.11.0")
void set_supports_target_humidity(bool supports_target_humidity) {
this->supports_target_humidity_ = supports_target_humidity;
if (supports_target_humidity) {
this->add_feature_flags(CLIMATE_SUPPORTS_TARGET_HUMIDITY);
} else {
this->clear_feature_flags(CLIMATE_SUPPORTS_TARGET_HUMIDITY);
}
}
ESPDEPRECATED("This method is deprecated, use get_feature_flags() instead", "2025.11.0")
bool get_supports_action() const { return this->has_feature_flags(CLIMATE_SUPPORTS_ACTION); }
ESPDEPRECATED("This method is deprecated, use add_feature_flags() instead", "2025.11.0")
void set_supports_action(bool supports_action) {
if (supports_action) {
this->add_feature_flags(CLIMATE_SUPPORTS_ACTION);
} else {
this->clear_feature_flags(CLIMATE_SUPPORTS_ACTION);
}
}
void set_supported_modes(std::set<ClimateMode> modes) { this->supported_modes_ = std::move(modes); }
void add_supported_mode(ClimateMode mode) { this->supported_modes_.insert(mode); }
ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
void set_supports_auto_mode(bool supports_auto_mode) { set_mode_support_(CLIMATE_MODE_AUTO, supports_auto_mode); }
ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
void set_supports_cool_mode(bool supports_cool_mode) { set_mode_support_(CLIMATE_MODE_COOL, supports_cool_mode); }
ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
void set_supports_heat_mode(bool supports_heat_mode) { set_mode_support_(CLIMATE_MODE_HEAT, supports_heat_mode); }
ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
void set_supports_heat_cool_mode(bool supported) { set_mode_support_(CLIMATE_MODE_HEAT_COOL, supported); }
ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
void set_supports_fan_only_mode(bool supports_fan_only_mode) {
set_mode_support_(CLIMATE_MODE_FAN_ONLY, supports_fan_only_mode);
}
ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
void set_supports_dry_mode(bool supports_dry_mode) { set_mode_support_(CLIMATE_MODE_DRY, supports_dry_mode); }
bool supports_mode(ClimateMode mode) const { return this->supported_modes_.count(mode); }
const std::set<ClimateMode> &get_supported_modes() const { return this->supported_modes_; }
void set_supports_action(bool supports_action) { this->supports_action_ = supports_action; }
bool get_supports_action() const { return this->supports_action_; }
void set_supported_fan_modes(std::set<ClimateFanMode> modes) { this->supported_fan_modes_ = std::move(modes); }
void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); }
void add_supported_custom_fan_mode(const std::string &mode) { this->supported_custom_fan_modes_.insert(mode); }
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
void set_supports_fan_mode_on(bool supported) { set_fan_mode_support_(CLIMATE_FAN_ON, supported); }
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
void set_supports_fan_mode_off(bool supported) { set_fan_mode_support_(CLIMATE_FAN_OFF, supported); }
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
void set_supports_fan_mode_auto(bool supported) { set_fan_mode_support_(CLIMATE_FAN_AUTO, supported); }
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
void set_supports_fan_mode_low(bool supported) { set_fan_mode_support_(CLIMATE_FAN_LOW, supported); }
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
void set_supports_fan_mode_medium(bool supported) { set_fan_mode_support_(CLIMATE_FAN_MEDIUM, supported); }
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
void set_supports_fan_mode_high(bool supported) { set_fan_mode_support_(CLIMATE_FAN_HIGH, supported); }
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
void set_supports_fan_mode_middle(bool supported) { set_fan_mode_support_(CLIMATE_FAN_MIDDLE, supported); }
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
void set_supports_fan_mode_focus(bool supported) { set_fan_mode_support_(CLIMATE_FAN_FOCUS, supported); }
ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
void set_supports_fan_mode_diffuse(bool supported) { set_fan_mode_support_(CLIMATE_FAN_DIFFUSE, supported); }
bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode); }
bool get_supports_fan_modes() const {
return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty();
@@ -137,16 +146,6 @@ class ClimateTraits {
void set_supported_swing_modes(std::set<ClimateSwingMode> modes) { this->supported_swing_modes_ = std::move(modes); }
void add_supported_swing_mode(ClimateSwingMode mode) { this->supported_swing_modes_.insert(mode); }
ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20")
void set_supports_swing_mode_off(bool supported) { set_swing_mode_support_(CLIMATE_SWING_OFF, supported); }
ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20")
void set_supports_swing_mode_both(bool supported) { set_swing_mode_support_(CLIMATE_SWING_BOTH, supported); }
ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20")
void set_supports_swing_mode_vertical(bool supported) { set_swing_mode_support_(CLIMATE_SWING_VERTICAL, supported); }
ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20")
void set_supports_swing_mode_horizontal(bool supported) {
set_swing_mode_support_(CLIMATE_SWING_HORIZONTAL, supported);
}
bool supports_swing_mode(ClimateSwingMode swing_mode) const { return this->supported_swing_modes_.count(swing_mode); }
bool get_supports_swing_modes() const { return !this->supported_swing_modes_.empty(); }
const std::set<ClimateSwingMode> &get_supported_swing_modes() const { return this->supported_swing_modes_; }
@@ -219,24 +218,20 @@ class ClimateTraits {
}
}
bool supports_current_temperature_{false};
bool supports_current_humidity_{false};
bool supports_two_point_target_temperature_{false};
bool supports_target_humidity_{false};
std::set<climate::ClimateMode> supported_modes_ = {climate::CLIMATE_MODE_OFF};
bool supports_action_{false};
std::set<climate::ClimateFanMode> supported_fan_modes_;
std::set<climate::ClimateSwingMode> supported_swing_modes_;
std::set<climate::ClimatePreset> supported_presets_;
std::set<std::string> supported_custom_fan_modes_;
std::set<std::string> supported_custom_presets_;
uint32_t feature_flags_{0};
float visual_min_temperature_{10};
float visual_max_temperature_{30};
float visual_target_temperature_step_{0.1};
float visual_current_temperature_step_{0.1};
float visual_min_humidity_{30};
float visual_max_humidity_{99};
std::set<climate::ClimateMode> supported_modes_ = {climate::CLIMATE_MODE_OFF};
std::set<climate::ClimateFanMode> supported_fan_modes_;
std::set<climate::ClimateSwingMode> supported_swing_modes_;
std::set<climate::ClimatePreset> supported_presets_;
std::set<std::string> supported_custom_fan_modes_;
std::set<std::string> supported_custom_presets_;
};
} // namespace climate

View File

@@ -5,7 +5,7 @@ namespace dashboard_import {
static std::string g_package_import_url; // NOLINT
std::string get_package_import_url() { return g_package_import_url; }
const std::string &get_package_import_url() { return g_package_import_url; }
void set_package_import_url(std::string url) { g_package_import_url = std::move(url); }
} // namespace dashboard_import

View File

@@ -5,7 +5,7 @@
namespace esphome {
namespace dashboard_import {
std::string get_package_import_url();
const std::string &get_package_import_url();
void set_package_import_url(std::string url);
} // namespace dashboard_import

View File

@@ -30,14 +30,12 @@ class DateTimeBase : public EntityBase {
#endif
};
#ifdef USE_TIME
class DateTimeStateTrigger : public Trigger<ESPTime> {
public:
explicit DateTimeStateTrigger(DateTimeBase *parent) {
parent->add_on_state_callback([this, parent]() { this->trigger(parent->state_as_esptime()); });
}
};
#endif
} // namespace datetime
} // namespace esphome

View File

@@ -11,8 +11,6 @@
#include <esp_chip_info.h>
#include <esp_partition.h>
#include <map>
#ifdef USE_ARDUINO
#include <Esp.h>
#endif
@@ -125,7 +123,12 @@ void DebugComponent::log_partition_info_() {
uint32_t DebugComponent::get_free_heap_() { return heap_caps_get_free_size(MALLOC_CAP_INTERNAL); }
static const std::map<int, const char *> CHIP_FEATURES = {
struct ChipFeature {
int bit;
const char *name;
};
static constexpr ChipFeature CHIP_FEATURES[] = {
{CHIP_FEATURE_BLE, "BLE"},
{CHIP_FEATURE_BT, "BT"},
{CHIP_FEATURE_EMB_FLASH, "EMB Flash"},
@@ -170,11 +173,13 @@ void DebugComponent::get_device_info_(std::string &device_info) {
esp_chip_info(&info);
const char *model = ESPHOME_VARIANT;
std::string features;
for (auto feature : CHIP_FEATURES) {
if (info.features & feature.first) {
features += feature.second;
// Check each known feature bit
for (const auto &feature : CHIP_FEATURES) {
if (info.features & feature.bit) {
features += feature.name;
features += ", ";
info.features &= ~feature.first;
info.features &= ~feature.bit;
}
}
if (info.features != 0)

View File

@@ -25,10 +25,37 @@ static void show_reset_reason(std::string &reset_reason, bool set, const char *r
reset_reason += reason;
}
inline uint32_t read_mem_u32(uintptr_t addr) {
static inline uint32_t read_mem_u32(uintptr_t addr) {
return *reinterpret_cast<volatile uint32_t *>(addr); // NOLINT(performance-no-int-to-ptr)
}
static inline uint8_t read_mem_u8(uintptr_t addr) {
return *reinterpret_cast<volatile uint8_t *>(addr); // NOLINT(performance-no-int-to-ptr)
}
// defines from https://github.com/adafruit/Adafruit_nRF52_Bootloader which prints those information
constexpr uint32_t SD_MAGIC_NUMBER = 0x51B1E5DB;
constexpr uintptr_t MBR_SIZE = 0x1000;
constexpr uintptr_t SOFTDEVICE_INFO_STRUCT_OFFSET = 0x2000;
constexpr uintptr_t SD_ID_OFFSET = SOFTDEVICE_INFO_STRUCT_OFFSET + 0x10;
constexpr uintptr_t SD_VERSION_OFFSET = SOFTDEVICE_INFO_STRUCT_OFFSET + 0x14;
static inline bool is_sd_present() {
return read_mem_u32(SOFTDEVICE_INFO_STRUCT_OFFSET + MBR_SIZE + 4) == SD_MAGIC_NUMBER;
}
static inline uint32_t sd_id_get() {
if (read_mem_u8(MBR_SIZE + SOFTDEVICE_INFO_STRUCT_OFFSET) > (SD_ID_OFFSET - SOFTDEVICE_INFO_STRUCT_OFFSET)) {
return read_mem_u32(MBR_SIZE + SD_ID_OFFSET);
}
return 0;
}
static inline uint32_t sd_version_get() {
if (read_mem_u8(MBR_SIZE + SOFTDEVICE_INFO_STRUCT_OFFSET) > (SD_VERSION_OFFSET - SOFTDEVICE_INFO_STRUCT_OFFSET)) {
return read_mem_u32(MBR_SIZE + SD_VERSION_OFFSET);
}
return 0;
}
std::string DebugComponent::get_reset_reason_() {
uint32_t cause;
auto ret = hwinfo_get_reset_cause(&cause);
@@ -271,6 +298,29 @@ void DebugComponent::get_device_info_(std::string &device_info) {
NRF_UICR->NRFFW[0]);
ESP_LOGD(TAG, "MBR param page addr 0x%08x, UICR param page addr 0x%08x", read_mem_u32(MBR_PARAM_PAGE_ADDR),
NRF_UICR->NRFFW[1]);
if (is_sd_present()) {
uint32_t const sd_id = sd_id_get();
uint32_t const sd_version = sd_version_get();
uint32_t ver[3];
ver[0] = sd_version / 1000000;
ver[1] = (sd_version - ver[0] * 1000000) / 1000;
ver[2] = (sd_version - ver[0] * 1000000 - ver[1] * 1000);
ESP_LOGD(TAG, "SoftDevice: S%u %u.%u.%u", sd_id, ver[0], ver[1], ver[2]);
#ifdef USE_SOFTDEVICE_ID
#ifdef USE_SOFTDEVICE_VERSION
if (USE_SOFTDEVICE_ID != sd_id || USE_SOFTDEVICE_VERSION != ver[0]) {
ESP_LOGE(TAG, "Built for SoftDevice S%u %u.x.y. It may crash due to mismatch of bootloader version.",
USE_SOFTDEVICE_ID, USE_SOFTDEVICE_VERSION);
}
#else
if (USE_SOFTDEVICE_ID != sd_id) {
ESP_LOGE(TAG, "Built for SoftDevice S%u. It may crash due to mismatch of bootloader version.", USE_SOFTDEVICE_ID);
}
#endif
#endif
}
#endif
}

View File

@@ -775,7 +775,7 @@ void Display::test_card() {
int shift_y = (h - image_h) / 2;
int line_w = (image_w - 6) / 6;
int image_c = image_w / 2;
for (auto i = 0; i <= image_h; i++) {
for (auto i = 0; i != image_h; i++) {
int c = esp_scale(i, image_h);
this->horizontal_line(shift_x + 0, shift_y + i, line_w, r.fade_to_white(c));
this->horizontal_line(shift_x + line_w, shift_y + i, line_w, r.fade_to_black(c)); //
@@ -809,8 +809,11 @@ void Display::test_card() {
}
}
}
this->rectangle(0, 0, w, h, Color(127, 0, 127));
this->filled_rectangle(0, 0, 10, 10, Color(255, 0, 255));
this->filled_rectangle(w - 10, 0, 10, 10, Color(255, 0, 255));
this->filled_rectangle(0, h - 10, 10, 10, Color(255, 0, 255));
this->filled_rectangle(w - 10, h - 10, 10, 10, Color(255, 0, 255));
this->rectangle(0, 0, w, h, Color(255, 255, 255));
this->stop_poller();
}

View File

@@ -0,0 +1 @@
CODEOWNERS = ["@esphome/core"]

View File

@@ -0,0 +1,80 @@
from esphome import core, pins
import esphome.codegen as cg
from esphome.components import display, spi
import esphome.config_validation as cv
from esphome.const import (
CONF_BUSY_PIN,
CONF_DC_PIN,
CONF_ID,
CONF_LAMBDA,
CONF_MODEL,
CONF_PAGES,
CONF_RESET_DURATION,
CONF_RESET_PIN,
)
AUTO_LOAD = ["split_buffer"]
DEPENDENCIES = ["spi"]
epaper_spi_ns = cg.esphome_ns.namespace("epaper_spi")
EPaperBase = epaper_spi_ns.class_(
"EPaperBase", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer
)
EPaperSpectraE6 = epaper_spi_ns.class_("EPaperSpectraE6", EPaperBase)
EPaper7p3InSpectraE6 = epaper_spi_ns.class_("EPaper7p3InSpectraE6", EPaperSpectraE6)
MODELS = {
"7.3in-spectra-e6": EPaper7p3InSpectraE6,
}
CONFIG_SCHEMA = cv.All(
display.FULL_DISPLAY_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(EPaperBase),
cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema,
cv.Required(CONF_MODEL): cv.one_of(*MODELS, lower=True, space="-"),
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_BUSY_PIN): pins.gpio_input_pin_schema,
cv.Optional(CONF_RESET_DURATION): cv.All(
cv.positive_time_period_milliseconds,
cv.Range(max=core.TimePeriod(milliseconds=500)),
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(spi.spi_device_schema()),
cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
)
FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
"epaper_spi", require_miso=False, require_mosi=True
)
async def to_code(config):
model = MODELS[config[CONF_MODEL]]
rhs = model.new()
var = cg.Pvariable(config[CONF_ID], rhs, model)
await display.register_display(var, config)
await spi.register_spi_device(var, config)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
cg.add(var.set_dc_pin(dc))
if CONF_LAMBDA in config:
lambda_ = await cg.process_lambda(
config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void
)
cg.add(var.set_writer(lambda_))
if CONF_RESET_PIN in config:
reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN])
cg.add(var.set_reset_pin(reset))
if CONF_BUSY_PIN in config:
busy = await cg.gpio_pin_expression(config[CONF_BUSY_PIN])
cg.add(var.set_busy_pin(busy))
if CONF_RESET_DURATION in config:
cg.add(var.set_reset_duration(config[CONF_RESET_DURATION]))

View File

@@ -0,0 +1,227 @@
#include "epaper_spi.h"
#include <cinttypes>
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::epaper_spi {
static const char *const TAG = "epaper_spi";
static const LogString *epaper_state_to_string(EPaperState state) {
switch (state) {
case EPaperState::IDLE:
return LOG_STR("IDLE");
case EPaperState::UPDATE:
return LOG_STR("UPDATE");
case EPaperState::RESET:
return LOG_STR("RESET");
case EPaperState::INITIALISE:
return LOG_STR("INITIALISE");
case EPaperState::TRANSFER_DATA:
return LOG_STR("TRANSFER_DATA");
case EPaperState::POWER_ON:
return LOG_STR("POWER_ON");
case EPaperState::REFRESH_SCREEN:
return LOG_STR("REFRESH_SCREEN");
case EPaperState::POWER_OFF:
return LOG_STR("POWER_OFF");
case EPaperState::DEEP_SLEEP:
return LOG_STR("DEEP_SLEEP");
default:
return LOG_STR("UNKNOWN");
}
}
void EPaperBase::setup() {
if (!this->init_buffer_(this->get_buffer_length())) {
this->mark_failed("Failed to initialise buffer");
return;
}
this->setup_pins_();
this->spi_setup();
}
bool EPaperBase::init_buffer_(size_t buffer_length) {
if (!this->buffer_.init(buffer_length)) {
return false;
}
this->clear();
return true;
}
void EPaperBase::setup_pins_() {
this->dc_pin_->setup(); // OUTPUT
this->dc_pin_->digital_write(false);
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup(); // OUTPUT
this->reset_pin_->digital_write(true);
}
if (this->busy_pin_ != nullptr) {
this->busy_pin_->setup(); // INPUT
}
}
float EPaperBase::get_setup_priority() const { return setup_priority::PROCESSOR; }
void EPaperBase::command(uint8_t value) {
this->start_command_();
this->write_byte(value);
this->end_command_();
}
void EPaperBase::data(uint8_t value) {
this->start_data_();
this->write_byte(value);
this->end_data_();
}
// write a command followed by zero or more bytes of data.
// The command is the first byte, length is the length of data only in the second byte, followed by the data.
// [COMMAND, LENGTH, DATA...]
void EPaperBase::cmd_data(const uint8_t *data) {
const uint8_t command = data[0];
const uint8_t length = data[1];
const uint8_t *ptr = data + 2;
ESP_LOGVV(TAG, "Command: 0x%02X, Length: %d, Data: %s", command, length,
format_hex_pretty(ptr, length, '.', false).c_str());
this->dc_pin_->digital_write(false);
this->enable();
this->write_byte(command);
if (length > 0) {
this->dc_pin_->digital_write(true);
this->write_array(ptr, length);
}
this->disable();
}
bool EPaperBase::is_idle_() {
if (this->busy_pin_ == nullptr) {
return true;
}
return !this->busy_pin_->digital_read();
}
void EPaperBase::reset() {
if (this->reset_pin_ != nullptr) {
this->reset_pin_->digital_write(false);
this->disable_loop();
this->set_timeout(this->reset_duration_, [this] {
this->reset_pin_->digital_write(true);
this->set_timeout(20, [this] { this->enable_loop(); });
});
}
}
void EPaperBase::update() {
if (!this->state_queue_.empty()) {
ESP_LOGE(TAG, "Display update already in progress - %s",
LOG_STR_ARG(epaper_state_to_string(this->state_queue_.front())));
return;
}
this->state_queue_.push(EPaperState::UPDATE);
this->state_queue_.push(EPaperState::RESET);
this->state_queue_.push(EPaperState::INITIALISE);
this->state_queue_.push(EPaperState::TRANSFER_DATA);
this->state_queue_.push(EPaperState::POWER_ON);
this->state_queue_.push(EPaperState::REFRESH_SCREEN);
this->state_queue_.push(EPaperState::POWER_OFF);
this->state_queue_.push(EPaperState::DEEP_SLEEP);
this->state_queue_.push(EPaperState::IDLE);
this->enable_loop();
}
void EPaperBase::loop() {
if (this->waiting_for_idle_) {
if (this->is_idle_()) {
this->waiting_for_idle_ = false;
} else {
if (App.get_loop_component_start_time() - this->waiting_for_idle_last_print_ >= 1000) {
ESP_LOGV(TAG, "Waiting for idle");
this->waiting_for_idle_last_print_ = App.get_loop_component_start_time();
}
return;
}
}
auto state = this->state_queue_.front();
switch (state) {
case EPaperState::IDLE:
this->disable_loop();
break;
case EPaperState::UPDATE:
this->do_update_(); // Calls ESPHome (current page) lambda
break;
case EPaperState::RESET:
this->reset();
break;
case EPaperState::INITIALISE:
this->initialise_();
break;
case EPaperState::TRANSFER_DATA:
if (!this->transfer_data()) {
return; // Not done yet, come back next loop
}
break;
case EPaperState::POWER_ON:
this->power_on();
break;
case EPaperState::REFRESH_SCREEN:
this->refresh_screen();
break;
case EPaperState::POWER_OFF:
this->power_off();
break;
case EPaperState::DEEP_SLEEP:
this->deep_sleep();
break;
}
this->state_queue_.pop();
}
void EPaperBase::start_command_() {
this->dc_pin_->digital_write(false);
this->enable();
}
void EPaperBase::end_command_() { this->disable(); }
void EPaperBase::start_data_() {
this->dc_pin_->digital_write(true);
this->enable();
}
void EPaperBase::end_data_() { this->disable(); }
void EPaperBase::on_safe_shutdown() { this->deep_sleep(); }
void EPaperBase::initialise_() {
size_t index = 0;
const auto &sequence = this->init_sequence_;
const size_t sequence_size = this->init_sequence_length_;
while (index != sequence_size) {
if (sequence_size - index < 2) {
this->mark_failed("Malformed init sequence");
return;
}
const auto *ptr = sequence + index;
const uint8_t length = ptr[1];
if (sequence_size - index < length + 2) {
this->mark_failed("Malformed init sequence");
return;
}
this->cmd_data(ptr);
index += length + 2;
}
this->power_on();
}
} // namespace esphome::epaper_spi

View File

@@ -0,0 +1,93 @@
#pragma once
#include "esphome/components/display/display_buffer.h"
#include "esphome/components/spi/spi.h"
#include "esphome/components/split_buffer/split_buffer.h"
#include "esphome/core/component.h"
#include <queue>
namespace esphome::epaper_spi {
enum class EPaperState : uint8_t {
IDLE,
UPDATE,
RESET,
INITIALISE,
TRANSFER_DATA,
POWER_ON,
REFRESH_SCREEN,
POWER_OFF,
DEEP_SLEEP,
};
static const uint8_t MAX_TRANSFER_TIME = 10; // Transfer in 10ms blocks to allow the loop to run
class EPaperBase : public display::DisplayBuffer,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
spi::DATA_RATE_2MHZ> {
public:
EPaperBase(const uint8_t *init_sequence, const size_t init_sequence_length)
: init_sequence_length_(init_sequence_length), init_sequence_(init_sequence) {}
void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; }
float get_setup_priority() const override;
void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; }
void set_busy_pin(GPIOPin *busy) { this->busy_pin_ = busy; }
void set_reset_duration(uint32_t reset_duration) { this->reset_duration_ = reset_duration; }
void command(uint8_t value);
void data(uint8_t value);
void cmd_data(const uint8_t *data);
void update() override;
void loop() override;
void setup() override;
void on_safe_shutdown() override;
protected:
bool is_idle_();
void setup_pins_();
virtual void reset();
void initialise_();
bool init_buffer_(size_t buffer_length);
virtual int get_width_controller() { return this->get_width_internal(); };
virtual void deep_sleep() = 0;
/**
* Send data to the device via SPI
* @return true if done, false if should be called next loop
*/
virtual bool transfer_data() = 0;
virtual void refresh_screen() = 0;
virtual void power_on() = 0;
virtual void power_off() = 0;
virtual uint32_t get_buffer_length() = 0;
void start_command_();
void end_command_();
void start_data_();
void end_data_();
const size_t init_sequence_length_{0};
size_t current_data_index_{0};
uint32_t reset_duration_{200};
uint32_t waiting_for_idle_last_print_{0};
GPIOPin *dc_pin_;
GPIOPin *busy_pin_{nullptr};
GPIOPin *reset_pin_{nullptr};
const uint8_t *init_sequence_{nullptr};
bool waiting_for_idle_{false};
split_buffer::SplitBuffer buffer_;
std::queue<EPaperState> state_queue_{{EPaperState::IDLE}};
};
} // namespace esphome::epaper_spi

View File

@@ -0,0 +1,42 @@
#include "epaper_spi_model_7p3in_spectra_e6.h"
namespace esphome::epaper_spi {
static constexpr const char *const TAG = "epaper_spi.7.3in-spectra-e6";
void EPaper7p3InSpectraE6::power_on() {
ESP_LOGI(TAG, "Power on");
this->command(0x04);
this->waiting_for_idle_ = true;
}
void EPaper7p3InSpectraE6::power_off() {
ESP_LOGI(TAG, "Power off");
this->command(0x02);
this->data(0x00);
this->waiting_for_idle_ = true;
}
void EPaper7p3InSpectraE6::refresh_screen() {
ESP_LOGI(TAG, "Refresh");
this->command(0x12);
this->data(0x00);
this->waiting_for_idle_ = true;
}
void EPaper7p3InSpectraE6::deep_sleep() {
ESP_LOGI(TAG, "Deep sleep");
this->command(0x07);
this->data(0xA5);
}
void EPaper7p3InSpectraE6::dump_config() {
LOG_DISPLAY("", "E-Paper SPI", this);
ESP_LOGCONFIG(TAG, " Model: 7.3in Spectra E6");
LOG_PIN(" Reset Pin: ", this->reset_pin_);
LOG_PIN(" DC Pin: ", this->dc_pin_);
LOG_PIN(" Busy Pin: ", this->busy_pin_);
LOG_UPDATE_INTERVAL(this);
}
} // namespace esphome::epaper_spi

View File

@@ -0,0 +1,45 @@
#pragma once
#include "epaper_spi_spectra_e6.h"
namespace esphome::epaper_spi {
class EPaper7p3InSpectraE6 : public EPaperSpectraE6 {
static constexpr const uint16_t WIDTH = 800;
static constexpr const uint16_t HEIGHT = 480;
// clang-format off
// Command, data length, data
static constexpr uint8_t INIT_SEQUENCE[] = {
0xAA, 6, 0x49, 0x55, 0x20, 0x08, 0x09, 0x18,
0x01, 1, 0x3F,
0x00, 2, 0x5F, 0x69,
0x03, 4, 0x00, 0x54, 0x00, 0x44,
0x05, 4, 0x40, 0x1F, 0x1F, 0x2C,
0x06, 4, 0x6F, 0x1F, 0x17, 0x49,
0x08, 4, 0x6F, 0x1F, 0x1F, 0x22,
0x30, 1, 0x03,
0x50, 1, 0x3F,
0x60, 2, 0x02, 0x00,
0x61, 4, WIDTH / 256, WIDTH % 256, HEIGHT / 256, HEIGHT % 256,
0x84, 1, 0x01,
0xE3, 1, 0x2F,
};
// clang-format on
public:
EPaper7p3InSpectraE6() : EPaperSpectraE6(INIT_SEQUENCE, sizeof(INIT_SEQUENCE)) {}
void dump_config() override;
protected:
int get_width_internal() override { return WIDTH; };
int get_height_internal() override { return HEIGHT; };
void refresh_screen() override;
void power_on() override;
void power_off() override;
void deep_sleep() override;
};
} // namespace esphome::epaper_spi

View File

@@ -0,0 +1,135 @@
#include "epaper_spi_spectra_e6.h"
#include "esphome/core/log.h"
namespace esphome::epaper_spi {
static constexpr const char *const TAG = "epaper_spi.6c";
static inline uint8_t color_to_hex(Color color) {
if (color.red > 127) {
if (color.green > 170) {
if (color.blue > 127) {
return 0x1; // White
} else {
return 0x2; // Yellow
}
} else {
return 0x3; // Red (or Magenta)
}
} else {
if (color.green > 127) {
if (color.blue > 127) {
return 0x5; // Cyan -> Blue
} else {
return 0x6; // Green
}
} else {
if (color.blue > 127) {
return 0x5; // Blue
} else {
return 0x0; // Black
}
}
}
}
void EPaperSpectraE6::fill(Color color) {
uint8_t pixel_color;
if (color.is_on()) {
pixel_color = color_to_hex(color);
} else {
pixel_color = 0x1;
}
// We store 8 bitset<3> in 3 bytes
// | byte 1 | byte 2 | byte 3 |
// |aaabbbaa|abbbaaab|bbaaabbb|
uint8_t byte_1 = pixel_color << 5 | pixel_color << 2 | pixel_color >> 1;
uint8_t byte_2 = pixel_color << 7 | pixel_color << 4 | pixel_color << 1 | pixel_color >> 2;
uint8_t byte_3 = pixel_color << 6 | pixel_color << 3 | pixel_color << 0;
const size_t buffer_length = this->get_buffer_length();
for (size_t i = 0; i < buffer_length; i += 3) {
this->buffer_[i + 0] = byte_1;
this->buffer_[i + 1] = byte_2;
this->buffer_[i + 2] = byte_3;
}
}
uint32_t EPaperSpectraE6::get_buffer_length() {
// 6 colors buffer, 1 pixel = 3 bits, we will store 8 pixels in 24 bits = 3 bytes
return this->get_width_controller() * this->get_height_internal() / 8u * 3u;
}
void HOT EPaperSpectraE6::draw_absolute_pixel_internal(int x, int y, Color color) {
if (x >= this->get_width_internal() || y >= this->get_height_internal() || x < 0 || y < 0)
return;
uint8_t pixel_bits = color_to_hex(color);
uint32_t pixel_position = x + y * this->get_width_controller();
uint32_t first_bit_position = pixel_position * 3;
uint32_t byte_position = first_bit_position / 8u;
uint32_t byte_subposition = first_bit_position % 8u;
if (byte_subposition <= 5) {
this->buffer_[byte_position] = (this->buffer_[byte_position] & (0xFF ^ (0b111 << (5 - byte_subposition)))) |
(pixel_bits << (5 - byte_subposition));
} else {
this->buffer_[byte_position] = (this->buffer_[byte_position] & (0xFF ^ (0b111 >> (byte_subposition - 5)))) |
(pixel_bits >> (byte_subposition - 5));
this->buffer_[byte_position + 1] =
(this->buffer_[byte_position + 1] & (0xFF ^ (0xFF & (0b111 << (13 - byte_subposition))))) |
(pixel_bits << (13 - byte_subposition));
}
}
bool HOT EPaperSpectraE6::transfer_data() {
const uint32_t start_time = App.get_loop_component_start_time();
if (this->current_data_index_ == 0) {
ESP_LOGV(TAG, "Sending data");
this->command(0x10);
}
uint8_t bytes_to_send[4]{0};
const size_t buffer_length = this->get_buffer_length();
for (size_t i = this->current_data_index_; i < buffer_length; i += 3) {
const uint32_t triplet = encode_uint24(this->buffer_[i + 0], this->buffer_[i + 1], this->buffer_[i + 2]);
// 8 pixels are stored in 3 bytes
// |aaabbbaa|abbbaaab|bbaaabbb|
// | byte 1 | byte 2 | byte 3 |
bytes_to_send[0] = ((triplet >> 17) & 0b01110000) | ((triplet >> 18) & 0b00000111);
bytes_to_send[1] = ((triplet >> 11) & 0b01110000) | ((triplet >> 12) & 0b00000111);
bytes_to_send[2] = ((triplet >> 5) & 0b01110000) | ((triplet >> 6) & 0b00000111);
bytes_to_send[3] = ((triplet << 1) & 0b01110000) | ((triplet << 0) & 0b00000111);
this->start_data_();
this->write_array(bytes_to_send, sizeof(bytes_to_send));
this->end_data_();
if (millis() - start_time > MAX_TRANSFER_TIME) {
// Let the main loop run and come back next loop
this->current_data_index_ = i + 3;
return false;
}
}
// Finished the entire dataset
this->current_data_index_ = 0;
return true;
}
void EPaperSpectraE6::reset() {
if (this->reset_pin_ != nullptr) {
this->disable_loop();
this->reset_pin_->digital_write(true);
this->set_timeout(20, [this] {
this->reset_pin_->digital_write(false);
delay(2);
this->reset_pin_->digital_write(true);
this->set_timeout(20, [this] { this->enable_loop(); });
});
}
}
} // namespace esphome::epaper_spi

View File

@@ -0,0 +1,23 @@
#pragma once
#include "epaper_spi.h"
namespace esphome::epaper_spi {
class EPaperSpectraE6 : public EPaperBase {
public:
EPaperSpectraE6(const uint8_t *init_sequence, const size_t init_sequence_length)
: EPaperBase(init_sequence, init_sequence_length) {}
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
void fill(Color color) override;
protected:
void draw_absolute_pixel_internal(int x, int y, Color color) override;
uint32_t get_buffer_length() override;
bool transfer_data() override;
void reset() override;
};
} // namespace esphome::epaper_spi

View File

@@ -1,3 +1,4 @@
import contextlib
from dataclasses import dataclass
import itertools
import logging
@@ -102,6 +103,10 @@ COMPILER_OPTIMIZATIONS = {
"SIZE": "CONFIG_COMPILER_OPTIMIZATION_SIZE",
}
# Socket limit configuration for ESP-IDF
# ESP-IDF CONFIG_LWIP_MAX_SOCKETS has range 1-253, default 10
DEFAULT_MAX_SOCKETS = 10 # ESP-IDF default
ARDUINO_ALLOWED_VARIANTS = [
VARIANT_ESP32,
VARIANT_ESP32C3,
@@ -304,6 +309,17 @@ 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)}.zip"
def _is_framework_url(source: str) -> str:
# platformio accepts many URL schemes for framework repositories and archives including http, https, git, file, and symlink
import urllib.parse
try:
parsed = urllib.parse.urlparse(source)
except ValueError:
return False
return bool(parsed.scheme)
# NOTE: Keep this in mind when updating the recommended version:
# * New framework historically have had some regressions, especially for WiFi.
# The new version needs to be thoroughly validated before changing the
@@ -313,12 +329,13 @@ def _format_framework_espidf_version(ver: cv.Version, release: str) -> str:
# The default/recommended arduino framework version
# - https://github.com/espressif/arduino-esp32/releases
ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
"recommended": cv.Version(3, 2, 1),
"latest": cv.Version(3, 3, 1),
"dev": cv.Version(3, 3, 1),
"recommended": cv.Version(3, 3, 2),
"latest": cv.Version(3, 3, 2),
"dev": cv.Version(3, 3, 2),
}
ARDUINO_PLATFORM_VERSION_LOOKUP = {
cv.Version(3, 3, 1): cv.Version(55, 3, 31),
cv.Version(3, 3, 2): cv.Version(55, 3, 31, "1"),
cv.Version(3, 3, 1): cv.Version(55, 3, 31, "1"),
cv.Version(3, 3, 0): cv.Version(55, 3, 30, "2"),
cv.Version(3, 2, 1): cv.Version(54, 3, 21, "2"),
cv.Version(3, 2, 0): cv.Version(54, 3, 20),
@@ -331,13 +348,13 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = {
# The default/recommended esp-idf framework version
# - https://github.com/espressif/esp-idf/releases
ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
"recommended": cv.Version(5, 4, 2),
"recommended": cv.Version(5, 5, 1),
"latest": cv.Version(5, 5, 1),
"dev": cv.Version(5, 5, 1),
}
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
cv.Version(5, 5, 1): cv.Version(55, 3, 31),
cv.Version(5, 5, 0): cv.Version(55, 3, 31),
cv.Version(5, 5, 1): cv.Version(55, 3, 31, "1"),
cv.Version(5, 5, 0): cv.Version(55, 3, 31, "1"),
cv.Version(5, 4, 2): cv.Version(54, 3, 21, "2"),
cv.Version(5, 4, 1): cv.Version(54, 3, 21, "2"),
cv.Version(5, 4, 0): cv.Version(54, 3, 21, "2"),
@@ -351,9 +368,9 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = {
# The platform-espressif32 version
# - https://github.com/pioarduino/platform-espressif32/releases
PLATFORM_VERSION_LOOKUP = {
"recommended": cv.Version(54, 3, 21, "2"),
"latest": cv.Version(55, 3, 31),
"dev": "https://github.com/pioarduino/platform-espressif32.git#develop",
"recommended": cv.Version(55, 3, 31, "1"),
"latest": cv.Version(55, 3, 31, "1"),
"dev": cv.Version(55, 3, 31, "1"),
}
@@ -386,6 +403,10 @@ def _check_versions(value):
value[CONF_SOURCE] = value.get(
CONF_SOURCE, _format_framework_arduino_version(version)
)
if _is_framework_url(value[CONF_SOURCE]):
value[CONF_SOURCE] = (
f"pioarduino/framework-arduinoespressif32@{value[CONF_SOURCE]}"
)
else:
if version < cv.Version(5, 0, 0):
raise cv.Invalid("Only ESP-IDF 5.0+ is supported.")
@@ -395,6 +416,8 @@ def _check_versions(value):
CONF_SOURCE,
_format_framework_espidf_version(version, value.get(CONF_RELEASE, None)),
)
if _is_framework_url(value[CONF_SOURCE]):
value[CONF_SOURCE] = f"pioarduino/framework-espidf@{value[CONF_SOURCE]}"
if CONF_PLATFORM_VERSION not in value:
if platform_lookup is None:
@@ -526,6 +549,7 @@ CONF_ENABLE_LWIP_MDNS_QUERIES = "enable_lwip_mdns_queries"
CONF_ENABLE_LWIP_BRIDGE_INTERFACE = "enable_lwip_bridge_interface"
CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING = "enable_lwip_tcpip_core_locking"
CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY = "enable_lwip_check_thread_safety"
CONF_DISABLE_LIBC_LOCKS_IN_IRAM = "disable_libc_locks_in_iram"
def _validate_idf_component(config: ConfigType) -> ConfigType:
@@ -588,6 +612,9 @@ FRAMEWORK_SCHEMA = cv.All(
cv.Optional(
CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, default=True
): cv.boolean,
cv.Optional(
CONF_DISABLE_LIBC_LOCKS_IN_IRAM, default=True
): cv.boolean,
cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean,
}
),
@@ -639,6 +666,7 @@ def _show_framework_migration_message(name: str, variant: str) -> None:
+ "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, " 📦 Custom-built firmware for your exact needs\n")
+ color(
AnsiFore.GREEN,
@@ -646,7 +674,6 @@ def _show_framework_migration_message(name: str, variant: str) -> None:
)
+ "\n"
+ "Trade-offs:\n"
+ color(AnsiFore.YELLOW, " ⏱️ Compile times are ~25% longer\n")
+ color(AnsiFore.YELLOW, " 🔄 Some components need migration\n")
+ "\n"
+ "What should I do?\n"
@@ -724,6 +751,72 @@ CONFIG_SCHEMA = cv.All(
FINAL_VALIDATE_SCHEMA = cv.Schema(final_validate)
def _configure_lwip_max_sockets(conf: dict) -> None:
"""Calculate and set CONFIG_LWIP_MAX_SOCKETS based on component needs.
Socket component tracks consumer needs via consume_sockets() called during config validation.
This function runs in to_code() after all components have registered their socket needs.
User-provided sdkconfig_options take precedence.
"""
from esphome.components.socket import KEY_SOCKET_CONSUMERS
# Check if user manually specified CONFIG_LWIP_MAX_SOCKETS
user_max_sockets = conf.get(CONF_SDKCONFIG_OPTIONS, {}).get(
"CONFIG_LWIP_MAX_SOCKETS"
)
socket_consumers: dict[str, int] = CORE.data.get(KEY_SOCKET_CONSUMERS, {})
total_sockets = sum(socket_consumers.values())
# Early return if no sockets registered and no user override
if total_sockets == 0 and user_max_sockets is None:
return
components_list = ", ".join(
f"{name}={count}" for name, count in sorted(socket_consumers.items())
)
# User specified their own value - respect it but warn if insufficient
if user_max_sockets is not None:
_LOGGER.info(
"Using user-provided CONFIG_LWIP_MAX_SOCKETS: %s",
user_max_sockets,
)
# Warn if user's value is less than what components need
if total_sockets > 0:
user_sockets_int = 0
with contextlib.suppress(ValueError, TypeError):
user_sockets_int = int(user_max_sockets)
if user_sockets_int < total_sockets:
_LOGGER.warning(
"CONFIG_LWIP_MAX_SOCKETS is set to %d but your configuration "
"needs %d sockets (registered: %s). You may experience socket "
"exhaustion errors. Consider increasing to at least %d.",
user_sockets_int,
total_sockets,
components_list,
total_sockets,
)
# User's value already added via sdkconfig_options processing
return
# Auto-calculate based on component needs
# Use at least the ESP-IDF default (10), or the total needed by components
max_sockets = max(DEFAULT_MAX_SOCKETS, total_sockets)
log_level = logging.INFO if max_sockets > DEFAULT_MAX_SOCKETS else logging.DEBUG
_LOGGER.log(
log_level,
"Setting CONFIG_LWIP_MAX_SOCKETS to %d (registered: %s)",
max_sockets,
components_list,
)
add_idf_sdkconfig_option("CONFIG_LWIP_MAX_SOCKETS", max_sockets)
async def to_code(config):
cg.add_platformio_option("board", config[CONF_BOARD])
cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE])
@@ -806,6 +899,9 @@ async def to_code(config):
# Disable dynamic log level control to save memory
add_idf_sdkconfig_option("CONFIG_LOG_DYNAMIC_LEVEL_CONTROL", False)
# Reduce PHY TX power in the event of a brownout
add_idf_sdkconfig_option("CONFIG_ESP_PHY_REDUCE_TX_POWER", True)
# Set default CPU frequency
add_idf_sdkconfig_option(
f"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_{config[CONF_CPU_FREQUENCY][:-3]}", True
@@ -830,6 +926,9 @@ async def to_code(config):
add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False)
if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False):
add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0)
_configure_lwip_max_sockets(conf)
if advanced.get(CONF_EXECUTE_FROM_PSRAM, False):
add_idf_sdkconfig_option("CONFIG_SPIRAM_FETCH_INSTRUCTIONS", True)
add_idf_sdkconfig_option("CONFIG_SPIRAM_RODATA", True)
@@ -846,6 +945,12 @@ async def to_code(config):
if advanced.get(CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, True):
add_idf_sdkconfig_option("CONFIG_LWIP_CHECK_THREAD_SAFETY", True)
# Disable placing libc locks in IRAM to save RAM
# This is safe for ESPHome since no IRAM ISRs (interrupts that run while cache is disabled)
# use libc lock APIs. Saves approximately 1.3KB (1,356 bytes) of IRAM.
if advanced.get(CONF_DISABLE_LIBC_LOCKS_IN_IRAM, True):
add_idf_sdkconfig_option("CONFIG_LIBC_LOCKS_PLACE_IN_IRAM", False)
cg.add_platformio_option("board_build.partitions", "partitions.csv")
if CONF_PARTITIONS in config:
add_extra_build_file(

View File

@@ -1564,6 +1564,10 @@ BOARDS = {
"name": "DFRobot Beetle ESP32-C3",
"variant": VARIANT_ESP32C3,
},
"dfrobot_firebeetle2_esp32c6": {
"name": "DFRobot FireBeetle 2 ESP32-C6",
"variant": VARIANT_ESP32C6,
},
"dfrobot_firebeetle2_esp32e": {
"name": "DFRobot Firebeetle 2 ESP32-E",
"variant": VARIANT_ESP32,
@@ -1604,6 +1608,22 @@ BOARDS = {
"name": "Ai-Thinker ESP-C3-M1-I-Kit",
"variant": VARIANT_ESP32C3,
},
"esp32-c5-devkitc-1": {
"name": "Espressif ESP32-C5-DevKitC-1 4MB no PSRAM",
"variant": VARIANT_ESP32C5,
},
"esp32-c5-devkitc1-n16r4": {
"name": "Espressif ESP32-C5-DevKitC-1 N16R4 (16 MB Flash Quad, 4 MB PSRAM Quad)",
"variant": VARIANT_ESP32C5,
},
"esp32-c5-devkitc1-n4": {
"name": "Espressif ESP32-C5-DevKitC-1 N4 (4MB no PSRAM)",
"variant": VARIANT_ESP32C5,
},
"esp32-c5-devkitc1-n8r4": {
"name": "Espressif ESP32-C5-DevKitC-1 N8R4 (8 MB Flash Quad, 4 MB PSRAM Quad)",
"variant": VARIANT_ESP32C5,
},
"esp32-c6-devkitc-1": {
"name": "Espressif ESP32-C6-DevKitC-1",
"variant": VARIANT_ESP32C6,
@@ -2048,6 +2068,10 @@ BOARDS = {
"name": "M5Stack Station",
"variant": VARIANT_ESP32,
},
"m5stack-tab5-p4": {
"name": "M5STACK Tab5 esp32-p4 Board",
"variant": VARIANT_ESP32P4,
},
"m5stack-timer-cam": {
"name": "M5Stack Timer CAM",
"variant": VARIANT_ESP32,
@@ -2476,6 +2500,10 @@ BOARDS = {
"name": "YelloByte YB-ESP32-S3-AMP (Rev.3)",
"variant": VARIANT_ESP32S3,
},
"yb_esp32s3_drv": {
"name": "YelloByte YB-ESP32-S3-DRV",
"variant": VARIANT_ESP32S3,
},
"yb_esp32s3_eth": {
"name": "YelloByte YB-ESP32-S3-ETH",
"variant": VARIANT_ESP32S3,

View File

@@ -6,6 +6,7 @@
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <esp_idf_version.h>
#include <esp_ota_ops.h>
#include <esp_task_wdt.h>
#include <esp_timer.h>
#include <soc/rtc.h>
@@ -52,6 +53,16 @@ void arch_init() {
disableCore1WDT();
#endif
#endif
// If the bootloader was compiled with CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE the current
// partition will get rolled back unless it is marked as valid.
esp_ota_img_states_t state;
const esp_partition_t *running = esp_ota_get_running_partition();
if (esp_ota_get_state_partition(running, &state) == ESP_OK) {
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(); }

View File

@@ -1,4 +1,5 @@
from collections.abc import Callable, MutableMapping
from dataclasses import dataclass
from enum import Enum
import logging
import re
@@ -16,7 +17,7 @@ from esphome.const import (
CONF_NAME,
CONF_NAME_ADD_MAC_SUFFIX,
)
from esphome.core import CORE, TimePeriod
from esphome.core import CORE, CoroPriority, TimePeriod, coroutine_with_priority
import esphome.final_validate as fv
DEPENDENCIES = ["esp32"]
@@ -107,8 +108,65 @@ class BTLoggers(Enum):
"""ESP32 WiFi provisioning over Bluetooth"""
# Set to track which loggers are needed by components
_required_loggers: set[BTLoggers] = set()
# Key for storing required loggers in CORE.data
ESP32_BLE_REQUIRED_LOGGERS_KEY = "esp32_ble_required_loggers"
def _get_required_loggers() -> set[BTLoggers]:
"""Get the set of required Bluetooth loggers from CORE.data."""
return CORE.data.setdefault(ESP32_BLE_REQUIRED_LOGGERS_KEY, set())
# Dataclass for handler registration counts
@dataclass
class HandlerCounts:
gap_event: int = 0
gap_scan_event: int = 0
gattc_event: int = 0
gatts_event: int = 0
ble_status_event: int = 0
# Track handler registration counts for StaticVector sizing
_handler_counts = HandlerCounts()
def register_gap_event_handler(parent_var: cg.MockObj, handler_var: cg.MockObj) -> None:
"""Register a GAP event handler and track the count."""
_handler_counts.gap_event += 1
cg.add(parent_var.register_gap_event_handler(handler_var))
def register_gap_scan_event_handler(
parent_var: cg.MockObj, handler_var: cg.MockObj
) -> None:
"""Register a GAP scan event handler and track the count."""
_handler_counts.gap_scan_event += 1
cg.add(parent_var.register_gap_scan_event_handler(handler_var))
def register_gattc_event_handler(
parent_var: cg.MockObj, handler_var: cg.MockObj
) -> None:
"""Register a GATTc event handler and track the count."""
_handler_counts.gattc_event += 1
cg.add(parent_var.register_gattc_event_handler(handler_var))
def register_gatts_event_handler(
parent_var: cg.MockObj, handler_var: cg.MockObj
) -> None:
"""Register a GATTs event handler and track the count."""
_handler_counts.gatts_event += 1
cg.add(parent_var.register_gatts_event_handler(handler_var))
def register_ble_status_event_handler(
parent_var: cg.MockObj, handler_var: cg.MockObj
) -> None:
"""Register a BLE status event handler and track the count."""
_handler_counts.ble_status_event += 1
cg.add(parent_var.register_ble_status_event_handler(handler_var))
def register_bt_logger(*loggers: BTLoggers) -> None:
@@ -117,12 +175,13 @@ def register_bt_logger(*loggers: BTLoggers) -> None:
Args:
*loggers: One or more BTLoggers enum members
"""
required_loggers = _get_required_loggers()
for logger in loggers:
if not isinstance(logger, BTLoggers):
raise TypeError(
f"Logger must be a BTLoggers enum member, got {type(logger)}"
)
_required_loggers.add(logger)
required_loggers.add(logger)
CONF_BLE_ID = "ble_id"
@@ -285,6 +344,10 @@ def consume_connection_slots(
def validate_connection_slots(max_connections: int) -> None:
"""Validate that BLE connection slots don't exceed the configured maximum."""
# Skip validation in testing mode to allow component grouping
if CORE.testing_mode:
return
ble_data = CORE.data.get(KEY_ESP32_BLE, {})
used_slots = ble_data.get(KEY_USED_CONNECTION_SLOTS, [])
num_used = len(used_slots)
@@ -330,14 +393,27 @@ def final_validation(config):
max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS)
validate_connection_slots(max_connections)
# Check if hosted bluetooth is being used
if "esp32_hosted" in full_config:
add_idf_sdkconfig_option("CONFIG_BT_CLASSIC_ENABLED", False)
add_idf_sdkconfig_option("CONFIG_BT_BLE_ENABLED", True)
add_idf_sdkconfig_option("CONFIG_BT_BLUEDROID_ENABLED", True)
add_idf_sdkconfig_option("CONFIG_BT_CONTROLLER_DISABLED", True)
add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID", True)
add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_BLUEDROID_HCI_VHCI", True)
# Check if BLE Server is needed
has_ble_server = "esp32_ble_server" in full_config
add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server)
# Check if BLE Client is needed (via esp32_ble_tracker or esp32_ble_client)
has_ble_client = (
"esp32_ble_tracker" in full_config or "esp32_ble_client" in full_config
)
# ESP-IDF BLE stack requires GATT Server to be enabled when GATT Client is enabled
# This is an internal dependency in the Bluedroid stack (tested ESP-IDF 5.4.2-5.5.1)
# See: https://github.com/espressif/esp-idf/issues/17724
add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server or has_ble_client)
add_idf_sdkconfig_option("CONFIG_BT_GATTC_ENABLE", has_ble_client)
# Handle max_connections: check for deprecated location in esp32_ble_tracker
@@ -366,6 +442,36 @@ def final_validation(config):
FINAL_VALIDATE_SCHEMA = final_validation
# This needs to be run as a job with CoroPriority.FINAL priority so that all components have
# a chance to register their handlers before the counts are added to defines.
@coroutine_with_priority(CoroPriority.FINAL)
async def _add_ble_handler_defines():
# Add defines for StaticVector sizing based on handler registration counts
# Only define if count > 0 to avoid allocating unnecessary memory
if _handler_counts.gap_event > 0:
cg.add_define(
"ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT", _handler_counts.gap_event
)
if _handler_counts.gap_scan_event > 0:
cg.add_define(
"ESPHOME_ESP32_BLE_GAP_SCAN_EVENT_HANDLER_COUNT",
_handler_counts.gap_scan_event,
)
if _handler_counts.gattc_event > 0:
cg.add_define(
"ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT", _handler_counts.gattc_event
)
if _handler_counts.gatts_event > 0:
cg.add_define(
"ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT", _handler_counts.gatts_event
)
if _handler_counts.ble_status_event > 0:
cg.add_define(
"ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT",
_handler_counts.ble_status_event,
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT]))
@@ -388,8 +494,9 @@ async def to_code(config):
# Apply logger settings if log disabling is enabled
if config.get(CONF_DISABLE_BT_LOGS, False):
# Disable all Bluetooth loggers that are not required
required_loggers = _get_required_loggers()
for logger in BTLoggers:
if logger not in _required_loggers:
if logger not in required_loggers:
add_idf_sdkconfig_option(f"{logger.value}_NONE", True)
# Set BLE connection establishment timeout to match aioesphomeapi/bleak-retry-connector
@@ -420,6 +527,9 @@ async def to_code(config):
cg.add_define("USE_ESP32_BLE_ADVERTISING")
cg.add_define("USE_ESP32_BLE_UUID")
# Schedule the handler defines to be added after all components register
CORE.add_job(_add_ble_handler_defines)
@automation.register_condition("ble.enabled", BLEEnabledCondition, cv.Schema({}))
async def ble_enabled_to_code(config, condition_id, template_arg, args):

View File

@@ -6,7 +6,15 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
#include <esp_bt.h>
#else
extern "C" {
#include <esp_hosted.h>
#include <esp_hosted_misc.h>
#include <esp_hosted_bluedroid.h>
}
#endif
#include <esp_bt_device.h>
#include <esp_bt_main.h>
#include <esp_gap_ble_api.h>
@@ -140,6 +148,7 @@ void ESP32BLE::advertising_init_() {
bool ESP32BLE::ble_setup_() {
esp_err_t err;
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
#ifdef USE_ARDUINO
if (!btStart()) {
ESP_LOGE(TAG, "btStart failed: %d", esp_bt_controller_get_status());
@@ -173,6 +182,28 @@ bool ESP32BLE::ble_setup_() {
#endif
esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT);
#else
esp_hosted_connect_to_slave(); // NOLINT
if (esp_hosted_bt_controller_init() != ESP_OK) {
ESP_LOGW(TAG, "esp_hosted_bt_controller_init failed");
return false;
}
if (esp_hosted_bt_controller_enable() != ESP_OK) {
ESP_LOGW(TAG, "esp_hosted_bt_controller_enable failed");
return false;
}
hosted_hci_bluedroid_open();
esp_bluedroid_hci_driver_operations_t operations = {
.send = hosted_hci_bluedroid_send,
.check_send_available = hosted_hci_bluedroid_check_send_available,
.register_host_callback = hosted_hci_bluedroid_register_host_callback,
};
esp_bluedroid_attach_hci_driver(&operations);
#endif
err = esp_bluedroid_init();
if (err != ESP_OK) {
@@ -185,31 +216,27 @@ bool ESP32BLE::ble_setup_() {
return false;
}
if (!this->gap_event_handlers_.empty()) {
err = esp_ble_gap_register_callback(ESP32BLE::gap_event_handler);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gap_register_callback failed: %d", err);
return false;
}
}
#ifdef USE_ESP32_BLE_SERVER
if (!this->gatts_event_handlers_.empty()) {
err = esp_ble_gatts_register_callback(ESP32BLE::gatts_event_handler);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gatts_register_callback failed: %d", err);
return false;
}
#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT
err = esp_ble_gap_register_callback(ESP32BLE::gap_event_handler);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gap_register_callback failed: %d", err);
return false;
}
#endif
#ifdef USE_ESP32_BLE_CLIENT
if (!this->gattc_event_handlers_.empty()) {
err = esp_ble_gattc_register_callback(ESP32BLE::gattc_event_handler);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gattc_register_callback failed: %d", err);
return false;
}
#if defined(USE_ESP32_BLE_SERVER) && defined(ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT)
err = esp_ble_gatts_register_callback(ESP32BLE::gatts_event_handler);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gatts_register_callback failed: %d", err);
return false;
}
#endif
#if defined(USE_ESP32_BLE_CLIENT) && defined(ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT)
err = esp_ble_gattc_register_callback(ESP32BLE::gattc_event_handler);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gattc_register_callback failed: %d", err);
return false;
}
#endif
@@ -217,8 +244,11 @@ bool ESP32BLE::ble_setup_() {
if (this->name_.has_value()) {
name = this->name_.value();
if (App.is_name_add_mac_suffix_enabled()) {
name += "-";
name += get_mac_address().substr(6);
// MAC address suffix length (last 6 characters of 12-char MAC address string)
constexpr size_t mac_address_suffix_len = 6;
const std::string mac_addr = get_mac_address();
const char *mac_suffix_ptr = mac_addr.c_str() + mac_address_suffix_len;
name = make_name_with_suffix(name, '-', mac_suffix_ptr, mac_address_suffix_len);
}
} else {
name = App.get_name();
@@ -262,6 +292,7 @@ bool ESP32BLE::ble_dismantle_() {
return false;
}
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
#ifdef USE_ARDUINO
if (!btStop()) {
ESP_LOGE(TAG, "btStop failed: %d", esp_bt_controller_get_status());
@@ -291,6 +322,19 @@ bool ESP32BLE::ble_dismantle_() {
return false;
}
}
#endif
#else
if (esp_hosted_bt_controller_disable() != ESP_OK) {
ESP_LOGW(TAG, "esp_hosted_bt_controller_disable failed");
return false;
}
if (esp_hosted_bt_controller_deinit(false) != ESP_OK) {
ESP_LOGW(TAG, "esp_hosted_bt_controller_deinit failed");
return false;
}
hosted_hci_bluedroid_close();
#endif
return true;
}
@@ -303,9 +347,11 @@ void ESP32BLE::loop() {
case BLE_COMPONENT_STATE_DISABLE: {
ESP_LOGD(TAG, "Disabling");
#ifdef ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT
for (auto *ble_event_handler : this->ble_status_event_handlers_) {
ble_event_handler->ble_before_disabled_event_handler();
}
#endif
if (!ble_dismantle_()) {
ESP_LOGE(TAG, "Could not be dismantled");
@@ -335,7 +381,7 @@ void ESP32BLE::loop() {
BLEEvent *ble_event = this->ble_events_.pop();
while (ble_event != nullptr) {
switch (ble_event->type_) {
#ifdef USE_ESP32_BLE_SERVER
#if defined(USE_ESP32_BLE_SERVER) && defined(ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT)
case BLEEvent::GATTS: {
esp_gatts_cb_event_t event = ble_event->event_.gatts.gatts_event;
esp_gatt_if_t gatts_if = ble_event->event_.gatts.gatts_if;
@@ -347,7 +393,7 @@ void ESP32BLE::loop() {
break;
}
#endif
#ifdef USE_ESP32_BLE_CLIENT
#if defined(USE_ESP32_BLE_CLIENT) && defined(ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT)
case BLEEvent::GATTC: {
esp_gattc_cb_event_t event = ble_event->event_.gattc.gattc_event;
esp_gatt_if_t gattc_if = ble_event->event_.gattc.gattc_if;
@@ -363,10 +409,12 @@ void ESP32BLE::loop() {
esp_gap_ble_cb_event_t gap_event = ble_event->event_.gap.gap_event;
switch (gap_event) {
case ESP_GAP_BLE_SCAN_RESULT_EVT:
#ifdef ESPHOME_ESP32_BLE_GAP_SCAN_EVENT_HANDLER_COUNT
// Use the new scan event handler - no memcpy!
for (auto *scan_handler : this->gap_scan_event_handlers_) {
scan_handler->gap_scan_event_handler(ble_event->scan_result());
}
#endif
break;
// Scan complete events
@@ -378,10 +426,12 @@ void ESP32BLE::loop() {
// This is verified at compile-time by static_assert checks in ble_event.h
// The struct already contains our copy of the status (copied in BLEEvent constructor)
ESP_LOGV(TAG, "gap_event_handler - %d", gap_event);
#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT
for (auto *gap_handler : this->gap_event_handlers_) {
gap_handler->gap_event_handler(
gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.scan_complete));
}
#endif
break;
// Advertising complete events
@@ -392,19 +442,23 @@ void ESP32BLE::loop() {
case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
// All advertising complete events have the same structure with just status
ESP_LOGV(TAG, "gap_event_handler - %d", gap_event);
#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT
for (auto *gap_handler : this->gap_event_handlers_) {
gap_handler->gap_event_handler(
gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.adv_complete));
}
#endif
break;
// RSSI complete event
case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT:
ESP_LOGV(TAG, "gap_event_handler - %d", gap_event);
#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT
for (auto *gap_handler : this->gap_event_handlers_) {
gap_handler->gap_event_handler(
gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.read_rssi_complete));
}
#endif
break;
// Security events
@@ -414,10 +468,12 @@ void ESP32BLE::loop() {
case ESP_GAP_BLE_PASSKEY_REQ_EVT:
case ESP_GAP_BLE_NC_REQ_EVT:
ESP_LOGV(TAG, "gap_event_handler - %d", gap_event);
#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT
for (auto *gap_handler : this->gap_event_handlers_) {
gap_handler->gap_event_handler(
gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.security));
}
#endif
break;
default:

View File

@@ -126,19 +126,25 @@ class ESP32BLE : public Component {
void advertising_register_raw_advertisement_callback(std::function<void(bool)> &&callback);
#endif
#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT
void register_gap_event_handler(GAPEventHandler *handler) { this->gap_event_handlers_.push_back(handler); }
#endif
#ifdef ESPHOME_ESP32_BLE_GAP_SCAN_EVENT_HANDLER_COUNT
void register_gap_scan_event_handler(GAPScanEventHandler *handler) {
this->gap_scan_event_handlers_.push_back(handler);
}
#ifdef USE_ESP32_BLE_CLIENT
#endif
#if defined(USE_ESP32_BLE_CLIENT) && defined(ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT)
void register_gattc_event_handler(GATTcEventHandler *handler) { this->gattc_event_handlers_.push_back(handler); }
#endif
#ifdef USE_ESP32_BLE_SERVER
#if defined(USE_ESP32_BLE_SERVER) && defined(ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT)
void register_gatts_event_handler(GATTsEventHandler *handler) { this->gatts_event_handlers_.push_back(handler); }
#endif
#ifdef ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT
void register_ble_status_event_handler(BLEStatusEventHandler *handler) {
this->ble_status_event_handlers_.push_back(handler);
}
#endif
void set_enable_on_boot(bool enable_on_boot) { this->enable_on_boot_ = enable_on_boot; }
protected:
@@ -160,16 +166,22 @@ class ESP32BLE : public Component {
private:
template<typename... Args> friend void enqueue_ble_event(Args... args);
// Vectors (12 bytes each on 32-bit, naturally aligned to 4 bytes)
std::vector<GAPEventHandler *> gap_event_handlers_;
std::vector<GAPScanEventHandler *> gap_scan_event_handlers_;
#ifdef USE_ESP32_BLE_CLIENT
std::vector<GATTcEventHandler *> gattc_event_handlers_;
// Handler vectors - use StaticVector when counts are known at compile time
#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT
StaticVector<GAPEventHandler *, ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT> gap_event_handlers_;
#endif
#ifdef USE_ESP32_BLE_SERVER
std::vector<GATTsEventHandler *> gatts_event_handlers_;
#ifdef ESPHOME_ESP32_BLE_GAP_SCAN_EVENT_HANDLER_COUNT
StaticVector<GAPScanEventHandler *, ESPHOME_ESP32_BLE_GAP_SCAN_EVENT_HANDLER_COUNT> gap_scan_event_handlers_;
#endif
#if defined(USE_ESP32_BLE_CLIENT) && defined(ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT)
StaticVector<GATTcEventHandler *, ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT> gattc_event_handlers_;
#endif
#if defined(USE_ESP32_BLE_SERVER) && defined(ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT)
StaticVector<GATTsEventHandler *, ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT> gatts_event_handlers_;
#endif
#ifdef ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT
StaticVector<BLEStatusEventHandler *, ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT> ble_status_event_handlers_;
#endif
std::vector<BLEStatusEventHandler *> ble_status_event_handlers_;
// Large objects (size depends on template parameters, but typically aligned to 4 bytes)
esphome::LockFreeQueue<BLEEvent, MAX_BLE_QUEUE_SIZE> ble_events_;

View File

@@ -10,7 +10,9 @@
#ifdef USE_ESP32
#ifdef USE_ESP32_BLE_ADVERTISING
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
#include <esp_bt.h>
#endif
#include <esp_gap_ble_api.h>
#include <esp_gatts_api.h>

View File

@@ -74,7 +74,7 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID], uuid_arr)
parent = await cg.get_variable(config[esp32_ble.CONF_BLE_ID])
cg.add(parent.register_gap_event_handler(var))
esp32_ble.register_gap_event_handler(parent, var)
await cg.register_component(var, config)
cg.add(var.set_major(config[CONF_MAJOR]))

View File

@@ -4,7 +4,9 @@
#ifdef USE_ESP32
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
#include <esp_bt.h>
#endif
#include <esp_bt_main.h>
#include <esp_gap_ble_api.h>
#include <freertos/FreeRTOS.h>
@@ -15,10 +17,6 @@
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#ifdef USE_ARDUINO
#include <esp32-hal-bt.h>
#endif
namespace esphome {
namespace esp32_ble_beacon {

View File

@@ -5,7 +5,9 @@
#ifdef USE_ESP32
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
#include <esp_bt.h>
#endif
#include <esp_gap_ble_api.h>
namespace esphome {

View File

@@ -61,12 +61,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
this->address_str_ = "";
} else {
char buf[18];
uint8_t mac[6] = {
(uint8_t) ((this->address_ >> 40) & 0xff), (uint8_t) ((this->address_ >> 32) & 0xff),
(uint8_t) ((this->address_ >> 24) & 0xff), (uint8_t) ((this->address_ >> 16) & 0xff),
(uint8_t) ((this->address_ >> 8) & 0xff), (uint8_t) ((this->address_ >> 0) & 0xff),
};
format_mac_addr_upper(mac, buf);
format_mac_addr_upper(this->remote_bda_, buf);
this->address_str_ = buf;
}
}

View File

@@ -546,8 +546,8 @@ async def to_code(config):
await cg.register_component(var, config)
parent = await cg.get_variable(config[esp32_ble.CONF_BLE_ID])
cg.add(parent.register_gatts_event_handler(var))
cg.add(parent.register_ble_status_event_handler(var))
esp32_ble.register_gatts_event_handler(parent, var)
esp32_ble.register_ble_status_event_handler(parent, var)
cg.add(var.set_parent(parent))
cg.add(parent.advertising_set_appearance(config[CONF_APPEARANCE]))
if CONF_MANUFACTURER_DATA in config:

View File

@@ -10,7 +10,9 @@
#include <nvs_flash.h>
#include <freertos/FreeRTOSConfig.h>
#include <esp_bt_main.h>
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
#include <esp_bt.h>
#endif
#include <freertos/task.h>
#include <esp_gap_ble_api.h>

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
from dataclasses import dataclass
import logging
from esphome import automation
@@ -52,8 +53,28 @@ class BLEFeatures(StrEnum):
ESP_BT_DEVICE = "ESP_BT_DEVICE"
# Set to track which features are needed by components
_required_features: set[BLEFeatures] = set()
# Dataclass for registration counts
@dataclass
class RegistrationCounts:
listeners: int = 0
clients: int = 0
# CORE.data keys for state management
ESP32_BLE_TRACKER_REQUIRED_FEATURES_KEY = "esp32_ble_tracker_required_features"
ESP32_BLE_TRACKER_REGISTRATION_COUNTS_KEY = "esp32_ble_tracker_registration_counts"
def _get_required_features() -> set[BLEFeatures]:
"""Get the set of required BLE features from CORE.data."""
return CORE.data.setdefault(ESP32_BLE_TRACKER_REQUIRED_FEATURES_KEY, set())
def _get_registration_counts() -> RegistrationCounts:
"""Get the registration counts from CORE.data."""
return CORE.data.setdefault(
ESP32_BLE_TRACKER_REGISTRATION_COUNTS_KEY, RegistrationCounts()
)
def register_ble_features(features: set[BLEFeatures]) -> None:
@@ -62,7 +83,7 @@ def register_ble_features(features: set[BLEFeatures]) -> None:
Args:
features: Set of BLEFeatures enum members
"""
_required_features.update(features)
_get_required_features().update(features)
esp32_ble_tracker_ns = cg.esphome_ns.namespace("esp32_ble_tracker")
@@ -235,10 +256,10 @@ async def to_code(config):
await cg.register_component(var, config)
parent = await cg.get_variable(config[esp32_ble.CONF_BLE_ID])
cg.add(parent.register_gap_event_handler(var))
cg.add(parent.register_gap_scan_event_handler(var))
cg.add(parent.register_gattc_event_handler(var))
cg.add(parent.register_ble_status_event_handler(var))
esp32_ble.register_gap_event_handler(parent, var)
esp32_ble.register_gap_scan_event_handler(parent, var)
esp32_ble.register_gattc_event_handler(parent, var)
esp32_ble.register_ble_status_event_handler(parent, var)
cg.add(var.set_parent(parent))
params = config[CONF_SCAN_PARAMETERS]
@@ -256,13 +277,17 @@ async def to_code(config):
):
register_ble_features({BLEFeatures.ESP_BT_DEVICE})
registration_counts = _get_registration_counts()
for conf in config.get(CONF_ON_BLE_ADVERTISE, []):
registration_counts.listeners += 1
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
if CONF_MAC_ADDRESS in conf:
addr_list = [it.as_hex for it in conf[CONF_MAC_ADDRESS]]
cg.add(trigger.set_addresses(addr_list))
await automation.build_automation(trigger, [(ESPBTDeviceConstRef, "x")], conf)
for conf in config.get(CONF_ON_BLE_SERVICE_DATA_ADVERTISE, []):
registration_counts.listeners += 1
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
if len(conf[CONF_SERVICE_UUID]) == len(bt_uuid16_format):
cg.add(trigger.set_service_uuid16(as_hex(conf[CONF_SERVICE_UUID])))
@@ -275,6 +300,7 @@ async def to_code(config):
cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex))
await automation.build_automation(trigger, [(adv_data_t_const_ref, "x")], conf)
for conf in config.get(CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE, []):
registration_counts.listeners += 1
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
if len(conf[CONF_MANUFACTURER_ID]) == len(bt_uuid16_format):
cg.add(trigger.set_manufacturer_uuid16(as_hex(conf[CONF_MANUFACTURER_ID])))
@@ -287,6 +313,7 @@ async def to_code(config):
cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex))
await automation.build_automation(trigger, [(adv_data_t_const_ref, "x")], conf)
for conf in config.get(CONF_ON_SCAN_END, []):
registration_counts.listeners += 1
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
@@ -316,10 +343,23 @@ async def to_code(config):
@coroutine_with_priority(CoroPriority.FINAL)
async def _add_ble_features():
# Add feature-specific defines based on what's needed
if BLEFeatures.ESP_BT_DEVICE in _required_features:
required_features = _get_required_features()
if BLEFeatures.ESP_BT_DEVICE in required_features:
cg.add_define("USE_ESP32_BLE_DEVICE")
cg.add_define("USE_ESP32_BLE_UUID")
# Add defines for StaticVector sizing based on registration counts
# Only define if count > 0 to avoid allocating unnecessary memory
registration_counts = _get_registration_counts()
if registration_counts.listeners > 0:
cg.add_define(
"ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT", registration_counts.listeners
)
if registration_counts.clients > 0:
cg.add_define(
"ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT", registration_counts.clients
)
ESP32_BLE_START_SCAN_ACTION_SCHEMA = cv.Schema(
{
@@ -369,6 +409,7 @@ async def register_ble_device(
var: cg.SafeExpType, config: ConfigType
) -> cg.SafeExpType:
register_ble_features({BLEFeatures.ESP_BT_DEVICE})
_get_registration_counts().listeners += 1
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
cg.add(paren.register_listener(var))
return var
@@ -376,6 +417,7 @@ async def register_ble_device(
async def register_client(var: cg.SafeExpType, config: ConfigType) -> cg.SafeExpType:
register_ble_features({BLEFeatures.ESP_BT_DEVICE})
_get_registration_counts().clients += 1
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
cg.add(paren.register_client(var))
return var
@@ -389,6 +431,7 @@ async def register_raw_ble_device(
This does NOT register the ESP_BT_DEVICE feature, meaning ESPBTDevice
will not be compiled in if this is the only registration method used.
"""
_get_registration_counts().listeners += 1
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
cg.add(paren.register_listener(var))
return var
@@ -402,6 +445,7 @@ async def register_raw_client(
This does NOT register the ESP_BT_DEVICE feature, meaning ESPBTDevice
will not be compiled in if this is the only registration method used.
"""
_get_registration_counts().clients += 1
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
cg.add(paren.register_client(var))
return var

View File

@@ -7,7 +7,9 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
#include <esp_bt.h>
#endif
#include <esp_bt_defs.h>
#include <esp_bt_main.h>
#include <esp_gap_ble_api.h>
@@ -25,10 +27,6 @@
#include <esp_coexist.h>
#endif
#ifdef USE_ARDUINO
#include <esp32-hal-bt.h>
#endif
#define MBEDTLS_AES_ALT
#include <aes_alt.h>
@@ -78,9 +76,11 @@ void ESP32BLETracker::setup() {
[this](ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) {
if (state == ota::OTA_STARTED) {
this->stop_scan();
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
for (auto *client : this->clients_) {
client->disconnect();
}
#endif
}
});
#endif
@@ -210,8 +210,10 @@ void ESP32BLETracker::start_scan_(bool first) {
this->set_scanner_state_(ScannerState::STARTING);
ESP_LOGD(TAG, "Starting scan, set scanner state to STARTING.");
if (!first) {
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
for (auto *listener : this->listeners_)
listener->on_scan_end();
#endif
}
#ifdef USE_ESP32_BLE_DEVICE
this->already_discovered_.clear();
@@ -240,20 +242,25 @@ void ESP32BLETracker::start_scan_(bool first) {
}
void ESP32BLETracker::register_client(ESPBTClient *client) {
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
client->app_id = ++this->app_id_;
this->clients_.push_back(client);
this->recalculate_advertisement_parser_types();
#endif
}
void ESP32BLETracker::register_listener(ESPBTDeviceListener *listener) {
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
listener->set_parent(this);
this->listeners_.push_back(listener);
this->recalculate_advertisement_parser_types();
#endif
}
void ESP32BLETracker::recalculate_advertisement_parser_types() {
this->raw_advertisements_ = false;
this->parse_advertisements_ = false;
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
for (auto *listener : this->listeners_) {
if (listener->get_advertisement_parser_type() == AdvertisementParserType::PARSED_ADVERTISEMENTS) {
this->parse_advertisements_ = true;
@@ -261,6 +268,8 @@ void ESP32BLETracker::recalculate_advertisement_parser_types() {
this->raw_advertisements_ = true;
}
}
#endif
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
for (auto *client : this->clients_) {
if (client->get_advertisement_parser_type() == AdvertisementParserType::PARSED_ADVERTISEMENTS) {
this->parse_advertisements_ = true;
@@ -268,6 +277,7 @@ void ESP32BLETracker::recalculate_advertisement_parser_types() {
this->raw_advertisements_ = true;
}
}
#endif
}
void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
@@ -286,10 +296,12 @@ void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_ga
default:
break;
}
// Forward all events to clients (scan results are handled separately via gap_scan_event_handler)
// Forward all events to clients (scan results are handled separately via gap_scan_event_handler)
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
for (auto *client : this->clients_) {
client->gap_event_handler(event, param);
}
#endif
}
void ESP32BLETracker::gap_scan_event_handler(const BLEScanResult &scan_result) {
@@ -352,9 +364,11 @@ void ESP32BLETracker::gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_
void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) {
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
for (auto *client : this->clients_) {
client->gattc_event_handler(event, gattc_if, param);
}
#endif
}
void ESP32BLETracker::set_scanner_state_(ScannerState state) {
@@ -708,12 +722,16 @@ bool ESPBTDevice::resolve_irk(const uint8_t *irk) const {
void ESP32BLETracker::process_scan_result_(const BLEScanResult &scan_result) {
// Process raw advertisements
if (this->raw_advertisements_) {
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
for (auto *listener : this->listeners_) {
listener->parse_devices(&scan_result, 1);
}
#endif
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
for (auto *client : this->clients_) {
client->parse_devices(&scan_result, 1);
}
#endif
}
// Process parsed advertisements
@@ -723,16 +741,20 @@ void ESP32BLETracker::process_scan_result_(const BLEScanResult &scan_result) {
device.parse_scan_rst(scan_result);
bool found = false;
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
for (auto *listener : this->listeners_) {
if (listener->parse_device(device))
found = true;
}
#endif
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
for (auto *client : this->clients_) {
if (client->parse_device(device)) {
found = true;
}
}
#endif
if (!found && !this->scan_continuous_) {
this->print_bt_device_info(device);
@@ -749,8 +771,10 @@ void ESP32BLETracker::cleanup_scan_state_(bool is_stop_complete) {
// Reset timeout state machine instead of cancelling scheduler timeout
this->scan_timeout_state_ = ScanTimeoutState::INACTIVE;
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
for (auto *listener : this->listeners_)
listener->on_scan_end();
#endif
this->set_scanner_state_(ScannerState::IDLE);
}
@@ -774,6 +798,7 @@ void ESP32BLETracker::handle_scanner_failure_() {
void ESP32BLETracker::try_promote_discovered_clients_() {
// Only promote the first discovered client to avoid multiple simultaneous connections
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
for (auto *client : this->clients_) {
if (client->state() != ClientState::DISCOVERED) {
continue;
@@ -795,6 +820,7 @@ void ESP32BLETracker::try_promote_discovered_clients_() {
client->connect();
break;
}
#endif
}
const char *ESP32BLETracker::scanner_state_to_string_(ScannerState state) const {
@@ -821,6 +847,7 @@ void ESP32BLETracker::log_unexpected_state_(const char *operation, ScannerState
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
void ESP32BLETracker::update_coex_preference_(bool force_ble) {
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
if (force_ble && !this->coex_prefer_ble_) {
ESP_LOGD(TAG, "Setting coexistence to Bluetooth to make connection.");
this->coex_prefer_ble_ = true;
@@ -830,6 +857,7 @@ void ESP32BLETracker::update_coex_preference_(bool force_ble) {
this->coex_prefer_ble_ = false;
esp_coex_preference_set(ESP_COEX_PREFER_BALANCE); // Reset to default
}
#endif // CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
}
#endif

View File

@@ -302,6 +302,7 @@ class ESP32BLETracker : public Component,
/// Count clients in each state
ClientStateCounts count_client_states_() const {
ClientStateCounts counts;
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
for (auto *client : this->clients_) {
switch (client->state()) {
case ClientState::DISCONNECTING:
@@ -317,12 +318,17 @@ class ESP32BLETracker : public Component,
break;
}
}
#endif
return counts;
}
// Group 1: Large objects (12+ bytes) - vectors and callback manager
std::vector<ESPBTDeviceListener *> listeners_;
std::vector<ESPBTClient *> clients_;
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
StaticVector<ESPBTDeviceListener *, ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT> listeners_;
#endif
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
StaticVector<ESPBTClient *, ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT> clients_;
#endif
CallbackManager<void(ScannerState)> scanner_state_callbacks_;
#ifdef USE_ESP32_BLE_DEVICE
/// Vector of addresses that have already been printed in print_bt_device_info

View File

@@ -1,6 +1,7 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_MODE, CONF_PORT
from esphome.types import ConfigType
CODEOWNERS = ["@ayufan"]
AUTO_LOAD = ["camera"]
@@ -13,13 +14,27 @@ Mode = esp32_camera_web_server_ns.enum("Mode")
MODES = {"STREAM": Mode.STREAM, "SNAPSHOT": Mode.SNAPSHOT}
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(CameraWebServer),
cv.Required(CONF_PORT): cv.port,
cv.Required(CONF_MODE): cv.enum(MODES, upper=True),
},
).extend(cv.COMPONENT_SCHEMA)
def _consume_camera_web_server_sockets(config: ConfigType) -> ConfigType:
"""Register socket needs for camera web server."""
from esphome.components import socket
# Each camera web server instance needs 1 listening socket + 2 client connections
sockets_needed = 3
socket.consume_sockets(sockets_needed, "esp32_camera_web_server")(config)
return config
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(CameraWebServer),
cv.Required(CONF_PORT): cv.port,
cv.Required(CONF_MODE): cv.enum(MODES, upper=True),
},
).extend(cv.COMPONENT_SCHEMA),
_consume_camera_web_server_sockets,
)
async def to_code(config):

View File

@@ -92,9 +92,14 @@ async def to_code(config):
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
os.environ["ESP_IDF_VERSION"] = f"{framework_ver.major}.{framework_ver.minor}"
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.10.2")
esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0")
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.0.11")
if framework_ver >= cv.Version(5, 5, 0):
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.1.5")
esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.3")
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.5.11")
else:
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0")
esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0")
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.0.11")
esp32.add_extra_script(
"post",
"esp32_hosted.py",

View File

@@ -143,6 +143,7 @@ void ESP32ImprovComponent::loop() {
#else
this->set_state_(improv::STATE_AUTHORIZED);
#endif
this->check_wifi_connection_();
break;
}
case improv::STATE_AUTHORIZED: {
@@ -156,31 +157,12 @@ void ESP32ImprovComponent::loop() {
if (!this->check_identify_()) {
this->set_status_indicator_state_((now % 1000) < 500);
}
this->check_wifi_connection_();
break;
}
case improv::STATE_PROVISIONING: {
this->set_status_indicator_state_((now % 200) < 100);
if (wifi::global_wifi_component->is_connected()) {
wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(),
this->connecting_sta_.get_password());
this->connecting_sta_ = {};
this->cancel_timeout("wifi-connect-timeout");
this->set_state_(improv::STATE_PROVISIONED);
std::vector<std::string> urls = {ESPHOME_MY_LINK};
#ifdef USE_WEBSERVER
for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) {
if (ip.is_ip4()) {
std::string webserver_url = "http://" + ip.str() + ":" + to_string(USE_WEBSERVER_PORT);
urls.push_back(webserver_url);
break;
}
}
#endif
std::vector<uint8_t> data = improv::build_rpc_response(improv::WIFI_SETTINGS, urls);
this->send_response_(data);
this->stop();
}
this->check_wifi_connection_();
break;
}
case improv::STATE_PROVISIONED: {
@@ -392,6 +374,36 @@ void ESP32ImprovComponent::on_wifi_connect_timeout_() {
wifi::global_wifi_component->clear_sta();
}
void ESP32ImprovComponent::check_wifi_connection_() {
if (!wifi::global_wifi_component->is_connected()) {
return;
}
if (this->state_ == improv::STATE_PROVISIONING) {
wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(), this->connecting_sta_.get_password());
this->connecting_sta_ = {};
this->cancel_timeout("wifi-connect-timeout");
std::vector<std::string> urls = {ESPHOME_MY_LINK};
#ifdef USE_WEBSERVER
for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) {
if (ip.is_ip4()) {
std::string webserver_url = "http://" + ip.str() + ":" + to_string(USE_WEBSERVER_PORT);
urls.push_back(webserver_url);
break;
}
}
#endif
std::vector<uint8_t> data = improv::build_rpc_response(improv::WIFI_SETTINGS, urls);
this->send_response_(data);
} else if (this->is_active() && this->state_ != improv::STATE_PROVISIONED) {
ESP_LOGD(TAG, "WiFi provisioned externally");
}
this->set_state_(improv::STATE_PROVISIONED);
this->stop();
}
void ESP32ImprovComponent::advertise_service_data_() {
uint8_t service_data[IMPROV_SERVICE_DATA_SIZE] = {};
service_data[0] = IMPROV_PROTOCOL_ID_1; // PR

View File

@@ -111,6 +111,7 @@ class ESP32ImprovComponent : public Component {
void send_response_(std::vector<uint8_t> &response);
void process_incoming_data_();
void on_wifi_connect_timeout_();
void check_wifi_connection_();
bool check_identify_();
void advertise_service_data_();
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG

View File

@@ -42,6 +42,11 @@ static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size
symbols[i] = params->bit0;
}
}
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
if ((index + 1) >= size && params->reset.duration0 == 0 && params->reset.duration1 == 0) {
*done = true;
}
#endif
return RMT_SYMBOLS_PER_BYTE;
}

View File

@@ -190,7 +190,7 @@ async def to_code(config):
cg.add_define("ESPHOME_VARIANT", "ESP8266")
cg.add_define(ThreadModel.SINGLE)
cg.add_platformio_option("extra_scripts", ["post:post_build.py"])
cg.add_platformio_option("extra_scripts", ["pre:iram_fix.py", "post:post_build.py"])
conf = config[CONF_FRAMEWORK]
cg.add_platformio_option("framework", "arduino")
@@ -230,6 +230,12 @@ async def to_code(config):
# For cases where nullptrs can be handled, use nothrow: `new (std::nothrow) T;`
cg.add_build_flag("-DNEW_OOM_ABORT")
# In testing mode, fake a larger IRAM to allow linking grouped component tests
# Real ESP8266 hardware only has 32KB IRAM, but for CI testing we pretend it has 2MB
# This is done via a pre-build script that generates a custom linker script
if CORE.testing_mode:
cg.add_build_flag("-DESPHOME_TESTING_MODE")
cg.add_platformio_option("board_build.flash_mode", config[CONF_BOARD_FLASH_MODE])
ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
@@ -265,3 +271,8 @@ def copy_files():
post_build_file,
CORE.relative_build_path("post_build.py"),
)
iram_fix_file = dir / "iram_fix.py.script"
copy_file_if_changed(
iram_fix_file,
CORE.relative_build_path("iram_fix.py"),
)

View File

@@ -0,0 +1,44 @@
import os
import re
# pylint: disable=E0602
Import("env") # noqa
def patch_linker_script_after_preprocess(source, target, env):
"""Patch the local linker script after PlatformIO preprocesses it."""
# Check if we're in testing mode by looking for the define
build_flags = env.get("BUILD_FLAGS", [])
testing_mode = any("-DESPHOME_TESTING_MODE" in flag for flag in build_flags)
if not testing_mode:
return
# Get the local linker script path
build_dir = env.subst("$BUILD_DIR")
local_ld = os.path.join(build_dir, "ld", "local.eagle.app.v6.common.ld")
if not os.path.exists(local_ld):
return
# Read the linker script
with open(local_ld, "r") as f:
content = f.read()
# Replace IRAM size from 0x8000 (32KB) to 0x200000 (2MB)
# The line looks like: iram1_0_seg : org = 0x40100000, len = 0x8000
updated = re.sub(
r"(iram1_0_seg\s*:\s*org\s*=\s*0x40100000\s*,\s*len\s*=\s*)0x8000",
r"\g<1>0x200000",
content,
)
if updated != content:
with open(local_ld, "w") as f:
f.write(updated)
print("ESPHome: Patched IRAM size to 2MB for testing mode")
# Hook into the build process right before linking
# This runs after PlatformIO has already preprocessed the linker scripts
env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", patch_linker_script_after_preprocess)

View File

@@ -19,6 +19,7 @@ from esphome.const import (
from esphome.core import CORE, coroutine_with_priority
from esphome.coroutine import CoroPriority
import esphome.final_validate as fv
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
@@ -102,7 +103,16 @@ def ota_esphome_final_validate(config):
)
CONFIG_SCHEMA = (
def _consume_ota_sockets(config: ConfigType) -> ConfigType:
"""Register socket needs for OTA component."""
from esphome.components import socket
# OTA needs 1 listening socket (client connections are temporary during updates)
socket.consume_sockets(1, "ota")(config)
return config
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(ESPHomeOTAComponent),
@@ -129,18 +139,20 @@ CONFIG_SCHEMA = (
}
)
.extend(BASE_OTA_SCHEMA)
.extend(cv.COMPONENT_SCHEMA)
.extend(cv.COMPONENT_SCHEMA),
_consume_ota_sockets,
)
FINAL_VALIDATE_SCHEMA = ota_esphome_final_validate
@coroutine_with_priority(CoroPriority.OTA_UPDATES)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_port(config[CONF_PORT]))
if CONF_PASSWORD in config:
# Password could be set to an empty string and we can assume that means no password
if config.get(CONF_PASSWORD):
cg.add(var.set_auth_password(config[CONF_PASSWORD]))
cg.add_define("USE_OTA_PASSWORD")
# Only include hash algorithms when password is configured

View File

@@ -29,7 +29,7 @@ namespace esphome {
static const char *const TAG = "esphome.ota";
static constexpr uint16_t OTA_BLOCK_SIZE = 8192;
static constexpr size_t OTA_BUFFER_SIZE = 1024; // buffer size for OTA data transfer
static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 10000; // milliseconds for initial handshake
static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 20000; // milliseconds for initial handshake
static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer
#ifdef USE_OTA_PASSWORD

View File

@@ -689,12 +689,9 @@ void EthernetComponent::add_phy_register(PHYRegister register_value) { this->phy
void EthernetComponent::set_type(EthernetType type) { this->type_ = type; }
void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; }
std::string EthernetComponent::get_use_address() const {
if (this->use_address_.empty()) {
return App.get_name() + ".local";
}
return this->use_address_;
}
// set_use_address() is guaranteed to be called during component setup by Python code generation,
// so use_address_ will always be valid when get_use_address() is called - no fallback needed.
const std::string &EthernetComponent::get_use_address() const { return this->use_address_; }
void EthernetComponent::set_use_address(const std::string &use_address) { this->use_address_ = use_address; }

View File

@@ -88,7 +88,7 @@ class EthernetComponent : public Component {
network::IPAddresses get_ip_addresses();
network::IPAddress get_dns_address(uint8_t num);
std::string get_use_address() const;
const std::string &get_use_address() const;
void set_use_address(const std::string &use_address);
void get_eth_mac_address_raw(uint8_t *mac);
std::string get_eth_mac_address_pretty();

View File

@@ -90,13 +90,12 @@ void HomeassistantNumber::control(float value) {
api::HomeassistantActionRequest resp;
resp.set_service(SERVICE_NAME);
resp.data.emplace_back();
auto &entity_id = resp.data.back();
resp.data.init(2);
auto &entity_id = resp.data.emplace_back();
entity_id.set_key(ENTITY_ID_KEY);
entity_id.value = this->entity_id_;
resp.data.emplace_back();
auto &entity_value = resp.data.back();
auto &entity_value = resp.data.emplace_back();
entity_value.set_key(VALUE_KEY);
entity_value.value = to_string(value);

View File

@@ -51,8 +51,8 @@ void HomeassistantSwitch::write_state(bool state) {
resp.set_service(SERVICE_OFF);
}
resp.data.emplace_back();
auto &entity_id_kv = resp.data.back();
resp.data.init(1);
auto &entity_id_kv = resp.data.emplace_back();
entity_id_kv.set_key(ENTITY_ID_KEY);
entity_id_kv.value = this->entity_id_;

View File

@@ -167,8 +167,8 @@ class HttpRequestComponent : public Component {
}
protected:
virtual std::shared_ptr<HttpContainer> perform(std::string url, std::string method, std::string body,
std::list<Header> request_headers,
virtual std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method,
const std::string &body, const std::list<Header> &request_headers,
std::set<std::string> collect_headers) = 0;
const char *useragent_{nullptr};
bool follow_redirects_{};

View File

@@ -14,8 +14,9 @@ namespace http_request {
static const char *const TAG = "http_request.arduino";
std::shared_ptr<HttpContainer> HttpRequestArduino::perform(std::string url, std::string method, std::string body,
std::list<Header> request_headers,
std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &url, const std::string &method,
const std::string &body,
const std::list<Header> &request_headers,
std::set<std::string> collect_headers) {
if (!network::is_connected()) {
this->status_momentary_error("failed", 1000);

View File

@@ -31,8 +31,8 @@ class HttpContainerArduino : public HttpContainer {
class HttpRequestArduino : public HttpRequestComponent {
protected:
std::shared_ptr<HttpContainer> perform(std::string url, std::string method, std::string body,
std::list<Header> request_headers,
std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body,
const std::list<Header> &request_headers,
std::set<std::string> collect_headers) override;
};

View File

@@ -17,8 +17,9 @@ namespace http_request {
static const char *const TAG = "http_request.host";
std::shared_ptr<HttpContainer> HttpRequestHost::perform(std::string url, std::string method, std::string body,
std::list<Header> request_headers,
std::shared_ptr<HttpContainer> HttpRequestHost::perform(const std::string &url, const std::string &method,
const std::string &body,
const std::list<Header> &request_headers,
std::set<std::string> response_headers) {
if (!network::is_connected()) {
this->status_momentary_error("failed", 1000);

View File

@@ -18,8 +18,8 @@ class HttpContainerHost : public HttpContainer {
class HttpRequestHost : public HttpRequestComponent {
public:
std::shared_ptr<HttpContainer> perform(std::string url, std::string method, std::string body,
std::list<Header> request_headers,
std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body,
const std::list<Header> &request_headers,
std::set<std::string> response_headers) override;
void set_ca_path(const char *ca_path) { this->ca_path_ = ca_path; }

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