mirror of
https://github.com/esphome/esphome.git
synced 2026-05-27 11:56:11 +08:00
Merge pull request #11880 from esphome/bump-2025.11.0b2
CI / Create common environment (push) Has been cancelled
CI / Check pylint (push) Has been cancelled
CI / Run script/ci-custom (push) Has been cancelled
CI / Run pytest (macOS-latest, 3.11) (push) Has been cancelled
CI / Run pytest (ubuntu-latest, 3.11) (push) Has been cancelled
CI / Run pytest (ubuntu-latest, 3.13) (push) Has been cancelled
CI / Run pytest (windows-latest, 3.11) (push) Has been cancelled
CI / Determine which jobs to run (push) Has been cancelled
CI / Run integration tests (push) Has been cancelled
CI / Run C++ unit tests (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 IDF (push) Has been cancelled
CI / Run script/clang-tidy for ESP8266 (push) Has been cancelled
CI / Run script/clang-tidy for ZEPHYR (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 1/4 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 2/4 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 3/4 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 4/4 (push) Has been cancelled
CI / Test components batch (${{ matrix.components }}) (push) Has been cancelled
CI / pre-commit.ci lite (push) Has been cancelled
CI / Build target branch for memory impact (push) Has been cancelled
CI / Build PR branch for memory impact (push) Has been cancelled
CI / Comment memory impact (push) Has been cancelled
CI / CI Status (push) Has been cancelled
CI / Create common environment (push) Has been cancelled
CI / Check pylint (push) Has been cancelled
CI / Run script/ci-custom (push) Has been cancelled
CI / Run pytest (macOS-latest, 3.11) (push) Has been cancelled
CI / Run pytest (ubuntu-latest, 3.11) (push) Has been cancelled
CI / Run pytest (ubuntu-latest, 3.13) (push) Has been cancelled
CI / Run pytest (windows-latest, 3.11) (push) Has been cancelled
CI / Determine which jobs to run (push) Has been cancelled
CI / Run integration tests (push) Has been cancelled
CI / Run C++ unit tests (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 IDF (push) Has been cancelled
CI / Run script/clang-tidy for ESP8266 (push) Has been cancelled
CI / Run script/clang-tidy for ZEPHYR (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 1/4 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 2/4 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 3/4 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 4/4 (push) Has been cancelled
CI / Test components batch (${{ matrix.components }}) (push) Has been cancelled
CI / pre-commit.ci lite (push) Has been cancelled
CI / Build target branch for memory impact (push) Has been cancelled
CI / Build PR branch for memory impact (push) Has been cancelled
CI / Comment memory impact (push) Has been cancelled
CI / CI Status (push) Has been cancelled
2025.11.0b2
This commit is contained in:
@@ -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.0b1
|
PROJECT_NUMBER = 2025.11.0b2
|
||||||
|
|
||||||
# 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);
|
||||||
|
|||||||
@@ -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"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
@@ -743,6 +745,14 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
@@ -1022,7 +1032,10 @@ void WiFiComponent::check_scanning_finished() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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_();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1047,6 +1060,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_();
|
||||||
|
|
||||||
@@ -1133,6 +1150,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;
|
||||||
|
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
@@ -870,7 +870,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()}; }
|
||||||
|
|||||||
@@ -1029,7 +1029,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());
|
||||||
@@ -1039,7 +1040,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);
|
||||||
@@ -1050,8 +1052,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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -484,7 +484,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()}; }
|
||||||
|
|||||||
@@ -200,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() {
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ from enum import Enum
|
|||||||
|
|
||||||
from esphome.enum import StrEnum
|
from esphome.enum import StrEnum
|
||||||
|
|
||||||
__version__ = "2025.11.0b1"
|
__version__ = "2025.11.0b2"
|
||||||
|
|
||||||
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 {
|
||||||
|
|||||||
+11
-2
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<<: !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,4 +0,0 @@
|
|||||||
packages:
|
|
||||||
i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
|
|
||||||
|
|
||||||
<<: !include common.yaml
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
substitutions:
|
|
||||||
pin: GPIO4
|
|
||||||
|
|
||||||
<<: !include common.yaml
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
substitutions:
|
|
||||||
pin: GPIO1
|
|
||||||
|
|
||||||
<<: !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
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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