1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-14 13:55:45 +00:00

Compare commits

..

134 Commits

Author SHA1 Message Date
J. Nick Koston
3c0969d8a4 Merge branch 'template_alarm_control_panel_only_iterates' into integration 2025-11-13 12:38:08 -06:00
J. Nick Koston
1f408ce41c [template.alarm_control_panel] Use FixedVector for iteration-only sensor storage 2025-11-13 12:35:43 -06:00
J. Nick Koston
79b97452b7 Merge remote-tracking branch 'upstream/dev' into integration 2025-11-13 11:55:39 -06:00
Clyde Stubbs
0afcf67c32 [esp32] Add sdkconfig flag to make OTA work for 32MB flash (#11883)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-11-13 10:52:08 -05:00
Clyde Stubbs
952bdfaac2 [esp32] Make esp-idf default framework for P4 (#11884) 2025-11-13 09:55:48 -05:00
Jesse Hills
ed7e5cd325 Bump version to 2025.12.0-dev 2025-11-13 17:00:47 +13:00
J. Nick Koston
9f5a6017af Merge branch 'light_loop' into integration 2025-11-12 21:50:20 -06:00
J. Nick Koston
20649ce8ce safer 2025-11-12 21:49:58 -06:00
Jonathan Swoboda
a15f46e741 Merge branch 'beta' into dev 2025-11-12 22:46:34 -05:00
Jonathan Swoboda
050a27a409 Merge pull request #11880 from esphome/bump-2025.11.0b2
2025.11.0b2
2025-11-12 22:46:23 -05:00
J. Nick Koston
8c9d1cc392 Merge branch 'light_loop' into integration 2025-11-12 21:30:53 -06:00
J. Nick Koston
9b458d25ea light loop 2025-11-12 21:20:11 -06:00
J. Nick Koston
4e23a7a3e1 light loop 2025-11-12 21:11:45 -06:00
Jonathan Swoboda
382483b063 Bump version to 2025.11.0b2 2025-11-12 21:56:11 -05:00
J. Nick Koston
1675408161 [wifi] Fix slow reconnection after connection loss for all network types (#11873)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-12 21:56:11 -05:00
J. Nick Koston
1d8b08dcce [wifi][ethernet] Fix spurious warnings and unclear status after PR #9823 (#11871) 2025-11-12 21:56:11 -05:00
J. Nick Koston
afed581079 [light] Fix dangling reference in compute_color_mode causing memory corruption (#11868) 2025-11-12 21:56:11 -05:00
J. Nick Koston
ff107a0674 [mqtt] Fix crash with empty broker during upload/logs (#11866)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-12 21:56:11 -05:00
J. Nick Koston
72da3d0f1e [thermostat] Replace std::map with FixedVector, reduce flash usage (#11875) 2025-11-12 21:56:11 -05:00
J. Nick Koston
5a2e6697e0 [api][event] Send events immediately to prevent loss during rapid triggers (#11777) 2025-11-12 21:56:11 -05:00
J. Nick Koston
799cfe1de4 [esp32_ble_tracker] Use initializer_list to eliminate compiler warning and reduce flash usage (#11861) 2025-11-12 21:56:11 -05:00
J. Nick Koston
6df0264d51 [api] Eliminate heap allocations when transmitting Event types (#11773) 2025-11-12 21:56:11 -05:00
J. Nick Koston
a859ecaad1 [core] Fix wait_until hanging when used in on_boot automations (#11869) 2025-11-12 21:56:11 -05:00
Jonathan Swoboda
4f088c93c9 [esp32] Update the recommended platform to 55.03.31-2 (#11865) 2025-11-12 21:56:11 -05:00
J. Nick Koston
a1ab19d127 [ci] Reduce release time by removing 21 redundant ESP32-S3 IDF tests (#11850) 2025-11-12 21:56:11 -05:00
J. Nick Koston
f196b1c867 Merge remote-tracking branch 'upstream/dev' into integration 2025-11-12 20:08:51 -06:00
tomaszduda23
d869108416 [nrf52] add settings for dcdc converter (#11841) 2025-11-12 20:06:20 -06:00
J. Nick Koston
2d6618da3c [wifi] Fix slow reconnection after connection loss for all network types (#11873)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-13 13:44:22 +13:00
J. Nick Koston
47fe84e922 [wifi][ethernet] Fix spurious warnings and unclear status after PR #9823 (#11871) 2025-11-13 13:43:51 +13:00
J. Nick Koston
735bf9930a [light] Fix dangling reference in compute_color_mode causing memory corruption (#11868) 2025-11-13 13:41:28 +13:00
J. Nick Koston
769137fc09 [mqtt] Fix crash with empty broker during upload/logs (#11866)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-13 13:40:26 +13:00
J. Nick Koston
3a5b3ad77d [thermostat] Replace std::map with FixedVector, reduce flash usage (#11875) 2025-11-12 17:55:06 -06:00
J. Nick Koston
859101ddc9 [api][event] Send events immediately to prevent loss during rapid triggers (#11777) 2025-11-13 12:42:50 +13:00
J. Nick Koston
b8717fee4e Merge branch 'thermostat_storage' into integration 2025-11-12 15:01:13 -06:00
J. Nick Koston
4eb471b316 tweaks 2025-11-12 15:01:05 -06:00
J. Nick Koston
72513897cc Merge branch 'thermostat_storage' into integration 2025-11-12 14:55:55 -06:00
J. Nick Koston
20388ce848 [thermostat] Replace std::map with FixedVector, reduce flash usage 2025-11-12 14:54:23 -06:00
J. Nick Koston
9e8e5fd8aa Merge branch 'no_batch_events' into integration 2025-11-12 14:32:19 -06:00
J. Nick Koston
8a71a6bfdc Merge remote-tracking branch 'upstream/dev' into no_batch_events 2025-11-12 14:31:26 -06:00
J. Nick Koston
29a50da635 [wifi] Use stack allocation for BSSID formatting in logging (#11859) 2025-11-12 14:27:06 -06:00
J. Nick Koston
5f0fa68d73 [esp32_ble] Use stack allocation for MAC formatting in dump_config (#11860) 2025-11-12 14:26:57 -06:00
J. Nick Koston
2f39b10baa [esp32_ble_tracker] Use initializer_list to eliminate compiler warning and reduce flash usage (#11861) 2025-11-12 14:26:46 -06:00
J. Nick Koston
5a550cc579 [api] Eliminate heap allocations when transmitting Event types (#11773) 2025-11-12 14:26:36 -06:00
J. Nick Koston
4b58cb4ce6 [wifi] Pass ManualIP by const reference to reduce stack usage (#11858) 2025-11-12 14:01:19 -06:00
J. Nick Koston
3872a2fd91 [captive_portal] Warn when enabled without WiFi AP configured (#11856) 2025-11-12 14:01:07 -06:00
dependabot[bot]
5d613ada83 Bump pytest from 9.0.0 to 9.0.1 (#11874)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-12 14:00:50 -06:00
J. Nick Koston
24701d95e5 Merge branch 'wifi_reconnect_fixes' into integration 2025-11-12 12:51:55 -06:00
J. Nick Koston
08127d0225 [wifi] Fix phase transition and error state on reconnection 2025-11-12 12:48:44 -06:00
J. Nick Koston
e7a5790276 Merge branch 'wifi_ethernet_now_connected_later_logconfig' into integration 2025-11-12 12:18:19 -06:00
J. Nick Koston
97d2f5ee25 [wifi][ethernet] Fix spurious warnings and unclear status after PR #9823 2025-11-12 12:17:47 -06:00
J. Nick Koston
8c0c38be21 Merge remote-tracking branch 'upstream/dev' into integration 2025-11-12 12:01:57 -06:00
J. Nick Koston
8037dbe2d3 Merge branch 'color_mode_dangling_ref_fix' into integration 2025-11-12 12:01:53 -06:00
J. Nick Koston
6395439d33 Merge branch 'mqtt_raise_early_invalid_host' into integration 2025-11-12 12:01:50 -06:00
J. Nick Koston
9de80b635a [core] Fix wait_until hanging when used in on_boot automations (#11869) 2025-11-12 17:56:19 +00:00
J. Nick Koston
c299361753 some basic tests 2025-11-12 11:51:56 -06:00
J. Nick Koston
4b3d3c4ca2 some basic tests 2025-11-12 11:51:35 -06:00
J. Nick Koston
8ce4d5cd4f by value 2025-11-12 10:39:18 -06:00
J. Nick Koston
c1fb8dae37 [light] Fix dangling reference in compute_color_mode causing memory corruption 2025-11-12 10:33:19 -06:00
Jonathan Swoboda
748aee584a [esp32] Update the recommended platform to 55.03.31-2 (#11865) 2025-11-12 10:41:22 -05:00
J. Nick Koston
d8454e7c0a Update esphome/mqtt.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-12 09:33:29 -06:00
J. Nick Koston
fb00f75192 [mqtt] Fix crash with empty broker during upload/logs 2025-11-12 09:30:46 -06:00
J. Nick Koston
1b487988c9 [mqtt] Fix crash with empty broker during upload/logs 2025-11-12 09:29:40 -06:00
Jonathan Swoboda
3cbfddcc83 Merge branch 'beta' into dev 2025-11-11 23:27:24 -05:00
Jonathan Swoboda
1d71b6b93e Merge pull request #11862 from esphome/bump-2025.11.0b1
2025.11.0b1
2025-11-11 23:27:12 -05:00
J. Nick Koston
398dba4fc8 [ci] Reduce release time by removing 21 redundant ESP32-S3 IDF tests (#11850) 2025-11-12 16:44:19 +13:00
Jonathan Swoboda
298813d4fa Bump version to 2025.11.0b1 2025-11-11 22:14:22 -05:00
J. Nick Koston
016de16181 Merge branch 'ble_mac_format' into integration 2025-11-11 20:42:39 -06:00
J. Nick Koston
0d46bc57d6 [esp32_ble] Use stack allocation for MAC formatting in dump_config 2025-11-11 20:42:14 -06:00
J. Nick Koston
c0507b2ad0 Merge branch 'bssid_logging_cleanup' into integration 2025-11-11 20:38:50 -06:00
J. Nick Koston
7cefb8d92c a few more 2025-11-11 20:38:19 -06:00
J. Nick Koston
8de3bcd304 Merge branch 'bssid_logging_cleanup' into integration 2025-11-11 20:34:28 -06:00
J. Nick Koston
32456bee58 Merge remote-tracking branch 'origin/bssid_logging_cleanup' into bssid_logging_cleanup 2025-11-11 20:33:30 -06:00
J. Nick Koston
e104103366 two more 2025-11-11 20:33:20 -06:00
J. Nick Koston
304e0a2c65 Merge branch 'dev' into bssid_logging_cleanup 2025-11-11 20:27:30 -06:00
J. Nick Koston
276ef4ee1d Merge branch 'bssid_logging_cleanup' into integration 2025-11-11 20:25:47 -06:00
J. Nick Koston
dd65e39d16 [wifi] Use stack allocation for BSSID formatting in start_connecting 2025-11-11 20:25:18 -06:00
Jonathan Swoboda
56d141c741 Merge branch 'release' into dev 2025-11-11 20:09:55 -05:00
Jonathan Swoboda
47a7f729dd Merge pull request #11857 from esphome/bump-2025.10.5
2025.10.5
2025-11-11 20:09:41 -05:00
J. Nick Koston
a6c8319ade Merge branch 'wifi_pass_manualip_by_const_ref' into integration 2025-11-11 19:01:44 -06:00
J. Nick Koston
7c6f6acf60 [wifi] Pass ManualIP by const reference to reduce stack usage 2025-11-11 19:01:11 -06:00
Jonathan Swoboda
7806eb980f Bump version to 2025.12.0-dev 2025-11-11 19:50:47 -05:00
Jonathan Swoboda
a59888224c Bump version to 2025.10.5 2025-11-11 19:44:37 -05:00
Clyde Stubbs
58ad4759f0 [lvgl] Fix rotation with unusual width (#11680) 2025-11-11 19:44:37 -05:00
Clyde Stubbs
87f79290ba [usb_uart] Fixes for transfer queue allocation (#11548) 2025-11-11 19:44:37 -05:00
Jonathan Swoboda
9326d78439 [core] Don't allow python 3.14 (#11527) 2025-11-11 19:44:37 -05:00
Stuart Parmenter
a93887a790 [const] Add CONF_ROWS (#11249) 2025-11-11 19:44:37 -05:00
Kevin Ahrendt
d7fa131a8a [network, psram, speaker wifi] Use CORE.data to enable high performance networking (#11812) 2025-11-11 18:43:06 -06:00
J. Nick Koston
0c127b6998 Merge remote-tracking branch 'origin/captive_portal_without_ap' into integration 2025-11-11 18:39:05 -06:00
J. Nick Koston
037620d75a [captive_portal] Warn when enabled without WiFi AP configured 2025-11-11 18:36:35 -06:00
J. Nick Koston
0ab3d3da4e Merge remote-tracking branch 'upstream/dev' into integration 2025-11-11 18:10:09 -06:00
J. Nick Koston
79a4444928 [wifi] Conditionally compile manual_ip to save 24-72 bytes RAM (#11833) 2025-11-11 23:27:08 +00:00
J. Nick Koston
572fae5c7d [wifi] Restore two-attempt BSSID filtering for mesh networks (#11844) 2025-11-12 12:12:53 +13:00
J. Nick Koston
5dafaaced4 [wifi] Fix scan and connection failures after adapter restart (#11851)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-11 23:12:10 +00:00
J. Nick Koston
65a303d48f [wifi] Add min_auth_mode configuration option (#11814) 2025-11-11 16:39:55 -06:00
J. Nick Koston
00c71b7236 [wifi] Fix all-hidden networks duplicate attempts and scan skipping (#11848)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-11 22:33:37 +00:00
J. Nick Koston
ccf5591d14 Merge branch 'fix_scan_failing_after_restart' into integration 2025-11-11 16:23:01 -06:00
J. Nick Koston
1c7c559b69 touch ups 2025-11-11 16:22:24 -06:00
J. Nick Koston
088964de4d Merge branch 'fix_scan_failing_after_restart' into integration 2025-11-11 16:19:57 -06:00
J. Nick Koston
f0a9ee871b keep 2025-11-11 16:19:47 -06:00
J. Nick Koston
3280ce936a Merge branch 'fix_scan_failing_after_restart' into integration 2025-11-11 16:15:22 -06:00
J. Nick Koston
efe6e58404 clear failure on restart 2025-11-11 16:15:12 -06:00
J. Nick Koston
ef04903a7a [wifi] Change priority type from float to int8_t (#11830) 2025-11-12 11:10:17 +13:00
J. Nick Koston
08ea53633f Merge branch 'fix_scan_failing_after_restart' into integration 2025-11-11 16:09:53 -06:00
J. Nick Koston
d54179fd12 Merge remote-tracking branch 'origin/fix_scan_failing_after_restart' into fix_scan_failing_after_restart 2025-11-11 16:09:42 -06:00
J. Nick Koston
fb5b37c17a avoid breaking change 2025-11-11 16:09:33 -06:00
J. Nick Koston
9c6d9badf2 Merge branch 'retry_hidden_no_stuck_last_networks_visible' into fix_scan_failing_after_restart 2025-11-11 16:05:01 -06:00
J. Nick Koston
55bdbf60c9 Merge branch 'dev' into retry_hidden_no_stuck_last_networks_visible 2025-11-11 16:04:55 -06:00
J. Nick Koston
a2ec7f622c [wifi] Fix infinite retry loop when no hidden networks and captive portal active (#11831) 2025-11-11 16:04:37 -06:00
J. Nick Koston
530fc8f030 Merge branch 'fix_scan_failing_after_restart' into integration 2025-11-11 16:02:19 -06:00
J. Nick Koston
a8f253eecf tweaks on failure paths 2025-11-11 16:02:01 -06:00
J. Nick Koston
64c79f6363 Merge branch 'fix_scan_failing_after_restart' into integration 2025-11-11 15:58:06 -06:00
J. Nick Koston
7b60a8a21a cleanup conflicting logic 2025-11-11 15:55:09 -06:00
J. Nick Koston
42fa0b61a7 cleanup conflicting logic 2025-11-11 15:52:51 -06:00
tomaszduda23
2f91e7bd47 [nrf52] fix boot loop (#11854) 2025-11-11 15:33:53 -06:00
J. Nick Koston
ec79eb3065 Merge branch 'fix_scan_failing_after_restart' into integration 2025-11-11 15:22:05 -06:00
J. Nick Koston
e589542bd3 make message more sane 2025-11-11 15:21:51 -06:00
J. Nick Koston
8ce7f9dfe8 Merge branch 'fix_scan_failing_after_restart' into integration 2025-11-11 15:16:00 -06:00
J. Nick Koston
c3967df6ce tweak 2025-11-11 15:15:41 -06:00
J. Nick Koston
f0d1cde35c Merge branch 'fix_scan_failing_after_restart' into integration 2025-11-11 15:00:31 -06:00
J. Nick Koston
191cf1b03c preen 2025-11-11 14:59:12 -06:00
tomaszduda23
80a7c6d3c3 [nrf52,debug] add partition dump (#11839)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2025-11-11 14:52:41 -06:00
J. Nick Koston
374d039f60 Merge branch 'fix_scan_failing_after_restart' into integration 2025-11-11 14:24:40 -06:00
J. Nick Koston
e19bdf63a5 Merge remote-tracking branch 'origin/retry_hidden_no_stuck_last_networks_visible' into fix_scan_failing_after_restart 2025-11-11 14:22:56 -06:00
J. Nick Koston
1bde521380 cleanups 2025-11-11 14:05:44 -06:00
J. Nick Koston
db9af4a862 cleanup 2025-11-11 14:00:39 -06:00
J. Nick Koston
d092e59a79 Merge branch 'dev' into retry_hidden_no_stuck_last_networks_visible 2025-11-11 13:56:26 -06:00
J. Nick Koston
5a77f63103 Merge remote-tracking branch 'upstream/dev' into fix_scan_failing_after_restart 2025-11-11 13:55:58 -06:00
J. Nick Koston
f6ac916bb2 cleanups 2025-11-11 13:55:43 -06:00
CzBiX
7a92565a0c [lvgl] Fix compile when using transform_zoom (#11845) 2025-11-12 06:24:52 +11:00
J. Nick Koston
93f8e40111 Fix scan failing after restart 2025-11-11 13:13:28 -06:00
J. Nick Koston
99bb6ead7e Merge branch 'dev' into no_batch_events 2025-11-09 22:04:58 -06:00
J. Nick Koston
d171406dff Merge branch 'dev' into no_batch_events 2025-11-09 17:35:33 -06:00
J. Nick Koston
65fd784fa7 tidy 2025-11-08 10:39:09 -06:00
J. Nick Koston
b9f208b63a [api][event] Send events immediately to prevent loss during rapid triggers 2025-11-08 10:16:18 -06:00
81 changed files with 1467 additions and 639 deletions

View File

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

View File

@@ -1,7 +1,6 @@
"""CLI interface for memory analysis with report generation."""
from collections import defaultdict
import json
import sys
from . import (
@@ -284,28 +283,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
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

View File

@@ -476,8 +476,9 @@ 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();
auto supported_modes = traits.get_supported_color_modes();
// Pass pointer to ColorModeMask so the iterator can encode actual ColorMode enum values
msg.supported_color_modes = &traits.get_supported_color_modes();
msg.supported_color_modes = &supported_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();
@@ -1295,8 +1296,8 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe
#ifdef USE_EVENT
void APIConnection::send_event(event::Event *event, const char *event_type) {
this->schedule_message_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE,
EventResponse::ESTIMATED_SIZE);
this->send_message_smart_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE,
EventResponse::ESTIMATED_SIZE);
}
uint16_t APIConnection::try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn,
uint32_t remaining_size, bool is_single) {

View File

@@ -650,21 +650,30 @@ class APIConnection final : public APIServerConnection {
}
#endif
// Helper to check if a message type should bypass batching
// Returns true if:
// 1. It's an UpdateStateResponse (always send immediately to handle cases where
// the main loop is blocked, e.g., during OTA updates)
// 2. It's an EventResponse (events are edge-triggered - every occurrence matters)
// 3. OR: User has opted into immediate sending (should_try_send_immediately = true
// AND batch_delay = 0)
inline bool should_send_immediately_(uint8_t message_type) const {
return (
#ifdef USE_UPDATE
message_type == UpdateStateResponse::MESSAGE_TYPE ||
#endif
#ifdef USE_EVENT
message_type == EventResponse::MESSAGE_TYPE ||
#endif
(this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0));
}
// Helper method to send a message either immediately or via batching
// Tries immediate send if should_send_immediately_() returns true and buffer has space
// Falls back to batching if immediate send fails or isn't applicable
bool send_message_smart_(EntityBase *entity, MessageCreatorPtr creator, uint8_t message_type,
uint8_t estimated_size) {
// Try to send immediately if:
// 1. It's an UpdateStateResponse (always send immediately to handle cases where
// the main loop is blocked, e.g., during OTA updates)
// 2. OR: We should try to send immediately (should_try_send_immediately = true)
// AND Batch delay is 0 (user has opted in to immediate sending)
// 3. AND: Buffer has space available
if ((
#ifdef USE_UPDATE
message_type == UpdateStateResponse::MESSAGE_TYPE ||
#endif
(this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0)) &&
this->helper_->can_write_without_blocking()) {
if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) {
// Now actually encode and send
if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true) &&
this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) {
@@ -682,6 +691,27 @@ class APIConnection final : public APIServerConnection {
return this->schedule_message_(entity, creator, message_type, estimated_size);
}
// Overload for MessageCreator (used by events which need to capture event_type)
bool send_message_smart_(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) {
// Try to send immediately if message type should bypass batching and buffer has space
if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) {
// Now actually encode and send
if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true, message_type) &&
this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) {
#ifdef HAS_PROTO_MESSAGE_DUMP
// Log the message in verbose mode
this->log_proto_message_(entity, creator, message_type);
#endif
return true;
}
// If immediate send failed, fall through to batching
}
// Fall back to scheduled batching
return this->schedule_message_(entity, std::move(creator), message_type, estimated_size);
}
// Helper function to schedule a deferred message with known message type
bool schedule_message_(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) {
this->deferred_batch_.add_item(entity, std::move(creator), message_type, estimated_size);

View File

@@ -1,9 +1,12 @@
import logging
import esphome.codegen as cg
from esphome.components import web_server_base
from esphome.components.web_server_base import CONF_WEB_SERVER_BASE_ID
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv
from esphome.const import (
CONF_AP,
CONF_ID,
PLATFORM_BK72XX,
PLATFORM_ESP32,
@@ -14,6 +17,10 @@ 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__)
def AUTO_LOAD() -> list[str]:
@@ -50,6 +57,27 @@ CONFIG_SCHEMA = cv.All(
)
def _final_validate(config: ConfigType) -> ConfigType:
full_config = fv.full_config.get()
wifi_conf = full_config.get("wifi")
if wifi_conf is None:
# This shouldn't happen due to DEPENDENCIES = ["wifi"], but check anyway
raise cv.Invalid("Captive portal requires the wifi component to be configured")
if CONF_AP not in wifi_conf:
_LOGGER.warning(
"Captive portal is enabled but no WiFi AP is configured. "
"The captive portal will not be accessible. "
"Add 'ap:' to your WiFi configuration to enable the captive portal."
)
return config
FINAL_VALIDATE_SCHEMA = _final_validate
@coroutine_with_priority(CoroPriority.CAPTIVE_PORTAL)
async def to_code(config):
paren = await cg.get_variable(config[CONF_WEB_SERVER_BASE_ID])

View File

@@ -49,9 +49,9 @@ void DebugComponent::dump_config() {
}
#endif // USE_TEXT_SENSOR
#ifdef USE_ESP32
this->log_partition_info_(); // Log partition information for ESP32
#endif // USE_ESP32
#if defined(USE_ESP32) || defined(USE_ZEPHYR)
this->log_partition_info_(); // Log partition information
#endif
}
void DebugComponent::loop() {

View File

@@ -62,19 +62,19 @@ class DebugComponent : public PollingComponent {
sensor::Sensor *cpu_frequency_sensor_{nullptr};
#endif // USE_SENSOR
#ifdef USE_ESP32
#if defined(USE_ESP32) || defined(USE_ZEPHYR)
/**
* @brief Logs information about the device's partition table.
*
* This function iterates through the ESP32's partition table and logs details
* This function iterates through the partition table and logs details
* about each partition, including its name, type, subtype, starting address,
* and size. The information is useful for diagnosing issues related to flash
* memory or verifying the partition configuration dynamically at runtime.
*
* Only available when compiled for ESP32 platforms.
* Only available when compiled for ESP32 and ZEPHYR platforms.
*/
void log_partition_info_();
#endif // USE_ESP32
#endif
#ifdef USE_TEXT_SENSOR
text_sensor::TextSensor *device_info_{nullptr};

View File

@@ -5,6 +5,7 @@
#include <zephyr/drivers/hwinfo.h>
#include <hal/nrf_power.h>
#include <cstdint>
#include <zephyr/storage/flash_map.h>
#define BOOTLOADER_VERSION_REGISTER NRF_TIMER2->CC[0]
@@ -86,6 +87,37 @@ std::string DebugComponent::get_reset_reason_() {
uint32_t DebugComponent::get_free_heap_() { return INT_MAX; }
static void fa_cb(const struct flash_area *fa, void *user_data) {
#if CONFIG_FLASH_MAP_LABELS
const char *fa_label = flash_area_label(fa);
if (fa_label == nullptr) {
fa_label = "-";
}
ESP_LOGCONFIG(TAG, "%2d 0x%0*" PRIxPTR " %-26s %-24.24s 0x%-10x 0x%-12x", (int) fa->fa_id,
sizeof(uintptr_t) * 2, (uintptr_t) fa->fa_dev, fa->fa_dev->name, fa_label, (uint32_t) fa->fa_off,
fa->fa_size);
#else
ESP_LOGCONFIG(TAG, "%2d 0x%0*" PRIxPTR " %-26s 0x%-10x 0x%-12x", (int) fa->fa_id, sizeof(uintptr_t) * 2,
(uintptr_t) fa->fa_dev, fa->fa_dev->name, (uint32_t) fa->fa_off, fa->fa_size);
#endif
}
void DebugComponent::log_partition_info_() {
#if CONFIG_FLASH_MAP_LABELS
ESP_LOGCONFIG(TAG, "ID | Device | Device Name "
"| Label | Offset | Size");
ESP_LOGCONFIG(TAG, "--------------------------------------------"
"-----------------------------------------------");
#else
ESP_LOGCONFIG(TAG, "ID | Device | Device Name "
"| Offset | Size");
ESP_LOGCONFIG(TAG, "-----------------------------------------"
"------------------------------");
#endif
flash_area_foreach(fa_cb, nullptr);
}
void DebugComponent::get_device_info_(std::string &device_info) {
std::string supply = "Main supply status: ";
if (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_NORMAL) {

View File

@@ -334,12 +334,14 @@ def _is_framework_url(source: str) -> str:
# - https://github.com/espressif/arduino-esp32/releases
ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
"recommended": cv.Version(3, 3, 2),
"latest": cv.Version(3, 3, 2),
"dev": cv.Version(3, 3, 2),
"latest": cv.Version(3, 3, 4),
"dev": cv.Version(3, 3, 4),
}
ARDUINO_PLATFORM_VERSION_LOOKUP = {
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, 4): cv.Version(55, 3, 31, "2"),
cv.Version(3, 3, 3): cv.Version(55, 3, 31, "2"),
cv.Version(3, 3, 2): cv.Version(55, 3, 31, "2"),
cv.Version(3, 3, 1): cv.Version(55, 3, 31, "2"),
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),
@@ -357,8 +359,8 @@ ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
"dev": cv.Version(5, 5, 1),
}
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
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, 5, 1): cv.Version(55, 3, 31, "2"),
cv.Version(5, 5, 0): cv.Version(55, 3, 31, "2"),
cv.Version(5, 4, 3): cv.Version(55, 3, 32),
cv.Version(5, 4, 2): cv.Version(54, 3, 21, "2"),
cv.Version(5, 4, 1): cv.Version(54, 3, 21, "2"),
@@ -373,14 +375,15 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = {
# The platform-espressif32 version
# - https://github.com/pioarduino/platform-espressif32/releases
PLATFORM_VERSION_LOOKUP = {
"recommended": cv.Version(55, 3, 31, "1"),
"latest": cv.Version(55, 3, 31, "1"),
"dev": cv.Version(55, 3, 31, "1"),
"recommended": cv.Version(55, 3, 31, "2"),
"latest": cv.Version(55, 3, 31, "2"),
"dev": cv.Version(55, 3, 31, "2"),
}
def _check_versions(value):
value = value.copy()
def _check_versions(config):
config = config.copy()
value = config[CONF_FRAMEWORK]
if value[CONF_VERSION] in PLATFORM_VERSION_LOOKUP:
if CONF_SOURCE in value or CONF_PLATFORM_VERSION in value:
@@ -445,7 +448,7 @@ def _check_versions(value):
"If there are connectivity or build issues please remove the manual version."
)
return value
return config
def _parse_platform_version(value):
@@ -495,6 +498,8 @@ def final_validate(config):
from esphome.components.psram import DOMAIN as PSRAM_DOMAIN
errs = []
conf_fw = config[CONF_FRAMEWORK]
advanced = conf_fw[CONF_ADVANCED]
full_config = fv.full_config.get()
if pio_options := full_config[CONF_ESPHOME].get(CONF_PLATFORMIO_OPTIONS):
pio_flash_size_key = "board_upload.flash_size"
@@ -511,22 +516,14 @@ def final_validate(config):
f"Please specify {CONF_FLASH_SIZE} within esp32 configuration only"
)
)
if (
config[CONF_VARIANT] != VARIANT_ESP32
and CONF_ADVANCED in (conf_fw := config[CONF_FRAMEWORK])
and CONF_IGNORE_EFUSE_MAC_CRC in conf_fw[CONF_ADVANCED]
):
if config[CONF_VARIANT] != VARIANT_ESP32 and advanced[CONF_IGNORE_EFUSE_MAC_CRC]:
errs.append(
cv.Invalid(
f"'{CONF_IGNORE_EFUSE_MAC_CRC}' is not supported on {config[CONF_VARIANT]}",
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_IGNORE_EFUSE_MAC_CRC],
)
)
if (
config.get(CONF_FRAMEWORK, {})
.get(CONF_ADVANCED, {})
.get(CONF_EXECUTE_FROM_PSRAM)
):
if advanced[CONF_EXECUTE_FROM_PSRAM]:
if config[CONF_VARIANT] != VARIANT_ESP32S3:
errs.append(
cv.Invalid(
@@ -542,6 +539,17 @@ def final_validate(config):
)
)
if (
config[CONF_FLASH_SIZE] == "32MB"
and "ota" in full_config
and not advanced[CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES]
):
errs.append(
cv.Invalid(
f"OTA with 32MB flash requires '{CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES}' to be set in the '{CONF_ADVANCED}' section of the esp32 configuration",
path=[CONF_FLASH_SIZE],
)
)
if errs:
raise cv.MultipleInvalid(errs)
@@ -596,89 +604,74 @@ def _validate_idf_component(config: ConfigType) -> ConfigType:
FRAMEWORK_ESP_IDF = "esp-idf"
FRAMEWORK_ARDUINO = "arduino"
FRAMEWORK_SCHEMA = cv.All(
cv.Schema(
{
cv.Optional(CONF_TYPE, default=FRAMEWORK_ARDUINO): cv.one_of(
FRAMEWORK_ESP_IDF, FRAMEWORK_ARDUINO
),
cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict,
cv.Optional(CONF_RELEASE): cv.string_strict,
cv.Optional(CONF_SOURCE): cv.string_strict,
cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version,
cv.Optional(CONF_SDKCONFIG_OPTIONS, default={}): {
cv.string_strict: cv.string_strict
},
cv.Optional(CONF_LOG_LEVEL, default="ERROR"): cv.one_of(
*LOG_LEVELS_IDF, upper=True
),
cv.Optional(CONF_ADVANCED, default={}): cv.Schema(
{
cv.Optional(CONF_ASSERTION_LEVEL): cv.one_of(
*ASSERTION_LEVELS, upper=True
),
cv.Optional(CONF_COMPILER_OPTIMIZATION, default="SIZE"): cv.one_of(
*COMPILER_OPTIMIZATIONS, upper=True
),
cv.Optional(CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES): cv.boolean,
cv.Optional(CONF_ENABLE_LWIP_ASSERT, default=True): cv.boolean,
cv.Optional(
CONF_IGNORE_EFUSE_CUSTOM_MAC, default=False
): cv.boolean,
cv.Optional(CONF_IGNORE_EFUSE_MAC_CRC): cv.boolean,
# DHCP server is needed for WiFi AP mode. When WiFi component is used,
# it will handle disabling DHCP server when AP is not configured.
# Default to false (disabled) when WiFi is not used.
cv.OnlyWithout(
CONF_ENABLE_LWIP_DHCP_SERVER, "wifi", default=False
): cv.boolean,
cv.Optional(
CONF_ENABLE_LWIP_MDNS_QUERIES, default=True
): cv.boolean,
cv.Optional(
CONF_ENABLE_LWIP_BRIDGE_INTERFACE, default=False
): cv.boolean,
cv.Optional(
CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING, default=True
): cv.boolean,
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_DISABLE_VFS_SUPPORT_TERMIOS, default=True
): cv.boolean,
cv.Optional(
CONF_DISABLE_VFS_SUPPORT_SELECT, default=True
): cv.boolean,
cv.Optional(CONF_DISABLE_VFS_SUPPORT_DIR, default=True): cv.boolean,
cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean,
cv.Optional(CONF_LOOP_TASK_STACK_SIZE, default=8192): cv.int_range(
min=8192, max=32768
),
}
),
cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list(
cv.All(
cv.Schema(
{
cv.Required(CONF_NAME): cv.string_strict,
cv.Optional(CONF_SOURCE): cv.git_ref,
cv.Optional(CONF_REF): cv.string,
cv.Optional(CONF_PATH): cv.string,
cv.Optional(CONF_REFRESH): cv.All(
cv.string, cv.source_refresh
),
}
),
_validate_idf_component,
)
),
}
),
_check_versions,
FRAMEWORK_SCHEMA = cv.Schema(
{
cv.Optional(CONF_TYPE): cv.one_of(FRAMEWORK_ESP_IDF, FRAMEWORK_ARDUINO),
cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict,
cv.Optional(CONF_RELEASE): cv.string_strict,
cv.Optional(CONF_SOURCE): cv.string_strict,
cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version,
cv.Optional(CONF_SDKCONFIG_OPTIONS, default={}): {
cv.string_strict: cv.string_strict
},
cv.Optional(CONF_LOG_LEVEL, default="ERROR"): cv.one_of(
*LOG_LEVELS_IDF, upper=True
),
cv.Optional(CONF_ADVANCED, default={}): cv.Schema(
{
cv.Optional(CONF_ASSERTION_LEVEL): cv.one_of(
*ASSERTION_LEVELS, upper=True
),
cv.Optional(CONF_COMPILER_OPTIMIZATION, default="SIZE"): cv.one_of(
*COMPILER_OPTIMIZATIONS, upper=True
),
cv.Optional(
CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES, default=False
): cv.boolean,
cv.Optional(CONF_ENABLE_LWIP_ASSERT, default=True): cv.boolean,
cv.Optional(CONF_IGNORE_EFUSE_CUSTOM_MAC, default=False): cv.boolean,
cv.Optional(CONF_IGNORE_EFUSE_MAC_CRC, default=False): cv.boolean,
# DHCP server is needed for WiFi AP mode. When WiFi component is used,
# it will handle disabling DHCP server when AP is not configured.
# Default to false (disabled) when WiFi is not used.
cv.OnlyWithout(
CONF_ENABLE_LWIP_DHCP_SERVER, "wifi", default=False
): cv.boolean,
cv.Optional(CONF_ENABLE_LWIP_MDNS_QUERIES, default=True): cv.boolean,
cv.Optional(
CONF_ENABLE_LWIP_BRIDGE_INTERFACE, default=False
): cv.boolean,
cv.Optional(
CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING, default=True
): cv.boolean,
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_DISABLE_VFS_SUPPORT_TERMIOS, default=True): cv.boolean,
cv.Optional(CONF_DISABLE_VFS_SUPPORT_SELECT, default=True): cv.boolean,
cv.Optional(CONF_DISABLE_VFS_SUPPORT_DIR, default=True): cv.boolean,
cv.Optional(CONF_EXECUTE_FROM_PSRAM, default=False): cv.boolean,
cv.Optional(CONF_LOOP_TASK_STACK_SIZE, default=8192): cv.int_range(
min=8192, max=32768
),
}
),
cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list(
cv.All(
cv.Schema(
{
cv.Required(CONF_NAME): cv.string_strict,
cv.Optional(CONF_SOURCE): cv.git_ref,
cv.Optional(CONF_REF): cv.string,
cv.Optional(CONF_PATH): cv.string,
cv.Optional(CONF_REFRESH): cv.All(cv.string, cv.source_refresh),
}
),
_validate_idf_component,
)
),
}
)
@@ -741,11 +734,11 @@ def _show_framework_migration_message(name: str, variant: str) -> None:
def _set_default_framework(config):
config = config.copy()
if CONF_FRAMEWORK not in config:
config = config.copy()
variant = config[CONF_VARIANT]
config[CONF_FRAMEWORK] = FRAMEWORK_SCHEMA({})
if CONF_TYPE not in config[CONF_FRAMEWORK]:
variant = config[CONF_VARIANT]
if variant in ARDUINO_ALLOWED_VARIANTS:
config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ARDUINO
_show_framework_migration_message(
@@ -785,6 +778,7 @@ CONFIG_SCHEMA = cv.All(
),
_detect_variant,
_set_default_framework,
_check_versions,
set_core_data,
cv.has_at_least_one_key(CONF_BOARD, CONF_VARIANT),
)
@@ -803,9 +797,7 @@ def _configure_lwip_max_sockets(conf: dict) -> None:
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"
)
user_max_sockets = conf[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())
@@ -975,23 +967,18 @@ async def to_code(config):
# WiFi component handles its own optimization when AP mode is not used
# When using Arduino with Ethernet, DHCP server functions must be available
# for the Network library to compile, even if not actively used
if (
CONF_ENABLE_LWIP_DHCP_SERVER in advanced
and not advanced[CONF_ENABLE_LWIP_DHCP_SERVER]
and not (
conf[CONF_TYPE] == FRAMEWORK_ARDUINO
and "ethernet" in CORE.loaded_integrations
)
if advanced.get(CONF_ENABLE_LWIP_DHCP_SERVER) is False and not (
conf[CONF_TYPE] == FRAMEWORK_ARDUINO and "ethernet" in CORE.loaded_integrations
):
add_idf_sdkconfig_option("CONFIG_LWIP_DHCPS", False)
if not advanced.get(CONF_ENABLE_LWIP_MDNS_QUERIES, True):
if not advanced[CONF_ENABLE_LWIP_MDNS_QUERIES]:
add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False)
if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False):
if not advanced[CONF_ENABLE_LWIP_BRIDGE_INTERFACE]:
add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0)
_configure_lwip_max_sockets(conf)
if advanced.get(CONF_EXECUTE_FROM_PSRAM, False):
if advanced[CONF_EXECUTE_FROM_PSRAM]:
add_idf_sdkconfig_option("CONFIG_SPIRAM_FETCH_INSTRUCTIONS", True)
add_idf_sdkconfig_option("CONFIG_SPIRAM_RODATA", True)
@@ -1002,23 +989,22 @@ async def to_code(config):
# - select() on 4 sockets: ~190μs (Arduino/core locking) vs ~235μs (ESP-IDF default)
# - Up to 200% slower under load when all operations queue through tcpip_thread
# Enabling this makes ESP-IDF socket performance match Arduino framework.
if advanced.get(CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING, True):
if advanced[CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING]:
add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_CORE_LOCKING", True)
if advanced.get(CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, True):
if advanced[CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY]:
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):
if advanced[CONF_DISABLE_LIBC_LOCKS_IN_IRAM]:
add_idf_sdkconfig_option("CONFIG_LIBC_LOCKS_PLACE_IN_IRAM", False)
# Disable VFS support for termios (terminal I/O functions)
# ESPHome doesn't use termios functions on ESP32 (only used in host UART driver).
# Saves approximately 1.8KB of flash when disabled (default).
add_idf_sdkconfig_option(
"CONFIG_VFS_SUPPORT_TERMIOS",
not advanced.get(CONF_DISABLE_VFS_SUPPORT_TERMIOS, True),
"CONFIG_VFS_SUPPORT_TERMIOS", not advanced[CONF_DISABLE_VFS_SUPPORT_TERMIOS]
)
# Disable VFS support for select() with file descriptors
@@ -1032,8 +1018,7 @@ async def to_code(config):
else:
# No component needs it - allow user to control (default: disabled)
add_idf_sdkconfig_option(
"CONFIG_VFS_SUPPORT_SELECT",
not advanced.get(CONF_DISABLE_VFS_SUPPORT_SELECT, True),
"CONFIG_VFS_SUPPORT_SELECT", not advanced[CONF_DISABLE_VFS_SUPPORT_SELECT]
)
# Disable VFS support for directory functions (opendir, readdir, mkdir, etc.)
@@ -1046,8 +1031,7 @@ async def to_code(config):
else:
# No component needs it - allow user to control (default: disabled)
add_idf_sdkconfig_option(
"CONFIG_VFS_SUPPORT_DIR",
not advanced.get(CONF_DISABLE_VFS_SUPPORT_DIR, True),
"CONFIG_VFS_SUPPORT_DIR", not advanced[CONF_DISABLE_VFS_SUPPORT_DIR]
)
cg.add_platformio_option("board_build.partitions", "partitions.csv")
@@ -1061,7 +1045,7 @@ async def to_code(config):
add_idf_sdkconfig_option(flag, assertion_level == key)
add_idf_sdkconfig_option("CONFIG_COMPILER_OPTIMIZATION_DEFAULT", False)
compiler_optimization = advanced.get(CONF_COMPILER_OPTIMIZATION)
compiler_optimization = advanced[CONF_COMPILER_OPTIMIZATION]
for key, flag in COMPILER_OPTIMIZATIONS.items():
add_idf_sdkconfig_option(flag, compiler_optimization == key)
@@ -1070,18 +1054,20 @@ async def to_code(config):
conf[CONF_ADVANCED][CONF_ENABLE_LWIP_ASSERT],
)
if advanced.get(CONF_IGNORE_EFUSE_MAC_CRC):
if advanced[CONF_IGNORE_EFUSE_MAC_CRC]:
add_idf_sdkconfig_option("CONFIG_ESP_MAC_IGNORE_MAC_CRC_ERROR", True)
add_idf_sdkconfig_option("CONFIG_ESP_PHY_CALIBRATION_AND_DATA_STORAGE", False)
if advanced.get(CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES):
if advanced[CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES]:
_LOGGER.warning(
"Using experimental features in ESP-IDF may result in unexpected failures."
)
add_idf_sdkconfig_option("CONFIG_IDF_EXPERIMENTAL_FEATURES", True)
if config[CONF_FLASH_SIZE] == "32MB":
add_idf_sdkconfig_option(
"CONFIG_BOOTLOADER_CACHE_32BIT_ADDR_QUAD_FLASH", True
)
cg.add_define(
"ESPHOME_LOOP_TASK_STACK_SIZE", advanced.get(CONF_LOOP_TASK_STACK_SIZE)
)
cg.add_define("ESPHOME_LOOP_TASK_STACK_SIZE", advanced[CONF_LOOP_TASK_STACK_SIZE])
cg.add_define(
"USE_ESP_IDF_VERSION_CODE",

View File

@@ -638,11 +638,13 @@ void ESP32BLE::dump_config() {
io_capability_s = "invalid";
break;
}
char mac_s[18];
format_mac_addr_upper(mac_address, mac_s);
ESP_LOGCONFIG(TAG,
"BLE:\n"
" MAC address: %s\n"
" IO Capability: %s",
format_mac_address_pretty(mac_address).c_str(), io_capability_s);
mac_s, io_capability_s);
} else {
ESP_LOGCONFIG(TAG, "Bluetooth stack is not enabled");
}

View File

@@ -389,7 +389,8 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
if (this->conn_id_ != param->search_res.conn_id)
return false;
this->service_count_++;
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE ||
this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
// V3 clients don't need services initialized since
// as they use the ESP APIs to get services.
break;

View File

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

View File

@@ -10,7 +10,7 @@ namespace esphome::esp32_ble_tracker {
class ESPBTAdvertiseTrigger : public Trigger<const ESPBTDevice &>, public ESPBTDeviceListener {
public:
explicit ESPBTAdvertiseTrigger(ESP32BLETracker *parent) { parent->register_listener(this); }
void set_addresses(const std::vector<uint64_t> &addresses) { this->address_vec_ = addresses; }
void set_addresses(std::initializer_list<uint64_t> addresses) { this->address_vec_ = addresses; }
bool parse_device(const ESPBTDevice &device) override {
uint64_t u64_addr = device.address_uint64();

View File

@@ -336,7 +336,7 @@ void ESP32ImprovComponent::process_incoming_data_() {
this->connecting_sta_ = sta;
wifi::global_wifi_component->set_sta(sta);
wifi::global_wifi_component->start_connecting(sta, false);
wifi::global_wifi_component->start_connecting(sta);
this->set_state_(improv::STATE_PROVISIONING);
ESP_LOGD(TAG, "Received Improv Wi-Fi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
command.password.c_str());

View File

@@ -381,7 +381,10 @@ void EthernetComponent::dump_config() {
break;
}
ESP_LOGCONFIG(TAG, "Ethernet:");
ESP_LOGCONFIG(TAG,
"Ethernet:\n"
" Connected: %s",
YESNO(this->is_connected()));
this->dump_connect_params_();
#ifdef USE_ETHERNET_SPI
ESP_LOGCONFIG(TAG,

View File

@@ -231,7 +231,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
this->connecting_sta_ = sta;
wifi::global_wifi_component->set_sta(sta);
wifi::global_wifi_component->start_connecting(sta, false);
wifi::global_wifi_component->start_connecting(sta);
this->set_state_(improv::STATE_PROVISIONING);
ESP_LOGD(TAG, "Received settings: SSID=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
command.password.c_str());

View File

@@ -406,7 +406,7 @@ void LightCall::transform_parameters_() {
}
}
ColorMode LightCall::compute_color_mode_() {
const auto &supported_modes = this->parent_->get_traits().get_supported_color_modes();
auto supported_modes = this->parent_->get_traits().get_supported_color_modes();
int supported_count = supported_modes.size();
// Some lights don't support any color modes (e.g. monochromatic light), leave it at unknown.

View File

@@ -24,6 +24,9 @@ void LightState::setup() {
effect->init_internal(this);
}
// Start with loop disabled if idle - respects any effects/transitions set up during initialization
this->disable_loop_if_idle_();
// When supported color temperature range is known, initialize color temperature setting within bounds.
auto traits = this->get_traits();
float min_mireds = traits.get_min_mireds();
@@ -126,6 +129,9 @@ void LightState::loop() {
this->is_transformer_active_ = false;
this->transformer_ = nullptr;
this->target_state_reached_callback_.call();
// Disable loop if idle (no transformer and no effect)
this->disable_loop_if_idle_();
}
}
@@ -133,6 +139,8 @@ void LightState::loop() {
if (this->next_write_) {
this->next_write_ = false;
this->output_->write_state(this);
// Disable loop if idle (no transformer and no effect)
this->disable_loop_if_idle_();
}
}
@@ -228,6 +236,8 @@ void LightState::start_effect_(uint32_t effect_index) {
this->active_effect_index_ = effect_index;
auto *effect = this->get_active_effect_();
effect->start_internal();
// Enable loop while effect is active
this->enable_loop();
}
LightEffect *LightState::get_active_effect_() {
if (this->active_effect_index_ == 0) {
@@ -242,6 +252,8 @@ void LightState::stop_effect_() {
effect->stop();
}
this->active_effect_index_ = 0;
// Disable loop if idle (no effect and no transformer)
this->disable_loop_if_idle_();
}
void LightState::start_transition_(const LightColorValues &target, uint32_t length, bool set_remote_values) {
@@ -251,6 +263,8 @@ void LightState::start_transition_(const LightColorValues &target, uint32_t leng
if (set_remote_values) {
this->remote_values = target;
}
// Enable loop while transition is active
this->enable_loop();
}
void LightState::start_flash_(const LightColorValues &target, uint32_t length, bool set_remote_values) {
@@ -266,6 +280,8 @@ void LightState::start_flash_(const LightColorValues &target, uint32_t length, b
if (set_remote_values) {
this->remote_values = target;
};
// Enable loop while flash is active
this->enable_loop();
}
void LightState::set_immediately_(const LightColorValues &target, bool set_remote_values) {
@@ -277,6 +293,14 @@ void LightState::set_immediately_(const LightColorValues &target, bool set_remot
}
this->output_->update_state(this);
this->next_write_ = true;
this->enable_loop();
}
void LightState::disable_loop_if_idle_() {
// Only disable loop if both transformer and effect are inactive, and no pending writes
if (this->transformer_ == nullptr && this->get_active_effect_() == nullptr && !this->next_write_) {
this->disable_loop();
}
}
void LightState::save_remote_values_() {

View File

@@ -256,6 +256,9 @@ class LightState : public EntityBase, public Component {
/// Internal method to save the current remote_values to the preferences
void save_remote_values_();
/// Disable loop if neither transformer nor effect is active
void disable_loop_if_idle_();
/// Store the output to allow effects to have more access.
LightOutput *output_;
/// The currently active transformer for this light (transition/flash).

View File

@@ -18,7 +18,8 @@ class LightTraits {
public:
LightTraits() = default;
const ColorModeMask &get_supported_color_modes() const { return this->supported_color_modes_; }
// Return by value to avoid dangling reference when get_traits() returns a temporary
ColorModeMask get_supported_color_modes() const { return this->supported_color_modes_; }
void set_supported_color_modes(ColorModeMask supported_color_modes) {
this->supported_color_modes_ = supported_color_modes;
}

View File

@@ -331,7 +331,7 @@ async def to_code(configs):
# This must be done after all widgets are created
for comp in helpers.lvgl_components_required:
cg.add_define(f"USE_LVGL_{comp.upper()}")
if "transform_angle" in styles_used:
if {"transform_angle", "transform_zoom"} & styles_used:
add_define("LV_COLOR_SCREEN_TRANSP", "1")
for use in helpers.lv_uses:
add_define(f"LV_USE_{use.upper()}")

View File

@@ -1,7 +1,9 @@
import ipaddress
import logging
import esphome.codegen as cg
from esphome.components.esp32 import add_idf_sdkconfig_option
from esphome.components.psram import is_guaranteed as psram_is_guaranteed
import esphome.config_validation as cv
from esphome.const import CONF_ENABLE_IPV6, CONF_MIN_IPV6_ADDR_COUNT
from esphome.core import CORE, CoroPriority, coroutine_with_priority
@@ -9,6 +11,13 @@ from esphome.core import CORE, CoroPriority, coroutine_with_priority
CODEOWNERS = ["@esphome/core"]
AUTO_LOAD = ["mdns"]
_LOGGER = logging.getLogger(__name__)
# High performance networking tracking infrastructure
# Components can request high performance networking and this configures lwip and WiFi settings
KEY_HIGH_PERFORMANCE_NETWORKING = "high_performance_networking"
CONF_ENABLE_HIGH_PERFORMANCE = "enable_high_performance"
network_ns = cg.esphome_ns.namespace("network")
IPAddress = network_ns.class_("IPAddress")
@@ -47,6 +56,55 @@ def ip_address_literal(ip: str | int | None) -> cg.MockObj:
return IPAddress(str(ip))
def require_high_performance_networking() -> None:
"""Request high performance networking for network and WiFi.
Call this from components that need optimized network performance for streaming
or high-throughput data transfer. This enables high performance mode which
configures both lwip TCP settings and WiFi driver settings for improved
network performance.
Settings applied (ESP-IDF only):
- lwip: Larger TCP buffers, windows, and mailbox sizes
- WiFi: Increased RX/TX buffers, AMPDU aggregation, PSRAM allocation (set by wifi component)
Configuration is PSRAM-aware:
- With PSRAM guaranteed: Aggressive settings (512 RX buffers, 512KB TCP windows)
- Without PSRAM: Conservative optimized settings (64 buffers, 65KB TCP windows)
Example:
from esphome.components import network
def _request_high_performance_networking(config):
network.require_high_performance_networking()
return config
CONFIG_SCHEMA = cv.All(
...,
_request_high_performance_networking,
)
"""
# Only set up once (idempotent - multiple components can call this)
if not CORE.data.get(KEY_HIGH_PERFORMANCE_NETWORKING, False):
CORE.data[KEY_HIGH_PERFORMANCE_NETWORKING] = True
def has_high_performance_networking() -> bool:
"""Check if high performance networking mode is enabled.
Returns True when high performance networking has been requested by a
component or explicitly enabled in the network configuration. This indicates
that lwip and WiFi will use optimized buffer sizes and settings.
This function should be called during code generation (to_code phase) by
components that need to apply performance-related settings.
Returns:
bool: True if high performance networking is enabled, False otherwise
"""
return CORE.data.get(KEY_HIGH_PERFORMANCE_NETWORKING, False)
CONFIG_SCHEMA = cv.Schema(
{
cv.SplitDefault(
@@ -71,6 +129,7 @@ CONFIG_SCHEMA = cv.Schema(
),
),
cv.Optional(CONF_MIN_IPV6_ADDR_COUNT, default=0): cv.positive_int,
cv.Optional(CONF_ENABLE_HIGH_PERFORMANCE): cv.All(cv.boolean, cv.only_on_esp32),
}
)
@@ -80,6 +139,70 @@ async def to_code(config):
cg.add_define("USE_NETWORK")
if CORE.using_arduino and CORE.is_esp32:
cg.add_library("Networking", None)
# Apply high performance networking settings
# Config can explicitly enable/disable, or default to component-driven behavior
enable_high_perf = config.get(CONF_ENABLE_HIGH_PERFORMANCE)
component_requested = CORE.data.get(KEY_HIGH_PERFORMANCE_NETWORKING, False)
# Explicit config overrides component request
should_enable = (
enable_high_perf if enable_high_perf is not None else component_requested
)
# Log when user explicitly disables but a component requested it
if enable_high_perf is False and component_requested:
_LOGGER.info(
"High performance networking disabled by user configuration (overriding component request)"
)
if CORE.is_esp32 and CORE.using_esp_idf and should_enable:
# Check if PSRAM is guaranteed (set by psram component during final validation)
psram_guaranteed = psram_is_guaranteed()
if psram_guaranteed:
_LOGGER.info(
"Applying high-performance lwip settings (PSRAM guaranteed): 512KB TCP windows, 512 mailbox sizes"
)
# PSRAM is guaranteed - use aggressive settings
# Higher maximum values are allowed because CONFIG_LWIP_WND_SCALE is set to true
# CONFIG_LWIP_WND_SCALE can only be enabled if CONFIG_SPIRAM_IGNORE_NOTFOUND isn't set
# Based on https://github.com/espressif/esp-adf/issues/297#issuecomment-783811702
# Enable window scaling for much larger TCP windows
add_idf_sdkconfig_option("CONFIG_LWIP_WND_SCALE", True)
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_RCV_SCALE", 3)
# Large TCP buffers and windows (requires PSRAM)
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT", 65534)
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_WND_DEFAULT", 512000)
# Large mailboxes for high throughput
add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_RECVMBOX_SIZE", 512)
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_RECVMBOX_SIZE", 512)
# TCP connection limits
add_idf_sdkconfig_option("CONFIG_LWIP_MAX_ACTIVE_TCP", 16)
add_idf_sdkconfig_option("CONFIG_LWIP_MAX_LISTENING_TCP", 16)
# TCP optimizations
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_MAXRTX", 12)
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_SYNMAXRTX", 6)
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_MSS", 1436)
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_MSL", 60000)
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_OVERSIZE_MSS", True)
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_QUEUE_OOSEQ", True)
else:
_LOGGER.info(
"Applying optimized lwip settings: 65KB TCP windows, 64 mailbox sizes"
)
# PSRAM not guaranteed - use more conservative, but still optimized settings
# Based on https://github.com/espressif/esp-idf/blob/release/v5.4/examples/wifi/iperf/sdkconfig.defaults.esp32
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT", 65534)
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_WND_DEFAULT", 65534)
add_idf_sdkconfig_option("CONFIG_LWIP_TCP_RECVMBOX_SIZE", 64)
add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_RECVMBOX_SIZE", 64)
if (enable_ipv6 := config.get(CONF_ENABLE_IPV6, None)) is not None:
cg.add_define("USE_NETWORK_IPV6", enable_ipv6)
if enable_ipv6:

View File

@@ -118,10 +118,10 @@ struct IPAddress {
operator arduino_ns::IPAddress() const { return ip_addr_get_ip4_u32(&ip_addr_); }
#endif
bool is_set() { return !ip_addr_isany(&ip_addr_); } // NOLINT(readability-simplify-boolean-expr)
bool is_ip4() { return IP_IS_V4(&ip_addr_); }
bool is_ip6() { return IP_IS_V6(&ip_addr_); }
bool is_multicast() { return ip_addr_ismulticast(&ip_addr_); }
bool is_set() const { return !ip_addr_isany(&ip_addr_); } // NOLINT(readability-simplify-boolean-expr)
bool is_ip4() const { return IP_IS_V4(&ip_addr_); }
bool is_ip6() const { return IP_IS_V6(&ip_addr_); }
bool is_multicast() const { return ip_addr_ismulticast(&ip_addr_); }
std::string str() const { return str_lower_case(ipaddr_ntoa(&ip_addr_)); }
bool operator==(const IPAddress &other) const { return ip_addr_cmp(&ip_addr_, &other.ip_addr_); }
bool operator!=(const IPAddress &other) const { return !ip_addr_cmp(&ip_addr_, &other.ip_addr_); }

View File

@@ -103,11 +103,11 @@ nrf52_ns = cg.esphome_ns.namespace("nrf52")
DeviceFirmwareUpdate = nrf52_ns.class_("DeviceFirmwareUpdate", cg.Component)
CONF_DFU = "dfu"
CONF_DCDC = "dcdc"
CONF_REG0 = "reg0"
CONF_UICR_ERASE = "uicr_erase"
VOLTAGE_LEVELS = [1.8, 2.1, 2.4, 2.7, 3.0, 3.3]
DEFAULT_VOLTAGE_LEVEL = "default"
CONFIG_SCHEMA = cv.All(
_detect_bootloader,
@@ -122,14 +122,12 @@ CONFIG_SCHEMA = cv.All(
cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema,
}
),
cv.Optional(CONF_DCDC, default=True): cv.boolean,
cv.Optional(CONF_REG0): cv.Schema(
{
cv.Required(CONF_VOLTAGE): cv.Any(
cv.All(
cv.voltage,
cv.one_of(*VOLTAGE_LEVELS, float=True),
),
cv.one_of(*[DEFAULT_VOLTAGE_LEVEL], lower=True),
cv.Required(CONF_VOLTAGE): cv.All(
cv.voltage,
cv.one_of(*VOLTAGE_LEVELS, float=True),
),
cv.Optional(CONF_UICR_ERASE, default=False): cv.boolean,
}
@@ -200,11 +198,10 @@ async def to_code(config: ConfigType) -> None:
if dfu_config := config.get(CONF_DFU):
CORE.add_job(_dfu_to_code, dfu_config)
zephyr_add_prj_conf("BOARD_ENABLE_DCDC", config[CONF_DCDC])
if reg0_config := config.get(CONF_REG0):
value = 7 # DEFAULT_VOLTAGE_LEVEL
if reg0_config[CONF_VOLTAGE] in VOLTAGE_LEVELS:
value = VOLTAGE_LEVELS.index(reg0_config[CONF_VOLTAGE])
value = VOLTAGE_LEVELS.index(reg0_config[CONF_VOLTAGE])
cg.add_define("USE_NRF52_REG0_VOUT", value)
if reg0_config[CONF_UICR_ERASE]:
cg.add_define("USE_NRF52_UICR_ERASE")

View File

@@ -69,9 +69,20 @@ static StatusFlags fix_bootloader() {
}
#endif
#define BOOTLOADER_VERSION_REGISTER NRF_TIMER2->CC[0]
static StatusFlags set_uicr() {
StatusFlags status = StatusFlags::OK;
status |= set_regout0();
#ifndef USE_BOOTLOADER_MCUBOOT
if (BOOTLOADER_VERSION_REGISTER <= 0x902) {
#ifdef CONFIG_PRINTK
printk("cannot control regout0 for %#x\n", BOOTLOADER_VERSION_REGISTER);
#endif
} else
#endif
{
status |= set_regout0();
}
#ifndef USE_BOOTLOADER_MCUBOOT
status |= fix_bootloader();
#endif

View File

@@ -174,6 +174,9 @@ FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config):
cg.add_define("USE_OPENTHREAD")
# OpenThread uses esp_vfs_eventfd which requires VFS select support
require_vfs_select()
# OpenThread SRP needs access to mDNS services after setup
enable_mdns_storage()

View File

@@ -35,6 +35,9 @@ DOMAIN = "psram"
DEPENDENCIES = [PLATFORM_ESP32]
# PSRAM availability tracking for cross-component coordination
KEY_PSRAM_GUARANTEED = "psram_guaranteed"
_LOGGER = logging.getLogger(__name__)
psram_ns = cg.esphome_ns.namespace(DOMAIN)
@@ -71,6 +74,23 @@ def supported() -> bool:
return variant in SPIRAM_MODES
def is_guaranteed() -> bool:
"""Check if PSRAM is guaranteed to be available.
Returns True when PSRAM is configured with both 'disabled: false' and
'ignore_not_found: false', meaning the device will fail to boot if PSRAM
is not found. This ensures safe use of high buffer configurations that
depend on PSRAM.
This function should be called during code generation (to_code phase) by
components that need to know PSRAM availability for configuration decisions.
Returns:
bool: True if PSRAM is guaranteed, False otherwise
"""
return CORE.data.get(KEY_PSRAM_GUARANTEED, False)
def validate_psram_mode(config):
esp32_config = fv.full_config.get()[PLATFORM_ESP32]
if config[CONF_SPEED] == "120MHZ":
@@ -131,7 +151,22 @@ def get_config_schema(config):
CONFIG_SCHEMA = get_config_schema
FINAL_VALIDATE_SCHEMA = validate_psram_mode
def _store_psram_guaranteed(config):
"""Store PSRAM guaranteed status in CORE.data for other components.
PSRAM is "guaranteed" when it will fail if not found, ensuring safe use
of high buffer configurations in network/wifi components.
Called during final validation to ensure the flag is available
before any to_code() functions run.
"""
psram_guaranteed = not config[CONF_DISABLED] and not config[CONF_IGNORE_NOT_FOUND]
CORE.data[KEY_PSRAM_GUARANTEED] = psram_guaranteed
return config
FINAL_VALIDATE_SCHEMA = cv.All(validate_psram_mode, _store_psram_guaranteed)
async def to_code(config):

View File

@@ -6,7 +6,7 @@ from pathlib import Path
from esphome import automation, external_files
import esphome.codegen as cg
from esphome.components import audio, esp32, media_player, psram, speaker
from esphome.components import audio, esp32, media_player, network, psram, speaker
import esphome.config_validation as cv
from esphome.const import (
CONF_BUFFER_SIZE,
@@ -32,6 +32,7 @@ _LOGGER = logging.getLogger(__name__)
AUTO_LOAD = ["audio"]
DEPENDENCIES = ["network"]
CODEOWNERS = ["@kahrendt", "@synesthesiam"]
DOMAIN = "media_player"
@@ -280,6 +281,18 @@ PIPELINE_SCHEMA = cv.Schema(
}
)
def _request_high_performance_networking(config):
"""Request high performance networking for streaming media.
Speaker media player streams audio data, so it always benefits from
optimized WiFi and lwip settings regardless of codec support.
Called during config validation to ensure flags are set before to_code().
"""
network.require_high_performance_networking()
return config
CONFIG_SCHEMA = cv.All(
media_player.media_player_schema(SpeakerMediaPlayer).extend(
{
@@ -304,6 +317,7 @@ CONFIG_SCHEMA = cv.All(
),
cv.only_with_esp_idf,
_validate_repeated_speaker,
_request_high_performance_networking,
)
@@ -321,28 +335,10 @@ FINAL_VALIDATE_SCHEMA = cv.All(
async def to_code(config):
if CORE.data[DOMAIN][config[CONF_ID].id][CONF_CODEC_SUPPORT_ENABLED]:
# Compile all supported audio codecs and optimize the wifi settings
# Compile all supported audio codecs
cg.add_define("USE_AUDIO_FLAC_SUPPORT", True)
cg.add_define("USE_AUDIO_MP3_SUPPORT", True)
# Based on https://github.com/espressif/esp-idf/blob/release/v5.4/examples/wifi/iperf/sdkconfig.defaults.esp32
esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM", 16)
esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM", 64)
esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM", 64)
esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_TX_ENABLED", True)
esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_TX_BA_WIN", 32)
esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_RX_ENABLED", True)
esp32.add_idf_sdkconfig_option("CONFIG_ESP_WIFI_RX_BA_WIN", 32)
esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCP_SND_BUF_DEFAULT", 65534)
esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCP_WND_DEFAULT", 65534)
esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCP_RECVMBOX_SIZE", 64)
esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_RECVMBOX_SIZE", 64)
# Allocate wifi buffers in PSRAM
esp32.add_idf_sdkconfig_option("CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP", True)
var = await media_player.new_media_player(config)
await cg.register_component(var, config)

View File

@@ -137,7 +137,11 @@ async def to_code(config):
cg.add(var.set_arming_night_time(config[CONF_ARMING_NIGHT_TIME]))
supports_arm_night = True
for sensor in config.get(CONF_BINARY_SENSORS, []):
if sensors := config.get(CONF_BINARY_SENSORS, []):
# Initialize FixedVector with the exact number of sensors
cg.add(var.init_sensors(len(sensors)))
for sensor in sensors:
bs = await cg.get_variable(sensor[CONF_INPUT])
flags = BinarySensorFlags[FLAG_NORMAL]

View File

@@ -20,10 +20,13 @@ void TemplateAlarmControlPanel::add_sensor(binary_sensor::BinarySensor *sensor,
// Save the flags and type. Assign a store index for the per sensor data type.
SensorDataStore sd;
sd.last_chime_state = false;
this->sensor_map_[sensor].flags = flags;
this->sensor_map_[sensor].type = type;
AlarmSensor alarm_sensor;
alarm_sensor.sensor = sensor;
alarm_sensor.info.flags = flags;
alarm_sensor.info.type = type;
alarm_sensor.info.store_index = this->next_store_index_++;
this->sensors_.push_back(alarm_sensor);
this->sensor_data_.push_back(sd);
this->sensor_map_[sensor].store_index = this->next_store_index_++;
};
static const LogString *sensor_type_to_string(AlarmSensorType type) {
@@ -45,7 +48,7 @@ void TemplateAlarmControlPanel::dump_config() {
ESP_LOGCONFIG(TAG,
"TemplateAlarmControlPanel:\n"
" Current State: %s\n"
" Number of Codes: %u\n"
" Number of Codes: %zu\n"
" Requires Code To Arm: %s\n"
" Arming Away Time: %" PRIu32 "s\n"
" Arming Home Time: %" PRIu32 "s\n"
@@ -58,7 +61,8 @@ void TemplateAlarmControlPanel::dump_config() {
(this->arming_home_time_ / 1000), (this->arming_night_time_ / 1000), (this->pending_time_ / 1000),
(this->trigger_time_ / 1000), this->get_supported_features());
#ifdef USE_BINARY_SENSOR
for (auto const &[sensor, info] : this->sensor_map_) {
for (const auto &alarm_sensor : this->sensors_) {
const uint16_t flags = alarm_sensor.info.flags;
ESP_LOGCONFIG(TAG,
" Binary Sensor:\n"
" Name: %s\n"
@@ -67,11 +71,10 @@ void TemplateAlarmControlPanel::dump_config() {
" Armed night bypass: %s\n"
" Auto bypass: %s\n"
" Chime mode: %s",
sensor->get_name().c_str(), LOG_STR_ARG(sensor_type_to_string(info.type)),
TRUEFALSE(info.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME),
TRUEFALSE(info.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT),
TRUEFALSE(info.flags & BINARY_SENSOR_MODE_BYPASS_AUTO),
TRUEFALSE(info.flags & BINARY_SENSOR_MODE_CHIME));
alarm_sensor.sensor->get_name().c_str(), LOG_STR_ARG(sensor_type_to_string(alarm_sensor.info.type)),
TRUEFALSE(flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME),
TRUEFALSE(flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT),
TRUEFALSE(flags & BINARY_SENSOR_MODE_BYPASS_AUTO), TRUEFALSE(flags & BINARY_SENSOR_MODE_CHIME));
}
#endif
}
@@ -121,7 +124,9 @@ void TemplateAlarmControlPanel::loop() {
#ifdef USE_BINARY_SENSOR
// Test all of the sensors regardless of the alarm panel state
for (auto const &[sensor, info] : this->sensor_map_) {
for (const auto &alarm_sensor : this->sensors_) {
const auto &info = alarm_sensor.info;
auto *sensor = alarm_sensor.sensor;
// Check for chime zones
if (info.flags & BINARY_SENSOR_MODE_CHIME) {
// Look for the transition from closed to open
@@ -242,11 +247,11 @@ void TemplateAlarmControlPanel::arm_(optional<std::string> code, alarm_control_p
void TemplateAlarmControlPanel::bypass_before_arming() {
#ifdef USE_BINARY_SENSOR
for (auto const &[sensor, info] : this->sensor_map_) {
for (const auto &alarm_sensor : this->sensors_) {
// Check for faulted bypass_auto sensors and remove them from monitoring
if ((info.flags & BINARY_SENSOR_MODE_BYPASS_AUTO) && (sensor->state)) {
ESP_LOGW(TAG, "'%s' is faulted and will be automatically bypassed", sensor->get_name().c_str());
this->bypassed_sensor_indicies_.push_back(info.store_index);
if ((alarm_sensor.info.flags & BINARY_SENSOR_MODE_BYPASS_AUTO) && (alarm_sensor.sensor->state)) {
ESP_LOGW(TAG, "'%s' is faulted and will be automatically bypassed", alarm_sensor.sensor->get_name().c_str());
this->bypassed_sensor_indicies_.push_back(alarm_sensor.info.store_index);
}
}
#endif

View File

@@ -1,11 +1,12 @@
#pragma once
#include <cinttypes>
#include <map>
#include <vector>
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/components/alarm_control_panel/alarm_control_panel.h"
@@ -49,6 +50,13 @@ struct SensorInfo {
uint8_t store_index;
};
#ifdef USE_BINARY_SENSOR
struct AlarmSensor {
binary_sensor::BinarySensor *sensor;
SensorInfo info;
};
#endif
class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControlPanel, public Component {
public:
TemplateAlarmControlPanel();
@@ -63,6 +71,12 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl
void bypass_before_arming();
#ifdef USE_BINARY_SENSOR
/** Initialize the sensors vector with the specified capacity.
*
* @param capacity The number of sensors to allocate space for.
*/
void init_sensors(size_t capacity) { this->sensors_.init(capacity); }
/** Add a binary_sensor to the alarm_panel.
*
* @param sensor The BinarySensor instance.
@@ -122,8 +136,8 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl
protected:
void control(const alarm_control_panel::AlarmControlPanelCall &call) override;
#ifdef USE_BINARY_SENSOR
// This maps a binary sensor to its alarm specific info
std::map<binary_sensor::BinarySensor *, SensorInfo> sensor_map_;
// List of binary sensors with their alarm-specific info
FixedVector<AlarmSensor> sensors_;
// a list of automatically bypassed sensors
std::vector<uint8_t> bypassed_sensor_indicies_;
#endif

View File

@@ -945,6 +945,10 @@ async def to_code(config):
cg.add(var.set_humidity_hysteresis(config[CONF_HUMIDITY_HYSTERESIS]))
if CONF_PRESET in config:
# Separate standard and custom presets, and build preset config variables
standard_presets: list[tuple[cg.MockObj, cg.MockObj]] = []
custom_presets: list[tuple[str, cg.MockObj]] = []
for preset_config in config[CONF_PRESET]:
name = preset_config[CONF_NAME]
standard_preset = None
@@ -987,9 +991,39 @@ async def to_code(config):
)
if standard_preset is not None:
cg.add(var.set_preset_config(standard_preset, preset_target_variable))
standard_presets.append((standard_preset, preset_target_variable))
else:
cg.add(var.set_custom_preset_config(name, preset_target_variable))
custom_presets.append((name, preset_target_variable))
# Build initializer list for standard presets
if standard_presets:
cg.add(
var.set_preset_config(
[
cg.StructInitializer(
thermostat_ns.struct("ThermostatPresetEntry"),
("preset", preset),
("config", preset_var),
)
for preset, preset_var in standard_presets
]
)
)
# Build initializer list for custom presets
if custom_presets:
cg.add(
var.set_custom_preset_config(
[
cg.StructInitializer(
thermostat_ns.struct("ThermostatCustomPresetEntry"),
("name", cg.RawExpression(f'"{name}"')),
("config", preset_var),
)
for name, preset_var in custom_presets
]
)
)
if CONF_DEFAULT_PRESET in config:
default_preset_name = config[CONF_DEFAULT_PRESET]

View File

@@ -53,8 +53,8 @@ void ThermostatClimate::setup() {
if (use_default_preset) {
if (this->default_preset_ != climate::ClimatePreset::CLIMATE_PRESET_NONE) {
this->change_preset_(this->default_preset_);
} else if (!this->default_custom_preset_.empty()) {
this->change_custom_preset_(this->default_custom_preset_.c_str());
} else if (this->default_custom_preset_ != nullptr) {
this->change_custom_preset_(this->default_custom_preset_);
}
}
@@ -319,16 +319,16 @@ climate::ClimateTraits ThermostatClimate::traits() {
if (this->supports_swing_mode_vertical_)
traits.add_supported_swing_mode(climate::CLIMATE_SWING_VERTICAL);
for (auto &it : this->preset_config_) {
traits.add_supported_preset(it.first);
for (const auto &entry : this->preset_config_) {
traits.add_supported_preset(entry.preset);
}
// Extract custom preset names from the custom_preset_config_ map
// Extract custom preset names from the custom_preset_config_ vector
if (!this->custom_preset_config_.empty()) {
std::vector<const char *> custom_preset_names;
custom_preset_names.reserve(this->custom_preset_config_.size());
for (const auto &it : this->custom_preset_config_) {
custom_preset_names.push_back(it.first.c_str());
for (const auto &entry : this->custom_preset_config_) {
custom_preset_names.push_back(entry.name);
}
traits.set_supported_custom_presets(custom_preset_names);
}
@@ -1154,12 +1154,18 @@ void ThermostatClimate::dump_preset_config_(const char *preset_name, const Therm
}
void ThermostatClimate::change_preset_(climate::ClimatePreset preset) {
auto config = this->preset_config_.find(preset);
// Linear search through preset configurations
const ThermostatClimateTargetTempConfig *config = nullptr;
for (const auto &entry : this->preset_config_) {
if (entry.preset == preset) {
config = &entry.config;
break;
}
}
if (config != this->preset_config_.end()) {
if (config != nullptr) {
ESP_LOGV(TAG, "Preset %s requested", LOG_STR_ARG(climate::climate_preset_to_string(preset)));
if (this->change_preset_internal_(config->second) || (!this->preset.has_value()) ||
this->preset.value() != preset) {
if (this->change_preset_internal_(*config) || (!this->preset.has_value()) || this->preset.value() != preset) {
// Fire any preset changed trigger if defined
Trigger<> *trig = this->preset_change_trigger_;
this->set_preset_(preset);
@@ -1178,11 +1184,18 @@ void ThermostatClimate::change_preset_(climate::ClimatePreset preset) {
}
void ThermostatClimate::change_custom_preset_(const char *custom_preset) {
auto config = this->custom_preset_config_.find(custom_preset);
// Linear search through custom preset configurations
const ThermostatClimateTargetTempConfig *config = nullptr;
for (const auto &entry : this->custom_preset_config_) {
if (strcmp(entry.name, custom_preset) == 0) {
config = &entry.config;
break;
}
}
if (config != this->custom_preset_config_.end()) {
if (config != nullptr) {
ESP_LOGV(TAG, "Custom preset %s requested", custom_preset);
if (this->change_preset_internal_(config->second) || !this->has_custom_preset() ||
if (this->change_preset_internal_(*config) || !this->has_custom_preset() ||
strcmp(this->get_custom_preset(), custom_preset) != 0) {
// Fire any preset changed trigger if defined
Trigger<> *trig = this->preset_change_trigger_;
@@ -1247,14 +1260,12 @@ bool ThermostatClimate::change_preset_internal_(const ThermostatClimateTargetTem
return something_changed;
}
void ThermostatClimate::set_preset_config(climate::ClimatePreset preset,
const ThermostatClimateTargetTempConfig &config) {
this->preset_config_[preset] = config;
void ThermostatClimate::set_preset_config(std::initializer_list<PresetEntry> presets) {
this->preset_config_ = presets;
}
void ThermostatClimate::set_custom_preset_config(const std::string &name,
const ThermostatClimateTargetTempConfig &config) {
this->custom_preset_config_[name] = config;
void ThermostatClimate::set_custom_preset_config(std::initializer_list<CustomPresetEntry> presets) {
this->custom_preset_config_ = presets;
}
ThermostatClimate::ThermostatClimate()
@@ -1293,8 +1304,16 @@ ThermostatClimate::ThermostatClimate()
humidity_control_humidify_action_trigger_(new Trigger<>()),
humidity_control_off_action_trigger_(new Trigger<>()) {}
void ThermostatClimate::set_default_preset(const std::string &custom_preset) {
this->default_custom_preset_ = custom_preset;
void ThermostatClimate::set_default_preset(const char *custom_preset) {
// Find the preset in custom_preset_config_ and store pointer from there
for (const auto &entry : this->custom_preset_config_) {
if (strcmp(entry.name, custom_preset) == 0) {
this->default_custom_preset_ = entry.name;
return;
}
}
// If not found, it will be caught during validation
this->default_custom_preset_ = nullptr;
}
void ThermostatClimate::set_default_preset(climate::ClimatePreset preset) { this->default_preset_ = preset; }
@@ -1605,19 +1624,22 @@ void ThermostatClimate::dump_config() {
if (!this->preset_config_.empty()) {
ESP_LOGCONFIG(TAG, " Supported PRESETS:");
for (auto &it : this->preset_config_) {
const auto *preset_name = LOG_STR_ARG(climate::climate_preset_to_string(it.first));
ESP_LOGCONFIG(TAG, " %s:%s", preset_name, it.first == this->default_preset_ ? " (default)" : "");
this->dump_preset_config_(preset_name, it.second);
for (const auto &entry : this->preset_config_) {
const auto *preset_name = LOG_STR_ARG(climate::climate_preset_to_string(entry.preset));
ESP_LOGCONFIG(TAG, " %s:%s", preset_name, entry.preset == this->default_preset_ ? " (default)" : "");
this->dump_preset_config_(preset_name, entry.config);
}
}
if (!this->custom_preset_config_.empty()) {
ESP_LOGCONFIG(TAG, " Supported CUSTOM PRESETS:");
for (auto &it : this->custom_preset_config_) {
const auto *preset_name = it.first.c_str();
ESP_LOGCONFIG(TAG, " %s:%s", preset_name, it.first == this->default_custom_preset_ ? " (default)" : "");
this->dump_preset_config_(preset_name, it.second);
for (const auto &entry : this->custom_preset_config_) {
const auto *preset_name = entry.name;
ESP_LOGCONFIG(TAG, " %s:%s", preset_name,
(this->default_custom_preset_ != nullptr && strcmp(entry.name, this->default_custom_preset_) == 0)
? " (default)"
: "");
this->dump_preset_config_(preset_name, entry.config);
}
}
}

View File

@@ -3,12 +3,12 @@
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/components/climate/climate.h"
#include "esphome/components/sensor/sensor.h"
#include <array>
#include <cinttypes>
#include <map>
namespace esphome {
namespace thermostat {
@@ -72,14 +72,29 @@ struct ThermostatClimateTargetTempConfig {
optional<climate::ClimateMode> mode_{};
};
/// Entry for standard preset lookup
struct ThermostatPresetEntry {
climate::ClimatePreset preset;
ThermostatClimateTargetTempConfig config;
};
/// Entry for custom preset lookup
struct ThermostatCustomPresetEntry {
const char *name;
ThermostatClimateTargetTempConfig config;
};
class ThermostatClimate : public climate::Climate, public Component {
public:
using PresetEntry = ThermostatPresetEntry;
using CustomPresetEntry = ThermostatCustomPresetEntry;
ThermostatClimate();
void setup() override;
void dump_config() override;
void loop() override;
void set_default_preset(const std::string &custom_preset);
void set_default_preset(const char *custom_preset);
void set_default_preset(climate::ClimatePreset preset);
void set_on_boot_restore_from(OnBootRestoreFrom on_boot_restore_from);
void set_set_point_minimum_differential(float differential);
@@ -131,8 +146,8 @@ class ThermostatClimate : public climate::Climate, public Component {
void set_supports_humidification(bool supports_humidification);
void set_supports_two_points(bool supports_two_points);
void set_preset_config(climate::ClimatePreset preset, const ThermostatClimateTargetTempConfig &config);
void set_custom_preset_config(const std::string &name, const ThermostatClimateTargetTempConfig &config);
void set_preset_config(std::initializer_list<PresetEntry> presets);
void set_custom_preset_config(std::initializer_list<CustomPresetEntry> presets);
Trigger<> *get_cool_action_trigger() const;
Trigger<> *get_supplemental_cool_action_trigger() const;
@@ -516,9 +531,6 @@ class ThermostatClimate : public climate::Climate, public Component {
Trigger<> *prev_swing_mode_trigger_{nullptr};
Trigger<> *prev_humidity_control_trigger_{nullptr};
/// Default custom preset to use on start up
std::string default_custom_preset_{};
/// Climate action timers
std::array<ThermostatClimateTimer, THERMOSTAT_TIMER_COUNT> timer_{
ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::cooling_max_run_time_timer_callback_, this)),
@@ -534,9 +546,12 @@ class ThermostatClimate : public climate::Climate, public Component {
};
/// The set of standard preset configurations this thermostat supports (Eg. AWAY, ECO, etc)
std::map<climate::ClimatePreset, ThermostatClimateTargetTempConfig> preset_config_{};
FixedVector<PresetEntry> preset_config_{};
/// The set of custom preset configurations this thermostat supports (eg. "My Custom Preset")
std::map<std::string, ThermostatClimateTargetTempConfig> custom_preset_config_{};
FixedVector<CustomPresetEntry> custom_preset_config_{};
/// Default custom preset to use on start up (pointer to entry in custom_preset_config_)
private:
const char *default_custom_preset_{nullptr};
};
} // namespace thermostat

View File

@@ -5,7 +5,11 @@ from esphome.automation import Condition
import esphome.codegen as cg
from esphome.components.const import CONF_USE_PSRAM
from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant
from esphome.components.network import ip_address_literal
from esphome.components.network import (
has_high_performance_networking,
ip_address_literal,
)
from esphome.components.psram import is_guaranteed as psram_is_guaranteed
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv
from esphome.config_validation import only_with_esp_idf
@@ -56,6 +60,8 @@ _LOGGER = logging.getLogger(__name__)
AUTO_LOAD = ["network"]
_LOGGER = logging.getLogger(__name__)
NO_WIFI_VARIANTS = [const.VARIANT_ESP32H2, const.VARIANT_ESP32P4]
CONF_SAVE = "save"
CONF_MIN_AUTH_MODE = "min_auth_mode"
@@ -496,6 +502,56 @@ async def to_code(config):
if config.get(CONF_USE_PSRAM):
add_idf_sdkconfig_option("CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP", True)
# Apply high performance WiFi settings if high performance networking is enabled
if CORE.is_esp32 and CORE.using_esp_idf and has_high_performance_networking():
# Check if PSRAM is guaranteed (set by psram component during final validation)
psram_guaranteed = psram_is_guaranteed()
# Always allocate WiFi buffers in PSRAM if available
add_idf_sdkconfig_option("CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP", True)
if psram_guaranteed:
_LOGGER.info(
"Applying high-performance WiFi settings (PSRAM guaranteed): 512 RX buffers, 32 TX buffers"
)
# PSRAM is guaranteed - use aggressive settings
# Higher maximum values are allowed because CONFIG_LWIP_WND_SCALE is set to true in networking component
# Based on https://github.com/espressif/esp-adf/issues/297#issuecomment-783811702
# Large dynamic RX buffers (requires PSRAM)
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM", 16)
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM", 512)
# Static TX buffers for better performance
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_STATIC_TX_BUFFER", True)
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_TX_BUFFER_TYPE", 0)
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_CACHE_TX_BUFFER_NUM", 32)
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_STATIC_TX_BUFFER_NUM", 8)
# AMPDU settings optimized for PSRAM
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_TX_ENABLED", True)
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_TX_BA_WIN", 16)
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_RX_ENABLED", True)
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_RX_BA_WIN", 32)
else:
_LOGGER.info(
"Applying optimized WiFi settings: 64 RX buffers, 64 TX buffers"
)
# PSRAM not guaranteed - use more conservative, but still optimized settings
# Based on https://github.com/espressif/esp-idf/blob/release/v5.4/examples/wifi/iperf/sdkconfig.defaults.esp32
# Standard buffer counts
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM", 16)
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM", 64)
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM", 64)
# Standard AMPDU settings
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_TX_ENABLED", True)
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_TX_BA_WIN", 32)
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_AMPDU_RX_ENABLED", True)
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_RX_BA_WIN", 32)
cg.add_define("USE_WIFI")
# must register before OTA safe mode check

View File

@@ -197,6 +197,10 @@ static constexpr uint8_t WIFI_RETRY_COUNT_PER_SSID = 1;
// Rationale: Fast connect prioritizes speed - try each AP once to find a working one quickly
static constexpr uint8_t WIFI_RETRY_COUNT_PER_AP = 1;
/// Cooldown duration in milliseconds after adapter restart or repeated failures
/// Allows WiFi hardware to stabilize before next connection attempt
static constexpr uint32_t WIFI_COOLDOWN_DURATION_MS = 1000;
static constexpr uint8_t get_max_retries_for_phase(WiFiRetryPhase phase) {
switch (phase) {
case WiFiRetryPhase::INITIAL_CONNECT:
@@ -275,7 +279,7 @@ int8_t WiFiComponent::find_next_hidden_sta_(int8_t start_index) {
ESP_LOGD(TAG, "Hidden candidate " LOG_SECRET("'%s'") " at index %d", sta.get_ssid().c_str(), static_cast<int>(i));
return static_cast<int8_t>(i);
}
ESP_LOGD(TAG, "Skipping " LOG_SECRET("'%s'") " (visible in scan)", sta.get_ssid().c_str());
ESP_LOGD(TAG, "Skipping hidden retry for visible network " LOG_SECRET("'%s'"), sta.get_ssid().c_str());
}
// No hidden SSIDs found
return -1;
@@ -289,7 +293,7 @@ void WiFiComponent::start_initial_connection_() {
this->selected_sta_index_ = 0;
this->retry_phase_ = WiFiRetryPhase::EXPLICIT_HIDDEN;
WiFiAP params = this->build_params_for_current_phase_();
this->start_connecting(params, false);
this->start_connecting(params);
} else {
ESP_LOGI(TAG, "Starting scan");
this->start_scanning();
@@ -371,13 +375,13 @@ void WiFiComponent::start() {
// Without saved data, try first configured network or use normal flow
if (loaded_fast_connect) {
ESP_LOGI(TAG, "Starting fast_connect (saved) " LOG_SECRET("'%s'"), params.get_ssid().c_str());
this->start_connecting(params, false);
this->start_connecting(params);
} else if (!this->sta_.empty() && !this->sta_[0].get_hidden()) {
// No saved data, but have configured networks - try first non-hidden network
ESP_LOGI(TAG, "Starting fast_connect (config) " LOG_SECRET("'%s'"), this->sta_[0].get_ssid().c_str());
this->selected_sta_index_ = 0;
params = this->build_params_for_current_phase_();
this->start_connecting(params, false);
this->start_connecting(params);
} else {
// No saved data and (no networks OR first is hidden) - use normal flow
this->start_initial_connection_();
@@ -417,6 +421,7 @@ void WiFiComponent::restart_adapter() {
// Don't set retry_phase_ or num_retried_ here - state machine handles transitions
this->state_ = WIFI_COMPONENT_STATE_COOLDOWN;
this->action_started_ = millis();
this->error_from_callback_ = false;
}
void WiFiComponent::loop() {
@@ -436,7 +441,7 @@ void WiFiComponent::loop() {
switch (this->state_) {
case WIFI_COMPONENT_STATE_COOLDOWN: {
this->status_set_warning(LOG_STR("waiting to reconnect"));
if (millis() - this->action_started_ > 5000) {
if (now - this->action_started_ > WIFI_COOLDOWN_DURATION_MS) {
// After cooldown we either restarted the adapter because of
// a failure, or something tried to connect over and over
// so we entered cooldown. In both cases we call
@@ -450,8 +455,7 @@ void WiFiComponent::loop() {
this->check_scanning_finished();
break;
}
case WIFI_COMPONENT_STATE_STA_CONNECTING:
case WIFI_COMPONENT_STATE_STA_CONNECTING_2: {
case WIFI_COMPONENT_STATE_STA_CONNECTING: {
this->status_set_warning(LOG_STR("associating to network"));
this->check_connecting_finished();
break;
@@ -461,6 +465,8 @@ void WiFiComponent::loop() {
if (!this->is_connected()) {
ESP_LOGW(TAG, "Connection lost; reconnecting");
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING;
// Clear error flag before reconnecting so first attempt is not seen as immediate failure
this->error_from_callback_ = false;
this->retry_connect();
} else {
this->status_clear_warning();
@@ -662,27 +668,27 @@ void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &pa
this->set_sta(sta);
}
void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) {
void WiFiComponent::start_connecting(const WiFiAP &ap) {
// Log connection attempt at INFO level with priority
std::string bssid_formatted;
char bssid_s[18];
int8_t priority = 0;
if (ap.get_bssid().has_value()) {
bssid_formatted = format_mac_address_pretty(ap.get_bssid().value().data());
format_mac_addr_upper(ap.get_bssid().value().data(), bssid_s);
priority = this->get_sta_priority(ap.get_bssid().value());
}
ESP_LOGI(TAG,
"Connecting to " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") " (priority %d, attempt %u/%u in phase %s)...",
ap.get_ssid().c_str(), ap.get_bssid().has_value() ? bssid_formatted.c_str() : LOG_STR_LITERAL("any"),
priority, this->num_retried_ + 1, get_max_retries_for_phase(this->retry_phase_),
ap.get_ssid().c_str(), ap.get_bssid().has_value() ? bssid_s : LOG_STR_LITERAL("any"), priority,
this->num_retried_ + 1, get_max_retries_for_phase(this->retry_phase_),
LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_)));
#ifdef ESPHOME_LOG_HAS_VERBOSE
ESP_LOGV(TAG, "Connection Params:");
ESP_LOGV(TAG, " SSID: '%s'", ap.get_ssid().c_str());
if (ap.get_bssid().has_value()) {
ESP_LOGV(TAG, " BSSID: %s", format_mac_address_pretty(ap.get_bssid()->data()).c_str());
ESP_LOGV(TAG, " BSSID: %s", bssid_s);
} else {
ESP_LOGV(TAG, " BSSID: Not Set");
}
@@ -729,19 +735,24 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) {
if (!this->wifi_sta_connect_(ap)) {
ESP_LOGE(TAG, "wifi_sta_connect_ failed");
this->retry_connect();
return;
}
if (!two) {
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING;
// Enter cooldown to allow WiFi hardware to stabilize
// (immediate failure suggests hardware not ready, different from connection timeout)
this->state_ = WIFI_COMPONENT_STATE_COOLDOWN;
} else {
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING_2;
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING;
}
this->action_started_ = millis();
}
const LogString *get_signal_bars(int8_t rssi) {
// Check for disconnected sentinel value first
if (rssi == WIFI_RSSI_DISCONNECTED) {
// MULTIPLICATION SIGN
// Unicode: U+00D7, UTF-8: C3 97
return LOG_STR("\033[0;31m" // red
"\xc3\x97\xc3\x97\xc3\x97\xc3\x97"
"\033[0m");
}
// LOWER ONE QUARTER BLOCK
// Unicode: U+2582, UTF-8: E2 96 82
// LOWER HALF BLOCK
@@ -786,6 +797,8 @@ const LogString *get_signal_bars(int8_t rssi) {
void WiFiComponent::print_connect_params_() {
bssid_t bssid = wifi_bssid();
char bssid_s[18];
format_mac_addr_upper(bssid.data(), bssid_s);
ESP_LOGCONFIG(TAG, " Local MAC: %s", get_mac_address_pretty().c_str());
if (this->is_disabled()) {
@@ -808,9 +821,9 @@ void WiFiComponent::print_connect_params_() {
" Gateway: %s\n"
" DNS1: %s\n"
" DNS2: %s",
wifi_ssid().c_str(), format_mac_address_pretty(bssid.data()).c_str(), App.get_name().c_str(), rssi,
LOG_STR_ARG(get_signal_bars(rssi)), get_wifi_channel(), wifi_subnet_mask_().str().c_str(),
wifi_gateway_ip_().str().c_str(), wifi_dns_ip_(0).str().c_str(), wifi_dns_ip_(1).str().c_str());
wifi_ssid().c_str(), bssid_s, App.get_name().c_str(), rssi, LOG_STR_ARG(get_signal_bars(rssi)),
get_wifi_channel(), wifi_subnet_mask_().str().c_str(), wifi_gateway_ip_().str().c_str(),
wifi_dns_ip_(0).str().c_str(), wifi_dns_ip_(1).str().c_str());
#ifdef ESPHOME_LOG_HAS_VERBOSE
if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_bssid().has_value()) {
ESP_LOGV(TAG, " Priority: %d", this->get_sta_priority(*config->get_bssid()));
@@ -1017,11 +1030,14 @@ void WiFiComponent::check_scanning_finished() {
WiFiAP params = this->build_params_for_current_phase_();
// Ensure we're in SCAN_CONNECTING phase when connecting with scan results
// (needed when scan was started directly without transition_to_phase_, e.g., initial scan)
this->start_connecting(params, false);
this->start_connecting(params);
}
void WiFiComponent::dump_config() {
ESP_LOGCONFIG(TAG, "WiFi:");
ESP_LOGCONFIG(TAG,
"WiFi:\n"
" Connected: %s",
YESNO(this->is_connected()));
this->print_connect_params_();
}
@@ -1046,6 +1062,10 @@ void WiFiComponent::check_connecting_finished() {
// Reset to initial phase on successful connection (don't log transition, just reset state)
this->retry_phase_ = WiFiRetryPhase::INITIAL_CONNECT;
this->num_retried_ = 0;
// Ensure next connection attempt does not inherit error state
// so when WiFi disconnects later we start fresh and don't see
// the first connection as a failure.
this->error_from_callback_ = false;
this->print_connect_params_();
@@ -1086,18 +1106,12 @@ void WiFiComponent::check_connecting_finished() {
uint32_t now = millis();
if (now - this->action_started_ > 30000) {
ESP_LOGW(TAG, "Connection timeout");
// Move from STA_CONNECTING_2 back to STA_CONNECTING state
// since we know the connection attempt has failed
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING;
this->retry_connect();
return;
}
if (this->error_from_callback_) {
ESP_LOGW(TAG, "Connecting to network failed (callback)");
// Move from STA_CONNECTING_2 back to STA_CONNECTING state
// since we know the connection attempt is finished
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING;
this->retry_connect();
return;
}
@@ -1106,9 +1120,6 @@ void WiFiComponent::check_connecting_finished() {
return;
}
// Move from STA_CONNECTING_2 back to STA_CONNECTING state
// since we know the connection attempt is finished
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING;
if (status == WiFiSTAConnectStatus::ERROR_NETWORK_NOT_FOUND) {
ESP_LOGW(TAG, "Network no longer found");
this->retry_connect();
@@ -1141,6 +1152,11 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() {
return WiFiRetryPhase::FAST_CONNECT_CYCLING_APS; // Move to next AP
}
#endif
// Check if we should try explicit hidden networks before scanning
// This handles reconnection after connection loss where first network is hidden
if (!this->sta_.empty() && this->sta_[0].get_hidden()) {
return WiFiRetryPhase::EXPLICIT_HIDDEN;
}
// No more APs to try, fall back to scan
return WiFiRetryPhase::SCAN_CONNECTING;
@@ -1399,8 +1415,10 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() {
this->set_sta_priority(failed_bssid.value(), new_priority);
}
ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", ssid.c_str(),
format_mac_address_pretty(failed_bssid.value().data()).c_str(), old_priority, new_priority);
char bssid_s[18];
format_mac_addr_upper(failed_bssid.value().data(), bssid_s);
ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", ssid.c_str(), bssid_s,
old_priority, new_priority);
// After adjusting priority, check if all priorities are now at minimum
// If so, clear the vector to save memory and reset for fresh start
@@ -1492,27 +1510,14 @@ void WiFiComponent::retry_connect() {
this->error_from_callback_ = false;
if (this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTING) {
yield();
// Check if we have a valid target before building params
// After exhausting all networks in a phase, selected_sta_index_ may be -1
// In that case, skip connection and let next wifi_loop() handle phase transition
if (this->selected_sta_index_ >= 0) {
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING_2;
WiFiAP params = this->build_params_for_current_phase_();
this->start_connecting(params, true);
}
return;
yield();
// Check if we have a valid target before building params
// After exhausting all networks in a phase, selected_sta_index_ may be -1
// In that case, skip connection and let next wifi_loop() handle phase transition
if (this->selected_sta_index_ >= 0) {
WiFiAP params = this->build_params_for_current_phase_();
this->start_connecting(params);
}
// If we can't progress forward its likely because scanning failed
// or the stack is in a bad state after restart so we cooldown first
// and once it finishes, cooldown will call check_connecting_finished()
// which will progress the state machine
ESP_LOGD(TAG, "Entering cooldown from state %d and phase %s", this->state_,
LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_)));
this->state_ = WIFI_COMPONENT_STATE_COOLDOWN;
this->action_started_ = millis();
}
void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }

View File

@@ -52,6 +52,9 @@ extern "C" {
namespace esphome {
namespace wifi {
/// Sentinel value for RSSI when WiFi is not connected
static constexpr int8_t WIFI_RSSI_DISCONNECTED = -127;
struct SavedWifiSettings {
char ssid[33];
char password[65];
@@ -74,12 +77,6 @@ enum WiFiComponentState : uint8_t {
WIFI_COMPONENT_STATE_STA_SCANNING,
/** WiFi is in STA(+AP) mode and currently connecting to an AP. */
WIFI_COMPONENT_STATE_STA_CONNECTING,
/** WiFi is in STA(+AP) mode and currently connecting to an AP a second time.
*
* This is required because for some reason ESPs don't like to connect to WiFi APs directly after
* a scan.
* */
WIFI_COMPONENT_STATE_STA_CONNECTING_2,
/** WiFi is in STA(+AP) mode and successfully connected. */
WIFI_COMPONENT_STATE_STA_CONNECTED,
/** WiFi is in AP-only mode and internal AP is already enabled. */
@@ -275,7 +272,9 @@ class WiFiComponent : public Component {
bool is_disabled();
void start_scanning();
void check_scanning_finished();
void start_connecting(const WiFiAP &ap, bool two);
void start_connecting(const WiFiAP &ap);
// Backward compatibility overload - ignores 'two' parameter
void start_connecting(const WiFiAP &ap, bool /* two */) { this->start_connecting(ap); }
void check_connecting_finished();
@@ -430,7 +429,7 @@ class WiFiComponent : public Component {
bool wifi_sta_pre_setup_();
bool wifi_apply_output_power_(float output_power);
bool wifi_apply_power_save_();
bool wifi_sta_ip_config_(optional<ManualIP> manual_ip);
bool wifi_sta_ip_config_(const optional<ManualIP> &manual_ip);
bool wifi_apply_hostname_();
bool wifi_sta_connect_(const WiFiAP &ap);
void wifi_pre_setup_();
@@ -438,7 +437,7 @@ class WiFiComponent : public Component {
bool wifi_scan_start_(bool passive);
#ifdef USE_WIFI_AP
bool wifi_ap_ip_config_(optional<ManualIP> manual_ip);
bool wifi_ap_ip_config_(const optional<ManualIP> &manual_ip);
bool wifi_start_ap_(const WiFiAP &ap);
#endif // USE_WIFI_AP

View File

@@ -117,7 +117,7 @@ void netif_set_addr(struct netif *netif, const ip4_addr_t *ip, const ip4_addr_t
};
#endif
bool WiFiComponent::wifi_sta_ip_config_(optional<ManualIP> manual_ip) {
bool WiFiComponent::wifi_sta_ip_config_(const optional<ManualIP> &manual_ip) {
// enable STA
if (!this->wifi_mode_(true, {}))
return false;
@@ -525,8 +525,10 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf);
s_sta_connect_not_found = true;
} else {
ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf,
format_mac_address_pretty(it.bssid).c_str(), LOG_STR_ARG(get_disconnect_reason_str(it.reason)));
char bssid_s[18];
format_mac_addr_upper(it.bssid, bssid_s);
ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, bssid_s,
LOG_STR_ARG(get_disconnect_reason_str(it.reason)));
s_sta_connect_error = true;
}
s_sta_connected = false;
@@ -730,7 +732,7 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) {
}
#ifdef USE_WIFI_AP
bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
bool WiFiComponent::wifi_ap_ip_config_(const optional<ManualIP> &manual_ip) {
// enable AP
if (!this->wifi_mode_({}, true))
return false;
@@ -870,7 +872,7 @@ bssid_t WiFiComponent::wifi_bssid() {
return bssid;
}
std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); }
int8_t WiFiComponent::wifi_rssi() { return WiFi.RSSI(); }
int8_t WiFiComponent::wifi_rssi() { return WiFi.status() == WL_CONNECTED ? WiFi.RSSI() : WIFI_RSSI_DISCONNECTED; }
int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); }
network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {(const ip_addr_t *) WiFi.subnetMask()}; }
network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {(const ip_addr_t *) WiFi.gatewayIP()}; }

View File

@@ -487,7 +487,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
return true;
}
bool WiFiComponent::wifi_sta_ip_config_(optional<ManualIP> manual_ip) {
bool WiFiComponent::wifi_sta_ip_config_(const optional<ManualIP> &manual_ip) {
// enable STA
if (!this->wifi_mode_(true, {}))
return false;
@@ -746,8 +746,10 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
ESP_LOGI(TAG, "Disconnected ssid='%s' reason='Station Roaming'", buf);
return;
} else {
ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf,
format_mac_address_pretty(it.bssid).c_str(), get_disconnect_reason_str(it.reason));
char bssid_s[18];
format_mac_addr_upper(it.bssid, bssid_s);
ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, bssid_s,
get_disconnect_reason_str(it.reason));
s_sta_connect_error = true;
}
s_sta_connected = false;
@@ -884,7 +886,7 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
}
#ifdef USE_WIFI_AP
bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
bool WiFiComponent::wifi_ap_ip_config_(const optional<ManualIP> &manual_ip) {
esp_err_t err;
// enable AP
@@ -1029,7 +1031,8 @@ bssid_t WiFiComponent::wifi_bssid() {
wifi_ap_record_t info;
esp_err_t err = esp_wifi_sta_get_ap_info(&info);
if (err != ESP_OK) {
ESP_LOGW(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err));
// Very verbose only: this is expected during dump_config() before connection is established (PR #9823)
ESP_LOGVV(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err));
return bssid;
}
std::copy(info.bssid, info.bssid + 6, bssid.begin());
@@ -1039,7 +1042,8 @@ std::string WiFiComponent::wifi_ssid() {
wifi_ap_record_t info{};
esp_err_t err = esp_wifi_sta_get_ap_info(&info);
if (err != ESP_OK) {
ESP_LOGW(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err));
// Very verbose only: this is expected during dump_config() before connection is established (PR #9823)
ESP_LOGVV(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err));
return "";
}
auto *ssid_s = reinterpret_cast<const char *>(info.ssid);
@@ -1050,8 +1054,9 @@ int8_t WiFiComponent::wifi_rssi() {
wifi_ap_record_t info;
esp_err_t err = esp_wifi_sta_get_ap_info(&info);
if (err != ESP_OK) {
ESP_LOGW(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err));
return 0;
// Very verbose only: this is expected during dump_config() before connection is established (PR #9823)
ESP_LOGVV(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err));
return WIFI_RSSI_DISCONNECTED;
}
return info.rssi;
}

View File

@@ -68,7 +68,7 @@ bool WiFiComponent::wifi_sta_pre_setup_() {
return true;
}
bool WiFiComponent::wifi_apply_power_save_() { return WiFi.setSleep(this->power_save_ != WIFI_POWER_SAVE_NONE); }
bool WiFiComponent::wifi_sta_ip_config_(optional<ManualIP> manual_ip) {
bool WiFiComponent::wifi_sta_ip_config_(const optional<ManualIP> &manual_ip) {
// enable STA
if (!this->wifi_mode_(true, {}))
return false;
@@ -299,8 +299,10 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_
if (it.reason == WIFI_REASON_NO_AP_FOUND) {
ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf);
} else {
ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf,
format_mac_address_pretty(it.bssid).c_str(), get_disconnect_reason_str(it.reason));
char bssid_s[18];
format_mac_addr_upper(it.bssid, bssid_s);
ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf, bssid_s,
get_disconnect_reason_str(it.reason));
}
uint8_t reason = it.reason;
@@ -434,7 +436,7 @@ void WiFiComponent::wifi_scan_done_callback_() {
}
#ifdef USE_WIFI_AP
bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
bool WiFiComponent::wifi_ap_ip_config_(const optional<ManualIP> &manual_ip) {
// enable AP
if (!this->wifi_mode_({}, true))
return false;
@@ -484,7 +486,7 @@ bssid_t WiFiComponent::wifi_bssid() {
return bssid;
}
std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); }
int8_t WiFiComponent::wifi_rssi() { return WiFi.RSSI(); }
int8_t WiFiComponent::wifi_rssi() { return WiFi.status() == WL_CONNECTED ? WiFi.RSSI() : WIFI_RSSI_DISCONNECTED; }
int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); }
network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {WiFi.subnetMask()}; }
network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {WiFi.gatewayIP()}; }

View File

@@ -72,7 +72,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
bool WiFiComponent::wifi_sta_pre_setup_() { return this->wifi_mode_(true, {}); }
bool WiFiComponent::wifi_sta_ip_config_(optional<ManualIP> manual_ip) {
bool WiFiComponent::wifi_sta_ip_config_(const optional<ManualIP> &manual_ip) {
if (!manual_ip.has_value()) {
return true;
}
@@ -146,7 +146,7 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
}
#ifdef USE_WIFI_AP
bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
bool WiFiComponent::wifi_ap_ip_config_(const optional<ManualIP> &manual_ip) {
esphome::network::IPAddress ip_address, gateway, subnet, dns;
if (manual_ip.has_value()) {
ip_address = manual_ip->static_ip;
@@ -200,7 +200,7 @@ bssid_t WiFiComponent::wifi_bssid() {
return bssid;
}
std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); }
int8_t WiFiComponent::wifi_rssi() { return WiFi.RSSI(); }
int8_t WiFiComponent::wifi_rssi() { return WiFi.status() == WL_CONNECTED ? WiFi.RSSI() : WIFI_RSSI_DISCONNECTED; }
int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); }
network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() {

View File

@@ -4,7 +4,7 @@ from enum import Enum
from esphome.enum import StrEnum
__version__ = "2025.11.0-dev"
__version__ = "2025.12.0-dev"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = (

View File

@@ -412,7 +412,12 @@ template<typename... Ts> class WaitUntilAction : public Action<Ts...>, public Co
void setup() override {
// Start with loop disabled - only enable when there's work to do
this->disable_loop();
// IMPORTANT: Only disable if num_running_ is 0, otherwise play_complex() was already
// called before our setup() (e.g., from on_boot trigger at same priority level)
// and we must not undo its enable_loop() call
if (this->num_running_ == 0) {
this->disable_loop();
}
}
void play_complex(const Ts &...x) override {

View File

@@ -94,9 +94,10 @@ class Scheduler {
} name_;
uint32_t interval;
// Split time to handle millis() rollover. The scheduler combines the 32-bit millis()
// with a 16-bit rollover counter to create a 48-bit time space (stored as 64-bit
// for compatibility). With 49.7 days per 32-bit rollover, the 16-bit counter
// supports 49.7 days × 65536 = ~8900 years. This ensures correct scheduling
// with a 16-bit rollover counter to create a 48-bit time space (using 32+16 bits).
// This is intentionally limited to 48 bits, not stored as a full 64-bit value.
// With 49.7 days per 32-bit rollover, the 16-bit counter supports
// 49.7 days × 65536 = ~8900 years. This ensures correct scheduling
// even when devices run for months. Split into two fields for better memory
// alignment on 32-bit systems.
uint32_t next_execution_low_; // Lower 32 bits of execution time (millis value)

View File

@@ -30,6 +30,7 @@ from esphome.const import (
from esphome.core import CORE, EsphomeError
from esphome.helpers import get_int_env, get_str_env
from esphome.log import AnsiFore, color
from esphome.types import ConfigType
from esphome.util import safe_print
_LOGGER = logging.getLogger(__name__)
@@ -154,8 +155,12 @@ def show_discover(config, username=None, password=None, client_id=None):
def get_esphome_device_ip(
config, username=None, password=None, client_id=None, timeout=25
):
config: ConfigType,
username: str | None = None,
password: str | None = None,
client_id: str | None = None,
timeout: int | float = 25,
) -> list[str]:
if CONF_MQTT not in config:
raise EsphomeError(
"Cannot discover IP via MQTT as the config does not include the mqtt: "
@@ -166,6 +171,10 @@ def get_esphome_device_ip(
"Cannot discover IP via MQTT as the config does not include the device name: "
"component"
)
if not config[CONF_MQTT].get(CONF_BROKER):
raise EsphomeError(
"Cannot discover IP via MQTT as the broker is not configured"
)
dev_name = config[CONF_ESPHOME][CONF_NAME]
dev_ip = None

View File

@@ -408,8 +408,7 @@ class IDEData:
def analyze_memory_usage(config: dict[str, Any]) -> None:
"""Analyze memory usage by component after compilation."""
# Lazy import to avoid overhead when not needed
from esphome.analyze_memory.cli import MemoryAnalyzerCLI
from esphome.analyze_memory.helpers import get_esphome_components
from esphome.analyze_memory import MemoryAnalyzer
idedata = get_idedata(config)
@@ -436,6 +435,8 @@ def analyze_memory_usage(config: dict[str, Any]) -> None:
external_components = set()
# Get the list of built-in ESPHome components
from esphome.analyze_memory import get_esphome_components
builtin_components = get_esphome_components()
# Special non-component keys that appear in configs
@@ -456,9 +457,7 @@ def analyze_memory_usage(config: dict[str, Any]) -> None:
_LOGGER.debug("Detected external components: %s", external_components)
# Create analyzer and run analysis
analyzer = MemoryAnalyzerCLI(
elf_path, objdump_path, readelf_path, external_components
)
analyzer = MemoryAnalyzer(elf_path, objdump_path, readelf_path, external_components)
analyzer.analyze()
# Generate and print report

View File

@@ -5,7 +5,7 @@ pyupgrade==3.21.1 # also change in .pre-commit-config.yaml when updating
pre-commit
# Unit tests
pytest==9.0.0
pytest==9.0.1
pytest-cov==7.0.0
pytest-mock==3.15.1
pytest-asyncio==1.3.0

View File

@@ -1,2 +0,0 @@
packages:
common: !include common.yaml

View File

@@ -1,4 +0,0 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
<<: !include common.yaml

View File

@@ -0,0 +1,27 @@
esp32:
variant: esp32p4
flash_size: 32MB
cpu_frequency: 400MHz
framework:
type: esp-idf
advanced:
enable_idf_experimental_features: yes
ota:
platform: esphome
wifi:
ssid: MySSID
password: password1
esp32_hosted:
variant: ESP32C6
slot: 1
active_high: true
reset_pin: GPIO15
cmd_pin: GPIO13
clk_pin: GPIO12
d0_pin: GPIO11
d1_pin: GPIO10
d2_pin: GPIO9
d3_pin: GPIO8

View File

@@ -1,15 +0,0 @@
packages:
common: !include common.yaml
matrix_keypad:
id: keypad
rows:
- pin: 10
- pin: 11
columns:
- pin: 12
- pin: 13
keys: "1234"
has_pulldowns: true
on_key:
- lambda: ESP_LOGI("KEY", "key %d pressed", x);

View File

@@ -1,4 +0,0 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
<<: !include common.yaml

View File

@@ -1,4 +0,0 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
<<: !include common.yaml

View File

@@ -1 +1,4 @@
<<: !include common.yaml
network:
enable_high_performance: true

View File

@@ -1,4 +0,0 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
<<: !include common.yaml

View File

@@ -15,6 +15,7 @@ nrf52:
inverted: true
mode:
output: true
dcdc: False
reg0:
voltage: 2.1V
uicr_erase: true

View File

@@ -6,4 +6,4 @@ nrf52:
mode:
output: true
reg0:
voltage: default
voltage: 1.8V

View File

@@ -1,4 +0,0 @@
substitutions:
pin: GPIO4
<<: !include common.yaml

View File

@@ -1,4 +0,0 @@
substitutions:
pin: GPIO1
<<: !include common.yaml

View File

@@ -1,2 +0,0 @@
packages:
common: !include common.yaml

View File

@@ -1,8 +0,0 @@
substitutions:
scl_pin: GPIO40
sda_pin: GPIO41
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
<<: !include common.yaml

View File

@@ -1,2 +0,0 @@
packages:
common: !include common.yaml

View File

@@ -1,48 +0,0 @@
<<: !include ../logger/common-usb_serial_jtag.yaml
esphome:
on_boot:
then:
- uart.write:
id: uart_1
data: 'Hello World'
- uart.write:
id: uart_1
data: [0x00, 0x20, 0x42]
uart:
- id: uart_1
tx_pin: 4
rx_pin: 5
flow_control_pin: 6
baud_rate: 9600
data_bits: 8
rx_buffer_size: 512
rx_full_threshold: 10
rx_timeout: 1
parity: EVEN
stop_bits: 2
- id: uart_2
tx_pin: 7
rx_pin: 8
flow_control_pin: 9
baud_rate: 9600
data_bits: 8
rx_buffer_size: 512
rx_full_threshold: 10
rx_timeout: 1
parity: EVEN
stop_bits: 2
- id: uart_3
tx_pin: 10
rx_pin: 11
flow_control_pin: 12
baud_rate: 9600
data_bits: 8
rx_buffer_size: 512
rx_full_threshold: 10
rx_timeout: 1
parity: EVEN
stop_bits: 2

View File

@@ -1,9 +0,0 @@
substitutions:
scl_pin: GPIO40
sda_pin: GPIO41
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
uart_bridge_2: !include ../../test_build_components/common/uart_bridge_2/esp32-s3-idf.yaml
<<: !include common.yaml

View File

@@ -1,11 +0,0 @@
substitutions:
clk_pin: GPIO40
miso_pin: GPIO41
mosi_pin: GPIO6
cs_pin: GPIO19
packages:
spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml
uart_bridge_2: !include ../../test_build_components/common/uart_bridge_2/esp32-s3-idf.yaml
<<: !include common.yaml

View File

@@ -1,9 +0,0 @@
substitutions:
scl_pin: GPIO40
sda_pin: GPIO41
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml
<<: !include common.yaml

View File

@@ -1,11 +0,0 @@
substitutions:
clk_pin: GPIO40
miso_pin: GPIO41
mosi_pin: GPIO6
cs_pin: GPIO19
packages:
spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml
uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml
<<: !include common.yaml

View File

@@ -1,9 +0,0 @@
substitutions:
scl_pin: GPIO40
sda_pin: GPIO41
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml
<<: !include common.yaml

View File

@@ -1,11 +0,0 @@
substitutions:
clk_pin: GPIO40
miso_pin: GPIO41
mosi_pin: GPIO6
cs_pin: GPIO19
packages:
spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml
uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml
<<: !include common.yaml

View File

@@ -1,9 +0,0 @@
substitutions:
scl_pin: GPIO40
sda_pin: GPIO41
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml
<<: !include common.yaml

View File

@@ -1,11 +0,0 @@
substitutions:
clk_pin: GPIO40
miso_pin: GPIO41
mosi_pin: GPIO6
cs_pin: GPIO19
packages:
spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml
uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml
<<: !include common.yaml

View File

@@ -1,59 +0,0 @@
esphome:
name: test-user-services-union
friendly_name: Test User Services Union Storage
esp32:
board: esp32dev
framework:
type: esp-idf
logger:
level: DEBUG
wifi:
ssid: "test"
password: "password"
api:
actions:
# Test service with no arguments
- action: test_no_args
then:
- logger.log: "No args service called"
# Test service with one argument
- action: test_one_arg
variables:
value: int
then:
- logger.log:
format: "One arg service: %d"
args: [value]
# Test service with multiple arguments of different types
- action: test_multi_args
variables:
int_val: int
float_val: float
str_val: string
bool_val: bool
then:
- logger.log:
format: "Multi args: %d, %.2f, %s, %d"
args: [int_val, float_val, str_val.c_str(), bool_val]
# Test service with max typical arguments
- action: test_many_args
variables:
arg1: int
arg2: int
arg3: int
arg4: string
arg5: float
then:
- logger.log: "Many args service called"
binary_sensor:
- platform: template
name: "Test Binary Sensor"
id: test_sensor

View File

@@ -14,6 +14,7 @@ climate:
id: test_thermostat
name: Test Thermostat Custom Modes
sensor: thermostat_sensor
default_preset: "Eco Plus"
preset:
- name: Away
default_target_temperature_low: 16°C

View File

@@ -0,0 +1,136 @@
esphome:
name: template-alarm-many-sensors
friendly_name: "Template Alarm Control Panel with Many Sensors"
logger:
host:
api:
binary_sensor:
- platform: template
id: sensor1
name: "Door 1"
- platform: template
id: sensor2
name: "Door 2"
- platform: template
id: sensor3
name: "Window 1"
- platform: template
id: sensor4
name: "Window 2"
- platform: template
id: sensor5
name: "Motion 1"
- platform: template
id: sensor6
name: "Motion 2"
- platform: template
id: sensor7
name: "Glass Break 1"
- platform: template
id: sensor8
name: "Glass Break 2"
- platform: template
id: sensor9
name: "Smoke Detector"
- platform: template
id: sensor10
name: "CO Detector"
alarm_control_panel:
- platform: template
id: test_alarm
name: "Test Alarm"
codes:
- "1234"
requires_code_to_arm: true
arming_away_time: 5s
arming_home_time: 3s
arming_night_time: 3s
pending_time: 10s
trigger_time: 300s
restore_mode: ALWAYS_DISARMED
binary_sensors:
- input: sensor1
bypass_armed_home: false
bypass_armed_night: false
bypass_auto: true
chime: true
trigger_mode: DELAYED
- input: sensor2
bypass_armed_home: false
bypass_armed_night: false
bypass_auto: true
chime: true
trigger_mode: DELAYED
- input: sensor3
bypass_armed_home: true
bypass_armed_night: false
bypass_auto: false
chime: false
trigger_mode: DELAYED
- input: sensor4
bypass_armed_home: true
bypass_armed_night: false
bypass_auto: false
chime: false
trigger_mode: DELAYED
- input: sensor5
bypass_armed_home: false
bypass_armed_night: true
bypass_auto: false
chime: false
trigger_mode: INSTANT
- input: sensor6
bypass_armed_home: false
bypass_armed_night: true
bypass_auto: false
chime: false
trigger_mode: INSTANT
- input: sensor7
bypass_armed_home: false
bypass_armed_night: false
bypass_auto: false
chime: false
trigger_mode: INSTANT
- input: sensor8
bypass_armed_home: false
bypass_armed_night: false
bypass_auto: false
chime: false
trigger_mode: INSTANT
- input: sensor9
bypass_armed_home: false
bypass_armed_night: false
bypass_auto: false
chime: false
trigger_mode: INSTANT_ALWAYS
- input: sensor10
bypass_armed_home: false
bypass_armed_night: false
bypass_auto: false
chime: false
trigger_mode: INSTANT_ALWAYS
on_disarmed:
- logger.log: "Alarm disarmed"
on_arming:
- logger.log: "Alarm arming"
on_armed_away:
- logger.log: "Alarm armed away"
on_armed_home:
- logger.log: "Alarm armed home"
on_armed_night:
- logger.log: "Alarm armed night"
on_pending:
- logger.log: "Alarm pending"
on_triggered:
- logger.log: "Alarm triggered"
on_cleared:
- logger.log: "Alarm cleared"
on_chime:
- logger.log: "Chime activated"
on_ready:
- logger.log: "Sensors ready state changed"

View File

@@ -0,0 +1,47 @@
# Test for wait_until in on_boot automation
# Reproduces bug where wait_until in on_boot would hang forever
# because WaitUntilAction::setup() would disable_loop() after
# play_complex() had already enabled it.
esphome:
name: wait-until-on-boot
on_boot:
then:
- logger.log: "on_boot: Starting wait_until test"
- globals.set:
id: on_boot_started
value: 'true'
- wait_until:
condition:
lambda: return id(test_flag);
timeout: 5s
- logger.log: "on_boot: wait_until completed successfully"
host:
logger:
level: DEBUG
globals:
- id: on_boot_started
type: bool
initial_value: 'false'
- id: test_flag
type: bool
initial_value: 'false'
api:
actions:
- action: set_test_flag
then:
- globals.set:
id: test_flag
value: 'true'
- action: check_on_boot_started
then:
- lambda: |-
if (id(on_boot_started)) {
ESP_LOGI("test", "on_boot has started");
} else {
ESP_LOGI("test", "on_boot has NOT started");
}

View File

@@ -2,9 +2,13 @@
from __future__ import annotations
from aioesphomeapi import ClimateInfo, ClimatePreset
import asyncio
import aioesphomeapi
from aioesphomeapi import ClimateInfo, ClimatePreset, EntityState
import pytest
from .state_utils import InitialStateHelper
from .types import APIClientConnectedFactory, RunCompiledFunction
@@ -14,15 +18,50 @@ async def test_climate_custom_fan_modes_and_presets(
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test that custom presets are properly exposed via API."""
"""Test that custom presets are properly exposed and can be changed."""
loop = asyncio.get_running_loop()
async with run_compiled(yaml_config), api_client_connected() as client:
# Get entities and services
states: dict[int, EntityState] = {}
super_saver_future: asyncio.Future[EntityState] = loop.create_future()
vacation_future: asyncio.Future[EntityState] = loop.create_future()
def on_state(state: EntityState) -> None:
states[state.key] = state
if isinstance(state, aioesphomeapi.ClimateState):
# Wait for Super Saver preset
if (
state.custom_preset == "Super Saver"
and state.target_temperature_low == 20.0
and state.target_temperature_high == 24.0
and not super_saver_future.done()
):
super_saver_future.set_result(state)
# Wait for Vacation Mode preset
elif (
state.custom_preset == "Vacation Mode"
and state.target_temperature_low == 15.0
and state.target_temperature_high == 18.0
and not vacation_future.done()
):
vacation_future.set_result(state)
# Get entities and set up state synchronization
entities, services = await client.list_entities_services()
initial_state_helper = InitialStateHelper(entities)
climate_infos = [e for e in entities if isinstance(e, ClimateInfo)]
assert len(climate_infos) == 1, "Expected exactly 1 climate entity"
test_climate = climate_infos[0]
# Subscribe with the wrapper that filters initial states
client.subscribe_states(initial_state_helper.on_state_wrapper(on_state))
# Wait for all initial states to be broadcast
try:
await initial_state_helper.wait_for_initial_states()
except TimeoutError:
pytest.fail("Timeout waiting for initial states")
# Verify enum presets are exposed (from preset: config map)
assert ClimatePreset.AWAY in test_climate.supported_presets, (
"Expected AWAY in enum presets"
@@ -40,3 +79,43 @@ async def test_climate_custom_fan_modes_and_presets(
assert "Vacation Mode" in custom_presets, (
"Expected 'Vacation Mode' in custom presets"
)
# Get initial state and verify default preset
initial_state = initial_state_helper.initial_states.get(test_climate.key)
assert initial_state is not None, "Climate initial state not found"
assert isinstance(initial_state, aioesphomeapi.ClimateState)
assert initial_state.custom_preset == "Eco Plus", (
f"Expected default preset 'Eco Plus', got '{initial_state.custom_preset}'"
)
assert initial_state.target_temperature_low == 18.0, (
f"Expected low temp 18.0, got {initial_state.target_temperature_low}"
)
assert initial_state.target_temperature_high == 22.0, (
f"Expected high temp 22.0, got {initial_state.target_temperature_high}"
)
# Test changing to "Super Saver" custom preset
client.climate_command(test_climate.key, custom_preset="Super Saver")
try:
super_saver_state = await asyncio.wait_for(super_saver_future, timeout=5.0)
except TimeoutError:
pytest.fail("Super Saver preset change not received within 5 seconds")
assert isinstance(super_saver_state, aioesphomeapi.ClimateState)
assert super_saver_state.custom_preset == "Super Saver"
assert super_saver_state.target_temperature_low == 20.0
assert super_saver_state.target_temperature_high == 24.0
# Test changing to "Vacation Mode" custom preset
client.climate_command(test_climate.key, custom_preset="Vacation Mode")
try:
vacation_state = await asyncio.wait_for(vacation_future, timeout=5.0)
except TimeoutError:
pytest.fail("Vacation Mode preset change not received within 5 seconds")
assert isinstance(vacation_state, aioesphomeapi.ClimateState)
assert vacation_state.custom_preset == "Vacation Mode"
assert vacation_state.target_temperature_low == 15.0
assert vacation_state.target_temperature_high == 18.0

View File

@@ -0,0 +1,118 @@
"""Integration test for template alarm control panel with many sensors."""
from __future__ import annotations
import aioesphomeapi
from aioesphomeapi.model import APIIntEnum
import pytest
from .state_utils import InitialStateHelper
from .types import APIClientConnectedFactory, RunCompiledFunction
class EspHomeACPFeatures(APIIntEnum):
"""ESPHome AlarmControlPanel feature numbers."""
ARM_HOME = 1
ARM_AWAY = 2
ARM_NIGHT = 4
TRIGGER = 8
ARM_CUSTOM_BYPASS = 16
ARM_VACATION = 32
@pytest.mark.asyncio
async def test_template_alarm_control_panel_many_sensors(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test template alarm control panel with 10 binary sensors using FixedVector."""
async with run_compiled(yaml_config), api_client_connected() as client:
# Get entity info first
entities, _ = await client.list_entities_services()
# Find the alarm control panel and binary sensors
alarm_info: aioesphomeapi.AlarmControlPanelInfo | None = None
binary_sensors: list[aioesphomeapi.BinarySensorInfo] = []
for entity in entities:
if isinstance(entity, aioesphomeapi.AlarmControlPanelInfo):
alarm_info = entity
elif isinstance(entity, aioesphomeapi.BinarySensorInfo):
binary_sensors.append(entity)
assert alarm_info is not None, "Alarm control panel entity info not found"
assert alarm_info.name == "Test Alarm"
assert alarm_info.requires_code is True
assert alarm_info.requires_code_to_arm is True
# Verify we have 10 binary sensors
assert len(binary_sensors) == 10, (
f"Expected 10 binary sensors, got {len(binary_sensors)}"
)
# Verify sensor names
expected_sensor_names = {
"Door 1",
"Door 2",
"Window 1",
"Window 2",
"Motion 1",
"Motion 2",
"Glass Break 1",
"Glass Break 2",
"Smoke Detector",
"CO Detector",
}
actual_sensor_names = {sensor.name for sensor in binary_sensors}
assert actual_sensor_names == expected_sensor_names, (
f"Sensor names mismatch. Expected: {expected_sensor_names}, "
f"Got: {actual_sensor_names}"
)
# Use InitialStateHelper to wait for all initial states
state_helper = InitialStateHelper(entities)
def on_state(state: aioesphomeapi.EntityState) -> None:
# We'll receive subsequent states here after initial states
pass
client.subscribe_states(state_helper.on_state_wrapper(on_state))
# Wait for all initial states
await state_helper.wait_for_initial_states(timeout=5.0)
# Verify the alarm state is disarmed initially
alarm_state = state_helper.initial_states.get(alarm_info.key)
assert alarm_state is not None, "Alarm control panel initial state not received"
assert isinstance(alarm_state, aioesphomeapi.AlarmControlPanelEntityState)
assert alarm_state.state == aioesphomeapi.AlarmControlPanelState.DISARMED, (
f"Expected initial state DISARMED, got {alarm_state.state}"
)
# Verify all 10 binary sensors have initial states
binary_sensor_states = [
state_helper.initial_states.get(sensor.key) for sensor in binary_sensors
]
assert all(state is not None for state in binary_sensor_states), (
"Not all binary sensors have initial states"
)
# Verify all binary sensor states are BinarySensorState type
for i, state in enumerate(binary_sensor_states):
assert isinstance(state, aioesphomeapi.BinarySensorState), (
f"Binary sensor {i} state is not BinarySensorState: {type(state)}"
)
# Verify supported features
expected_features = (
EspHomeACPFeatures.ARM_HOME
| EspHomeACPFeatures.ARM_AWAY
| EspHomeACPFeatures.ARM_NIGHT
| EspHomeACPFeatures.TRIGGER
)
assert alarm_info.supported_features == expected_features, (
f"Expected supported_features={expected_features} (ARM_HOME|ARM_AWAY|ARM_NIGHT|TRIGGER), "
f"got {alarm_info.supported_features}"
)

View File

@@ -0,0 +1,91 @@
"""Integration test for wait_until in on_boot automation.
This test validates that wait_until works correctly when triggered from on_boot,
which runs at the same setup priority as WaitUntilAction itself. This was broken
before the fix because WaitUntilAction::setup() would unconditionally disable_loop(),
even if play_complex() had already been called and enabled the loop.
The bug: on_boot fires during StartupTrigger::setup(), which calls WaitUntilAction::play_complex()
before WaitUntilAction::setup() has run. Then when WaitUntilAction::setup() runs, it calls
disable_loop(), undoing the enable_loop() from play_complex(), causing wait_until to hang forever.
"""
from __future__ import annotations
import asyncio
import re
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_wait_until_on_boot(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test that wait_until works in on_boot automation with a condition that becomes true later."""
loop = asyncio.get_running_loop()
on_boot_started = False
on_boot_completed = False
on_boot_started_pattern = re.compile(r"on_boot: Starting wait_until test")
on_boot_complete_pattern = re.compile(r"on_boot: wait_until completed successfully")
on_boot_started_future = loop.create_future()
on_boot_complete_future = loop.create_future()
def check_output(line: str) -> None:
"""Check log output for test progress."""
nonlocal on_boot_started, on_boot_completed
if on_boot_started_pattern.search(line):
on_boot_started = True
if not on_boot_started_future.done():
on_boot_started_future.set_result(True)
if on_boot_complete_pattern.search(line):
on_boot_completed = True
if not on_boot_complete_future.done():
on_boot_complete_future.set_result(True)
async with (
run_compiled(yaml_config, line_callback=check_output),
api_client_connected() as client,
):
# Wait for on_boot to start
await asyncio.wait_for(on_boot_started_future, timeout=10.0)
assert on_boot_started, "on_boot did not start"
# At this point, on_boot is blocked in wait_until waiting for test_flag to become true
# If the bug exists, wait_until's loop is disabled and it will never complete
# even after we set the flag
# Give a moment for setup to complete
await asyncio.sleep(0.5)
# Now set the flag that wait_until is waiting for
_, services = await client.list_entities_services()
set_flag_service = next(
(s for s in services if s.name == "set_test_flag"), None
)
assert set_flag_service is not None, "set_test_flag service not found"
client.execute_service(set_flag_service, {})
# If the fix works, wait_until's loop() will check the condition and proceed
# If the bug exists, wait_until is stuck with disabled loop and will timeout
try:
await asyncio.wait_for(on_boot_complete_future, timeout=2.0)
assert on_boot_completed, (
"on_boot wait_until did not complete after flag was set"
)
except TimeoutError:
pytest.fail(
"wait_until in on_boot did not complete within 2s after condition became true. "
"This indicates the bug where WaitUntilAction::setup() disables the loop "
"after play_complex() has already enabled it."
)

View File

@@ -1166,6 +1166,56 @@ def test_upload_program_ota_with_mqtt_resolution(
)
def test_upload_program_ota_with_mqtt_empty_broker(
mock_mqtt_get_ip: Mock,
mock_is_ip_address: Mock,
mock_run_ota: Mock,
tmp_path: Path,
caplog: CaptureFixture,
) -> None:
"""Test upload_program with OTA when MQTT broker is empty (issue #11653)."""
setup_core(address="192.168.1.50", platform=PLATFORM_ESP32, tmp_path=tmp_path)
mock_is_ip_address.return_value = True
mock_mqtt_get_ip.side_effect = EsphomeError(
"Cannot discover IP via MQTT as the broker is not configured"
)
mock_run_ota.return_value = (0, "192.168.1.50")
config = {
CONF_OTA: [
{
CONF_PLATFORM: CONF_ESPHOME,
CONF_PORT: 3232,
}
],
CONF_MQTT: {
CONF_BROKER: "",
},
CONF_MDNS: {
CONF_DISABLED: True,
},
}
args = MockArgs(username="user", password="pass", client_id="client")
devices = ["MQTTIP", "192.168.1.50"]
exit_code, host = upload_program(config, args, devices)
assert exit_code == 0
assert host == "192.168.1.50"
# Verify MQTT was attempted but failed gracefully
mock_mqtt_get_ip.assert_called_once_with(config, "user", "pass", "client")
# Verify we fell back to the IP address
expected_firmware = (
tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin"
)
mock_run_ota.assert_called_once_with(
["192.168.1.50"], 3232, None, expected_firmware
)
# Verify warning was logged
assert "MQTT IP discovery failed" in caplog.text
@patch("esphome.__main__.importlib.import_module")
def test_upload_program_platform_specific_handler(
mock_import: Mock,

View File

@@ -0,0 +1,91 @@
"""Unit tests for esphome.mqtt module."""
from __future__ import annotations
import pytest
from esphome.const import CONF_BROKER, CONF_ESPHOME, CONF_MQTT, CONF_NAME
from esphome.core import EsphomeError
from esphome.mqtt import get_esphome_device_ip
def test_get_esphome_device_ip_empty_broker() -> None:
"""Test that get_esphome_device_ip raises EsphomeError when broker is empty."""
config = {
CONF_MQTT: {
CONF_BROKER: "",
},
CONF_ESPHOME: {
CONF_NAME: "test-device",
},
}
with pytest.raises(
EsphomeError,
match="Cannot discover IP via MQTT as the broker is not configured",
):
get_esphome_device_ip(config)
def test_get_esphome_device_ip_none_broker() -> None:
"""Test that get_esphome_device_ip raises EsphomeError when broker is None."""
config = {
CONF_MQTT: {
CONF_BROKER: None,
},
CONF_ESPHOME: {
CONF_NAME: "test-device",
},
}
with pytest.raises(
EsphomeError,
match="Cannot discover IP via MQTT as the broker is not configured",
):
get_esphome_device_ip(config)
def test_get_esphome_device_ip_missing_mqtt() -> None:
"""Test that get_esphome_device_ip raises EsphomeError when mqtt config is missing."""
config = {
CONF_ESPHOME: {
CONF_NAME: "test-device",
},
}
with pytest.raises(
EsphomeError,
match="Cannot discover IP via MQTT as the config does not include the mqtt:",
):
get_esphome_device_ip(config)
def test_get_esphome_device_ip_missing_esphome() -> None:
"""Test that get_esphome_device_ip raises EsphomeError when esphome config is missing."""
config = {
CONF_MQTT: {
CONF_BROKER: "mqtt.local",
},
}
with pytest.raises(
EsphomeError,
match="Cannot discover IP via MQTT as the config does not include the device name:",
):
get_esphome_device_ip(config)
def test_get_esphome_device_ip_missing_name() -> None:
"""Test that get_esphome_device_ip raises EsphomeError when device name is missing."""
config = {
CONF_MQTT: {
CONF_BROKER: "mqtt.local",
},
CONF_ESPHOME: {},
}
with pytest.raises(
EsphomeError,
match="Cannot discover IP via MQTT as the config does not include the device name:",
):
get_esphome_device_ip(config)