mirror of
https://github.com/esphome/esphome.git
synced 2025-11-13 13:25:50 +00:00
Compare commits
61 Commits
ethernet_m
...
light_loop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
085aeeb8d5 | ||
|
|
ed7e5cd325 | ||
|
|
20649ce8ce | ||
|
|
a15f46e741 | ||
|
|
050a27a409 | ||
|
|
9b458d25ea | ||
|
|
4e23a7a3e1 | ||
|
|
382483b063 | ||
|
|
1675408161 | ||
|
|
1d8b08dcce | ||
|
|
afed581079 | ||
|
|
ff107a0674 | ||
|
|
72da3d0f1e | ||
|
|
5a2e6697e0 | ||
|
|
799cfe1de4 | ||
|
|
6df0264d51 | ||
|
|
a859ecaad1 | ||
|
|
4f088c93c9 | ||
|
|
a1ab19d127 | ||
|
|
d869108416 | ||
|
|
2d6618da3c | ||
|
|
47fe84e922 | ||
|
|
735bf9930a | ||
|
|
769137fc09 | ||
|
|
3a5b3ad77d | ||
|
|
859101ddc9 | ||
|
|
29a50da635 | ||
|
|
5f0fa68d73 | ||
|
|
2f39b10baa | ||
|
|
5a550cc579 | ||
|
|
4b58cb4ce6 | ||
|
|
3872a2fd91 | ||
|
|
5d613ada83 | ||
|
|
9de80b635a | ||
|
|
748aee584a | ||
|
|
3cbfddcc83 | ||
|
|
1d71b6b93e | ||
|
|
398dba4fc8 | ||
|
|
298813d4fa | ||
|
|
56d141c741 | ||
|
|
47a7f729dd | ||
|
|
7806eb980f | ||
|
|
a59888224c | ||
|
|
58ad4759f0 | ||
|
|
87f79290ba | ||
|
|
9326d78439 | ||
|
|
a93887a790 | ||
|
|
d7fa131a8a | ||
|
|
79a4444928 | ||
|
|
572fae5c7d | ||
|
|
5dafaaced4 | ||
|
|
65a303d48f | ||
|
|
00c71b7236 | ||
|
|
ef04903a7a | ||
|
|
a2ec7f622c | ||
|
|
2f91e7bd47 | ||
|
|
80a7c6d3c3 | ||
|
|
7a92565a0c | ||
|
|
661920c51e | ||
|
|
a6b905e148 | ||
|
|
a6b7c1f18c |
2
Doxyfile
2
Doxyfile
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
|||||||
# could be handy for archiving the generated documentation or if some version
|
# could be handy for archiving the generated documentation or if some version
|
||||||
# control system is used.
|
# 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
|
# 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
|
# for a project that appears at the top of each page and should give viewer a
|
||||||
|
|||||||
@@ -476,8 +476,9 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c
|
|||||||
auto *light = static_cast<light::LightState *>(entity);
|
auto *light = static_cast<light::LightState *>(entity);
|
||||||
ListEntitiesLightResponse msg;
|
ListEntitiesLightResponse msg;
|
||||||
auto traits = light->get_traits();
|
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
|
// 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) ||
|
if (traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) ||
|
||||||
traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)) {
|
traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)) {
|
||||||
msg.min_mireds = traits.get_min_mireds();
|
msg.min_mireds = traits.get_min_mireds();
|
||||||
@@ -1294,11 +1295,11 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_EVENT
|
#ifdef USE_EVENT
|
||||||
void APIConnection::send_event(event::Event *event, const std::string &event_type) {
|
void APIConnection::send_event(event::Event *event, const char *event_type) {
|
||||||
this->schedule_message_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE,
|
this->send_message_smart_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE,
|
||||||
EventResponse::ESTIMATED_SIZE);
|
EventResponse::ESTIMATED_SIZE);
|
||||||
}
|
}
|
||||||
uint16_t APIConnection::try_send_event_response(event::Event *event, const std::string &event_type, APIConnection *conn,
|
uint16_t APIConnection::try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn,
|
||||||
uint32_t remaining_size, bool is_single) {
|
uint32_t remaining_size, bool is_single) {
|
||||||
EventResponse resp;
|
EventResponse resp;
|
||||||
resp.set_event_type(StringRef(event_type));
|
resp.set_event_type(StringRef(event_type));
|
||||||
@@ -1650,9 +1651,7 @@ void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator c
|
|||||||
// O(n) but optimized for RAM and not performance.
|
// O(n) but optimized for RAM and not performance.
|
||||||
for (auto &item : items) {
|
for (auto &item : items) {
|
||||||
if (item.entity == entity && item.message_type == message_type) {
|
if (item.entity == entity && item.message_type == message_type) {
|
||||||
// Clean up old creator before replacing
|
// Replace with new creator
|
||||||
item.creator.cleanup(message_type);
|
|
||||||
// Move assign the new creator
|
|
||||||
item.creator = std::move(creator);
|
item.creator = std::move(creator);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1822,7 +1821,7 @@ void APIConnection::process_batch_() {
|
|||||||
|
|
||||||
// Handle remaining items more efficiently
|
// Handle remaining items more efficiently
|
||||||
if (items_processed < this->deferred_batch_.size()) {
|
if (items_processed < this->deferred_batch_.size()) {
|
||||||
// Remove processed items from the beginning with proper cleanup
|
// Remove processed items from the beginning
|
||||||
this->deferred_batch_.remove_front(items_processed);
|
this->deferred_batch_.remove_front(items_processed);
|
||||||
// Reschedule for remaining items
|
// Reschedule for remaining items
|
||||||
this->schedule_batch_();
|
this->schedule_batch_();
|
||||||
@@ -1835,10 +1834,10 @@ void APIConnection::process_batch_() {
|
|||||||
uint16_t APIConnection::MessageCreator::operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
uint16_t APIConnection::MessageCreator::operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||||
bool is_single, uint8_t message_type) const {
|
bool is_single, uint8_t message_type) const {
|
||||||
#ifdef USE_EVENT
|
#ifdef USE_EVENT
|
||||||
// Special case: EventResponse uses string pointer
|
// Special case: EventResponse uses const char * pointer
|
||||||
if (message_type == EventResponse::MESSAGE_TYPE) {
|
if (message_type == EventResponse::MESSAGE_TYPE) {
|
||||||
auto *e = static_cast<event::Event *>(entity);
|
auto *e = static_cast<event::Event *>(entity);
|
||||||
return APIConnection::try_send_event_response(e, *data_.string_ptr, conn, remaining_size, is_single);
|
return APIConnection::try_send_event_response(e, data_.const_char_ptr, conn, remaining_size, is_single);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ class APIConnection final : public APIServerConnection {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_EVENT
|
#ifdef USE_EVENT
|
||||||
void send_event(event::Event *event, const std::string &event_type);
|
void send_event(event::Event *event, const char *event_type);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_UPDATE
|
#ifdef USE_UPDATE
|
||||||
@@ -450,7 +450,7 @@ class APIConnection final : public APIServerConnection {
|
|||||||
bool is_single);
|
bool is_single);
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_EVENT
|
#ifdef USE_EVENT
|
||||||
static uint16_t try_send_event_response(event::Event *event, const std::string &event_type, APIConnection *conn,
|
static uint16_t try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn,
|
||||||
uint32_t remaining_size, bool is_single);
|
uint32_t remaining_size, bool is_single);
|
||||||
static uint16_t try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single);
|
static uint16_t try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single);
|
||||||
#endif
|
#endif
|
||||||
@@ -508,10 +508,8 @@ class APIConnection final : public APIServerConnection {
|
|||||||
// Constructor for function pointer
|
// Constructor for function pointer
|
||||||
MessageCreator(MessageCreatorPtr ptr) { data_.function_ptr = ptr; }
|
MessageCreator(MessageCreatorPtr ptr) { data_.function_ptr = ptr; }
|
||||||
|
|
||||||
// Constructor for string state capture
|
// Constructor for const char * (Event types - no allocation needed)
|
||||||
explicit MessageCreator(const std::string &str_value) { data_.string_ptr = new std::string(str_value); }
|
explicit MessageCreator(const char *str_value) { data_.const_char_ptr = str_value; }
|
||||||
|
|
||||||
// No destructor - cleanup must be called explicitly with message_type
|
|
||||||
|
|
||||||
// Delete copy operations - MessageCreator should only be moved
|
// Delete copy operations - MessageCreator should only be moved
|
||||||
MessageCreator(const MessageCreator &other) = delete;
|
MessageCreator(const MessageCreator &other) = delete;
|
||||||
@@ -523,8 +521,6 @@ class APIConnection final : public APIServerConnection {
|
|||||||
// Move assignment
|
// Move assignment
|
||||||
MessageCreator &operator=(MessageCreator &&other) noexcept {
|
MessageCreator &operator=(MessageCreator &&other) noexcept {
|
||||||
if (this != &other) {
|
if (this != &other) {
|
||||||
// IMPORTANT: Caller must ensure cleanup() was called if this contains a string!
|
|
||||||
// In our usage, this happens in add_item() deduplication and vector::erase()
|
|
||||||
data_ = other.data_;
|
data_ = other.data_;
|
||||||
other.data_.function_ptr = nullptr;
|
other.data_.function_ptr = nullptr;
|
||||||
}
|
}
|
||||||
@@ -535,20 +531,10 @@ class APIConnection final : public APIServerConnection {
|
|||||||
uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single,
|
uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single,
|
||||||
uint8_t message_type) const;
|
uint8_t message_type) const;
|
||||||
|
|
||||||
// Manual cleanup method - must be called before destruction for string types
|
|
||||||
void cleanup(uint8_t message_type) {
|
|
||||||
#ifdef USE_EVENT
|
|
||||||
if (message_type == EventResponse::MESSAGE_TYPE && data_.string_ptr != nullptr) {
|
|
||||||
delete data_.string_ptr;
|
|
||||||
data_.string_ptr = nullptr;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
union Data {
|
union Data {
|
||||||
MessageCreatorPtr function_ptr;
|
MessageCreatorPtr function_ptr;
|
||||||
std::string *string_ptr;
|
const char *const_char_ptr;
|
||||||
} data_; // 4 bytes on 32-bit, 8 bytes on 64-bit - same as before
|
} data_; // 4 bytes on 32-bit, 8 bytes on 64-bit - same as before
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -568,42 +554,24 @@ class APIConnection final : public APIServerConnection {
|
|||||||
std::vector<BatchItem> items;
|
std::vector<BatchItem> items;
|
||||||
uint32_t batch_start_time{0};
|
uint32_t batch_start_time{0};
|
||||||
|
|
||||||
private:
|
|
||||||
// Helper to cleanup items from the beginning
|
|
||||||
void cleanup_items_(size_t count) {
|
|
||||||
for (size_t i = 0; i < count; i++) {
|
|
||||||
items[i].creator.cleanup(items[i].message_type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public:
|
|
||||||
DeferredBatch() {
|
DeferredBatch() {
|
||||||
// Pre-allocate capacity for typical batch sizes to avoid reallocation
|
// Pre-allocate capacity for typical batch sizes to avoid reallocation
|
||||||
items.reserve(8);
|
items.reserve(8);
|
||||||
}
|
}
|
||||||
|
|
||||||
~DeferredBatch() {
|
|
||||||
// Ensure cleanup of any remaining items
|
|
||||||
clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add item to the batch
|
// Add item to the batch
|
||||||
void add_item(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size);
|
void add_item(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size);
|
||||||
// Add item to the front of the batch (for high priority messages like ping)
|
// Add item to the front of the batch (for high priority messages like ping)
|
||||||
void add_item_front(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size);
|
void add_item_front(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size);
|
||||||
|
|
||||||
// Clear all items with proper cleanup
|
// Clear all items
|
||||||
void clear() {
|
void clear() {
|
||||||
cleanup_items_(items.size());
|
|
||||||
items.clear();
|
items.clear();
|
||||||
batch_start_time = 0;
|
batch_start_time = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove processed items from the front with proper cleanup
|
// Remove processed items from the front
|
||||||
void remove_front(size_t count) {
|
void remove_front(size_t count) { items.erase(items.begin(), items.begin() + count); }
|
||||||
cleanup_items_(count);
|
|
||||||
items.erase(items.begin(), items.begin() + count);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool empty() const { return items.empty(); }
|
bool empty() const { return items.empty(); }
|
||||||
size_t size() const { return items.size(); }
|
size_t size() const { return items.size(); }
|
||||||
@@ -682,21 +650,30 @@ class APIConnection final : public APIServerConnection {
|
|||||||
}
|
}
|
||||||
#endif
|
#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
|
// 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,
|
bool send_message_smart_(EntityBase *entity, MessageCreatorPtr creator, uint8_t message_type,
|
||||||
uint8_t estimated_size) {
|
uint8_t estimated_size) {
|
||||||
// Try to send immediately if:
|
if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) {
|
||||||
// 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()) {
|
|
||||||
// Now actually encode and send
|
// Now actually encode and send
|
||||||
if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true) &&
|
if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true) &&
|
||||||
this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) {
|
this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) {
|
||||||
@@ -714,6 +691,27 @@ class APIConnection final : public APIServerConnection {
|
|||||||
return this->schedule_message_(entity, creator, message_type, estimated_size);
|
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
|
// 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) {
|
bool schedule_message_(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) {
|
||||||
this->deferred_batch_.add_item(entity, std::move(creator), message_type, estimated_size);
|
this->deferred_batch_.add_item(entity, std::move(creator), message_type, estimated_size);
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components import web_server_base
|
from esphome.components import web_server_base
|
||||||
from esphome.components.web_server_base import CONF_WEB_SERVER_BASE_ID
|
from esphome.components.web_server_base import CONF_WEB_SERVER_BASE_ID
|
||||||
from esphome.config_helpers import filter_source_files_from_platform
|
from esphome.config_helpers import filter_source_files_from_platform
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
|
CONF_AP,
|
||||||
CONF_ID,
|
CONF_ID,
|
||||||
PLATFORM_BK72XX,
|
PLATFORM_BK72XX,
|
||||||
PLATFORM_ESP32,
|
PLATFORM_ESP32,
|
||||||
@@ -14,6 +17,10 @@ from esphome.const import (
|
|||||||
)
|
)
|
||||||
from esphome.core import CORE, coroutine_with_priority
|
from esphome.core import CORE, coroutine_with_priority
|
||||||
from esphome.coroutine import CoroPriority
|
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]:
|
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)
|
@coroutine_with_priority(CoroPriority.CAPTIVE_PORTAL)
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
paren = await cg.get_variable(config[CONF_WEB_SERVER_BASE_ID])
|
paren = await cg.get_variable(config[CONF_WEB_SERVER_BASE_ID])
|
||||||
|
|||||||
@@ -49,9 +49,9 @@ void DebugComponent::dump_config() {
|
|||||||
}
|
}
|
||||||
#endif // USE_TEXT_SENSOR
|
#endif // USE_TEXT_SENSOR
|
||||||
|
|
||||||
#ifdef USE_ESP32
|
#if defined(USE_ESP32) || defined(USE_ZEPHYR)
|
||||||
this->log_partition_info_(); // Log partition information for ESP32
|
this->log_partition_info_(); // Log partition information
|
||||||
#endif // USE_ESP32
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void DebugComponent::loop() {
|
void DebugComponent::loop() {
|
||||||
|
|||||||
@@ -62,19 +62,19 @@ class DebugComponent : public PollingComponent {
|
|||||||
sensor::Sensor *cpu_frequency_sensor_{nullptr};
|
sensor::Sensor *cpu_frequency_sensor_{nullptr};
|
||||||
#endif // USE_SENSOR
|
#endif // USE_SENSOR
|
||||||
|
|
||||||
#ifdef USE_ESP32
|
#if defined(USE_ESP32) || defined(USE_ZEPHYR)
|
||||||
/**
|
/**
|
||||||
* @brief Logs information about the device's partition table.
|
* @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,
|
* about each partition, including its name, type, subtype, starting address,
|
||||||
* and size. The information is useful for diagnosing issues related to flash
|
* and size. The information is useful for diagnosing issues related to flash
|
||||||
* memory or verifying the partition configuration dynamically at runtime.
|
* 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_();
|
void log_partition_info_();
|
||||||
#endif // USE_ESP32
|
#endif
|
||||||
|
|
||||||
#ifdef USE_TEXT_SENSOR
|
#ifdef USE_TEXT_SENSOR
|
||||||
text_sensor::TextSensor *device_info_{nullptr};
|
text_sensor::TextSensor *device_info_{nullptr};
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <zephyr/drivers/hwinfo.h>
|
#include <zephyr/drivers/hwinfo.h>
|
||||||
#include <hal/nrf_power.h>
|
#include <hal/nrf_power.h>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <zephyr/storage/flash_map.h>
|
||||||
|
|
||||||
#define BOOTLOADER_VERSION_REGISTER NRF_TIMER2->CC[0]
|
#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; }
|
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) {
|
void DebugComponent::get_device_info_(std::string &device_info) {
|
||||||
std::string supply = "Main supply status: ";
|
std::string supply = "Main supply status: ";
|
||||||
if (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_NORMAL) {
|
if (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_NORMAL) {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ void DS1307Component::dump_config() {
|
|||||||
if (this->is_failed()) {
|
if (this->is_failed()) {
|
||||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||||
}
|
}
|
||||||
ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str());
|
RealTimeClock::dump_config();
|
||||||
}
|
}
|
||||||
|
|
||||||
float DS1307Component::get_setup_priority() const { return setup_priority::DATA; }
|
float DS1307Component::get_setup_priority() const { return setup_priority::DATA; }
|
||||||
|
|||||||
@@ -334,12 +334,14 @@ def _is_framework_url(source: str) -> str:
|
|||||||
# - https://github.com/espressif/arduino-esp32/releases
|
# - https://github.com/espressif/arduino-esp32/releases
|
||||||
ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
|
ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
|
||||||
"recommended": cv.Version(3, 3, 2),
|
"recommended": cv.Version(3, 3, 2),
|
||||||
"latest": cv.Version(3, 3, 2),
|
"latest": cv.Version(3, 3, 4),
|
||||||
"dev": cv.Version(3, 3, 2),
|
"dev": cv.Version(3, 3, 4),
|
||||||
}
|
}
|
||||||
ARDUINO_PLATFORM_VERSION_LOOKUP = {
|
ARDUINO_PLATFORM_VERSION_LOOKUP = {
|
||||||
cv.Version(3, 3, 2): cv.Version(55, 3, 31, "1"),
|
cv.Version(3, 3, 4): cv.Version(55, 3, 31, "2"),
|
||||||
cv.Version(3, 3, 1): cv.Version(55, 3, 31, "1"),
|
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, 3, 0): cv.Version(55, 3, 30, "2"),
|
||||||
cv.Version(3, 2, 1): cv.Version(54, 3, 21, "2"),
|
cv.Version(3, 2, 1): cv.Version(54, 3, 21, "2"),
|
||||||
cv.Version(3, 2, 0): cv.Version(54, 3, 20),
|
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),
|
"dev": cv.Version(5, 5, 1),
|
||||||
}
|
}
|
||||||
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
||||||
cv.Version(5, 5, 1): 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, "1"),
|
cv.Version(5, 5, 0): cv.Version(55, 3, 31, "2"),
|
||||||
cv.Version(5, 4, 3): cv.Version(55, 3, 32),
|
cv.Version(5, 4, 3): cv.Version(55, 3, 32),
|
||||||
cv.Version(5, 4, 2): cv.Version(54, 3, 21, "2"),
|
cv.Version(5, 4, 2): cv.Version(54, 3, 21, "2"),
|
||||||
cv.Version(5, 4, 1): cv.Version(54, 3, 21, "2"),
|
cv.Version(5, 4, 1): cv.Version(54, 3, 21, "2"),
|
||||||
@@ -373,9 +375,9 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
|||||||
# The platform-espressif32 version
|
# The platform-espressif32 version
|
||||||
# - https://github.com/pioarduino/platform-espressif32/releases
|
# - https://github.com/pioarduino/platform-espressif32/releases
|
||||||
PLATFORM_VERSION_LOOKUP = {
|
PLATFORM_VERSION_LOOKUP = {
|
||||||
"recommended": cv.Version(55, 3, 31, "1"),
|
"recommended": cv.Version(55, 3, 31, "2"),
|
||||||
"latest": cv.Version(55, 3, 31, "1"),
|
"latest": cv.Version(55, 3, 31, "2"),
|
||||||
"dev": cv.Version(55, 3, 31, "1"),
|
"dev": cv.Version(55, 3, 31, "2"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -634,11 +634,13 @@ void ESP32BLE::dump_config() {
|
|||||||
io_capability_s = "invalid";
|
io_capability_s = "invalid";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
char mac_s[18];
|
||||||
|
format_mac_addr_upper(mac_address, mac_s);
|
||||||
ESP_LOGCONFIG(TAG,
|
ESP_LOGCONFIG(TAG,
|
||||||
"BLE:\n"
|
"BLE:\n"
|
||||||
" MAC address: %s\n"
|
" MAC address: %s\n"
|
||||||
" IO Capability: %s",
|
" IO Capability: %s",
|
||||||
format_mac_address_pretty(mac_address).c_str(), io_capability_s);
|
mac_s, io_capability_s);
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGCONFIG(TAG, "Bluetooth stack is not enabled");
|
ESP_LOGCONFIG(TAG, "Bluetooth stack is not enabled");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ namespace esphome::esp32_ble_tracker {
|
|||||||
class ESPBTAdvertiseTrigger : public Trigger<const ESPBTDevice &>, public ESPBTDeviceListener {
|
class ESPBTAdvertiseTrigger : public Trigger<const ESPBTDevice &>, public ESPBTDeviceListener {
|
||||||
public:
|
public:
|
||||||
explicit ESPBTAdvertiseTrigger(ESP32BLETracker *parent) { parent->register_listener(this); }
|
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 {
|
bool parse_device(const ESPBTDevice &device) override {
|
||||||
uint64_t u64_addr = device.address_uint64();
|
uint64_t u64_addr = device.address_uint64();
|
||||||
|
|||||||
@@ -336,7 +336,7 @@ void ESP32ImprovComponent::process_incoming_data_() {
|
|||||||
this->connecting_sta_ = sta;
|
this->connecting_sta_ = sta;
|
||||||
|
|
||||||
wifi::global_wifi_component->set_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);
|
this->set_state_(improv::STATE_PROVISIONING);
|
||||||
ESP_LOGD(TAG, "Received Improv Wi-Fi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
|
ESP_LOGD(TAG, "Received Improv Wi-Fi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
|
||||||
command.password.c_str());
|
command.password.c_str());
|
||||||
|
|||||||
@@ -383,7 +383,6 @@ async def to_code(config):
|
|||||||
cg.add(var.set_use_address(config[CONF_USE_ADDRESS]))
|
cg.add(var.set_use_address(config[CONF_USE_ADDRESS]))
|
||||||
|
|
||||||
if CONF_MANUAL_IP in config:
|
if CONF_MANUAL_IP in config:
|
||||||
cg.add_define("USE_ETHERNET_MANUAL_IP")
|
|
||||||
cg.add(var.set_manual_ip(manual_ip(config[CONF_MANUAL_IP])))
|
cg.add(var.set_manual_ip(manual_ip(config[CONF_MANUAL_IP])))
|
||||||
|
|
||||||
# Add compile-time define for PHY types with specific code
|
# Add compile-time define for PHY types with specific code
|
||||||
|
|||||||
@@ -381,7 +381,10 @@ void EthernetComponent::dump_config() {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
ESP_LOGCONFIG(TAG, "Ethernet:");
|
ESP_LOGCONFIG(TAG,
|
||||||
|
"Ethernet:\n"
|
||||||
|
" Connected: %s",
|
||||||
|
YESNO(this->is_connected()));
|
||||||
this->dump_connect_params_();
|
this->dump_connect_params_();
|
||||||
#ifdef USE_ETHERNET_SPI
|
#ifdef USE_ETHERNET_SPI
|
||||||
ESP_LOGCONFIG(TAG,
|
ESP_LOGCONFIG(TAG,
|
||||||
@@ -550,14 +553,11 @@ void EthernetComponent::start_connect_() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
esp_netif_ip_info_t info;
|
esp_netif_ip_info_t info;
|
||||||
#ifdef USE_ETHERNET_MANUAL_IP
|
|
||||||
if (this->manual_ip_.has_value()) {
|
if (this->manual_ip_.has_value()) {
|
||||||
info.ip = this->manual_ip_->static_ip;
|
info.ip = this->manual_ip_->static_ip;
|
||||||
info.gw = this->manual_ip_->gateway;
|
info.gw = this->manual_ip_->gateway;
|
||||||
info.netmask = this->manual_ip_->subnet;
|
info.netmask = this->manual_ip_->subnet;
|
||||||
} else
|
} else {
|
||||||
#endif
|
|
||||||
{
|
|
||||||
info.ip.addr = 0;
|
info.ip.addr = 0;
|
||||||
info.gw.addr = 0;
|
info.gw.addr = 0;
|
||||||
info.netmask.addr = 0;
|
info.netmask.addr = 0;
|
||||||
@@ -578,7 +578,6 @@ void EthernetComponent::start_connect_() {
|
|||||||
err = esp_netif_set_ip_info(this->eth_netif_, &info);
|
err = esp_netif_set_ip_info(this->eth_netif_, &info);
|
||||||
ESPHL_ERROR_CHECK(err, "DHCPC set IP info error");
|
ESPHL_ERROR_CHECK(err, "DHCPC set IP info error");
|
||||||
|
|
||||||
#ifdef USE_ETHERNET_MANUAL_IP
|
|
||||||
if (this->manual_ip_.has_value()) {
|
if (this->manual_ip_.has_value()) {
|
||||||
LwIPLock lock;
|
LwIPLock lock;
|
||||||
if (this->manual_ip_->dns1.is_set()) {
|
if (this->manual_ip_->dns1.is_set()) {
|
||||||
@@ -591,9 +590,7 @@ void EthernetComponent::start_connect_() {
|
|||||||
d = this->manual_ip_->dns2;
|
d = this->manual_ip_->dns2;
|
||||||
dns_setserver(1, &d);
|
dns_setserver(1, &d);
|
||||||
}
|
}
|
||||||
} else
|
} else {
|
||||||
#endif
|
|
||||||
{
|
|
||||||
err = esp_netif_dhcpc_start(this->eth_netif_);
|
err = esp_netif_dhcpc_start(this->eth_netif_);
|
||||||
if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STARTED) {
|
if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STARTED) {
|
||||||
ESPHL_ERROR_CHECK(err, "DHCPC start error");
|
ESPHL_ERROR_CHECK(err, "DHCPC start error");
|
||||||
@@ -691,9 +688,7 @@ void EthernetComponent::set_clk_mode(emac_rmii_clock_mode_t clk_mode) { this->cl
|
|||||||
void EthernetComponent::add_phy_register(PHYRegister register_value) { this->phy_registers_.push_back(register_value); }
|
void EthernetComponent::add_phy_register(PHYRegister register_value) { this->phy_registers_.push_back(register_value); }
|
||||||
#endif
|
#endif
|
||||||
void EthernetComponent::set_type(EthernetType type) { this->type_ = type; }
|
void EthernetComponent::set_type(EthernetType type) { this->type_ = type; }
|
||||||
#ifdef USE_ETHERNET_MANUAL_IP
|
|
||||||
void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; }
|
void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; }
|
||||||
#endif
|
|
||||||
|
|
||||||
// set_use_address() is guaranteed to be called during component setup by Python code generation,
|
// set_use_address() is guaranteed to be called during component setup by Python code generation,
|
||||||
// so use_address_ will always be valid when get_use_address() is called - no fallback needed.
|
// so use_address_ will always be valid when get_use_address() is called - no fallback needed.
|
||||||
|
|||||||
@@ -82,9 +82,7 @@ class EthernetComponent : public Component {
|
|||||||
void add_phy_register(PHYRegister register_value);
|
void add_phy_register(PHYRegister register_value);
|
||||||
#endif
|
#endif
|
||||||
void set_type(EthernetType type);
|
void set_type(EthernetType type);
|
||||||
#ifdef USE_ETHERNET_MANUAL_IP
|
|
||||||
void set_manual_ip(const ManualIP &manual_ip);
|
void set_manual_ip(const ManualIP &manual_ip);
|
||||||
#endif
|
|
||||||
void set_fixed_mac(const std::array<uint8_t, 6> &mac) { this->fixed_mac_ = mac; }
|
void set_fixed_mac(const std::array<uint8_t, 6> &mac) { this->fixed_mac_ = mac; }
|
||||||
|
|
||||||
network::IPAddresses get_ip_addresses();
|
network::IPAddresses get_ip_addresses();
|
||||||
@@ -139,9 +137,7 @@ class EthernetComponent : public Component {
|
|||||||
uint8_t mdc_pin_{23};
|
uint8_t mdc_pin_{23};
|
||||||
uint8_t mdio_pin_{18};
|
uint8_t mdio_pin_{18};
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_ETHERNET_MANUAL_IP
|
|
||||||
optional<ManualIP> manual_ip_{};
|
optional<ManualIP> manual_ip_{};
|
||||||
#endif
|
|
||||||
uint32_t connect_begin_;
|
uint32_t connect_begin_;
|
||||||
|
|
||||||
// Group all uint8_t types together (enums and bools)
|
// Group all uint8_t types together (enums and bools)
|
||||||
|
|||||||
@@ -7,10 +7,8 @@ namespace homeassistant {
|
|||||||
static const char *const TAG = "homeassistant.time";
|
static const char *const TAG = "homeassistant.time";
|
||||||
|
|
||||||
void HomeassistantTime::dump_config() {
|
void HomeassistantTime::dump_config() {
|
||||||
ESP_LOGCONFIG(TAG,
|
ESP_LOGCONFIG(TAG, "Home Assistant Time");
|
||||||
"Home Assistant Time:\n"
|
RealTimeClock::dump_config();
|
||||||
" Timezone: '%s'",
|
|
||||||
this->timezone_.c_str());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
float HomeassistantTime::get_setup_priority() const { return setup_priority::DATA; }
|
float HomeassistantTime::get_setup_priority() const { return setup_priority::DATA; }
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
|
|||||||
this->connecting_sta_ = sta;
|
this->connecting_sta_ = sta;
|
||||||
|
|
||||||
wifi::global_wifi_component->set_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);
|
this->set_state_(improv::STATE_PROVISIONING);
|
||||||
ESP_LOGD(TAG, "Received settings: SSID=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
|
ESP_LOGD(TAG, "Received settings: SSID=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
|
||||||
command.password.c_str());
|
command.password.c_str());
|
||||||
|
|||||||
@@ -406,7 +406,7 @@ void LightCall::transform_parameters_() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ColorMode LightCall::compute_color_mode_() {
|
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();
|
int supported_count = supported_modes.size();
|
||||||
|
|
||||||
// Some lights don't support any color modes (e.g. monochromatic light), leave it at unknown.
|
// Some lights don't support any color modes (e.g. monochromatic light), leave it at unknown.
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ void LightState::setup() {
|
|||||||
effect->init_internal(this);
|
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.
|
// When supported color temperature range is known, initialize color temperature setting within bounds.
|
||||||
auto traits = this->get_traits();
|
auto traits = this->get_traits();
|
||||||
float min_mireds = traits.get_min_mireds();
|
float min_mireds = traits.get_min_mireds();
|
||||||
@@ -126,6 +129,9 @@ void LightState::loop() {
|
|||||||
this->is_transformer_active_ = false;
|
this->is_transformer_active_ = false;
|
||||||
this->transformer_ = nullptr;
|
this->transformer_ = nullptr;
|
||||||
this->target_state_reached_callback_.call();
|
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_) {
|
if (this->next_write_) {
|
||||||
this->next_write_ = false;
|
this->next_write_ = false;
|
||||||
this->output_->write_state(this);
|
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;
|
this->active_effect_index_ = effect_index;
|
||||||
auto *effect = this->get_active_effect_();
|
auto *effect = this->get_active_effect_();
|
||||||
effect->start_internal();
|
effect->start_internal();
|
||||||
|
// Enable loop while effect is active
|
||||||
|
this->enable_loop();
|
||||||
}
|
}
|
||||||
LightEffect *LightState::get_active_effect_() {
|
LightEffect *LightState::get_active_effect_() {
|
||||||
if (this->active_effect_index_ == 0) {
|
if (this->active_effect_index_ == 0) {
|
||||||
@@ -242,6 +252,8 @@ void LightState::stop_effect_() {
|
|||||||
effect->stop();
|
effect->stop();
|
||||||
}
|
}
|
||||||
this->active_effect_index_ = 0;
|
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) {
|
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) {
|
if (set_remote_values) {
|
||||||
this->remote_values = target;
|
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) {
|
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) {
|
if (set_remote_values) {
|
||||||
this->remote_values = target;
|
this->remote_values = target;
|
||||||
};
|
};
|
||||||
|
// Enable loop while flash is active
|
||||||
|
this->enable_loop();
|
||||||
}
|
}
|
||||||
|
|
||||||
void LightState::set_immediately_(const LightColorValues &target, bool set_remote_values) {
|
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->output_->update_state(this);
|
||||||
this->next_write_ = true;
|
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_() {
|
void LightState::save_remote_values_() {
|
||||||
|
|||||||
@@ -256,6 +256,9 @@ class LightState : public EntityBase, public Component {
|
|||||||
/// Internal method to save the current remote_values to the preferences
|
/// Internal method to save the current remote_values to the preferences
|
||||||
void save_remote_values_();
|
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.
|
/// Store the output to allow effects to have more access.
|
||||||
LightOutput *output_;
|
LightOutput *output_;
|
||||||
/// The currently active transformer for this light (transition/flash).
|
/// The currently active transformer for this light (transition/flash).
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ class LightTraits {
|
|||||||
public:
|
public:
|
||||||
LightTraits() = default;
|
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) {
|
void set_supported_color_modes(ColorModeMask supported_color_modes) {
|
||||||
this->supported_color_modes_ = supported_color_modes;
|
this->supported_color_modes_ = supported_color_modes;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -331,7 +331,7 @@ async def to_code(configs):
|
|||||||
# This must be done after all widgets are created
|
# This must be done after all widgets are created
|
||||||
for comp in helpers.lvgl_components_required:
|
for comp in helpers.lvgl_components_required:
|
||||||
cg.add_define(f"USE_LVGL_{comp.upper()}")
|
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")
|
add_define("LV_COLOR_SCREEN_TRANSP", "1")
|
||||||
for use in helpers.lv_uses:
|
for use in helpers.lv_uses:
|
||||||
add_define(f"LV_USE_{use.upper()}")
|
add_define(f"LV_USE_{use.upper()}")
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import ipaddress
|
import ipaddress
|
||||||
|
import logging
|
||||||
|
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components.esp32 import add_idf_sdkconfig_option
|
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
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_ENABLE_IPV6, CONF_MIN_IPV6_ADDR_COUNT
|
from esphome.const import CONF_ENABLE_IPV6, CONF_MIN_IPV6_ADDR_COUNT
|
||||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
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"]
|
CODEOWNERS = ["@esphome/core"]
|
||||||
AUTO_LOAD = ["mdns"]
|
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")
|
network_ns = cg.esphome_ns.namespace("network")
|
||||||
IPAddress = network_ns.class_("IPAddress")
|
IPAddress = network_ns.class_("IPAddress")
|
||||||
|
|
||||||
@@ -47,6 +56,55 @@ def ip_address_literal(ip: str | int | None) -> cg.MockObj:
|
|||||||
return IPAddress(str(ip))
|
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(
|
CONFIG_SCHEMA = cv.Schema(
|
||||||
{
|
{
|
||||||
cv.SplitDefault(
|
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_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")
|
cg.add_define("USE_NETWORK")
|
||||||
if CORE.using_arduino and CORE.is_esp32:
|
if CORE.using_arduino and CORE.is_esp32:
|
||||||
cg.add_library("Networking", None)
|
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:
|
if (enable_ipv6 := config.get(CONF_ENABLE_IPV6, None)) is not None:
|
||||||
cg.add_define("USE_NETWORK_IPV6", enable_ipv6)
|
cg.add_define("USE_NETWORK_IPV6", enable_ipv6)
|
||||||
if enable_ipv6:
|
if enable_ipv6:
|
||||||
|
|||||||
@@ -118,10 +118,10 @@ struct IPAddress {
|
|||||||
operator arduino_ns::IPAddress() const { return ip_addr_get_ip4_u32(&ip_addr_); }
|
operator arduino_ns::IPAddress() const { return ip_addr_get_ip4_u32(&ip_addr_); }
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
bool is_set() { return !ip_addr_isany(&ip_addr_); } // NOLINT(readability-simplify-boolean-expr)
|
bool is_set() const { return !ip_addr_isany(&ip_addr_); } // NOLINT(readability-simplify-boolean-expr)
|
||||||
bool is_ip4() { return IP_IS_V4(&ip_addr_); }
|
bool is_ip4() const { return IP_IS_V4(&ip_addr_); }
|
||||||
bool is_ip6() { return IP_IS_V6(&ip_addr_); }
|
bool is_ip6() const { return IP_IS_V6(&ip_addr_); }
|
||||||
bool is_multicast() { return ip_addr_ismulticast(&ip_addr_); }
|
bool is_multicast() const { return ip_addr_ismulticast(&ip_addr_); }
|
||||||
std::string str() const { return str_lower_case(ipaddr_ntoa(&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_); }
|
||||||
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_); }
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ from esphome.const import (
|
|||||||
CONF_FRAMEWORK,
|
CONF_FRAMEWORK,
|
||||||
CONF_ID,
|
CONF_ID,
|
||||||
CONF_RESET_PIN,
|
CONF_RESET_PIN,
|
||||||
|
CONF_VOLTAGE,
|
||||||
KEY_CORE,
|
KEY_CORE,
|
||||||
KEY_FRAMEWORK_VERSION,
|
KEY_FRAMEWORK_VERSION,
|
||||||
KEY_TARGET_FRAMEWORK,
|
KEY_TARGET_FRAMEWORK,
|
||||||
@@ -102,6 +103,11 @@ nrf52_ns = cg.esphome_ns.namespace("nrf52")
|
|||||||
DeviceFirmwareUpdate = nrf52_ns.class_("DeviceFirmwareUpdate", cg.Component)
|
DeviceFirmwareUpdate = nrf52_ns.class_("DeviceFirmwareUpdate", cg.Component)
|
||||||
|
|
||||||
CONF_DFU = "dfu"
|
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]
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.All(
|
CONFIG_SCHEMA = cv.All(
|
||||||
_detect_bootloader,
|
_detect_bootloader,
|
||||||
@@ -116,6 +122,16 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema,
|
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.All(
|
||||||
|
cv.voltage,
|
||||||
|
cv.one_of(*VOLTAGE_LEVELS, float=True),
|
||||||
|
),
|
||||||
|
cv.Optional(CONF_UICR_ERASE, default=False): cv.boolean,
|
||||||
|
}
|
||||||
|
),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -182,6 +198,13 @@ async def to_code(config: ConfigType) -> None:
|
|||||||
|
|
||||||
if dfu_config := config.get(CONF_DFU):
|
if dfu_config := config.get(CONF_DFU):
|
||||||
CORE.add_job(_dfu_to_code, dfu_config)
|
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 = 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")
|
||||||
|
|
||||||
|
|
||||||
@coroutine_with_priority(CoroPriority.DIAGNOSTICS)
|
@coroutine_with_priority(CoroPriority.DIAGNOSTICS)
|
||||||
|
|||||||
121
esphome/components/nrf52/uicr.cpp
Normal file
121
esphome/components/nrf52/uicr.cpp
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
#include "esphome/core/defines.h"
|
||||||
|
|
||||||
|
#ifdef USE_NRF52_REG0_VOUT
|
||||||
|
#include <zephyr/init.h>
|
||||||
|
#include <hal/nrf_power.h>
|
||||||
|
#include <zephyr/sys/printk.h>
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
void nvmc_config(uint32_t mode);
|
||||||
|
void nvmc_wait();
|
||||||
|
nrfx_err_t nrfx_nvmc_uicr_erase();
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace esphome::nrf52 {
|
||||||
|
|
||||||
|
enum class StatusFlags : uint8_t {
|
||||||
|
OK = 0x00,
|
||||||
|
NEED_RESET = 0x01,
|
||||||
|
NEED_ERASE = 0x02,
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr StatusFlags &operator|=(StatusFlags &a, StatusFlags b) {
|
||||||
|
a = static_cast<StatusFlags>(static_cast<uint8_t>(a) | static_cast<uint8_t>(b));
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr bool operator&(StatusFlags a, StatusFlags b) {
|
||||||
|
return (static_cast<uint8_t>(a) & static_cast<uint8_t>(b)) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool regout0_ok() {
|
||||||
|
return (NRF_UICR->REGOUT0 & UICR_REGOUT0_VOUT_Msk) == (USE_NRF52_REG0_VOUT << UICR_REGOUT0_VOUT_Pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
static StatusFlags set_regout0() {
|
||||||
|
/* If the board is powered from USB (high voltage mode),
|
||||||
|
* GPIO output voltage is set to 1.8 volts by default.
|
||||||
|
*/
|
||||||
|
if (!regout0_ok()) {
|
||||||
|
nvmc_config(NVMC_CONFIG_WEN_Wen);
|
||||||
|
NRF_UICR->REGOUT0 =
|
||||||
|
(NRF_UICR->REGOUT0 & ~((uint32_t) UICR_REGOUT0_VOUT_Msk)) | (USE_NRF52_REG0_VOUT << UICR_REGOUT0_VOUT_Pos);
|
||||||
|
nvmc_wait();
|
||||||
|
nvmc_config(NVMC_CONFIG_WEN_Ren);
|
||||||
|
return regout0_ok() ? StatusFlags::NEED_RESET : StatusFlags::NEED_ERASE;
|
||||||
|
}
|
||||||
|
return StatusFlags::OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifndef USE_BOOTLOADER_MCUBOOT
|
||||||
|
// https://github.com/adafruit/Adafruit_nRF52_Bootloader/blob/6a9a6a3e6d0f86918e9286188426a279976645bd/lib/sdk11/components/libraries/bootloader_dfu/dfu_types.h#L61
|
||||||
|
constexpr uint32_t BOOTLOADER_REGION_START = 0x000F4000;
|
||||||
|
constexpr uint32_t BOOTLOADER_MBR_PARAMS_PAGE_ADDRESS = 0x000FE000;
|
||||||
|
|
||||||
|
static bool bootloader_ok() {
|
||||||
|
return NRF_UICR->NRFFW[0] == BOOTLOADER_REGION_START && NRF_UICR->NRFFW[1] == BOOTLOADER_MBR_PARAMS_PAGE_ADDRESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
static StatusFlags fix_bootloader() {
|
||||||
|
if (!bootloader_ok()) {
|
||||||
|
nvmc_config(NVMC_CONFIG_WEN_Wen);
|
||||||
|
NRF_UICR->NRFFW[0] = BOOTLOADER_REGION_START;
|
||||||
|
NRF_UICR->NRFFW[1] = BOOTLOADER_MBR_PARAMS_PAGE_ADDRESS;
|
||||||
|
nvmc_wait();
|
||||||
|
nvmc_config(NVMC_CONFIG_WEN_Ren);
|
||||||
|
return bootloader_ok() ? StatusFlags::NEED_RESET : StatusFlags::NEED_ERASE;
|
||||||
|
}
|
||||||
|
return StatusFlags::OK;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define BOOTLOADER_VERSION_REGISTER NRF_TIMER2->CC[0]
|
||||||
|
|
||||||
|
static StatusFlags set_uicr() {
|
||||||
|
StatusFlags status = StatusFlags::OK;
|
||||||
|
#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
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int board_esphome_init() {
|
||||||
|
StatusFlags status = set_uicr();
|
||||||
|
|
||||||
|
#ifdef USE_NRF52_UICR_ERASE
|
||||||
|
if (status & StatusFlags::NEED_ERASE) {
|
||||||
|
nrfx_err_t ret = nrfx_nvmc_uicr_erase();
|
||||||
|
if (ret != NRFX_SUCCESS) {
|
||||||
|
#ifdef CONFIG_PRINTK
|
||||||
|
printk("nrfx_nvmc_uicr_erase failed %d\n", ret);
|
||||||
|
#endif
|
||||||
|
} else {
|
||||||
|
status |= set_uicr();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (status & StatusFlags::NEED_RESET) {
|
||||||
|
/* a reset is required for changes to take effect */
|
||||||
|
NVIC_SystemReset();
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
} // namespace esphome::nrf52
|
||||||
|
|
||||||
|
static int board_esphome_init() { return esphome::nrf52::board_esphome_init(); }
|
||||||
|
|
||||||
|
SYS_INIT(board_esphome_init, PRE_KERNEL_1, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -23,7 +23,7 @@ void PCF85063Component::dump_config() {
|
|||||||
if (this->is_failed()) {
|
if (this->is_failed()) {
|
||||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||||
}
|
}
|
||||||
ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str());
|
RealTimeClock::dump_config();
|
||||||
}
|
}
|
||||||
|
|
||||||
float PCF85063Component::get_setup_priority() const { return setup_priority::DATA; }
|
float PCF85063Component::get_setup_priority() const { return setup_priority::DATA; }
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ void PCF8563Component::dump_config() {
|
|||||||
if (this->is_failed()) {
|
if (this->is_failed()) {
|
||||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||||
}
|
}
|
||||||
ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str());
|
RealTimeClock::dump_config();
|
||||||
}
|
}
|
||||||
|
|
||||||
float PCF8563Component::get_setup_priority() const { return setup_priority::DATA; }
|
float PCF8563Component::get_setup_priority() const { return setup_priority::DATA; }
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ DOMAIN = "psram"
|
|||||||
|
|
||||||
DEPENDENCIES = [PLATFORM_ESP32]
|
DEPENDENCIES = [PLATFORM_ESP32]
|
||||||
|
|
||||||
|
# PSRAM availability tracking for cross-component coordination
|
||||||
|
KEY_PSRAM_GUARANTEED = "psram_guaranteed"
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
psram_ns = cg.esphome_ns.namespace(DOMAIN)
|
psram_ns = cg.esphome_ns.namespace(DOMAIN)
|
||||||
@@ -71,6 +74,23 @@ def supported() -> bool:
|
|||||||
return variant in SPIRAM_MODES
|
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):
|
def validate_psram_mode(config):
|
||||||
esp32_config = fv.full_config.get()[PLATFORM_ESP32]
|
esp32_config = fv.full_config.get()[PLATFORM_ESP32]
|
||||||
if config[CONF_SPEED] == "120MHZ":
|
if config[CONF_SPEED] == "120MHZ":
|
||||||
@@ -131,7 +151,22 @@ def get_config_schema(config):
|
|||||||
|
|
||||||
CONFIG_SCHEMA = get_config_schema
|
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):
|
async def to_code(config):
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ void RX8130Component::update() { this->read_time(); }
|
|||||||
void RX8130Component::dump_config() {
|
void RX8130Component::dump_config() {
|
||||||
ESP_LOGCONFIG(TAG, "RX8130:");
|
ESP_LOGCONFIG(TAG, "RX8130:");
|
||||||
LOG_I2C_DEVICE(this);
|
LOG_I2C_DEVICE(this);
|
||||||
|
RealTimeClock::dump_config();
|
||||||
}
|
}
|
||||||
|
|
||||||
void RX8130Component::read_time() {
|
void RX8130Component::read_time() {
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ void SNTPComponent::dump_config() {
|
|||||||
for (auto &server : this->servers_) {
|
for (auto &server : this->servers_) {
|
||||||
ESP_LOGCONFIG(TAG, " Server %zu: '%s'", i++, server);
|
ESP_LOGCONFIG(TAG, " Server %zu: '%s'", i++, server);
|
||||||
}
|
}
|
||||||
|
RealTimeClock::dump_config();
|
||||||
}
|
}
|
||||||
void SNTPComponent::update() {
|
void SNTPComponent::update() {
|
||||||
#if !defined(USE_ESP32)
|
#if !defined(USE_ESP32)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
from esphome import automation, external_files
|
from esphome import automation, external_files
|
||||||
import esphome.codegen as cg
|
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
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_BUFFER_SIZE,
|
CONF_BUFFER_SIZE,
|
||||||
@@ -32,6 +32,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
AUTO_LOAD = ["audio"]
|
AUTO_LOAD = ["audio"]
|
||||||
|
DEPENDENCIES = ["network"]
|
||||||
|
|
||||||
CODEOWNERS = ["@kahrendt", "@synesthesiam"]
|
CODEOWNERS = ["@kahrendt", "@synesthesiam"]
|
||||||
DOMAIN = "media_player"
|
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(
|
CONFIG_SCHEMA = cv.All(
|
||||||
media_player.media_player_schema(SpeakerMediaPlayer).extend(
|
media_player.media_player_schema(SpeakerMediaPlayer).extend(
|
||||||
{
|
{
|
||||||
@@ -304,6 +317,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
),
|
),
|
||||||
cv.only_with_esp_idf,
|
cv.only_with_esp_idf,
|
||||||
_validate_repeated_speaker,
|
_validate_repeated_speaker,
|
||||||
|
_request_high_performance_networking,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -321,28 +335,10 @@ FINAL_VALIDATE_SCHEMA = cv.All(
|
|||||||
|
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
if CORE.data[DOMAIN][config[CONF_ID].id][CONF_CODEC_SUPPORT_ENABLED]:
|
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_FLAC_SUPPORT", True)
|
||||||
cg.add_define("USE_AUDIO_MP3_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)
|
var = await media_player.new_media_player(config)
|
||||||
await cg.register_component(var, config)
|
await cg.register_component(var, config)
|
||||||
|
|
||||||
|
|||||||
@@ -945,6 +945,10 @@ async def to_code(config):
|
|||||||
cg.add(var.set_humidity_hysteresis(config[CONF_HUMIDITY_HYSTERESIS]))
|
cg.add(var.set_humidity_hysteresis(config[CONF_HUMIDITY_HYSTERESIS]))
|
||||||
|
|
||||||
if CONF_PRESET in config:
|
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]:
|
for preset_config in config[CONF_PRESET]:
|
||||||
name = preset_config[CONF_NAME]
|
name = preset_config[CONF_NAME]
|
||||||
standard_preset = None
|
standard_preset = None
|
||||||
@@ -987,9 +991,39 @@ async def to_code(config):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if standard_preset is not None:
|
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:
|
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:
|
if CONF_DEFAULT_PRESET in config:
|
||||||
default_preset_name = config[CONF_DEFAULT_PRESET]
|
default_preset_name = config[CONF_DEFAULT_PRESET]
|
||||||
|
|||||||
@@ -53,8 +53,8 @@ void ThermostatClimate::setup() {
|
|||||||
if (use_default_preset) {
|
if (use_default_preset) {
|
||||||
if (this->default_preset_ != climate::ClimatePreset::CLIMATE_PRESET_NONE) {
|
if (this->default_preset_ != climate::ClimatePreset::CLIMATE_PRESET_NONE) {
|
||||||
this->change_preset_(this->default_preset_);
|
this->change_preset_(this->default_preset_);
|
||||||
} else if (!this->default_custom_preset_.empty()) {
|
} else if (this->default_custom_preset_ != nullptr) {
|
||||||
this->change_custom_preset_(this->default_custom_preset_.c_str());
|
this->change_custom_preset_(this->default_custom_preset_);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,16 +319,16 @@ climate::ClimateTraits ThermostatClimate::traits() {
|
|||||||
if (this->supports_swing_mode_vertical_)
|
if (this->supports_swing_mode_vertical_)
|
||||||
traits.add_supported_swing_mode(climate::CLIMATE_SWING_VERTICAL);
|
traits.add_supported_swing_mode(climate::CLIMATE_SWING_VERTICAL);
|
||||||
|
|
||||||
for (auto &it : this->preset_config_) {
|
for (const auto &entry : this->preset_config_) {
|
||||||
traits.add_supported_preset(it.first);
|
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()) {
|
if (!this->custom_preset_config_.empty()) {
|
||||||
std::vector<const char *> custom_preset_names;
|
std::vector<const char *> custom_preset_names;
|
||||||
custom_preset_names.reserve(this->custom_preset_config_.size());
|
custom_preset_names.reserve(this->custom_preset_config_.size());
|
||||||
for (const auto &it : this->custom_preset_config_) {
|
for (const auto &entry : this->custom_preset_config_) {
|
||||||
custom_preset_names.push_back(it.first.c_str());
|
custom_preset_names.push_back(entry.name);
|
||||||
}
|
}
|
||||||
traits.set_supported_custom_presets(custom_preset_names);
|
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) {
|
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)));
|
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()) ||
|
if (this->change_preset_internal_(*config) || (!this->preset.has_value()) || this->preset.value() != preset) {
|
||||||
this->preset.value() != preset) {
|
|
||||||
// Fire any preset changed trigger if defined
|
// Fire any preset changed trigger if defined
|
||||||
Trigger<> *trig = this->preset_change_trigger_;
|
Trigger<> *trig = this->preset_change_trigger_;
|
||||||
this->set_preset_(preset);
|
this->set_preset_(preset);
|
||||||
@@ -1178,11 +1184,18 @@ void ThermostatClimate::change_preset_(climate::ClimatePreset preset) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ThermostatClimate::change_custom_preset_(const char *custom_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);
|
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) {
|
strcmp(this->get_custom_preset(), custom_preset) != 0) {
|
||||||
// Fire any preset changed trigger if defined
|
// Fire any preset changed trigger if defined
|
||||||
Trigger<> *trig = this->preset_change_trigger_;
|
Trigger<> *trig = this->preset_change_trigger_;
|
||||||
@@ -1247,14 +1260,12 @@ bool ThermostatClimate::change_preset_internal_(const ThermostatClimateTargetTem
|
|||||||
return something_changed;
|
return something_changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ThermostatClimate::set_preset_config(climate::ClimatePreset preset,
|
void ThermostatClimate::set_preset_config(std::initializer_list<PresetEntry> presets) {
|
||||||
const ThermostatClimateTargetTempConfig &config) {
|
this->preset_config_ = presets;
|
||||||
this->preset_config_[preset] = config;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ThermostatClimate::set_custom_preset_config(const std::string &name,
|
void ThermostatClimate::set_custom_preset_config(std::initializer_list<CustomPresetEntry> presets) {
|
||||||
const ThermostatClimateTargetTempConfig &config) {
|
this->custom_preset_config_ = presets;
|
||||||
this->custom_preset_config_[name] = config;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ThermostatClimate::ThermostatClimate()
|
ThermostatClimate::ThermostatClimate()
|
||||||
@@ -1293,8 +1304,16 @@ ThermostatClimate::ThermostatClimate()
|
|||||||
humidity_control_humidify_action_trigger_(new Trigger<>()),
|
humidity_control_humidify_action_trigger_(new Trigger<>()),
|
||||||
humidity_control_off_action_trigger_(new Trigger<>()) {}
|
humidity_control_off_action_trigger_(new Trigger<>()) {}
|
||||||
|
|
||||||
void ThermostatClimate::set_default_preset(const std::string &custom_preset) {
|
void ThermostatClimate::set_default_preset(const char *custom_preset) {
|
||||||
this->default_custom_preset_ = 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; }
|
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()) {
|
if (!this->preset_config_.empty()) {
|
||||||
ESP_LOGCONFIG(TAG, " Supported PRESETS:");
|
ESP_LOGCONFIG(TAG, " Supported PRESETS:");
|
||||||
for (auto &it : this->preset_config_) {
|
for (const auto &entry : this->preset_config_) {
|
||||||
const auto *preset_name = LOG_STR_ARG(climate::climate_preset_to_string(it.first));
|
const auto *preset_name = LOG_STR_ARG(climate::climate_preset_to_string(entry.preset));
|
||||||
ESP_LOGCONFIG(TAG, " %s:%s", preset_name, it.first == this->default_preset_ ? " (default)" : "");
|
ESP_LOGCONFIG(TAG, " %s:%s", preset_name, entry.preset == this->default_preset_ ? " (default)" : "");
|
||||||
this->dump_preset_config_(preset_name, it.second);
|
this->dump_preset_config_(preset_name, entry.config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this->custom_preset_config_.empty()) {
|
if (!this->custom_preset_config_.empty()) {
|
||||||
ESP_LOGCONFIG(TAG, " Supported CUSTOM PRESETS:");
|
ESP_LOGCONFIG(TAG, " Supported CUSTOM PRESETS:");
|
||||||
for (auto &it : this->custom_preset_config_) {
|
for (const auto &entry : this->custom_preset_config_) {
|
||||||
const auto *preset_name = it.first.c_str();
|
const auto *preset_name = entry.name;
|
||||||
ESP_LOGCONFIG(TAG, " %s:%s", preset_name, it.first == this->default_custom_preset_ ? " (default)" : "");
|
ESP_LOGCONFIG(TAG, " %s:%s", preset_name,
|
||||||
this->dump_preset_config_(preset_name, it.second);
|
(this->default_custom_preset_ != nullptr && strcmp(entry.name, this->default_custom_preset_) == 0)
|
||||||
|
? " (default)"
|
||||||
|
: "");
|
||||||
|
this->dump_preset_config_(preset_name, entry.config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,12 @@
|
|||||||
#include "esphome/core/automation.h"
|
#include "esphome/core/automation.h"
|
||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
#include "esphome/core/hal.h"
|
#include "esphome/core/hal.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/components/climate/climate.h"
|
#include "esphome/components/climate/climate.h"
|
||||||
#include "esphome/components/sensor/sensor.h"
|
#include "esphome/components/sensor/sensor.h"
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <cinttypes>
|
#include <cinttypes>
|
||||||
#include <map>
|
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace thermostat {
|
namespace thermostat {
|
||||||
@@ -72,14 +72,29 @@ struct ThermostatClimateTargetTempConfig {
|
|||||||
optional<climate::ClimateMode> mode_{};
|
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 {
|
class ThermostatClimate : public climate::Climate, public Component {
|
||||||
public:
|
public:
|
||||||
|
using PresetEntry = ThermostatPresetEntry;
|
||||||
|
using CustomPresetEntry = ThermostatCustomPresetEntry;
|
||||||
|
|
||||||
ThermostatClimate();
|
ThermostatClimate();
|
||||||
void setup() override;
|
void setup() override;
|
||||||
void dump_config() override;
|
void dump_config() override;
|
||||||
void loop() 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_default_preset(climate::ClimatePreset preset);
|
||||||
void set_on_boot_restore_from(OnBootRestoreFrom on_boot_restore_from);
|
void set_on_boot_restore_from(OnBootRestoreFrom on_boot_restore_from);
|
||||||
void set_set_point_minimum_differential(float differential);
|
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_humidification(bool supports_humidification);
|
||||||
void set_supports_two_points(bool supports_two_points);
|
void set_supports_two_points(bool supports_two_points);
|
||||||
|
|
||||||
void set_preset_config(climate::ClimatePreset preset, const ThermostatClimateTargetTempConfig &config);
|
void set_preset_config(std::initializer_list<PresetEntry> presets);
|
||||||
void set_custom_preset_config(const std::string &name, const ThermostatClimateTargetTempConfig &config);
|
void set_custom_preset_config(std::initializer_list<CustomPresetEntry> presets);
|
||||||
|
|
||||||
Trigger<> *get_cool_action_trigger() const;
|
Trigger<> *get_cool_action_trigger() const;
|
||||||
Trigger<> *get_supplemental_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_swing_mode_trigger_{nullptr};
|
||||||
Trigger<> *prev_humidity_control_trigger_{nullptr};
|
Trigger<> *prev_humidity_control_trigger_{nullptr};
|
||||||
|
|
||||||
/// Default custom preset to use on start up
|
|
||||||
std::string default_custom_preset_{};
|
|
||||||
|
|
||||||
/// Climate action timers
|
/// Climate action timers
|
||||||
std::array<ThermostatClimateTimer, THERMOSTAT_TIMER_COUNT> timer_{
|
std::array<ThermostatClimateTimer, THERMOSTAT_TIMER_COUNT> timer_{
|
||||||
ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::cooling_max_run_time_timer_callback_, this)),
|
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)
|
/// 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")
|
/// 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
|
} // namespace thermostat
|
||||||
|
|||||||
@@ -23,6 +23,13 @@ namespace time {
|
|||||||
static const char *const TAG = "time";
|
static const char *const TAG = "time";
|
||||||
|
|
||||||
RealTimeClock::RealTimeClock() = default;
|
RealTimeClock::RealTimeClock() = default;
|
||||||
|
|
||||||
|
void RealTimeClock::dump_config() {
|
||||||
|
#ifdef USE_TIME_TIMEZONE
|
||||||
|
ESP_LOGCONFIG(TAG, "Timezone: '%s'", this->timezone_.c_str());
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
void RealTimeClock::synchronize_epoch_(uint32_t epoch) {
|
void RealTimeClock::synchronize_epoch_(uint32_t epoch) {
|
||||||
ESP_LOGVV(TAG, "Got epoch %" PRIu32, epoch);
|
ESP_LOGVV(TAG, "Got epoch %" PRIu32, epoch);
|
||||||
// Update UTC epoch time.
|
// Update UTC epoch time.
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ class RealTimeClock : public PollingComponent {
|
|||||||
this->time_sync_callback_.add(std::move(callback));
|
this->time_sync_callback_.add(std::move(callback));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
void dump_config() override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
/// Report a unix epoch as current time.
|
/// Report a unix epoch as current time.
|
||||||
void synchronize_epoch_(uint32_t epoch);
|
void synchronize_epoch_(uint32_t epoch);
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
from esphome import automation
|
from esphome import automation
|
||||||
from esphome.automation import Condition
|
from esphome.automation import Condition
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components.const import CONF_USE_PSRAM
|
from esphome.components.const import CONF_USE_PSRAM
|
||||||
from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant
|
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
|
from esphome.config_helpers import filter_source_files_from_platform
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.config_validation import only_with_esp_idf
|
from esphome.config_validation import only_with_esp_idf
|
||||||
@@ -42,6 +48,7 @@ from esphome.const import (
|
|||||||
CONF_TTLS_PHASE_2,
|
CONF_TTLS_PHASE_2,
|
||||||
CONF_USE_ADDRESS,
|
CONF_USE_ADDRESS,
|
||||||
CONF_USERNAME,
|
CONF_USERNAME,
|
||||||
|
Platform,
|
||||||
PlatformFramework,
|
PlatformFramework,
|
||||||
)
|
)
|
||||||
from esphome.core import CORE, CoroPriority, HexInt, coroutine_with_priority
|
from esphome.core import CORE, CoroPriority, HexInt, coroutine_with_priority
|
||||||
@@ -49,10 +56,15 @@ import esphome.final_validate as fv
|
|||||||
|
|
||||||
from . import wpa2_eap
|
from . import wpa2_eap
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
AUTO_LOAD = ["network"]
|
AUTO_LOAD = ["network"]
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
NO_WIFI_VARIANTS = [const.VARIANT_ESP32H2, const.VARIANT_ESP32P4]
|
NO_WIFI_VARIANTS = [const.VARIANT_ESP32H2, const.VARIANT_ESP32P4]
|
||||||
CONF_SAVE = "save"
|
CONF_SAVE = "save"
|
||||||
|
CONF_MIN_AUTH_MODE = "min_auth_mode"
|
||||||
|
|
||||||
# Maximum number of WiFi networks that can be configured
|
# Maximum number of WiFi networks that can be configured
|
||||||
# Limited to 127 because selected_sta_index_ is int8_t in C++
|
# Limited to 127 because selected_sta_index_ is int8_t in C++
|
||||||
@@ -70,6 +82,14 @@ WIFI_POWER_SAVE_MODES = {
|
|||||||
"LIGHT": WiFiPowerSaveMode.WIFI_POWER_SAVE_LIGHT,
|
"LIGHT": WiFiPowerSaveMode.WIFI_POWER_SAVE_LIGHT,
|
||||||
"HIGH": WiFiPowerSaveMode.WIFI_POWER_SAVE_HIGH,
|
"HIGH": WiFiPowerSaveMode.WIFI_POWER_SAVE_HIGH,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WifiMinAuthMode = wifi_ns.enum("WifiMinAuthMode")
|
||||||
|
WIFI_MIN_AUTH_MODES = {
|
||||||
|
"WPA": WifiMinAuthMode.WIFI_MIN_AUTH_MODE_WPA,
|
||||||
|
"WPA2": WifiMinAuthMode.WIFI_MIN_AUTH_MODE_WPA2,
|
||||||
|
"WPA3": WifiMinAuthMode.WIFI_MIN_AUTH_MODE_WPA3,
|
||||||
|
}
|
||||||
|
VALIDATE_WIFI_MIN_AUTH_MODE = cv.enum(WIFI_MIN_AUTH_MODES, upper=True)
|
||||||
WiFiConnectedCondition = wifi_ns.class_("WiFiConnectedCondition", Condition)
|
WiFiConnectedCondition = wifi_ns.class_("WiFiConnectedCondition", Condition)
|
||||||
WiFiEnabledCondition = wifi_ns.class_("WiFiEnabledCondition", Condition)
|
WiFiEnabledCondition = wifi_ns.class_("WiFiEnabledCondition", Condition)
|
||||||
WiFiEnableAction = wifi_ns.class_("WiFiEnableAction", automation.Action)
|
WiFiEnableAction = wifi_ns.class_("WiFiEnableAction", automation.Action)
|
||||||
@@ -174,7 +194,7 @@ WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend(
|
|||||||
{
|
{
|
||||||
cv.Optional(CONF_BSSID): cv.mac_address,
|
cv.Optional(CONF_BSSID): cv.mac_address,
|
||||||
cv.Optional(CONF_HIDDEN): cv.boolean,
|
cv.Optional(CONF_HIDDEN): cv.boolean,
|
||||||
cv.Optional(CONF_PRIORITY, default=0.0): cv.float_,
|
cv.Optional(CONF_PRIORITY, default=0): cv.int_range(min=-128, max=127),
|
||||||
cv.Optional(CONF_EAP): EAP_AUTH_SCHEMA,
|
cv.Optional(CONF_EAP): EAP_AUTH_SCHEMA,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -187,6 +207,27 @@ def validate_variant(_):
|
|||||||
raise cv.Invalid(f"WiFi requires component esp32_hosted on {variant}")
|
raise cv.Invalid(f"WiFi requires component esp32_hosted on {variant}")
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_min_auth_mode_default(config):
|
||||||
|
"""Apply platform-specific default for min_auth_mode and warn ESP8266 users."""
|
||||||
|
# Only apply defaults for platforms that support min_auth_mode
|
||||||
|
if CONF_MIN_AUTH_MODE not in config and (CORE.is_esp8266 or CORE.is_esp32):
|
||||||
|
if CORE.is_esp8266:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"The minimum WiFi authentication mode (wifi -> min_auth_mode) is not set. "
|
||||||
|
"This controls the weakest encryption your device will accept when connecting to WiFi. "
|
||||||
|
"Currently defaults to WPA (less secure), but will change to WPA2 (more secure) in 2026.6.0. "
|
||||||
|
"WPA uses TKIP encryption which has known security vulnerabilities and should be avoided. "
|
||||||
|
"WPA2 uses AES encryption which is significantly more secure. "
|
||||||
|
"To silence this warning, explicitly set min_auth_mode under 'wifi:'. "
|
||||||
|
"If your router supports WPA2 or WPA3, set 'min_auth_mode: WPA2'. "
|
||||||
|
"If your router only supports WPA, set 'min_auth_mode: WPA'."
|
||||||
|
)
|
||||||
|
config[CONF_MIN_AUTH_MODE] = VALIDATE_WIFI_MIN_AUTH_MODE("WPA")
|
||||||
|
elif CORE.is_esp32:
|
||||||
|
config[CONF_MIN_AUTH_MODE] = VALIDATE_WIFI_MIN_AUTH_MODE("WPA2")
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
def final_validate(config):
|
def final_validate(config):
|
||||||
has_sta = bool(config.get(CONF_NETWORKS, True))
|
has_sta = bool(config.get(CONF_NETWORKS, True))
|
||||||
has_ap = CONF_AP in config
|
has_ap = CONF_AP in config
|
||||||
@@ -287,6 +328,10 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
): cv.enum(WIFI_POWER_SAVE_MODES, upper=True),
|
): cv.enum(WIFI_POWER_SAVE_MODES, upper=True),
|
||||||
cv.Optional(CONF_FAST_CONNECT, default=False): cv.boolean,
|
cv.Optional(CONF_FAST_CONNECT, default=False): cv.boolean,
|
||||||
cv.Optional(CONF_USE_ADDRESS): cv.string_strict,
|
cv.Optional(CONF_USE_ADDRESS): cv.string_strict,
|
||||||
|
cv.Optional(CONF_MIN_AUTH_MODE): cv.All(
|
||||||
|
VALIDATE_WIFI_MIN_AUTH_MODE,
|
||||||
|
cv.only_on([Platform.ESP32, Platform.ESP8266]),
|
||||||
|
),
|
||||||
cv.SplitDefault(CONF_OUTPUT_POWER, esp8266=20.0): cv.All(
|
cv.SplitDefault(CONF_OUTPUT_POWER, esp8266=20.0): cv.All(
|
||||||
cv.decibel, cv.float_range(min=8.5, max=20.5)
|
cv.decibel, cv.float_range(min=8.5, max=20.5)
|
||||||
),
|
),
|
||||||
@@ -311,6 +356,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
_apply_min_auth_mode_default,
|
||||||
_validate,
|
_validate,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -385,6 +431,8 @@ async def to_code(config):
|
|||||||
|
|
||||||
# Track if any network uses Enterprise authentication
|
# Track if any network uses Enterprise authentication
|
||||||
has_eap = False
|
has_eap = False
|
||||||
|
# Track if any network uses manual IP
|
||||||
|
has_manual_ip = False
|
||||||
|
|
||||||
# Initialize FixedVector with the count of networks
|
# Initialize FixedVector with the count of networks
|
||||||
networks = config.get(CONF_NETWORKS, [])
|
networks = config.get(CONF_NETWORKS, [])
|
||||||
@@ -398,11 +446,15 @@ async def to_code(config):
|
|||||||
for network in networks:
|
for network in networks:
|
||||||
if CONF_EAP in network:
|
if CONF_EAP in network:
|
||||||
has_eap = True
|
has_eap = True
|
||||||
|
if network.get(CONF_MANUAL_IP) or config.get(CONF_MANUAL_IP):
|
||||||
|
has_manual_ip = True
|
||||||
cg.with_local_variable(network[CONF_ID], WiFiAP(), add_sta, network)
|
cg.with_local_variable(network[CONF_ID], WiFiAP(), add_sta, network)
|
||||||
|
|
||||||
if CONF_AP in config:
|
if CONF_AP in config:
|
||||||
conf = config[CONF_AP]
|
conf = config[CONF_AP]
|
||||||
ip_config = conf.get(CONF_MANUAL_IP)
|
ip_config = conf.get(CONF_MANUAL_IP)
|
||||||
|
if ip_config:
|
||||||
|
has_manual_ip = True
|
||||||
cg.with_local_variable(
|
cg.with_local_variable(
|
||||||
conf[CONF_ID],
|
conf[CONF_ID],
|
||||||
WiFiAP(),
|
WiFiAP(),
|
||||||
@@ -418,8 +470,14 @@ async def to_code(config):
|
|||||||
if CORE.is_esp32:
|
if CORE.is_esp32:
|
||||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT", has_eap)
|
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT", has_eap)
|
||||||
|
|
||||||
|
# Only define USE_WIFI_MANUAL_IP if any AP uses manual IP
|
||||||
|
if has_manual_ip:
|
||||||
|
cg.add_define("USE_WIFI_MANUAL_IP")
|
||||||
|
|
||||||
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
|
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
|
||||||
cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE]))
|
cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE]))
|
||||||
|
if CONF_MIN_AUTH_MODE in config:
|
||||||
|
cg.add(var.set_min_auth_mode(config[CONF_MIN_AUTH_MODE]))
|
||||||
if config[CONF_FAST_CONNECT]:
|
if config[CONF_FAST_CONNECT]:
|
||||||
cg.add_define("USE_WIFI_FAST_CONNECT")
|
cg.add_define("USE_WIFI_FAST_CONNECT")
|
||||||
cg.add(var.set_passive_scan(config[CONF_PASSIVE_SCAN]))
|
cg.add(var.set_passive_scan(config[CONF_PASSIVE_SCAN]))
|
||||||
@@ -444,6 +502,56 @@ async def to_code(config):
|
|||||||
|
|
||||||
if config.get(CONF_USE_PSRAM):
|
if config.get(CONF_USE_PSRAM):
|
||||||
add_idf_sdkconfig_option("CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP", True)
|
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")
|
cg.add_define("USE_WIFI")
|
||||||
|
|
||||||
# must register before OTA safe mode check
|
# must register before OTA safe mode check
|
||||||
|
|||||||
@@ -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
|
// 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;
|
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) {
|
static constexpr uint8_t get_max_retries_for_phase(WiFiRetryPhase phase) {
|
||||||
switch (phase) {
|
switch (phase) {
|
||||||
case WiFiRetryPhase::INITIAL_CONNECT:
|
case WiFiRetryPhase::INITIAL_CONNECT:
|
||||||
@@ -253,17 +257,19 @@ bool WiFiComponent::ssid_was_seen_in_scan_(const std::string &ssid) const {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int8_t WiFiComponent::find_next_hidden_sta_(int8_t start_index, bool include_explicit_hidden) {
|
int8_t WiFiComponent::find_next_hidden_sta_(int8_t start_index) {
|
||||||
// Find next SSID that wasn't in scan results (might be hidden)
|
// Find next SSID that wasn't in scan results (might be hidden)
|
||||||
|
bool include_explicit_hidden = !this->went_through_explicit_hidden_phase_();
|
||||||
// Start searching from start_index + 1
|
// Start searching from start_index + 1
|
||||||
for (size_t i = start_index + 1; i < this->sta_.size(); i++) {
|
for (size_t i = start_index + 1; i < this->sta_.size(); i++) {
|
||||||
const auto &sta = this->sta_[i];
|
const auto &sta = this->sta_[i];
|
||||||
|
|
||||||
// Skip networks that were already tried in EXPLICIT_HIDDEN phase
|
// Skip networks that were already tried in EXPLICIT_HIDDEN phase
|
||||||
// Those are: networks marked hidden:true that appear before the first non-hidden network
|
// Those are: networks marked hidden:true that appear before the first non-hidden network
|
||||||
|
// If all networks are hidden (first_non_hidden_idx == -1), skip all of them
|
||||||
if (!include_explicit_hidden && sta.get_hidden()) {
|
if (!include_explicit_hidden && sta.get_hidden()) {
|
||||||
int8_t first_non_hidden_idx = this->find_first_non_hidden_index_();
|
int8_t first_non_hidden_idx = this->find_first_non_hidden_index_();
|
||||||
if (first_non_hidden_idx >= 0 && static_cast<int8_t>(i) < first_non_hidden_idx) {
|
if (first_non_hidden_idx < 0 || static_cast<int8_t>(i) < first_non_hidden_idx) {
|
||||||
ESP_LOGD(TAG, "Skipping " LOG_SECRET("'%s'") " (explicit hidden, already tried)", sta.get_ssid().c_str());
|
ESP_LOGD(TAG, "Skipping " LOG_SECRET("'%s'") " (explicit hidden, already tried)", sta.get_ssid().c_str());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -273,7 +279,7 @@ int8_t WiFiComponent::find_next_hidden_sta_(int8_t start_index, bool include_exp
|
|||||||
ESP_LOGD(TAG, "Hidden candidate " LOG_SECRET("'%s'") " at index %d", sta.get_ssid().c_str(), static_cast<int>(i));
|
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);
|
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
|
// No hidden SSIDs found
|
||||||
return -1;
|
return -1;
|
||||||
@@ -287,7 +293,7 @@ void WiFiComponent::start_initial_connection_() {
|
|||||||
this->selected_sta_index_ = 0;
|
this->selected_sta_index_ = 0;
|
||||||
this->retry_phase_ = WiFiRetryPhase::EXPLICIT_HIDDEN;
|
this->retry_phase_ = WiFiRetryPhase::EXPLICIT_HIDDEN;
|
||||||
WiFiAP params = this->build_params_for_current_phase_();
|
WiFiAP params = this->build_params_for_current_phase_();
|
||||||
this->start_connecting(params, false);
|
this->start_connecting(params);
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGI(TAG, "Starting scan");
|
ESP_LOGI(TAG, "Starting scan");
|
||||||
this->start_scanning();
|
this->start_scanning();
|
||||||
@@ -369,13 +375,13 @@ void WiFiComponent::start() {
|
|||||||
// Without saved data, try first configured network or use normal flow
|
// Without saved data, try first configured network or use normal flow
|
||||||
if (loaded_fast_connect) {
|
if (loaded_fast_connect) {
|
||||||
ESP_LOGI(TAG, "Starting fast_connect (saved) " LOG_SECRET("'%s'"), params.get_ssid().c_str());
|
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()) {
|
} else if (!this->sta_.empty() && !this->sta_[0].get_hidden()) {
|
||||||
// No saved data, but have configured networks - try first non-hidden network
|
// 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());
|
ESP_LOGI(TAG, "Starting fast_connect (config) " LOG_SECRET("'%s'"), this->sta_[0].get_ssid().c_str());
|
||||||
this->selected_sta_index_ = 0;
|
this->selected_sta_index_ = 0;
|
||||||
params = this->build_params_for_current_phase_();
|
params = this->build_params_for_current_phase_();
|
||||||
this->start_connecting(params, false);
|
this->start_connecting(params);
|
||||||
} else {
|
} else {
|
||||||
// No saved data and (no networks OR first is hidden) - use normal flow
|
// No saved data and (no networks OR first is hidden) - use normal flow
|
||||||
this->start_initial_connection_();
|
this->start_initial_connection_();
|
||||||
@@ -411,8 +417,11 @@ void WiFiComponent::start() {
|
|||||||
void WiFiComponent::restart_adapter() {
|
void WiFiComponent::restart_adapter() {
|
||||||
ESP_LOGW(TAG, "Restarting adapter");
|
ESP_LOGW(TAG, "Restarting adapter");
|
||||||
this->wifi_mode_(false, {});
|
this->wifi_mode_(false, {});
|
||||||
delay(100); // NOLINT
|
// Enter cooldown state to allow WiFi hardware to stabilize after restart
|
||||||
// Don't set retry_phase_ or num_retried_ here - state machine handles transitions
|
// 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() {
|
void WiFiComponent::loop() {
|
||||||
@@ -432,20 +441,12 @@ void WiFiComponent::loop() {
|
|||||||
switch (this->state_) {
|
switch (this->state_) {
|
||||||
case WIFI_COMPONENT_STATE_COOLDOWN: {
|
case WIFI_COMPONENT_STATE_COOLDOWN: {
|
||||||
this->status_set_warning(LOG_STR("waiting to reconnect"));
|
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, connect based on current retry phase
|
// After cooldown we either restarted the adapter because of
|
||||||
this->reset_selected_ap_to_first_if_invalid_();
|
// a failure, or something tried to connect over and over
|
||||||
|
// so we entered cooldown. In both cases we call
|
||||||
// Check if we need to trigger a scan first
|
// check_connecting_finished to continue the state machine.
|
||||||
if (this->needs_scan_results_() && !this->all_networks_hidden_()) {
|
this->check_connecting_finished();
|
||||||
// Need scan results or no matching networks found - scan/rescan
|
|
||||||
ESP_LOGD(TAG, "Scanning required for phase %s", LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_)));
|
|
||||||
this->start_scanning();
|
|
||||||
} else {
|
|
||||||
// Have everything we need to connect (or all networks are hidden, skip scanning)
|
|
||||||
WiFiAP params = this->build_params_for_current_phase_();
|
|
||||||
this->start_connecting(params, false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -454,8 +455,7 @@ void WiFiComponent::loop() {
|
|||||||
this->check_scanning_finished();
|
this->check_scanning_finished();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case WIFI_COMPONENT_STATE_STA_CONNECTING:
|
case WIFI_COMPONENT_STATE_STA_CONNECTING: {
|
||||||
case WIFI_COMPONENT_STATE_STA_CONNECTING_2: {
|
|
||||||
this->status_set_warning(LOG_STR("associating to network"));
|
this->status_set_warning(LOG_STR("associating to network"));
|
||||||
this->check_connecting_finished();
|
this->check_connecting_finished();
|
||||||
break;
|
break;
|
||||||
@@ -465,6 +465,8 @@ void WiFiComponent::loop() {
|
|||||||
if (!this->is_connected()) {
|
if (!this->is_connected()) {
|
||||||
ESP_LOGW(TAG, "Connection lost; reconnecting");
|
ESP_LOGW(TAG, "Connection lost; reconnecting");
|
||||||
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING;
|
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();
|
this->retry_connect();
|
||||||
} else {
|
} else {
|
||||||
this->status_clear_warning();
|
this->status_clear_warning();
|
||||||
@@ -569,6 +571,7 @@ void WiFiComponent::setup_ap_config_() {
|
|||||||
" IP Address: %s",
|
" IP Address: %s",
|
||||||
this->ap_.get_ssid().c_str(), this->ap_.get_password().c_str(), ip_address.c_str());
|
this->ap_.get_ssid().c_str(), this->ap_.get_password().c_str(), ip_address.c_str());
|
||||||
|
|
||||||
|
#ifdef USE_WIFI_MANUAL_IP
|
||||||
auto manual_ip = this->ap_.get_manual_ip();
|
auto manual_ip = this->ap_.get_manual_ip();
|
||||||
if (manual_ip.has_value()) {
|
if (manual_ip.has_value()) {
|
||||||
ESP_LOGCONFIG(TAG,
|
ESP_LOGCONFIG(TAG,
|
||||||
@@ -578,6 +581,7 @@ void WiFiComponent::setup_ap_config_() {
|
|||||||
manual_ip->static_ip.str().c_str(), manual_ip->gateway.str().c_str(),
|
manual_ip->static_ip.str().c_str(), manual_ip->gateway.str().c_str(),
|
||||||
manual_ip->subnet.str().c_str());
|
manual_ip->subnet.str().c_str());
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
if (!this->has_sta()) {
|
if (!this->has_sta()) {
|
||||||
this->state_ = WIFI_COMPONENT_STATE_AP;
|
this->state_ = WIFI_COMPONENT_STATE_AP;
|
||||||
@@ -664,27 +668,27 @@ void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &pa
|
|||||||
this->set_sta(sta);
|
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
|
// Log connection attempt at INFO level with priority
|
||||||
std::string bssid_formatted;
|
char bssid_s[18];
|
||||||
float priority = 0.0f;
|
int8_t priority = 0;
|
||||||
|
|
||||||
if (ap.get_bssid().has_value()) {
|
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());
|
priority = this->get_sta_priority(ap.get_bssid().value());
|
||||||
}
|
}
|
||||||
|
|
||||||
ESP_LOGI(TAG,
|
ESP_LOGI(TAG,
|
||||||
"Connecting to " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") " (priority %.1f, attempt %u/%u in phase %s)...",
|
"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"),
|
ap.get_ssid().c_str(), ap.get_bssid().has_value() ? bssid_s : LOG_STR_LITERAL("any"), priority,
|
||||||
priority, this->num_retried_ + 1, get_max_retries_for_phase(this->retry_phase_),
|
this->num_retried_ + 1, get_max_retries_for_phase(this->retry_phase_),
|
||||||
LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_)));
|
LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_)));
|
||||||
|
|
||||||
#ifdef ESPHOME_LOG_HAS_VERBOSE
|
#ifdef ESPHOME_LOG_HAS_VERBOSE
|
||||||
ESP_LOGV(TAG, "Connection Params:");
|
ESP_LOGV(TAG, "Connection Params:");
|
||||||
ESP_LOGV(TAG, " SSID: '%s'", ap.get_ssid().c_str());
|
ESP_LOGV(TAG, " SSID: '%s'", ap.get_ssid().c_str());
|
||||||
if (ap.get_bssid().has_value()) {
|
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 {
|
} else {
|
||||||
ESP_LOGV(TAG, " BSSID: Not Set");
|
ESP_LOGV(TAG, " BSSID: Not Set");
|
||||||
}
|
}
|
||||||
@@ -716,11 +720,14 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) {
|
|||||||
} else {
|
} else {
|
||||||
ESP_LOGV(TAG, " Channel not set");
|
ESP_LOGV(TAG, " Channel not set");
|
||||||
}
|
}
|
||||||
|
#ifdef USE_WIFI_MANUAL_IP
|
||||||
if (ap.get_manual_ip().has_value()) {
|
if (ap.get_manual_ip().has_value()) {
|
||||||
ManualIP m = *ap.get_manual_ip();
|
ManualIP m = *ap.get_manual_ip();
|
||||||
ESP_LOGV(TAG, " Manual IP: Static IP=%s Gateway=%s Subnet=%s DNS1=%s DNS2=%s", m.static_ip.str().c_str(),
|
ESP_LOGV(TAG, " Manual IP: Static IP=%s Gateway=%s Subnet=%s DNS1=%s DNS2=%s", m.static_ip.str().c_str(),
|
||||||
m.gateway.str().c_str(), m.subnet.str().c_str(), m.dns1.str().c_str(), m.dns2.str().c_str());
|
m.gateway.str().c_str(), m.subnet.str().c_str(), m.dns1.str().c_str(), m.dns2.str().c_str());
|
||||||
} else {
|
} else
|
||||||
|
#endif
|
||||||
|
{
|
||||||
ESP_LOGV(TAG, " Using DHCP IP");
|
ESP_LOGV(TAG, " Using DHCP IP");
|
||||||
}
|
}
|
||||||
ESP_LOGV(TAG, " Hidden: %s", YESNO(ap.get_hidden()));
|
ESP_LOGV(TAG, " Hidden: %s", YESNO(ap.get_hidden()));
|
||||||
@@ -728,19 +735,24 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) {
|
|||||||
|
|
||||||
if (!this->wifi_sta_connect_(ap)) {
|
if (!this->wifi_sta_connect_(ap)) {
|
||||||
ESP_LOGE(TAG, "wifi_sta_connect_ failed");
|
ESP_LOGE(TAG, "wifi_sta_connect_ failed");
|
||||||
this->retry_connect();
|
// Enter cooldown to allow WiFi hardware to stabilize
|
||||||
return;
|
// (immediate failure suggests hardware not ready, different from connection timeout)
|
||||||
}
|
this->state_ = WIFI_COMPONENT_STATE_COOLDOWN;
|
||||||
|
|
||||||
if (!two) {
|
|
||||||
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING;
|
|
||||||
} else {
|
} else {
|
||||||
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING_2;
|
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING;
|
||||||
}
|
}
|
||||||
this->action_started_ = millis();
|
this->action_started_ = millis();
|
||||||
}
|
}
|
||||||
|
|
||||||
const LogString *get_signal_bars(int8_t rssi) {
|
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
|
// LOWER ONE QUARTER BLOCK
|
||||||
// Unicode: U+2582, UTF-8: E2 96 82
|
// Unicode: U+2582, UTF-8: E2 96 82
|
||||||
// LOWER HALF BLOCK
|
// LOWER HALF BLOCK
|
||||||
@@ -785,6 +797,8 @@ const LogString *get_signal_bars(int8_t rssi) {
|
|||||||
|
|
||||||
void WiFiComponent::print_connect_params_() {
|
void WiFiComponent::print_connect_params_() {
|
||||||
bssid_t bssid = wifi_bssid();
|
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());
|
ESP_LOGCONFIG(TAG, " Local MAC: %s", get_mac_address_pretty().c_str());
|
||||||
if (this->is_disabled()) {
|
if (this->is_disabled()) {
|
||||||
@@ -807,12 +821,12 @@ void WiFiComponent::print_connect_params_() {
|
|||||||
" Gateway: %s\n"
|
" Gateway: %s\n"
|
||||||
" DNS1: %s\n"
|
" DNS1: %s\n"
|
||||||
" DNS2: %s",
|
" DNS2: %s",
|
||||||
wifi_ssid().c_str(), format_mac_address_pretty(bssid.data()).c_str(), App.get_name().c_str(), rssi,
|
wifi_ssid().c_str(), bssid_s, App.get_name().c_str(), rssi, LOG_STR_ARG(get_signal_bars(rssi)),
|
||||||
LOG_STR_ARG(get_signal_bars(rssi)), get_wifi_channel(), wifi_subnet_mask_().str().c_str(),
|
get_wifi_channel(), wifi_subnet_mask_().str().c_str(), wifi_gateway_ip_().str().c_str(),
|
||||||
wifi_gateway_ip_().str().c_str(), wifi_dns_ip_(0).str().c_str(), wifi_dns_ip_(1).str().c_str());
|
wifi_dns_ip_(0).str().c_str(), wifi_dns_ip_(1).str().c_str());
|
||||||
#ifdef ESPHOME_LOG_HAS_VERBOSE
|
#ifdef ESPHOME_LOG_HAS_VERBOSE
|
||||||
if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_bssid().has_value()) {
|
if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_bssid().has_value()) {
|
||||||
ESP_LOGV(TAG, " Priority: %.1f", this->get_sta_priority(*config->get_bssid()));
|
ESP_LOGV(TAG, " Priority: %d", this->get_sta_priority(*config->get_bssid()));
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_WIFI_11KV_SUPPORT
|
#ifdef USE_WIFI_11KV_SUPPORT
|
||||||
@@ -933,8 +947,7 @@ __attribute__((noinline)) static void log_scan_result(const WiFiScanResult &res)
|
|||||||
ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(),
|
ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(),
|
||||||
res.get_is_hidden() ? LOG_STR_LITERAL("(HIDDEN) ") : LOG_STR_LITERAL(""), bssid_s,
|
res.get_is_hidden() ? LOG_STR_LITERAL("(HIDDEN) ") : LOG_STR_LITERAL(""), bssid_s,
|
||||||
LOG_STR_ARG(get_signal_bars(res.get_rssi())));
|
LOG_STR_ARG(get_signal_bars(res.get_rssi())));
|
||||||
ESP_LOGD(TAG, " Channel: %2u, RSSI: %3d dB, Priority: %4.1f", res.get_channel(), res.get_rssi(),
|
ESP_LOGD(TAG, " Channel: %2u, RSSI: %3d dB, Priority: %4d", res.get_channel(), res.get_rssi(), res.get_priority());
|
||||||
res.get_priority());
|
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGD(TAG, "- " LOG_SECRET("'%s'") " " LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), bssid_s,
|
ESP_LOGD(TAG, "- " LOG_SECRET("'%s'") " " LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), bssid_s,
|
||||||
LOG_STR_ARG(get_signal_bars(res.get_rssi())));
|
LOG_STR_ARG(get_signal_bars(res.get_rssi())));
|
||||||
@@ -1003,6 +1016,10 @@ void WiFiComponent::check_scanning_finished() {
|
|||||||
// No scan results matched our configured networks - transition directly to hidden mode
|
// No scan results matched our configured networks - transition directly to hidden mode
|
||||||
// Don't call retry_connect() since we never attempted a connection (no BSSID to penalize)
|
// Don't call retry_connect() since we never attempted a connection (no BSSID to penalize)
|
||||||
this->transition_to_phase_(WiFiRetryPhase::RETRY_HIDDEN);
|
this->transition_to_phase_(WiFiRetryPhase::RETRY_HIDDEN);
|
||||||
|
// If no hidden networks to try, skip connection attempt (will be handled on next loop)
|
||||||
|
if (this->selected_sta_index_ == -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Now start connection attempt in hidden mode
|
// Now start connection attempt in hidden mode
|
||||||
} else if (this->transition_to_phase_(WiFiRetryPhase::SCAN_CONNECTING)) {
|
} else if (this->transition_to_phase_(WiFiRetryPhase::SCAN_CONNECTING)) {
|
||||||
return; // scan started, wait for next loop iteration
|
return; // scan started, wait for next loop iteration
|
||||||
@@ -1013,11 +1030,14 @@ void WiFiComponent::check_scanning_finished() {
|
|||||||
WiFiAP params = this->build_params_for_current_phase_();
|
WiFiAP params = this->build_params_for_current_phase_();
|
||||||
// Ensure we're in SCAN_CONNECTING phase when connecting with scan results
|
// 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)
|
// (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() {
|
void WiFiComponent::dump_config() {
|
||||||
ESP_LOGCONFIG(TAG, "WiFi:");
|
ESP_LOGCONFIG(TAG,
|
||||||
|
"WiFi:\n"
|
||||||
|
" Connected: %s",
|
||||||
|
YESNO(this->is_connected()));
|
||||||
this->print_connect_params_();
|
this->print_connect_params_();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1042,6 +1062,10 @@ void WiFiComponent::check_connecting_finished() {
|
|||||||
// Reset to initial phase on successful connection (don't log transition, just reset state)
|
// Reset to initial phase on successful connection (don't log transition, just reset state)
|
||||||
this->retry_phase_ = WiFiRetryPhase::INITIAL_CONNECT;
|
this->retry_phase_ = WiFiRetryPhase::INITIAL_CONNECT;
|
||||||
this->num_retried_ = 0;
|
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_();
|
this->print_connect_params_();
|
||||||
|
|
||||||
@@ -1063,6 +1087,9 @@ void WiFiComponent::check_connecting_finished() {
|
|||||||
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTED;
|
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTED;
|
||||||
this->num_retried_ = 0;
|
this->num_retried_ = 0;
|
||||||
|
|
||||||
|
// Clear priority tracking if all priorities are at minimum
|
||||||
|
this->clear_priorities_if_all_min_();
|
||||||
|
|
||||||
#ifdef USE_WIFI_FAST_CONNECT
|
#ifdef USE_WIFI_FAST_CONNECT
|
||||||
this->save_fast_connect_settings_();
|
this->save_fast_connect_settings_();
|
||||||
#endif
|
#endif
|
||||||
@@ -1084,7 +1111,7 @@ void WiFiComponent::check_connecting_finished() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this->error_from_callback_) {
|
if (this->error_from_callback_) {
|
||||||
ESP_LOGW(TAG, "Connecting to network failed");
|
ESP_LOGW(TAG, "Connecting to network failed (callback)");
|
||||||
this->retry_connect();
|
this->retry_connect();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1125,6 +1152,11 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() {
|
|||||||
return WiFiRetryPhase::FAST_CONNECT_CYCLING_APS; // Move to next AP
|
return WiFiRetryPhase::FAST_CONNECT_CYCLING_APS; // Move to next AP
|
||||||
}
|
}
|
||||||
#endif
|
#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
|
// No more APs to try, fall back to scan
|
||||||
return WiFiRetryPhase::SCAN_CONNECTING;
|
return WiFiRetryPhase::SCAN_CONNECTING;
|
||||||
|
|
||||||
@@ -1142,7 +1174,12 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() {
|
|||||||
return WiFiRetryPhase::EXPLICIT_HIDDEN;
|
return WiFiRetryPhase::EXPLICIT_HIDDEN;
|
||||||
}
|
}
|
||||||
|
|
||||||
// No more consecutive explicitly hidden networks - proceed to scanning
|
// No more consecutive explicitly hidden networks
|
||||||
|
// If ALL networks are hidden, skip scanning and go directly to restart
|
||||||
|
if (this->find_first_non_hidden_index_() < 0) {
|
||||||
|
return WiFiRetryPhase::RESTARTING_ADAPTER;
|
||||||
|
}
|
||||||
|
// Otherwise proceed to scanning for non-hidden networks
|
||||||
return WiFiRetryPhase::SCAN_CONNECTING;
|
return WiFiRetryPhase::SCAN_CONNECTING;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1160,14 +1197,12 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() {
|
|||||||
// Its priority has been decreased, so on next scan it will be sorted lower
|
// Its priority has been decreased, so on next scan it will be sorted lower
|
||||||
// and we'll try the next best BSSID.
|
// and we'll try the next best BSSID.
|
||||||
// Check if there are any potentially hidden networks to try
|
// Check if there are any potentially hidden networks to try
|
||||||
if (this->find_next_hidden_sta_(-1, !this->went_through_explicit_hidden_phase_()) >= 0) {
|
if (this->find_next_hidden_sta_(-1) >= 0) {
|
||||||
return WiFiRetryPhase::RETRY_HIDDEN; // Found hidden networks to try
|
return WiFiRetryPhase::RETRY_HIDDEN; // Found hidden networks to try
|
||||||
}
|
}
|
||||||
// No hidden networks - skip directly to restart/rescan
|
// No hidden networks - always go through RESTARTING_ADAPTER phase
|
||||||
if (this->is_captive_portal_active_() || this->is_esp32_improv_active_()) {
|
// This ensures num_retried_ gets reset and a fresh scan is triggered
|
||||||
return this->went_through_explicit_hidden_phase_() ? WiFiRetryPhase::EXPLICIT_HIDDEN
|
// The actual adapter restart will be skipped if captive portal/improv is active
|
||||||
: WiFiRetryPhase::SCAN_CONNECTING;
|
|
||||||
}
|
|
||||||
return WiFiRetryPhase::RESTARTING_ADAPTER;
|
return WiFiRetryPhase::RESTARTING_ADAPTER;
|
||||||
|
|
||||||
case WiFiRetryPhase::RETRY_HIDDEN:
|
case WiFiRetryPhase::RETRY_HIDDEN:
|
||||||
@@ -1179,20 +1214,18 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() {
|
|||||||
|
|
||||||
// Exhausted retries on current SSID - check if there are more potentially hidden SSIDs to try
|
// Exhausted retries on current SSID - check if there are more potentially hidden SSIDs to try
|
||||||
if (this->selected_sta_index_ < static_cast<int8_t>(this->sta_.size()) - 1) {
|
if (this->selected_sta_index_ < static_cast<int8_t>(this->sta_.size()) - 1) {
|
||||||
// More SSIDs available - stay in RETRY_HIDDEN, advance will happen in retry_connect()
|
// Check if find_next_hidden_sta_() would actually find another hidden SSID
|
||||||
return WiFiRetryPhase::RETRY_HIDDEN;
|
// as it might have been seen in the scan results and we want to skip those
|
||||||
|
// otherwise we will get stuck in RETRY_HIDDEN phase
|
||||||
|
if (this->find_next_hidden_sta_(this->selected_sta_index_) != -1) {
|
||||||
|
// More hidden SSIDs available - stay in RETRY_HIDDEN, advance will happen in retry_connect()
|
||||||
|
return WiFiRetryPhase::RETRY_HIDDEN;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Exhausted all potentially hidden SSIDs - rescan to try next BSSID
|
// Exhausted all potentially hidden SSIDs - always go through RESTARTING_ADAPTER
|
||||||
// If captive portal/improv is active, skip adapter restart and go back to start
|
// This ensures num_retried_ gets reset and a fresh scan is triggered
|
||||||
// Otherwise restart adapter to clear any stuck state
|
// The actual adapter restart will be skipped if captive portal/improv is active
|
||||||
if (this->is_captive_portal_active_() || this->is_esp32_improv_active_()) {
|
|
||||||
// Go back to explicit hidden if we went through it initially, otherwise scan
|
|
||||||
return this->went_through_explicit_hidden_phase_() ? WiFiRetryPhase::EXPLICIT_HIDDEN
|
|
||||||
: WiFiRetryPhase::SCAN_CONNECTING;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restart adapter
|
|
||||||
return WiFiRetryPhase::RESTARTING_ADAPTER;
|
return WiFiRetryPhase::RESTARTING_ADAPTER;
|
||||||
|
|
||||||
case WiFiRetryPhase::RESTARTING_ADAPTER:
|
case WiFiRetryPhase::RESTARTING_ADAPTER:
|
||||||
@@ -1212,8 +1245,8 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() {
|
|||||||
/// - Performing phase-specific initialization (e.g., advancing AP index, starting scans)
|
/// - Performing phase-specific initialization (e.g., advancing AP index, starting scans)
|
||||||
///
|
///
|
||||||
/// @param new_phase The phase we're transitioning TO
|
/// @param new_phase The phase we're transitioning TO
|
||||||
/// @return true if an async scan was started (caller should wait for completion)
|
/// @return true if connection attempt should be skipped (scan started or no networks to try)
|
||||||
/// false if no scan started (caller can proceed with connection attempt)
|
/// false if caller can proceed with connection attempt
|
||||||
bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) {
|
bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) {
|
||||||
WiFiRetryPhase old_phase = this->retry_phase_;
|
WiFiRetryPhase old_phase = this->retry_phase_;
|
||||||
|
|
||||||
@@ -1271,7 +1304,7 @@ bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) {
|
|||||||
// If first network is marked hidden, we went through EXPLICIT_HIDDEN phase
|
// If first network is marked hidden, we went through EXPLICIT_HIDDEN phase
|
||||||
// In that case, skip networks marked hidden:true (already tried)
|
// In that case, skip networks marked hidden:true (already tried)
|
||||||
// Otherwise, include them (they haven't been tried yet)
|
// Otherwise, include them (they haven't been tried yet)
|
||||||
this->selected_sta_index_ = this->find_next_hidden_sta_(-1, !this->went_through_explicit_hidden_phase_());
|
this->selected_sta_index_ = this->find_next_hidden_sta_(-1);
|
||||||
|
|
||||||
if (this->selected_sta_index_ == -1) {
|
if (this->selected_sta_index_ == -1) {
|
||||||
ESP_LOGD(TAG, "All SSIDs visible or already tried, skipping hidden mode");
|
ESP_LOGD(TAG, "All SSIDs visible or already tried, skipping hidden mode");
|
||||||
@@ -1280,7 +1313,12 @@ bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case WiFiRetryPhase::RESTARTING_ADAPTER:
|
case WiFiRetryPhase::RESTARTING_ADAPTER:
|
||||||
this->restart_adapter();
|
// Skip actual adapter restart if captive portal/improv is active
|
||||||
|
// This allows state machine to reset num_retried_ and trigger fresh scan
|
||||||
|
// without disrupting the captive portal/improv connection
|
||||||
|
if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_()) {
|
||||||
|
this->restart_adapter();
|
||||||
|
}
|
||||||
// Return true to indicate we should wait (go to COOLDOWN) instead of immediately connecting
|
// Return true to indicate we should wait (go to COOLDOWN) instead of immediately connecting
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
@@ -1291,6 +1329,34 @@ bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) {
|
|||||||
return false; // Did not start scan, can proceed with connection
|
return false; // Did not start scan, can proceed with connection
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Clear BSSID priority tracking if all priorities are at minimum (saves memory)
|
||||||
|
/// At minimum priority, all BSSIDs are equally bad, so priority tracking is useless
|
||||||
|
/// Called after successful connection or after failed connection attempts
|
||||||
|
void WiFiComponent::clear_priorities_if_all_min_() {
|
||||||
|
if (this->sta_priorities_.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int8_t first_priority = this->sta_priorities_[0].priority;
|
||||||
|
|
||||||
|
// Only clear if all priorities have been decremented to the minimum value
|
||||||
|
// At this point, all BSSIDs have been equally penalized and priority info is useless
|
||||||
|
if (first_priority != std::numeric_limits<int8_t>::min()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto &pri : this->sta_priorities_) {
|
||||||
|
if (pri.priority != first_priority) {
|
||||||
|
return; // Not all same, nothing to do
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All priorities are at minimum - clear the vector to save memory and reset
|
||||||
|
ESP_LOGD(TAG, "Clearing BSSID priorities (all at minimum)");
|
||||||
|
this->sta_priorities_.clear();
|
||||||
|
this->sta_priorities_.shrink_to_fit();
|
||||||
|
}
|
||||||
|
|
||||||
/// Log failed connection attempt and decrease BSSID priority to avoid repeated failures
|
/// Log failed connection attempt and decrease BSSID priority to avoid repeated failures
|
||||||
/// This function identifies which BSSID was attempted (from scan results or config),
|
/// This function identifies which BSSID was attempted (from scan results or config),
|
||||||
/// decreases its priority by 1.0 to discourage future attempts, and logs the change.
|
/// decreases its priority by 1.0 to discourage future attempts, and logs the change.
|
||||||
@@ -1304,6 +1370,11 @@ bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) {
|
|||||||
/// - Other phases: Uses BSSID from config if explicitly specified by user or fast_connect
|
/// - Other phases: Uses BSSID from config if explicitly specified by user or fast_connect
|
||||||
///
|
///
|
||||||
/// If no BSSID is available (SSID-only connection), priority adjustment is skipped.
|
/// If no BSSID is available (SSID-only connection), priority adjustment is skipped.
|
||||||
|
///
|
||||||
|
/// IMPORTANT: Priority is only decreased on the LAST attempt for a BSSID in SCAN_CONNECTING phase.
|
||||||
|
/// This prevents false positives from transient WiFi stack state issues after scanning.
|
||||||
|
/// Single failures don't necessarily mean the AP is bad - two genuine failures provide
|
||||||
|
/// higher confidence before degrading priority and skipping the BSSID in future scans.
|
||||||
void WiFiComponent::log_and_adjust_priority_for_failed_connect_() {
|
void WiFiComponent::log_and_adjust_priority_for_failed_connect_() {
|
||||||
// Determine which BSSID we tried to connect to
|
// Determine which BSSID we tried to connect to
|
||||||
optional<bssid_t> failed_bssid;
|
optional<bssid_t> failed_bssid;
|
||||||
@@ -1320,11 +1391,6 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() {
|
|||||||
return; // No BSSID to penalize
|
return; // No BSSID to penalize
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decrease priority to avoid repeatedly trying the same failed BSSID
|
|
||||||
float old_priority = this->get_sta_priority(failed_bssid.value());
|
|
||||||
float new_priority = old_priority - 1.0f;
|
|
||||||
this->set_sta_priority(failed_bssid.value(), new_priority);
|
|
||||||
|
|
||||||
// Get SSID for logging
|
// Get SSID for logging
|
||||||
std::string ssid;
|
std::string ssid;
|
||||||
if (this->retry_phase_ == WiFiRetryPhase::SCAN_CONNECTING && !this->scan_result_.empty()) {
|
if (this->retry_phase_ == WiFiRetryPhase::SCAN_CONNECTING && !this->scan_result_.empty()) {
|
||||||
@@ -1333,8 +1399,29 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() {
|
|||||||
ssid = config->get_ssid();
|
ssid = config->get_ssid();
|
||||||
}
|
}
|
||||||
|
|
||||||
ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %.1f → %.1f", ssid.c_str(),
|
// Only decrease priority on the last attempt for this phase
|
||||||
format_mac_address_pretty(failed_bssid.value().data()).c_str(), old_priority, new_priority);
|
// This prevents false positives from transient WiFi stack issues
|
||||||
|
uint8_t max_retries = get_max_retries_for_phase(this->retry_phase_);
|
||||||
|
bool is_last_attempt = (this->num_retried_ + 1 >= max_retries);
|
||||||
|
|
||||||
|
// Decrease priority only on last attempt to avoid false positives from transient failures
|
||||||
|
int8_t old_priority = this->get_sta_priority(failed_bssid.value());
|
||||||
|
int8_t new_priority = old_priority;
|
||||||
|
|
||||||
|
if (is_last_attempt) {
|
||||||
|
// Decrease priority, but clamp to int8_t::min to prevent overflow
|
||||||
|
new_priority =
|
||||||
|
(old_priority > std::numeric_limits<int8_t>::min()) ? (old_priority - 1) : std::numeric_limits<int8_t>::min();
|
||||||
|
this->set_sta_priority(failed_bssid.value(), 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
|
||||||
|
this->clear_priorities_if_all_min_();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle target advancement or retry counter increment when staying in the same phase
|
/// Handle target advancement or retry counter increment when staying in the same phase
|
||||||
@@ -1379,8 +1466,7 @@ void WiFiComponent::advance_to_next_target_or_increment_retry_() {
|
|||||||
// If first network is marked hidden, we went through EXPLICIT_HIDDEN phase
|
// If first network is marked hidden, we went through EXPLICIT_HIDDEN phase
|
||||||
// In that case, skip networks marked hidden:true (already tried)
|
// In that case, skip networks marked hidden:true (already tried)
|
||||||
// Otherwise, include them (they haven't been tried yet)
|
// Otherwise, include them (they haven't been tried yet)
|
||||||
int8_t next_index =
|
int8_t next_index = this->find_next_hidden_sta_(this->selected_sta_index_);
|
||||||
this->find_next_hidden_sta_(this->selected_sta_index_, !this->went_through_explicit_hidden_phase_());
|
|
||||||
if (next_index != -1) {
|
if (next_index != -1) {
|
||||||
// Found another potentially hidden SSID
|
// Found another potentially hidden SSID
|
||||||
this->selected_sta_index_ = next_index;
|
this->selected_sta_index_ = next_index;
|
||||||
@@ -1408,15 +1494,13 @@ void WiFiComponent::advance_to_next_target_or_increment_retry_() {
|
|||||||
void WiFiComponent::retry_connect() {
|
void WiFiComponent::retry_connect() {
|
||||||
this->log_and_adjust_priority_for_failed_connect_();
|
this->log_and_adjust_priority_for_failed_connect_();
|
||||||
|
|
||||||
delay(10);
|
|
||||||
|
|
||||||
// Determine next retry phase based on current state
|
// Determine next retry phase based on current state
|
||||||
WiFiRetryPhase current_phase = this->retry_phase_;
|
WiFiRetryPhase current_phase = this->retry_phase_;
|
||||||
WiFiRetryPhase next_phase = this->determine_next_phase_();
|
WiFiRetryPhase next_phase = this->determine_next_phase_();
|
||||||
|
|
||||||
// Handle phase transitions (transition_to_phase_ handles same-phase no-op internally)
|
// Handle phase transitions (transition_to_phase_ handles same-phase no-op internally)
|
||||||
if (this->transition_to_phase_(next_phase)) {
|
if (this->transition_to_phase_(next_phase)) {
|
||||||
return; // Wait for scan to complete
|
return; // Scan started or adapter restarted (which sets its own state)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (next_phase == current_phase) {
|
if (next_phase == current_phase) {
|
||||||
@@ -1425,22 +1509,14 @@ void WiFiComponent::retry_connect() {
|
|||||||
|
|
||||||
this->error_from_callback_ = false;
|
this->error_from_callback_ = false;
|
||||||
|
|
||||||
if (this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTING) {
|
yield();
|
||||||
yield();
|
// Check if we have a valid target before building params
|
||||||
// Check if we have a valid target before building params
|
// After exhausting all networks in a phase, selected_sta_index_ may be -1
|
||||||
// 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
|
||||||
// In that case, skip connection and let next wifi_loop() handle phase transition
|
if (this->selected_sta_index_ >= 0) {
|
||||||
if (this->selected_sta_index_ >= 0) {
|
WiFiAP params = this->build_params_for_current_phase_();
|
||||||
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING_2;
|
this->start_connecting(params);
|
||||||
WiFiAP params = this->build_params_for_current_phase_();
|
|
||||||
this->start_connecting(params, true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// No valid target - fall through to set state to allow phase transition
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this->state_ = WIFI_COMPONENT_STATE_COOLDOWN;
|
|
||||||
this->action_started_ = millis();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
|
void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
|
||||||
@@ -1532,7 +1608,9 @@ void WiFiAP::set_password(const std::string &password) { this->password_ = passw
|
|||||||
void WiFiAP::set_eap(optional<EAPAuth> eap_auth) { this->eap_ = std::move(eap_auth); }
|
void WiFiAP::set_eap(optional<EAPAuth> eap_auth) { this->eap_ = std::move(eap_auth); }
|
||||||
#endif
|
#endif
|
||||||
void WiFiAP::set_channel(optional<uint8_t> channel) { this->channel_ = channel; }
|
void WiFiAP::set_channel(optional<uint8_t> channel) { this->channel_ = channel; }
|
||||||
|
#ifdef USE_WIFI_MANUAL_IP
|
||||||
void WiFiAP::set_manual_ip(optional<ManualIP> manual_ip) { this->manual_ip_ = manual_ip; }
|
void WiFiAP::set_manual_ip(optional<ManualIP> manual_ip) { this->manual_ip_ = manual_ip; }
|
||||||
|
#endif
|
||||||
void WiFiAP::set_hidden(bool hidden) { this->hidden_ = hidden; }
|
void WiFiAP::set_hidden(bool hidden) { this->hidden_ = hidden; }
|
||||||
const std::string &WiFiAP::get_ssid() const { return this->ssid_; }
|
const std::string &WiFiAP::get_ssid() const { return this->ssid_; }
|
||||||
const optional<bssid_t> &WiFiAP::get_bssid() const { return this->bssid_; }
|
const optional<bssid_t> &WiFiAP::get_bssid() const { return this->bssid_; }
|
||||||
@@ -1541,15 +1619,17 @@ const std::string &WiFiAP::get_password() const { return this->password_; }
|
|||||||
const optional<EAPAuth> &WiFiAP::get_eap() const { return this->eap_; }
|
const optional<EAPAuth> &WiFiAP::get_eap() const { return this->eap_; }
|
||||||
#endif
|
#endif
|
||||||
const optional<uint8_t> &WiFiAP::get_channel() const { return this->channel_; }
|
const optional<uint8_t> &WiFiAP::get_channel() const { return this->channel_; }
|
||||||
|
#ifdef USE_WIFI_MANUAL_IP
|
||||||
const optional<ManualIP> &WiFiAP::get_manual_ip() const { return this->manual_ip_; }
|
const optional<ManualIP> &WiFiAP::get_manual_ip() const { return this->manual_ip_; }
|
||||||
|
#endif
|
||||||
bool WiFiAP::get_hidden() const { return this->hidden_; }
|
bool WiFiAP::get_hidden() const { return this->hidden_; }
|
||||||
|
|
||||||
WiFiScanResult::WiFiScanResult(const bssid_t &bssid, std::string ssid, uint8_t channel, int8_t rssi, bool with_auth,
|
WiFiScanResult::WiFiScanResult(const bssid_t &bssid, std::string ssid, uint8_t channel, int8_t rssi, bool with_auth,
|
||||||
bool is_hidden)
|
bool is_hidden)
|
||||||
: bssid_(bssid),
|
: bssid_(bssid),
|
||||||
ssid_(std::move(ssid)),
|
|
||||||
channel_(channel),
|
channel_(channel),
|
||||||
rssi_(rssi),
|
rssi_(rssi),
|
||||||
|
ssid_(std::move(ssid)),
|
||||||
with_auth_(with_auth),
|
with_auth_(with_auth),
|
||||||
is_hidden_(is_hidden) {}
|
is_hidden_(is_hidden) {}
|
||||||
bool WiFiScanResult::matches(const WiFiAP &config) const {
|
bool WiFiScanResult::matches(const WiFiAP &config) const {
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ extern "C" {
|
|||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace wifi {
|
namespace wifi {
|
||||||
|
|
||||||
|
/// Sentinel value for RSSI when WiFi is not connected
|
||||||
|
static constexpr int8_t WIFI_RSSI_DISCONNECTED = -127;
|
||||||
|
|
||||||
struct SavedWifiSettings {
|
struct SavedWifiSettings {
|
||||||
char ssid[33];
|
char ssid[33];
|
||||||
char password[65];
|
char password[65];
|
||||||
@@ -74,12 +77,6 @@ enum WiFiComponentState : uint8_t {
|
|||||||
WIFI_COMPONENT_STATE_STA_SCANNING,
|
WIFI_COMPONENT_STATE_STA_SCANNING,
|
||||||
/** WiFi is in STA(+AP) mode and currently connecting to an AP. */
|
/** WiFi is in STA(+AP) mode and currently connecting to an AP. */
|
||||||
WIFI_COMPONENT_STATE_STA_CONNECTING,
|
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 is in STA(+AP) mode and successfully connected. */
|
||||||
WIFI_COMPONENT_STATE_STA_CONNECTED,
|
WIFI_COMPONENT_STATE_STA_CONNECTED,
|
||||||
/** WiFi is in AP-only mode and internal AP is already enabled. */
|
/** WiFi is in AP-only mode and internal AP is already enabled. */
|
||||||
@@ -157,8 +154,10 @@ class WiFiAP {
|
|||||||
void set_eap(optional<EAPAuth> eap_auth);
|
void set_eap(optional<EAPAuth> eap_auth);
|
||||||
#endif // USE_WIFI_WPA2_EAP
|
#endif // USE_WIFI_WPA2_EAP
|
||||||
void set_channel(optional<uint8_t> channel);
|
void set_channel(optional<uint8_t> channel);
|
||||||
void set_priority(float priority) { priority_ = priority; }
|
void set_priority(int8_t priority) { priority_ = priority; }
|
||||||
|
#ifdef USE_WIFI_MANUAL_IP
|
||||||
void set_manual_ip(optional<ManualIP> manual_ip);
|
void set_manual_ip(optional<ManualIP> manual_ip);
|
||||||
|
#endif
|
||||||
void set_hidden(bool hidden);
|
void set_hidden(bool hidden);
|
||||||
const std::string &get_ssid() const;
|
const std::string &get_ssid() const;
|
||||||
const optional<bssid_t> &get_bssid() const;
|
const optional<bssid_t> &get_bssid() const;
|
||||||
@@ -167,8 +166,10 @@ class WiFiAP {
|
|||||||
const optional<EAPAuth> &get_eap() const;
|
const optional<EAPAuth> &get_eap() const;
|
||||||
#endif // USE_WIFI_WPA2_EAP
|
#endif // USE_WIFI_WPA2_EAP
|
||||||
const optional<uint8_t> &get_channel() const;
|
const optional<uint8_t> &get_channel() const;
|
||||||
float get_priority() const { return priority_; }
|
int8_t get_priority() const { return priority_; }
|
||||||
|
#ifdef USE_WIFI_MANUAL_IP
|
||||||
const optional<ManualIP> &get_manual_ip() const;
|
const optional<ManualIP> &get_manual_ip() const;
|
||||||
|
#endif
|
||||||
bool get_hidden() const;
|
bool get_hidden() const;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
@@ -178,9 +179,11 @@ class WiFiAP {
|
|||||||
#ifdef USE_WIFI_WPA2_EAP
|
#ifdef USE_WIFI_WPA2_EAP
|
||||||
optional<EAPAuth> eap_;
|
optional<EAPAuth> eap_;
|
||||||
#endif // USE_WIFI_WPA2_EAP
|
#endif // USE_WIFI_WPA2_EAP
|
||||||
|
#ifdef USE_WIFI_MANUAL_IP
|
||||||
optional<ManualIP> manual_ip_;
|
optional<ManualIP> manual_ip_;
|
||||||
float priority_{0};
|
#endif
|
||||||
optional<uint8_t> channel_;
|
optional<uint8_t> channel_;
|
||||||
|
int8_t priority_{0};
|
||||||
bool hidden_{false};
|
bool hidden_{false};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -198,17 +201,17 @@ class WiFiScanResult {
|
|||||||
int8_t get_rssi() const;
|
int8_t get_rssi() const;
|
||||||
bool get_with_auth() const;
|
bool get_with_auth() const;
|
||||||
bool get_is_hidden() const;
|
bool get_is_hidden() const;
|
||||||
float get_priority() const { return priority_; }
|
int8_t get_priority() const { return priority_; }
|
||||||
void set_priority(float priority) { priority_ = priority; }
|
void set_priority(int8_t priority) { priority_ = priority; }
|
||||||
|
|
||||||
bool operator==(const WiFiScanResult &rhs) const;
|
bool operator==(const WiFiScanResult &rhs) const;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
bssid_t bssid_;
|
bssid_t bssid_;
|
||||||
std::string ssid_;
|
|
||||||
float priority_{0.0f};
|
|
||||||
uint8_t channel_;
|
uint8_t channel_;
|
||||||
int8_t rssi_;
|
int8_t rssi_;
|
||||||
|
std::string ssid_;
|
||||||
|
int8_t priority_{0};
|
||||||
bool matches_{false};
|
bool matches_{false};
|
||||||
bool with_auth_;
|
bool with_auth_;
|
||||||
bool is_hidden_;
|
bool is_hidden_;
|
||||||
@@ -216,7 +219,7 @@ class WiFiScanResult {
|
|||||||
|
|
||||||
struct WiFiSTAPriority {
|
struct WiFiSTAPriority {
|
||||||
bssid_t bssid;
|
bssid_t bssid;
|
||||||
float priority;
|
int8_t priority;
|
||||||
};
|
};
|
||||||
|
|
||||||
enum WiFiPowerSaveMode : uint8_t {
|
enum WiFiPowerSaveMode : uint8_t {
|
||||||
@@ -225,6 +228,12 @@ enum WiFiPowerSaveMode : uint8_t {
|
|||||||
WIFI_POWER_SAVE_HIGH,
|
WIFI_POWER_SAVE_HIGH,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum WifiMinAuthMode : uint8_t {
|
||||||
|
WIFI_MIN_AUTH_MODE_WPA = 0,
|
||||||
|
WIFI_MIN_AUTH_MODE_WPA2,
|
||||||
|
WIFI_MIN_AUTH_MODE_WPA3,
|
||||||
|
};
|
||||||
|
|
||||||
#ifdef USE_ESP32
|
#ifdef USE_ESP32
|
||||||
struct IDFWiFiEvent;
|
struct IDFWiFiEvent;
|
||||||
#endif
|
#endif
|
||||||
@@ -263,7 +272,9 @@ class WiFiComponent : public Component {
|
|||||||
bool is_disabled();
|
bool is_disabled();
|
||||||
void start_scanning();
|
void start_scanning();
|
||||||
void check_scanning_finished();
|
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();
|
void check_connecting_finished();
|
||||||
|
|
||||||
@@ -274,6 +285,7 @@ class WiFiComponent : public Component {
|
|||||||
bool is_connected();
|
bool is_connected();
|
||||||
|
|
||||||
void set_power_save_mode(WiFiPowerSaveMode power_save);
|
void set_power_save_mode(WiFiPowerSaveMode power_save);
|
||||||
|
void set_min_auth_mode(WifiMinAuthMode min_auth_mode) { min_auth_mode_ = min_auth_mode; }
|
||||||
void set_output_power(float output_power) { output_power_ = output_power; }
|
void set_output_power(float output_power) { output_power_ = output_power; }
|
||||||
|
|
||||||
void set_passive_scan(bool passive);
|
void set_passive_scan(bool passive);
|
||||||
@@ -317,14 +329,14 @@ class WiFiComponent : public Component {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
float get_sta_priority(const bssid_t bssid) {
|
int8_t get_sta_priority(const bssid_t bssid) {
|
||||||
for (auto &it : this->sta_priorities_) {
|
for (auto &it : this->sta_priorities_) {
|
||||||
if (it.bssid == bssid)
|
if (it.bssid == bssid)
|
||||||
return it.priority;
|
return it.priority;
|
||||||
}
|
}
|
||||||
return 0.0f;
|
return 0;
|
||||||
}
|
}
|
||||||
void set_sta_priority(const bssid_t bssid, float priority) {
|
void set_sta_priority(const bssid_t bssid, int8_t priority) {
|
||||||
for (auto &it : this->sta_priorities_) {
|
for (auto &it : this->sta_priorities_) {
|
||||||
if (it.bssid == bssid) {
|
if (it.bssid == bssid) {
|
||||||
it.priority = priority;
|
it.priority = priority;
|
||||||
@@ -379,10 +391,11 @@ class WiFiComponent : public Component {
|
|||||||
/// Find next SSID that wasn't in scan results (might be hidden)
|
/// Find next SSID that wasn't in scan results (might be hidden)
|
||||||
/// Returns index of next potentially hidden SSID, or -1 if none found
|
/// Returns index of next potentially hidden SSID, or -1 if none found
|
||||||
/// @param start_index Start searching from index after this (-1 to start from beginning)
|
/// @param start_index Start searching from index after this (-1 to start from beginning)
|
||||||
/// @param include_explicit_hidden If true, include SSIDs marked hidden:true. If false, only find truly hidden SSIDs.
|
int8_t find_next_hidden_sta_(int8_t start_index);
|
||||||
int8_t find_next_hidden_sta_(int8_t start_index, bool include_explicit_hidden = true);
|
|
||||||
/// Log failed connection and decrease BSSID priority to avoid repeated attempts
|
/// Log failed connection and decrease BSSID priority to avoid repeated attempts
|
||||||
void log_and_adjust_priority_for_failed_connect_();
|
void log_and_adjust_priority_for_failed_connect_();
|
||||||
|
/// Clear BSSID priority tracking if all priorities are at minimum (saves memory)
|
||||||
|
void clear_priorities_if_all_min_();
|
||||||
/// Advance to next target (AP/SSID) within current phase, or increment retry counter
|
/// Advance to next target (AP/SSID) within current phase, or increment retry counter
|
||||||
/// Called when staying in the same phase after a failed connection attempt
|
/// Called when staying in the same phase after a failed connection attempt
|
||||||
void advance_to_next_target_or_increment_retry_();
|
void advance_to_next_target_or_increment_retry_();
|
||||||
@@ -416,7 +429,7 @@ class WiFiComponent : public Component {
|
|||||||
bool wifi_sta_pre_setup_();
|
bool wifi_sta_pre_setup_();
|
||||||
bool wifi_apply_output_power_(float output_power);
|
bool wifi_apply_output_power_(float output_power);
|
||||||
bool wifi_apply_power_save_();
|
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_apply_hostname_();
|
||||||
bool wifi_sta_connect_(const WiFiAP &ap);
|
bool wifi_sta_connect_(const WiFiAP &ap);
|
||||||
void wifi_pre_setup_();
|
void wifi_pre_setup_();
|
||||||
@@ -424,7 +437,7 @@ class WiFiComponent : public Component {
|
|||||||
bool wifi_scan_start_(bool passive);
|
bool wifi_scan_start_(bool passive);
|
||||||
|
|
||||||
#ifdef USE_WIFI_AP
|
#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);
|
bool wifi_start_ap_(const WiFiAP &ap);
|
||||||
#endif // USE_WIFI_AP
|
#endif // USE_WIFI_AP
|
||||||
|
|
||||||
@@ -489,6 +502,7 @@ class WiFiComponent : public Component {
|
|||||||
// Group all 8-bit values together
|
// Group all 8-bit values together
|
||||||
WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF};
|
WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF};
|
||||||
WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE};
|
WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE};
|
||||||
|
WifiMinAuthMode min_auth_mode_{WIFI_MIN_AUTH_MODE_WPA2};
|
||||||
WiFiRetryPhase retry_phase_{WiFiRetryPhase::INITIAL_CONNECT};
|
WiFiRetryPhase retry_phase_{WiFiRetryPhase::INITIAL_CONNECT};
|
||||||
uint8_t num_retried_{0};
|
uint8_t num_retried_{0};
|
||||||
// Index into sta_ array for the currently selected AP configuration (-1 = none selected)
|
// Index into sta_ array for the currently selected AP configuration (-1 = none selected)
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ void netif_set_addr(struct netif *netif, const ip4_addr_t *ip, const ip4_addr_t
|
|||||||
};
|
};
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
bool WiFiComponent::wifi_sta_ip_config_(optional<ManualIP> manual_ip) {
|
bool WiFiComponent::wifi_sta_ip_config_(const optional<ManualIP> &manual_ip) {
|
||||||
// enable STA
|
// enable STA
|
||||||
if (!this->wifi_mode_(true, {}))
|
if (!this->wifi_mode_(true, {}))
|
||||||
return false;
|
return false;
|
||||||
@@ -258,8 +258,17 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
|||||||
if (ap.get_password().empty()) {
|
if (ap.get_password().empty()) {
|
||||||
conf.threshold.authmode = AUTH_OPEN;
|
conf.threshold.authmode = AUTH_OPEN;
|
||||||
} else {
|
} else {
|
||||||
// Only allow auth modes with at least WPA
|
// Set threshold based on configured minimum auth mode
|
||||||
conf.threshold.authmode = AUTH_WPA_PSK;
|
// Note: ESP8266 doesn't support WPA3
|
||||||
|
switch (this->min_auth_mode_) {
|
||||||
|
case WIFI_MIN_AUTH_MODE_WPA:
|
||||||
|
conf.threshold.authmode = AUTH_WPA_PSK;
|
||||||
|
break;
|
||||||
|
case WIFI_MIN_AUTH_MODE_WPA2:
|
||||||
|
case WIFI_MIN_AUTH_MODE_WPA3: // Fall back to WPA2 for ESP8266
|
||||||
|
conf.threshold.authmode = AUTH_WPA2_PSK;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
conf.threshold.rssi = -127;
|
conf.threshold.rssi = -127;
|
||||||
#endif
|
#endif
|
||||||
@@ -273,9 +282,15 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef USE_WIFI_MANUAL_IP
|
||||||
if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) {
|
if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
if (!this->wifi_sta_ip_config_({})) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// setup enterprise authentication if required
|
// setup enterprise authentication if required
|
||||||
#ifdef USE_WIFI_WPA2_EAP
|
#ifdef USE_WIFI_WPA2_EAP
|
||||||
@@ -510,8 +525,10 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
|
|||||||
ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf);
|
ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf);
|
||||||
s_sta_connect_not_found = true;
|
s_sta_connect_not_found = true;
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf,
|
char bssid_s[18];
|
||||||
format_mac_address_pretty(it.bssid).c_str(), LOG_STR_ARG(get_disconnect_reason_str(it.reason)));
|
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_connect_error = true;
|
||||||
}
|
}
|
||||||
s_sta_connected = false;
|
s_sta_connected = false;
|
||||||
@@ -715,7 +732,7 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#ifdef USE_WIFI_AP
|
#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
|
// enable AP
|
||||||
if (!this->wifi_mode_({}, true))
|
if (!this->wifi_mode_({}, true))
|
||||||
return false;
|
return false;
|
||||||
@@ -823,10 +840,17 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef USE_WIFI_MANUAL_IP
|
||||||
if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) {
|
if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) {
|
||||||
ESP_LOGV(TAG, "wifi_ap_ip_config_ failed");
|
ESP_LOGV(TAG, "wifi_ap_ip_config_ failed");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
if (!this->wifi_ap_ip_config_({})) {
|
||||||
|
ESP_LOGV(TAG, "wifi_ap_ip_config_ failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -848,7 +872,7 @@ bssid_t WiFiComponent::wifi_bssid() {
|
|||||||
return bssid;
|
return bssid;
|
||||||
}
|
}
|
||||||
std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); }
|
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(); }
|
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_subnet_mask_() { return {(const ip_addr_t *) WiFi.subnetMask()}; }
|
||||||
network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {(const ip_addr_t *) WiFi.gatewayIP()}; }
|
network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {(const ip_addr_t *) WiFi.gatewayIP()}; }
|
||||||
|
|||||||
@@ -308,7 +308,18 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
|||||||
if (ap.get_password().empty()) {
|
if (ap.get_password().empty()) {
|
||||||
conf.sta.threshold.authmode = WIFI_AUTH_OPEN;
|
conf.sta.threshold.authmode = WIFI_AUTH_OPEN;
|
||||||
} else {
|
} else {
|
||||||
conf.sta.threshold.authmode = WIFI_AUTH_WPA_WPA2_PSK;
|
// Set threshold based on configured minimum auth mode
|
||||||
|
switch (this->min_auth_mode_) {
|
||||||
|
case WIFI_MIN_AUTH_MODE_WPA:
|
||||||
|
conf.sta.threshold.authmode = WIFI_AUTH_WPA_PSK;
|
||||||
|
break;
|
||||||
|
case WIFI_MIN_AUTH_MODE_WPA2:
|
||||||
|
conf.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK;
|
||||||
|
break;
|
||||||
|
case WIFI_MIN_AUTH_MODE_WPA3:
|
||||||
|
conf.sta.threshold.authmode = WIFI_AUTH_WPA3_PSK;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef USE_WIFI_WPA2_EAP
|
#ifdef USE_WIFI_WPA2_EAP
|
||||||
@@ -347,8 +358,6 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
|||||||
// The minimum rssi to accept in the fast scan mode
|
// The minimum rssi to accept in the fast scan mode
|
||||||
conf.sta.threshold.rssi = -127;
|
conf.sta.threshold.rssi = -127;
|
||||||
|
|
||||||
conf.sta.threshold.authmode = WIFI_AUTH_OPEN;
|
|
||||||
|
|
||||||
wifi_config_t current_conf;
|
wifi_config_t current_conf;
|
||||||
esp_err_t err;
|
esp_err_t err;
|
||||||
err = esp_wifi_get_config(WIFI_IF_STA, ¤t_conf);
|
err = esp_wifi_get_config(WIFI_IF_STA, ¤t_conf);
|
||||||
@@ -371,9 +380,15 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef USE_WIFI_MANUAL_IP
|
||||||
if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) {
|
if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
if (!this->wifi_sta_ip_config_({})) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// setup enterprise authentication if required
|
// setup enterprise authentication if required
|
||||||
#ifdef USE_WIFI_WPA2_EAP
|
#ifdef USE_WIFI_WPA2_EAP
|
||||||
@@ -472,7 +487,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
|||||||
return true;
|
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
|
// enable STA
|
||||||
if (!this->wifi_mode_(true, {}))
|
if (!this->wifi_mode_(true, {}))
|
||||||
return false;
|
return false;
|
||||||
@@ -731,8 +746,10 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
|||||||
ESP_LOGI(TAG, "Disconnected ssid='%s' reason='Station Roaming'", buf);
|
ESP_LOGI(TAG, "Disconnected ssid='%s' reason='Station Roaming'", buf);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf,
|
char bssid_s[18];
|
||||||
format_mac_address_pretty(it.bssid).c_str(), get_disconnect_reason_str(it.reason));
|
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_connect_error = true;
|
||||||
}
|
}
|
||||||
s_sta_connected = false;
|
s_sta_connected = false;
|
||||||
@@ -869,7 +886,7 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#ifdef USE_WIFI_AP
|
#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;
|
esp_err_t err;
|
||||||
|
|
||||||
// enable AP
|
// enable AP
|
||||||
@@ -985,10 +1002,17 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef USE_WIFI_MANUAL_IP
|
||||||
if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) {
|
if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) {
|
||||||
ESP_LOGE(TAG, "wifi_ap_ip_config_ failed:");
|
ESP_LOGE(TAG, "wifi_ap_ip_config_ failed:");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
if (!this->wifi_ap_ip_config_({})) {
|
||||||
|
ESP_LOGE(TAG, "wifi_ap_ip_config_ failed:");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1007,7 +1031,8 @@ bssid_t WiFiComponent::wifi_bssid() {
|
|||||||
wifi_ap_record_t info;
|
wifi_ap_record_t info;
|
||||||
esp_err_t err = esp_wifi_sta_get_ap_info(&info);
|
esp_err_t err = esp_wifi_sta_get_ap_info(&info);
|
||||||
if (err != ESP_OK) {
|
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;
|
return bssid;
|
||||||
}
|
}
|
||||||
std::copy(info.bssid, info.bssid + 6, bssid.begin());
|
std::copy(info.bssid, info.bssid + 6, bssid.begin());
|
||||||
@@ -1017,7 +1042,8 @@ std::string WiFiComponent::wifi_ssid() {
|
|||||||
wifi_ap_record_t info{};
|
wifi_ap_record_t info{};
|
||||||
esp_err_t err = esp_wifi_sta_get_ap_info(&info);
|
esp_err_t err = esp_wifi_sta_get_ap_info(&info);
|
||||||
if (err != ESP_OK) {
|
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 "";
|
return "";
|
||||||
}
|
}
|
||||||
auto *ssid_s = reinterpret_cast<const char *>(info.ssid);
|
auto *ssid_s = reinterpret_cast<const char *>(info.ssid);
|
||||||
@@ -1028,8 +1054,9 @@ int8_t WiFiComponent::wifi_rssi() {
|
|||||||
wifi_ap_record_t info;
|
wifi_ap_record_t info;
|
||||||
esp_err_t err = esp_wifi_sta_get_ap_info(&info);
|
esp_err_t err = esp_wifi_sta_get_ap_info(&info);
|
||||||
if (err != ESP_OK) {
|
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)
|
||||||
return 0;
|
ESP_LOGVV(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err));
|
||||||
|
return WIFI_RSSI_DISCONNECTED;
|
||||||
}
|
}
|
||||||
return info.rssi;
|
return info.rssi;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ bool WiFiComponent::wifi_sta_pre_setup_() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
bool WiFiComponent::wifi_apply_power_save_() { return WiFi.setSleep(this->power_save_ != WIFI_POWER_SAVE_NONE); }
|
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
|
// enable STA
|
||||||
if (!this->wifi_mode_(true, {}))
|
if (!this->wifi_mode_(true, {}))
|
||||||
return false;
|
return false;
|
||||||
@@ -112,9 +112,15 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
|||||||
WiFi.disconnect();
|
WiFi.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef USE_WIFI_MANUAL_IP
|
||||||
if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) {
|
if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
if (!this->wifi_sta_ip_config_({})) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
this->wifi_apply_hostname_();
|
this->wifi_apply_hostname_();
|
||||||
|
|
||||||
@@ -293,8 +299,10 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_
|
|||||||
if (it.reason == WIFI_REASON_NO_AP_FOUND) {
|
if (it.reason == WIFI_REASON_NO_AP_FOUND) {
|
||||||
ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf);
|
ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf);
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf,
|
char bssid_s[18];
|
||||||
format_mac_address_pretty(it.bssid).c_str(), get_disconnect_reason_str(it.reason));
|
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;
|
uint8_t reason = it.reason;
|
||||||
@@ -428,7 +436,7 @@ void WiFiComponent::wifi_scan_done_callback_() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#ifdef USE_WIFI_AP
|
#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
|
// enable AP
|
||||||
if (!this->wifi_mode_({}, true))
|
if (!this->wifi_mode_({}, true))
|
||||||
return false;
|
return false;
|
||||||
@@ -445,10 +453,17 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
|
|||||||
if (!this->wifi_mode_({}, true))
|
if (!this->wifi_mode_({}, true))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
#ifdef USE_WIFI_MANUAL_IP
|
||||||
if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) {
|
if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) {
|
||||||
ESP_LOGV(TAG, "wifi_ap_ip_config_ failed");
|
ESP_LOGV(TAG, "wifi_ap_ip_config_ failed");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
if (!this->wifi_ap_ip_config_({})) {
|
||||||
|
ESP_LOGV(TAG, "wifi_ap_ip_config_ failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
yield();
|
yield();
|
||||||
|
|
||||||
@@ -471,7 +486,7 @@ bssid_t WiFiComponent::wifi_bssid() {
|
|||||||
return bssid;
|
return bssid;
|
||||||
}
|
}
|
||||||
std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); }
|
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(); }
|
int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); }
|
||||||
network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {WiFi.subnetMask()}; }
|
network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {WiFi.subnetMask()}; }
|
||||||
network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {WiFi.gatewayIP()}; }
|
network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {WiFi.gatewayIP()}; }
|
||||||
|
|||||||
@@ -55,8 +55,13 @@ bool WiFiComponent::wifi_apply_power_save_() {
|
|||||||
bool WiFiComponent::wifi_apply_output_power_(float output_power) { return true; }
|
bool WiFiComponent::wifi_apply_output_power_(float output_power) { return true; }
|
||||||
|
|
||||||
bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||||
|
#ifdef USE_WIFI_MANUAL_IP
|
||||||
if (!this->wifi_sta_ip_config_(ap.get_manual_ip()))
|
if (!this->wifi_sta_ip_config_(ap.get_manual_ip()))
|
||||||
return false;
|
return false;
|
||||||
|
#else
|
||||||
|
if (!this->wifi_sta_ip_config_({}))
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
|
||||||
auto ret = WiFi.begin(ap.get_ssid().c_str(), ap.get_password().c_str());
|
auto ret = WiFi.begin(ap.get_ssid().c_str(), ap.get_password().c_str());
|
||||||
if (ret != WL_CONNECTED)
|
if (ret != WL_CONNECTED)
|
||||||
@@ -67,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_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()) {
|
if (!manual_ip.has_value()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -141,7 +146,7 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#ifdef USE_WIFI_AP
|
#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;
|
esphome::network::IPAddress ip_address, gateway, subnet, dns;
|
||||||
if (manual_ip.has_value()) {
|
if (manual_ip.has_value()) {
|
||||||
ip_address = manual_ip->static_ip;
|
ip_address = manual_ip->static_ip;
|
||||||
@@ -161,10 +166,17 @@ bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
|
|||||||
bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
|
bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
|
||||||
if (!this->wifi_mode_({}, true))
|
if (!this->wifi_mode_({}, true))
|
||||||
return false;
|
return false;
|
||||||
|
#ifdef USE_WIFI_MANUAL_IP
|
||||||
if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) {
|
if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) {
|
||||||
ESP_LOGV(TAG, "wifi_ap_ip_config_ failed");
|
ESP_LOGV(TAG, "wifi_ap_ip_config_ failed");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
if (!this->wifi_ap_ip_config_({})) {
|
||||||
|
ESP_LOGV(TAG, "wifi_ap_ip_config_ failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
WiFi.beginAP(ap.get_ssid().c_str(), ap.get_password().c_str(), ap.get_channel().value_or(1));
|
WiFi.beginAP(ap.get_ssid().c_str(), ap.get_password().c_str(), ap.get_channel().value_or(1));
|
||||||
|
|
||||||
@@ -188,7 +200,7 @@ bssid_t WiFiComponent::wifi_bssid() {
|
|||||||
return bssid;
|
return bssid;
|
||||||
}
|
}
|
||||||
std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); }
|
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(); }
|
int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); }
|
||||||
|
|
||||||
network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() {
|
network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from enum import Enum
|
|||||||
|
|
||||||
from esphome.enum import StrEnum
|
from esphome.enum import StrEnum
|
||||||
|
|
||||||
__version__ = "2025.11.0-dev"
|
__version__ = "2025.12.0-dev"
|
||||||
|
|
||||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||||
|
|||||||
@@ -412,7 +412,12 @@ template<typename... Ts> class WaitUntilAction : public Action<Ts...>, public Co
|
|||||||
|
|
||||||
void setup() override {
|
void setup() override {
|
||||||
// Start with loop disabled - only enable when there's work to do
|
// 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 {
|
void play_complex(const Ts &...x) override {
|
||||||
|
|||||||
@@ -144,6 +144,7 @@
|
|||||||
#define USE_TIME_TIMEZONE
|
#define USE_TIME_TIMEZONE
|
||||||
#define USE_WIFI
|
#define USE_WIFI
|
||||||
#define USE_WIFI_AP
|
#define USE_WIFI_AP
|
||||||
|
#define USE_WIFI_MANUAL_IP
|
||||||
#define USE_WIREGUARD
|
#define USE_WIREGUARD
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -215,7 +216,6 @@
|
|||||||
#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 2)
|
#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 2)
|
||||||
#define USE_ETHERNET
|
#define USE_ETHERNET
|
||||||
#define USE_ETHERNET_KSZ8081
|
#define USE_ETHERNET_KSZ8081
|
||||||
#define USE_ETHERNET_MANUAL_IP
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_ESP_IDF
|
#ifdef USE_ESP_IDF
|
||||||
@@ -288,6 +288,8 @@
|
|||||||
|
|
||||||
#ifdef USE_NRF52
|
#ifdef USE_NRF52
|
||||||
#define USE_NRF52_DFU
|
#define USE_NRF52_DFU
|
||||||
|
#define USE_NRF52_REG0_VOUT 5
|
||||||
|
#define USE_NRF52_UICR_ERASE
|
||||||
#define USE_SOFTDEVICE_ID 7
|
#define USE_SOFTDEVICE_ID 7
|
||||||
#define USE_SOFTDEVICE_VERSION 1
|
#define USE_SOFTDEVICE_VERSION 1
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -6,3 +6,7 @@
|
|||||||
#ifdef USE_ARDUINO
|
#ifdef USE_ARDUINO
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef USE_ZEPHYR
|
||||||
|
#define M_PI 3.14159265358979323846
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ from esphome.const import (
|
|||||||
from esphome.core import CORE, EsphomeError
|
from esphome.core import CORE, EsphomeError
|
||||||
from esphome.helpers import get_int_env, get_str_env
|
from esphome.helpers import get_int_env, get_str_env
|
||||||
from esphome.log import AnsiFore, color
|
from esphome.log import AnsiFore, color
|
||||||
|
from esphome.types import ConfigType
|
||||||
from esphome.util import safe_print
|
from esphome.util import safe_print
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@@ -154,8 +155,12 @@ def show_discover(config, username=None, password=None, client_id=None):
|
|||||||
|
|
||||||
|
|
||||||
def get_esphome_device_ip(
|
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:
|
if CONF_MQTT not in config:
|
||||||
raise EsphomeError(
|
raise EsphomeError(
|
||||||
"Cannot discover IP via MQTT as the config does not include the mqtt: "
|
"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: "
|
"Cannot discover IP via MQTT as the config does not include the device name: "
|
||||||
"component"
|
"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_name = config[CONF_ESPHOME][CONF_NAME]
|
||||||
dev_ip = None
|
dev_ip = None
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ pyupgrade==3.21.1 # also change in .pre-commit-config.yaml when updating
|
|||||||
pre-commit
|
pre-commit
|
||||||
|
|
||||||
# Unit tests
|
# Unit tests
|
||||||
pytest==9.0.0
|
pytest==9.0.1
|
||||||
pytest-cov==7.0.0
|
pytest-cov==7.0.0
|
||||||
pytest-mock==3.15.1
|
pytest-mock==3.15.1
|
||||||
pytest-asyncio==1.3.0
|
pytest-asyncio==1.3.0
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
packages:
|
|
||||||
common: !include common.yaml
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
packages:
|
|
||||||
i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
|
|
||||||
|
|
||||||
<<: !include common.yaml
|
|
||||||
4
tests/components/ds1307/test.nrf52-adafruit.yaml
Normal file
4
tests/components/ds1307/test.nrf52-adafruit.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
packages:
|
||||||
|
i2c: !include ../../test_build_components/common/i2c/nrf52.yaml
|
||||||
|
|
||||||
|
<<: !include common.yaml
|
||||||
@@ -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);
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
packages:
|
|
||||||
i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
|
|
||||||
|
|
||||||
<<: !include common.yaml
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
packages:
|
|
||||||
i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
|
|
||||||
|
|
||||||
<<: !include common.yaml
|
|
||||||
@@ -1 +1,4 @@
|
|||||||
<<: !include common.yaml
|
<<: !include common.yaml
|
||||||
|
|
||||||
|
network:
|
||||||
|
enable_high_performance: true
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
packages:
|
|
||||||
i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
|
|
||||||
|
|
||||||
<<: !include common.yaml
|
|
||||||
@@ -15,3 +15,7 @@ nrf52:
|
|||||||
inverted: true
|
inverted: true
|
||||||
mode:
|
mode:
|
||||||
output: true
|
output: true
|
||||||
|
dcdc: False
|
||||||
|
reg0:
|
||||||
|
voltage: 2.1V
|
||||||
|
uicr_erase: true
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
nrf52:
|
||||||
|
reg0:
|
||||||
|
voltage: 3.3V
|
||||||
|
uicr_erase: true
|
||||||
|
|||||||
@@ -5,3 +5,5 @@ nrf52:
|
|||||||
inverted: true
|
inverted: true
|
||||||
mode:
|
mode:
|
||||||
output: true
|
output: true
|
||||||
|
reg0:
|
||||||
|
voltage: 1.8V
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
substitutions:
|
|
||||||
pin: GPIO4
|
|
||||||
|
|
||||||
<<: !include common.yaml
|
|
||||||
4
tests/components/pcf85063/test.nrf52-adafruit.yaml
Normal file
4
tests/components/pcf85063/test.nrf52-adafruit.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
packages:
|
||||||
|
i2c: !include ../../test_build_components/common/i2c/nrf52.yaml
|
||||||
|
|
||||||
|
<<: !include common.yaml
|
||||||
4
tests/components/pcf8563/test.nrf52-adafruit.yaml
Normal file
4
tests/components/pcf8563/test.nrf52-adafruit.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
packages:
|
||||||
|
i2c: !include ../../test_build_components/common/i2c/nrf52.yaml
|
||||||
|
|
||||||
|
<<: !include common.yaml
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
substitutions:
|
|
||||||
pin: GPIO1
|
|
||||||
|
|
||||||
<<: !include common.yaml
|
|
||||||
4
tests/components/rx8130/test.nrf52-adafruit.yaml
Normal file
4
tests/components/rx8130/test.nrf52-adafruit.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
packages:
|
||||||
|
i2c: !include ../../test_build_components/common/i2c/nrf52.yaml
|
||||||
|
|
||||||
|
<<: !include common.yaml
|
||||||
7
tests/components/ssd1306_i2c/test.nrf52-xiao-ble.yaml
Normal file
7
tests/components/ssd1306_i2c/test.nrf52-xiao-ble.yaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
substitutions:
|
||||||
|
reset_pin: P0.10
|
||||||
|
|
||||||
|
packages:
|
||||||
|
i2c: !include ../../test_build_components/common/i2c/nrf52.yaml
|
||||||
|
|
||||||
|
<<: !include common.yaml
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
packages:
|
|
||||||
common: !include common.yaml
|
|
||||||
@@ -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
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
packages:
|
|
||||||
common: !include common.yaml
|
|
||||||
@@ -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
|
|
||||||
@@ -15,5 +15,10 @@ wifi:
|
|||||||
networks:
|
networks:
|
||||||
- ssid: MySSID
|
- ssid: MySSID
|
||||||
password: password1
|
password: password1
|
||||||
|
priority: 10
|
||||||
- ssid: MySSID2
|
- ssid: MySSID2
|
||||||
password: password2
|
password: password2
|
||||||
|
priority: 5
|
||||||
|
- ssid: MySSID3
|
||||||
|
password: password3
|
||||||
|
priority: 0
|
||||||
|
|||||||
@@ -2,6 +2,22 @@ psram:
|
|||||||
|
|
||||||
wifi:
|
wifi:
|
||||||
use_psram: true
|
use_psram: true
|
||||||
|
min_auth_mode: WPA
|
||||||
|
manual_ip:
|
||||||
|
static_ip: 192.168.1.100
|
||||||
|
gateway: 192.168.1.1
|
||||||
|
subnet: 255.255.255.0
|
||||||
|
dns1: 1.1.1.1
|
||||||
|
dns2: 8.8.8.8
|
||||||
|
ap:
|
||||||
|
ssid: Fallback AP
|
||||||
|
password: fallback_password
|
||||||
|
manual_ip:
|
||||||
|
static_ip: 192.168.4.1
|
||||||
|
gateway: 192.168.4.1
|
||||||
|
subnet: 255.255.255.0
|
||||||
|
|
||||||
|
captive_portal:
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
- !include common.yaml
|
- !include common.yaml
|
||||||
|
|||||||
@@ -1 +1,5 @@
|
|||||||
<<: !include common.yaml
|
wifi:
|
||||||
|
min_auth_mode: WPA2
|
||||||
|
|
||||||
|
packages:
|
||||||
|
- !include common.yaml
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -14,6 +14,7 @@ climate:
|
|||||||
id: test_thermostat
|
id: test_thermostat
|
||||||
name: Test Thermostat Custom Modes
|
name: Test Thermostat Custom Modes
|
||||||
sensor: thermostat_sensor
|
sensor: thermostat_sensor
|
||||||
|
default_preset: "Eco Plus"
|
||||||
preset:
|
preset:
|
||||||
- name: Away
|
- name: Away
|
||||||
default_target_temperature_low: 16°C
|
default_target_temperature_low: 16°C
|
||||||
|
|||||||
47
tests/integration/fixtures/wait_until_on_boot.yaml
Normal file
47
tests/integration/fixtures/wait_until_on_boot.yaml
Normal 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");
|
||||||
|
}
|
||||||
@@ -2,9 +2,13 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from aioesphomeapi import ClimateInfo, ClimatePreset
|
import asyncio
|
||||||
|
|
||||||
|
import aioesphomeapi
|
||||||
|
from aioesphomeapi import ClimateInfo, ClimatePreset, EntityState
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from .state_utils import InitialStateHelper
|
||||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||||
|
|
||||||
|
|
||||||
@@ -14,15 +18,50 @@ async def test_climate_custom_fan_modes_and_presets(
|
|||||||
run_compiled: RunCompiledFunction,
|
run_compiled: RunCompiledFunction,
|
||||||
api_client_connected: APIClientConnectedFactory,
|
api_client_connected: APIClientConnectedFactory,
|
||||||
) -> None:
|
) -> 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:
|
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()
|
entities, services = await client.list_entities_services()
|
||||||
|
initial_state_helper = InitialStateHelper(entities)
|
||||||
climate_infos = [e for e in entities if isinstance(e, ClimateInfo)]
|
climate_infos = [e for e in entities if isinstance(e, ClimateInfo)]
|
||||||
assert len(climate_infos) == 1, "Expected exactly 1 climate entity"
|
assert len(climate_infos) == 1, "Expected exactly 1 climate entity"
|
||||||
|
|
||||||
test_climate = climate_infos[0]
|
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)
|
# Verify enum presets are exposed (from preset: config map)
|
||||||
assert ClimatePreset.AWAY in test_climate.supported_presets, (
|
assert ClimatePreset.AWAY in test_climate.supported_presets, (
|
||||||
"Expected AWAY in enum 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, (
|
assert "Vacation Mode" in custom_presets, (
|
||||||
"Expected '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
|
||||||
|
|||||||
91
tests/integration/test_wait_until_on_boot.py
Normal file
91
tests/integration/test_wait_until_on_boot.py
Normal 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."
|
||||||
|
)
|
||||||
@@ -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")
|
@patch("esphome.__main__.importlib.import_module")
|
||||||
def test_upload_program_platform_specific_handler(
|
def test_upload_program_platform_specific_handler(
|
||||||
mock_import: Mock,
|
mock_import: Mock,
|
||||||
|
|||||||
91
tests/unit_tests/test_mqtt.py
Normal file
91
tests/unit_tests/test_mqtt.py
Normal 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)
|
||||||
Reference in New Issue
Block a user