mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	Merge remote-tracking branch 'upstream/dev' into scheduler_copy
This commit is contained in:
		
							
								
								
									
										23
									
								
								.github/workflows/lock.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										23
									
								
								.github/workflows/lock.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,28 +1,11 @@ | |||||||
| --- | --- | ||||||
| name: Lock | name: Lock closed issues and PRs | ||||||
|  |  | ||||||
| on: | on: | ||||||
|   schedule: |   schedule: | ||||||
|     - cron: "30 0 * * *" |     - cron: "30 0 * * *"  # Run daily at 00:30 UTC | ||||||
|   workflow_dispatch: |   workflow_dispatch: | ||||||
|  |  | ||||||
| permissions: |  | ||||||
|   issues: write |  | ||||||
|   pull-requests: write |  | ||||||
|  |  | ||||||
| concurrency: |  | ||||||
|   group: lock |  | ||||||
|  |  | ||||||
| jobs: | jobs: | ||||||
|   lock: |   lock: | ||||||
|     runs-on: ubuntu-latest |     uses: esphome/workflows/.github/workflows/lock.yml@main | ||||||
|     steps: |  | ||||||
|       - uses: dessant/lock-threads@v5.0.1 |  | ||||||
|         with: |  | ||||||
|           pr-inactive-days: "1" |  | ||||||
|           pr-lock-reason: "" |  | ||||||
|           exclude-any-pr-labels: keep-open |  | ||||||
|  |  | ||||||
|           issue-inactive-days: "7" |  | ||||||
|           issue-lock-reason: "" |  | ||||||
|           exclude-any-issue-labels: keep-open |  | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ | |||||||
| repos: | repos: | ||||||
|   - repo: https://github.com/astral-sh/ruff-pre-commit |   - repo: https://github.com/astral-sh/ruff-pre-commit | ||||||
|     # Ruff version. |     # Ruff version. | ||||||
|     rev: v0.12.0 |     rev: v0.12.1 | ||||||
|     hooks: |     hooks: | ||||||
|       # Run the linter. |       # Run the linter. | ||||||
|       - id: ruff |       - id: ruff | ||||||
|   | |||||||
| @@ -146,6 +146,7 @@ esphome/components/esp32_ble_client/* @jesserockz | |||||||
| esphome/components/esp32_ble_server/* @Rapsssito @clydebarrow @jesserockz | esphome/components/esp32_ble_server/* @Rapsssito @clydebarrow @jesserockz | ||||||
| esphome/components/esp32_camera_web_server/* @ayufan | esphome/components/esp32_camera_web_server/* @ayufan | ||||||
| esphome/components/esp32_can/* @Sympatron | esphome/components/esp32_can/* @Sympatron | ||||||
|  | esphome/components/esp32_hosted/* @swoboda1337 | ||||||
| esphome/components/esp32_improv/* @jesserockz | esphome/components/esp32_improv/* @jesserockz | ||||||
| esphome/components/esp32_rmt/* @jesserockz | esphome/components/esp32_rmt/* @jesserockz | ||||||
| esphome/components/esp32_rmt_led_strip/* @jesserockz | esphome/components/esp32_rmt_led_strip/* @jesserockz | ||||||
| @@ -491,7 +492,7 @@ esphome/components/vbus/* @ssieb | |||||||
| esphome/components/veml3235/* @kbx81 | esphome/components/veml3235/* @kbx81 | ||||||
| esphome/components/veml7700/* @latonita | esphome/components/veml7700/* @latonita | ||||||
| esphome/components/version/* @esphome/core | esphome/components/version/* @esphome/core | ||||||
| esphome/components/voice_assistant/* @jesserockz | esphome/components/voice_assistant/* @jesserockz @kahrendt | ||||||
| esphome/components/wake_on_lan/* @clydebarrow @willwill2will54 | esphome/components/wake_on_lan/* @clydebarrow @willwill2will54 | ||||||
| esphome/components/watchdog/* @oarcher | esphome/components/watchdog/* @oarcher | ||||||
| esphome/components/waveshare_epaper/* @clydebarrow | esphome/components/waveshare_epaper/* @clydebarrow | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ | |||||||
| #include "esphome/core/helpers.h" | #include "esphome/core/helpers.h" | ||||||
| #include "esphome/core/log.h" | #include "esphome/core/log.h" | ||||||
| #include <cmath> | #include <cmath> | ||||||
|  | #include <numbers> | ||||||
|  |  | ||||||
| #ifdef USE_ESP8266 | #ifdef USE_ESP8266 | ||||||
| #include <core_esp8266_waveform.h> | #include <core_esp8266_waveform.h> | ||||||
| @@ -203,7 +204,7 @@ void AcDimmer::setup() { | |||||||
| #endif | #endif | ||||||
| } | } | ||||||
| void AcDimmer::write_state(float state) { | void AcDimmer::write_state(float state) { | ||||||
|   state = std::acos(1 - (2 * state)) / 3.14159;  // RMS power compensation |   state = std::acos(1 - (2 * state)) / std::numbers::pi;  // RMS power compensation | ||||||
|   auto new_value = static_cast<uint16_t>(roundf(state * 65535)); |   auto new_value = static_cast<uint16_t>(roundf(state * 65535)); | ||||||
|   if (new_value != 0 && this->store_.value == 0) |   if (new_value != 0 && this->store_.value == 0) | ||||||
|     this->store_.init_cycle = this->init_with_half_cycle_; |     this->store_.init_cycle = this->init_with_half_cycle_; | ||||||
|   | |||||||
| @@ -33,9 +33,14 @@ namespace api { | |||||||
| // Since each message could contain multiple protobuf messages when using packet batching, | // Since each message could contain multiple protobuf messages when using packet batching, | ||||||
| // this limits the number of messages processed, not the number of TCP packets. | // this limits the number of messages processed, not the number of TCP packets. | ||||||
| static constexpr uint8_t MAX_MESSAGES_PER_LOOP = 5; | static constexpr uint8_t MAX_MESSAGES_PER_LOOP = 5; | ||||||
|  | static constexpr uint8_t MAX_PING_RETRIES = 60; | ||||||
|  | static constexpr uint16_t PING_RETRY_INTERVAL = 1000; | ||||||
|  | static constexpr uint32_t KEEPALIVE_DISCONNECT_TIMEOUT = (KEEPALIVE_TIMEOUT_MS * 5) / 2; | ||||||
|  |  | ||||||
| static const char *const TAG = "api.connection"; | static const char *const TAG = "api.connection"; | ||||||
|  | #ifdef USE_ESP32_CAMERA | ||||||
| static const int ESP32_CAMERA_STOP_STREAM = 5000; | static const int ESP32_CAMERA_STOP_STREAM = 5000; | ||||||
|  | #endif | ||||||
|  |  | ||||||
| APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent) | APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent) | ||||||
|     : parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) { |     : parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) { | ||||||
| @@ -90,16 +95,6 @@ APIConnection::~APIConnection() { | |||||||
| } | } | ||||||
|  |  | ||||||
| void APIConnection::loop() { | void APIConnection::loop() { | ||||||
|   if (this->remove_) |  | ||||||
|     return; |  | ||||||
|  |  | ||||||
|   if (!network::is_connected()) { |  | ||||||
|     // when network is disconnected force disconnect immediately |  | ||||||
|     // don't wait for timeout |  | ||||||
|     this->on_fatal_error(); |  | ||||||
|     ESP_LOGW(TAG, "%s: Network unavailable; disconnecting", this->get_client_combined_info().c_str()); |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
|   if (this->next_close_) { |   if (this->next_close_) { | ||||||
|     // requested a disconnect |     // requested a disconnect | ||||||
|     this->helper_->close(); |     this->helper_->close(); | ||||||
| @@ -152,20 +147,19 @@ void APIConnection::loop() { | |||||||
|  |  | ||||||
|   // Process deferred batch if scheduled |   // Process deferred batch if scheduled | ||||||
|   if (this->deferred_batch_.batch_scheduled && |   if (this->deferred_batch_.batch_scheduled && | ||||||
|       App.get_loop_component_start_time() - this->deferred_batch_.batch_start_time >= this->get_batch_delay_ms_()) { |       now - this->deferred_batch_.batch_start_time >= this->get_batch_delay_ms_()) { | ||||||
|     this->process_batch_(); |     this->process_batch_(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (!this->list_entities_iterator_.completed()) |   if (!this->list_entities_iterator_.completed()) { | ||||||
|     this->list_entities_iterator_.advance(); |     this->list_entities_iterator_.advance(); | ||||||
|   if (!this->initial_state_iterator_.completed() && this->list_entities_iterator_.completed()) |   } else if (!this->initial_state_iterator_.completed()) { | ||||||
|     this->initial_state_iterator_.advance(); |     this->initial_state_iterator_.advance(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   static uint8_t max_ping_retries = 60; |  | ||||||
|   static uint16_t ping_retry_interval = 1000; |  | ||||||
|   if (this->sent_ping_) { |   if (this->sent_ping_) { | ||||||
|     // Disconnect if not responded within 2.5*keepalive |     // Disconnect if not responded within 2.5*keepalive | ||||||
|     if (now - this->last_traffic_ > (KEEPALIVE_TIMEOUT_MS * 5) / 2) { |     if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) { | ||||||
|       on_fatal_error(); |       on_fatal_error(); | ||||||
|       ESP_LOGW(TAG, "%s is unresponsive; disconnecting", this->get_client_combined_info().c_str()); |       ESP_LOGW(TAG, "%s is unresponsive; disconnecting", this->get_client_combined_info().c_str()); | ||||||
|     } |     } | ||||||
| @@ -173,17 +167,15 @@ void APIConnection::loop() { | |||||||
|     ESP_LOGVV(TAG, "Sending keepalive PING"); |     ESP_LOGVV(TAG, "Sending keepalive PING"); | ||||||
|     this->sent_ping_ = this->send_message(PingRequest()); |     this->sent_ping_ = this->send_message(PingRequest()); | ||||||
|     if (!this->sent_ping_) { |     if (!this->sent_ping_) { | ||||||
|       this->next_ping_retry_ = now + ping_retry_interval; |       this->next_ping_retry_ = now + PING_RETRY_INTERVAL; | ||||||
|       this->ping_retries_++; |       this->ping_retries_++; | ||||||
|       std::string warn_str = str_sprintf("%s: Sending keepalive failed %u time(s);", |       if (this->ping_retries_ >= MAX_PING_RETRIES) { | ||||||
|                                          this->get_client_combined_info().c_str(), this->ping_retries_); |  | ||||||
|       if (this->ping_retries_ >= max_ping_retries) { |  | ||||||
|         on_fatal_error(); |         on_fatal_error(); | ||||||
|         ESP_LOGE(TAG, "%s disconnecting", warn_str.c_str()); |         ESP_LOGE(TAG, "%s: Ping failed %u times", this->get_client_combined_info().c_str(), this->ping_retries_); | ||||||
|       } else if (this->ping_retries_ >= 10) { |       } else if (this->ping_retries_ >= 10) { | ||||||
|         ESP_LOGW(TAG, "%s retrying in %u ms", warn_str.c_str(), ping_retry_interval); |         ESP_LOGW(TAG, "%s: Ping retry %u", this->get_client_combined_info().c_str(), this->ping_retries_); | ||||||
|       } else { |       } else { | ||||||
|         ESP_LOGD(TAG, "%s retrying in %u ms", warn_str.c_str(), ping_retry_interval); |         ESP_LOGD(TAG, "%s: Ping retry %u", this->get_client_combined_info().c_str(), this->ping_retries_); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @@ -207,22 +199,20 @@ void APIConnection::loop() { | |||||||
|     // bool done = 3; |     // bool done = 3; | ||||||
|     buffer.encode_bool(3, done); |     buffer.encode_bool(3, done); | ||||||
|  |  | ||||||
|     bool success = this->send_buffer(buffer, 44); |     bool success = this->send_buffer(buffer, CameraImageResponse::MESSAGE_TYPE); | ||||||
|  |  | ||||||
|     if (success) { |     if (success) { | ||||||
|       this->image_reader_.consume_data(to_send); |       this->image_reader_.consume_data(to_send); | ||||||
|     } |       if (done) { | ||||||
|     if (success && done) { |  | ||||||
|         this->image_reader_.return_image(); |         this->image_reader_.return_image(); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |   } | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|   if (state_subs_at_ != -1) { |   if (state_subs_at_ >= 0) { | ||||||
|     const auto &subs = this->parent_->get_state_subs(); |     const auto &subs = this->parent_->get_state_subs(); | ||||||
|     if (state_subs_at_ >= (int) subs.size()) { |     if (state_subs_at_ < static_cast<int>(subs.size())) { | ||||||
|       state_subs_at_ = -1; |  | ||||||
|     } else { |  | ||||||
|       auto &it = subs[state_subs_at_]; |       auto &it = subs[state_subs_at_]; | ||||||
|       SubscribeHomeAssistantStateResponse resp; |       SubscribeHomeAssistantStateResponse resp; | ||||||
|       resp.entity_id = it.entity_id; |       resp.entity_id = it.entity_id; | ||||||
| @@ -231,6 +221,8 @@ void APIConnection::loop() { | |||||||
|       if (this->send_message(resp)) { |       if (this->send_message(resp)) { | ||||||
|         state_subs_at_++; |         state_subs_at_++; | ||||||
|       } |       } | ||||||
|  |     } else { | ||||||
|  |       state_subs_at_ = -1; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @@ -284,6 +276,11 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint16_t mes | |||||||
|   // Encode directly into buffer |   // Encode directly into buffer | ||||||
|   msg.encode(buffer); |   msg.encode(buffer); | ||||||
|  |  | ||||||
|  | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|  |   // Log the message for VV debugging | ||||||
|  |   conn->log_send_message_(msg.message_name(), msg.dump()); | ||||||
|  | #endif | ||||||
|  |  | ||||||
|   // Calculate actual encoded size (not including header that was already added) |   // Calculate actual encoded size (not including header that was already added) | ||||||
|   size_t actual_payload_size = shared_buf.size() - size_before_encode; |   size_t actual_payload_size = shared_buf.size() - size_before_encode; | ||||||
|  |  | ||||||
| @@ -1440,7 +1437,7 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe | |||||||
|  |  | ||||||
| #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 std::string &event_type) { | ||||||
|   this->schedule_message_(event, MessageCreator(event_type, EventResponse::MESSAGE_TYPE), EventResponse::MESSAGE_TYPE); |   this->schedule_message_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE); | ||||||
| } | } | ||||||
| void APIConnection::send_event_info(event::Event *event) { | void APIConnection::send_event_info(event::Event *event) { | ||||||
|   this->schedule_message_(event, &APIConnection::try_send_event_info, ListEntitiesEventResponse::MESSAGE_TYPE); |   this->schedule_message_(event, &APIConnection::try_send_event_info, ListEntitiesEventResponse::MESSAGE_TYPE); | ||||||
| @@ -1795,7 +1792,8 @@ void APIConnection::process_batch_() { | |||||||
|     const auto &item = this->deferred_batch_.items[0]; |     const auto &item = this->deferred_batch_.items[0]; | ||||||
|  |  | ||||||
|     // Let the creator calculate size and encode if it fits |     // Let the creator calculate size and encode if it fits | ||||||
|     uint16_t payload_size = item.creator(item.entity, this, std::numeric_limits<uint16_t>::max(), true); |     uint16_t payload_size = | ||||||
|  |         item.creator(item.entity, this, std::numeric_limits<uint16_t>::max(), true, item.message_type); | ||||||
|  |  | ||||||
|     if (payload_size > 0 && |     if (payload_size > 0 && | ||||||
|         this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, item.message_type)) { |         this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, item.message_type)) { | ||||||
| @@ -1845,7 +1843,7 @@ void APIConnection::process_batch_() { | |||||||
|   for (const auto &item : this->deferred_batch_.items) { |   for (const auto &item : this->deferred_batch_.items) { | ||||||
|     // Try to encode message |     // Try to encode message | ||||||
|     // The creator will calculate overhead to determine if the message fits |     // The creator will calculate overhead to determine if the message fits | ||||||
|     uint16_t payload_size = item.creator(item.entity, this, remaining_size, false); |     uint16_t payload_size = item.creator(item.entity, this, remaining_size, false, item.message_type); | ||||||
|  |  | ||||||
|     if (payload_size == 0) { |     if (payload_size == 0) { | ||||||
|       // Message won't fit, stop processing |       // Message won't fit, stop processing | ||||||
| @@ -1908,22 +1906,24 @@ 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) const { |                                                    bool is_single, uint16_t message_type) const { | ||||||
|   switch (message_type_) { |   if (has_tagged_string_ptr_()) { | ||||||
|     case 0:  // Function pointer |     // Handle string-based messages | ||||||
|       return data_.ptr(entity, conn, remaining_size, is_single); |     switch (message_type) { | ||||||
|  |  | ||||||
| #ifdef USE_EVENT | #ifdef USE_EVENT | ||||||
|       case EventResponse::MESSAGE_TYPE: { |       case 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, *get_string_ptr_(), conn, remaining_size, is_single); | ||||||
|       } |       } | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|       default: |       default: | ||||||
|         // Should not happen, return 0 to indicate no message |         // Should not happen, return 0 to indicate no message | ||||||
|         return 0; |         return 0; | ||||||
|     } |     } | ||||||
|  |   } else { | ||||||
|  |     // Function pointer case | ||||||
|  |     return data_.ptr(entity, conn, remaining_size, is_single); | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| uint16_t APIConnection::try_send_list_info_done(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, | uint16_t APIConnection::try_send_list_info_done(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, | ||||||
|   | |||||||
| @@ -483,55 +483,57 @@ class APIConnection : public APIServerConnection { | |||||||
|   // Function pointer type for message encoding |   // Function pointer type for message encoding | ||||||
|   using MessageCreatorPtr = uint16_t (*)(EntityBase *, APIConnection *, uint32_t remaining_size, bool is_single); |   using MessageCreatorPtr = uint16_t (*)(EntityBase *, APIConnection *, uint32_t remaining_size, bool is_single); | ||||||
|  |  | ||||||
|   // Optimized MessageCreator class using union dispatch |   // Optimized MessageCreator class using tagged pointer | ||||||
|   class MessageCreator { |   class MessageCreator { | ||||||
|  |     // Ensure pointer alignment allows LSB tagging | ||||||
|  |     static_assert(alignof(std::string *) > 1, "String pointer alignment must be > 1 for LSB tagging"); | ||||||
|  |  | ||||||
|    public: |    public: | ||||||
|     // Constructor for function pointer (message_type = 0) |     // Constructor for function pointer | ||||||
|     MessageCreator(MessageCreatorPtr ptr) : message_type_(0) { data_.ptr = ptr; } |     MessageCreator(MessageCreatorPtr ptr) { | ||||||
|  |       // Function pointers are always aligned, so LSB is 0 | ||||||
|  |       data_.ptr = ptr; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     // Constructor for string state capture |     // Constructor for string state capture | ||||||
|     MessageCreator(const std::string &value, uint16_t msg_type) : message_type_(msg_type) { |     explicit MessageCreator(const std::string &str_value) { | ||||||
|       data_.string_ptr = new std::string(value); |       // Allocate string and tag the pointer | ||||||
|  |       auto *str = new std::string(str_value); | ||||||
|  |       // Set LSB to 1 to indicate string pointer | ||||||
|  |       data_.tagged = reinterpret_cast<uintptr_t>(str) | 1; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // Destructor |     // Destructor | ||||||
|     ~MessageCreator() { |     ~MessageCreator() { | ||||||
|       // Clean up string data for string-based message types |       if (has_tagged_string_ptr_()) { | ||||||
|       if (uses_string_data_()) { |         delete get_string_ptr_(); | ||||||
|         delete data_.string_ptr; |  | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // Copy constructor |     // Copy constructor | ||||||
|     MessageCreator(const MessageCreator &other) : message_type_(other.message_type_) { |     MessageCreator(const MessageCreator &other) { | ||||||
|       if (message_type_ == 0) { |       if (other.has_tagged_string_ptr_()) { | ||||||
|         data_.ptr = other.data_.ptr; |         auto *str = new std::string(*other.get_string_ptr_()); | ||||||
|       } else if (uses_string_data_()) { |         data_.tagged = reinterpret_cast<uintptr_t>(str) | 1; | ||||||
|         data_.string_ptr = new std::string(*other.data_.string_ptr); |  | ||||||
|       } else { |       } else { | ||||||
|         data_ = other.data_;  // For POD types |         data_ = other.data_; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // Move constructor |     // Move constructor | ||||||
|     MessageCreator(MessageCreator &&other) noexcept : data_(other.data_), message_type_(other.message_type_) { |     MessageCreator(MessageCreator &&other) noexcept : data_(other.data_) { other.data_.ptr = nullptr; } | ||||||
|       other.message_type_ = 0;  // Reset other to function pointer type |  | ||||||
|       other.data_.ptr = nullptr; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // Assignment operators (needed for batch deduplication) |     // Assignment operators (needed for batch deduplication) | ||||||
|     MessageCreator &operator=(const MessageCreator &other) { |     MessageCreator &operator=(const MessageCreator &other) { | ||||||
|       if (this != &other) { |       if (this != &other) { | ||||||
|         // Clean up current string data if needed |         // Clean up current string data if needed | ||||||
|         if (uses_string_data_()) { |         if (has_tagged_string_ptr_()) { | ||||||
|           delete data_.string_ptr; |           delete get_string_ptr_(); | ||||||
|         } |         } | ||||||
|         // Copy new data |         // Copy new data | ||||||
|         message_type_ = other.message_type_; |         if (other.has_tagged_string_ptr_()) { | ||||||
|         if (other.message_type_ == 0) { |           auto *str = new std::string(*other.get_string_ptr_()); | ||||||
|           data_.ptr = other.data_.ptr; |           data_.tagged = reinterpret_cast<uintptr_t>(str) | 1; | ||||||
|         } else if (other.uses_string_data_()) { |  | ||||||
|           data_.string_ptr = new std::string(*other.data_.string_ptr); |  | ||||||
|         } else { |         } else { | ||||||
|           data_ = other.data_; |           data_ = other.data_; | ||||||
|         } |         } | ||||||
| @@ -542,30 +544,35 @@ class APIConnection : public APIServerConnection { | |||||||
|     MessageCreator &operator=(MessageCreator &&other) noexcept { |     MessageCreator &operator=(MessageCreator &&other) noexcept { | ||||||
|       if (this != &other) { |       if (this != &other) { | ||||||
|         // Clean up current string data if needed |         // Clean up current string data if needed | ||||||
|         if (uses_string_data_()) { |         if (has_tagged_string_ptr_()) { | ||||||
|           delete data_.string_ptr; |           delete get_string_ptr_(); | ||||||
|         } |         } | ||||||
|         // Move data |         // Move data | ||||||
|         message_type_ = other.message_type_; |  | ||||||
|         data_ = other.data_; |         data_ = other.data_; | ||||||
|         // Reset other to safe state |         // Reset other to safe state | ||||||
|         other.message_type_ = 0; |  | ||||||
|         other.data_.ptr = nullptr; |         other.data_.ptr = nullptr; | ||||||
|       } |       } | ||||||
|       return *this; |       return *this; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // Call operator |     // Call operator - now accepts message_type as parameter | ||||||
|     uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) const; |     uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single, | ||||||
|  |                         uint16_t message_type) const; | ||||||
|  |  | ||||||
|    private: |    private: | ||||||
|     // Helper to check if this message type uses heap-allocated strings |     // Check if this contains a string pointer | ||||||
|     bool uses_string_data_() const { return message_type_ == EventResponse::MESSAGE_TYPE; } |     bool has_tagged_string_ptr_() const { return (data_.tagged & 1) != 0; } | ||||||
|     union CreatorData { |  | ||||||
|       MessageCreatorPtr ptr;    // 8 bytes |     // Get the actual string pointer (clears the tag bit) | ||||||
|       std::string *string_ptr;  // 8 bytes |     std::string *get_string_ptr_() const { | ||||||
|     } data_;                    // 8 bytes |       // NOLINTNEXTLINE(performance-no-int-to-ptr) | ||||||
|     uint16_t message_type_;     // 2 bytes (0 = function ptr, >0 = state capture) |       return reinterpret_cast<std::string *>(data_.tagged & ~uintptr_t(1)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     union { | ||||||
|  |       MessageCreatorPtr ptr; | ||||||
|  |       uintptr_t tagged; | ||||||
|  |     } data_;  // 4 bytes on 32-bit | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   // Generic batching mechanism for both state updates and entity info |   // Generic batching mechanism for both state updates and entity info | ||||||
|   | |||||||
| @@ -66,6 +66,17 @@ const char *api_error_to_str(APIError err) { | |||||||
|   return "UNKNOWN"; |   return "UNKNOWN"; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // Default implementation for loop - handles sending buffered data | ||||||
|  | APIError APIFrameHelper::loop() { | ||||||
|  |   if (!this->tx_buf_.empty()) { | ||||||
|  |     APIError err = try_send_tx_buf_(); | ||||||
|  |     if (err != APIError::OK && err != APIError::WOULD_BLOCK) { | ||||||
|  |       return err; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return APIError::OK;  // Convert WOULD_BLOCK to OK to avoid connection termination | ||||||
|  | } | ||||||
|  |  | ||||||
| // Helper method to buffer data from IOVs | // Helper method to buffer data from IOVs | ||||||
| void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len) { | void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len) { | ||||||
|   SendBuffer buffer; |   SendBuffer buffer; | ||||||
| @@ -287,13 +298,8 @@ APIError APINoiseFrameHelper::loop() { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (!this->tx_buf_.empty()) { |   // Use base class implementation for buffer sending | ||||||
|     APIError err = try_send_tx_buf_(); |   return APIFrameHelper::loop(); | ||||||
|     if (err != APIError::OK && err != APIError::WOULD_BLOCK) { |  | ||||||
|       return err; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   return APIError::OK;  // Convert WOULD_BLOCK to OK to avoid connection termination |  | ||||||
| } | } | ||||||
|  |  | ||||||
| /** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter | /** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter | ||||||
| @@ -339,17 +345,15 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) { | |||||||
|       return APIError::WOULD_BLOCK; |       return APIError::WOULD_BLOCK; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     if (rx_header_buf_[0] != 0x01) { | ||||||
|  |       state_ = State::FAILED; | ||||||
|  |       HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]); | ||||||
|  |       return APIError::BAD_INDICATOR; | ||||||
|  |     } | ||||||
|     // header reading done |     // header reading done | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // read body |   // read body | ||||||
|   uint8_t indicator = rx_header_buf_[0]; |  | ||||||
|   if (indicator != 0x01) { |  | ||||||
|     state_ = State::FAILED; |  | ||||||
|     HELPER_LOG("Bad indicator byte %u", indicator); |  | ||||||
|     return APIError::BAD_INDICATOR; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2]; |   uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2]; | ||||||
|  |  | ||||||
|   if (state_ != State::DATA && msg_size > 128) { |   if (state_ != State::DATA && msg_size > 128) { | ||||||
| @@ -595,10 +599,6 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { | |||||||
|     return APIError::BAD_DATA_PACKET; |     return APIError::BAD_DATA_PACKET; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // uint16_t type; |  | ||||||
|   // uint16_t data_len; |  | ||||||
|   // uint8_t *data; |  | ||||||
|   // uint8_t *padding;  zero or more bytes to fill up the rest of the packet |  | ||||||
|   uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1]; |   uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1]; | ||||||
|   uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3]; |   uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3]; | ||||||
|   if (data_len > msg_size - 4) { |   if (data_len > msg_size - 4) { | ||||||
| @@ -831,18 +831,12 @@ APIError APIPlaintextFrameHelper::init() { | |||||||
|   state_ = State::DATA; |   state_ = State::DATA; | ||||||
|   return APIError::OK; |   return APIError::OK; | ||||||
| } | } | ||||||
| /// Not used for plaintext |  | ||||||
| APIError APIPlaintextFrameHelper::loop() { | APIError APIPlaintextFrameHelper::loop() { | ||||||
|   if (state_ != State::DATA) { |   if (state_ != State::DATA) { | ||||||
|     return APIError::BAD_STATE; |     return APIError::BAD_STATE; | ||||||
|   } |   } | ||||||
|   if (!this->tx_buf_.empty()) { |   // Use base class implementation for buffer sending | ||||||
|     APIError err = try_send_tx_buf_(); |   return APIFrameHelper::loop(); | ||||||
|     if (err != APIError::OK && err != APIError::WOULD_BLOCK) { |  | ||||||
|       return err; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   return APIError::OK;  // Convert WOULD_BLOCK to OK to avoid connection termination |  | ||||||
| } | } | ||||||
|  |  | ||||||
| /** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter | /** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter | ||||||
|   | |||||||
| @@ -38,7 +38,7 @@ struct PacketInfo { | |||||||
|       : message_type(type), offset(off), payload_size(size), padding(0) {} |       : message_type(type), offset(off), payload_size(size), padding(0) {} | ||||||
| }; | }; | ||||||
|  |  | ||||||
| enum class APIError : int { | enum class APIError : uint16_t { | ||||||
|   OK = 0, |   OK = 0, | ||||||
|   WOULD_BLOCK = 1001, |   WOULD_BLOCK = 1001, | ||||||
|   BAD_HANDSHAKE_PACKET_LEN = 1002, |   BAD_HANDSHAKE_PACKET_LEN = 1002, | ||||||
| @@ -74,7 +74,7 @@ class APIFrameHelper { | |||||||
|   } |   } | ||||||
|   virtual ~APIFrameHelper() = default; |   virtual ~APIFrameHelper() = default; | ||||||
|   virtual APIError init() = 0; |   virtual APIError init() = 0; | ||||||
|   virtual APIError loop() = 0; |   virtual APIError loop(); | ||||||
|   virtual APIError read_packet(ReadPacketBuffer *buffer) = 0; |   virtual APIError read_packet(ReadPacketBuffer *buffer) = 0; | ||||||
|   bool can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); } |   bool can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); } | ||||||
|   std::string getpeername() { return socket_->getpeername(); } |   std::string getpeername() { return socket_->getpeername(); } | ||||||
|   | |||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -19,7 +19,7 @@ class APIServerConnectionBase : public ProtoService { | |||||||
|  |  | ||||||
|   template<typename T> bool send_message(const T &msg) { |   template<typename T> bool send_message(const T &msg) { | ||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|     this->log_send_message_(T::message_name(), msg.dump()); |     this->log_send_message_(msg.message_name(), msg.dump()); | ||||||
| #endif | #endif | ||||||
|     return this->send_message_(msg, T::MESSAGE_TYPE); |     return this->send_message_(msg, T::MESSAGE_TYPE); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -47,6 +47,11 @@ void APIServer::setup() { | |||||||
|   } |   } | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|  |   // Schedule reboot if no clients connect within timeout | ||||||
|  |   if (this->reboot_timeout_ != 0) { | ||||||
|  |     this->schedule_reboot_timeout_(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0);  // monitored for incoming connections |   this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0);  // monitored for incoming connections | ||||||
|   if (this->socket_ == nullptr) { |   if (this->socket_ == nullptr) { | ||||||
|     ESP_LOGW(TAG, "Could not create socket"); |     ESP_LOGW(TAG, "Could not create socket"); | ||||||
| @@ -106,8 +111,6 @@ void APIServer::setup() { | |||||||
|   } |   } | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|   this->last_connected_ = App.get_loop_component_start_time(); |  | ||||||
|  |  | ||||||
| #ifdef USE_ESP32_CAMERA | #ifdef USE_ESP32_CAMERA | ||||||
|   if (esp32_camera::global_esp32_camera != nullptr && !esp32_camera::global_esp32_camera->is_internal()) { |   if (esp32_camera::global_esp32_camera != nullptr && !esp32_camera::global_esp32_camera->is_internal()) { | ||||||
|     esp32_camera::global_esp32_camera->add_image_callback( |     esp32_camera::global_esp32_camera->add_image_callback( | ||||||
| @@ -121,6 +124,16 @@ void APIServer::setup() { | |||||||
| #endif | #endif | ||||||
| } | } | ||||||
|  |  | ||||||
|  | void APIServer::schedule_reboot_timeout_() { | ||||||
|  |   this->status_set_warning(); | ||||||
|  |   this->set_timeout("api_reboot", this->reboot_timeout_, []() { | ||||||
|  |     if (!global_api_server->is_connected()) { | ||||||
|  |       ESP_LOGE(TAG, "No clients; rebooting"); | ||||||
|  |       App.reboot(); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
| void APIServer::loop() { | void APIServer::loop() { | ||||||
|   // Accept new clients only if the socket exists and has incoming connections |   // Accept new clients only if the socket exists and has incoming connections | ||||||
|   if (this->socket_ && this->socket_->ready()) { |   if (this->socket_ && this->socket_->ready()) { | ||||||
| @@ -130,51 +143,61 @@ void APIServer::loop() { | |||||||
|       auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len); |       auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len); | ||||||
|       if (!sock) |       if (!sock) | ||||||
|         break; |         break; | ||||||
|       ESP_LOGD(TAG, "Accepted %s", sock->getpeername().c_str()); |       ESP_LOGD(TAG, "Accept %s", sock->getpeername().c_str()); | ||||||
|  |  | ||||||
|       auto *conn = new APIConnection(std::move(sock), this); |       auto *conn = new APIConnection(std::move(sock), this); | ||||||
|       this->clients_.emplace_back(conn); |       this->clients_.emplace_back(conn); | ||||||
|       conn->start(); |       conn->start(); | ||||||
|  |  | ||||||
|  |       // Clear warning status and cancel reboot when first client connects | ||||||
|  |       if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) { | ||||||
|  |         this->status_clear_warning(); | ||||||
|  |         this->cancel_timeout("api_reboot"); | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   if (this->clients_.empty()) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   // Process clients and remove disconnected ones in a single pass |   // Process clients and remove disconnected ones in a single pass | ||||||
|   if (!this->clients_.empty()) { |   // Check network connectivity once for all clients | ||||||
|  |   if (!network::is_connected()) { | ||||||
|  |     // Network is down - disconnect all clients | ||||||
|  |     for (auto &client : this->clients_) { | ||||||
|  |       client->on_fatal_error(); | ||||||
|  |       ESP_LOGW(TAG, "%s: Network down; disconnect", client->get_client_combined_info().c_str()); | ||||||
|  |     } | ||||||
|  |     // Continue to process and clean up the clients below | ||||||
|  |   } | ||||||
|  |  | ||||||
|   size_t client_index = 0; |   size_t client_index = 0; | ||||||
|   while (client_index < this->clients_.size()) { |   while (client_index < this->clients_.size()) { | ||||||
|     auto &client = this->clients_[client_index]; |     auto &client = this->clients_[client_index]; | ||||||
|  |  | ||||||
|       if (client->remove_) { |     if (!client->remove_) { | ||||||
|         // Handle disconnection |       // Common case: process active client | ||||||
|  |       client->loop(); | ||||||
|  |       client_index++; | ||||||
|  |       continue; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Rare case: handle disconnection | ||||||
|     this->client_disconnected_trigger_->trigger(client->client_info_, client->client_peername_); |     this->client_disconnected_trigger_->trigger(client->client_info_, client->client_peername_); | ||||||
|         ESP_LOGV(TAG, "Removing connection to %s", client->client_info_.c_str()); |     ESP_LOGV(TAG, "Remove connection %s", client->client_info_.c_str()); | ||||||
|  |  | ||||||
|     // Swap with the last element and pop (avoids expensive vector shifts) |     // Swap with the last element and pop (avoids expensive vector shifts) | ||||||
|     if (client_index < this->clients_.size() - 1) { |     if (client_index < this->clients_.size() - 1) { | ||||||
|       std::swap(this->clients_[client_index], this->clients_.back()); |       std::swap(this->clients_[client_index], this->clients_.back()); | ||||||
|     } |     } | ||||||
|     this->clients_.pop_back(); |     this->clients_.pop_back(); | ||||||
|         // Don't increment client_index since we need to process the swapped element |  | ||||||
|       } else { |  | ||||||
|         // Process active client |  | ||||||
|         client->loop(); |  | ||||||
|         client_index++;  // Move to next client |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   if (this->reboot_timeout_ != 0) { |     // Schedule reboot when last client disconnects | ||||||
|     const uint32_t now = App.get_loop_component_start_time(); |     if (this->clients_.empty() && this->reboot_timeout_ != 0) { | ||||||
|     if (!this->is_connected()) { |       this->schedule_reboot_timeout_(); | ||||||
|       if (now - this->last_connected_ > this->reboot_timeout_) { |  | ||||||
|         ESP_LOGE(TAG, "No client connected; rebooting"); |  | ||||||
|         App.reboot(); |  | ||||||
|       } |  | ||||||
|       this->status_set_warning(); |  | ||||||
|     } else { |  | ||||||
|       this->last_connected_ = now; |  | ||||||
|       this->status_clear_warning(); |  | ||||||
|     } |     } | ||||||
|  |     // Don't increment client_index since we need to process the swapped element | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -142,6 +142,7 @@ class APIServer : public Component, public Controller { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|  |   void schedule_reboot_timeout_(); | ||||||
|   // Pointers and pointer-like types first (4 bytes each) |   // Pointers and pointer-like types first (4 bytes each) | ||||||
|   std::unique_ptr<socket::Socket> socket_ = nullptr; |   std::unique_ptr<socket::Socket> socket_ = nullptr; | ||||||
|   Trigger<std::string, std::string> *client_connected_trigger_ = new Trigger<std::string, std::string>(); |   Trigger<std::string, std::string> *client_connected_trigger_ = new Trigger<std::string, std::string>(); | ||||||
| @@ -150,7 +151,6 @@ class APIServer : public Component, public Controller { | |||||||
|   // 4-byte aligned types |   // 4-byte aligned types | ||||||
|   uint32_t reboot_timeout_{300000}; |   uint32_t reboot_timeout_{300000}; | ||||||
|   uint32_t batch_delay_{100}; |   uint32_t batch_delay_{100}; | ||||||
|   uint32_t last_connected_{0}; |  | ||||||
|  |  | ||||||
|   // Vectors and strings (12 bytes each on 32-bit) |   // Vectors and strings (12 bytes each on 32-bit) | ||||||
|   std::vector<std::unique_ptr<APIConnection>> clients_; |   std::vector<std::unique_ptr<APIConnection>> clients_; | ||||||
|   | |||||||
| @@ -4,9 +4,15 @@ import asyncio | |||||||
| from datetime import datetime | from datetime import datetime | ||||||
| import logging | import logging | ||||||
| from typing import TYPE_CHECKING, Any | from typing import TYPE_CHECKING, Any | ||||||
|  | import warnings | ||||||
|  |  | ||||||
| from aioesphomeapi import APIClient, parse_log_message | # Suppress protobuf version warnings | ||||||
| from aioesphomeapi.log_runner import async_run | with warnings.catch_warnings(): | ||||||
|  |     warnings.filterwarnings( | ||||||
|  |         "ignore", category=UserWarning, message=".*Protobuf gencode version.*" | ||||||
|  |     ) | ||||||
|  |     from aioesphomeapi import APIClient, parse_log_message | ||||||
|  |     from aioesphomeapi.log_runner import async_run | ||||||
|  |  | ||||||
| from esphome.const import CONF_KEY, CONF_PASSWORD, CONF_PORT, __version__ | from esphome.const import CONF_KEY, CONF_PASSWORD, CONF_PORT, __version__ | ||||||
| from esphome.core import CORE | from esphome.core import CORE | ||||||
|   | |||||||
| @@ -335,6 +335,7 @@ class ProtoMessage { | |||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|   std::string dump() const; |   std::string dump() const; | ||||||
|   virtual void dump_to(std::string &out) const = 0; |   virtual void dump_to(std::string &out) const = 0; | ||||||
|  |   virtual const char *message_name() const { return "unknown"; } | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| #include "atm90e32.h" | #include "atm90e32.h" | ||||||
| #include <cinttypes> | #include <cinttypes> | ||||||
| #include <cmath> | #include <cmath> | ||||||
|  | #include <numbers> | ||||||
| #include "esphome/core/log.h" | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| @@ -848,7 +849,7 @@ uint16_t ATM90E32Component::calculate_voltage_threshold(int line_freq, uint16_t | |||||||
|   float nominal_voltage = (line_freq == 60) ? 120.0f : 220.0f; |   float nominal_voltage = (line_freq == 60) ? 120.0f : 220.0f; | ||||||
|   float target_voltage = nominal_voltage * multiplier; |   float target_voltage = nominal_voltage * multiplier; | ||||||
|  |  | ||||||
|   float peak_01v = target_voltage * 100.0f * std::sqrt(2.0f);  // convert RMS → peak, scale to 0.01V |   float peak_01v = target_voltage * 100.0f * std::numbers::sqrt2_v<float>;  // convert RMS → peak, scale to 0.01V | ||||||
|   float divider = (2.0f * ugain) / 32768.0f; |   float divider = (2.0f * ugain) / 32768.0f; | ||||||
|  |  | ||||||
|   float threshold = peak_01v / divider; |   float threshold = peak_01v / divider; | ||||||
|   | |||||||
| @@ -312,7 +312,7 @@ FileDecoderState AudioDecoder::decode_mp3_() { | |||||||
|   if (err) { |   if (err) { | ||||||
|     switch (err) { |     switch (err) { | ||||||
|       case esp_audio_libs::helix_decoder::ERR_MP3_OUT_OF_MEMORY: |       case esp_audio_libs::helix_decoder::ERR_MP3_OUT_OF_MEMORY: | ||||||
|         // Intentional fallthrough |         [[fallthrough]]; | ||||||
|       case esp_audio_libs::helix_decoder::ERR_MP3_NULL_POINTER: |       case esp_audio_libs::helix_decoder::ERR_MP3_NULL_POINTER: | ||||||
|         return FileDecoderState::FAILED; |         return FileDecoderState::FAILED; | ||||||
|         break; |         break; | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ | |||||||
| #include "esphome/core/defines.h" | #include "esphome/core/defines.h" | ||||||
| #include "esphome/core/hal.h" | #include "esphome/core/hal.h" | ||||||
| #include "esphome/core/helpers.h" | #include "esphome/core/helpers.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
| #if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE | #if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE | ||||||
| #include "esp_crt_bundle.h" | #include "esp_crt_bundle.h" | ||||||
| @@ -16,13 +17,13 @@ namespace audio { | |||||||
| static const uint32_t READ_WRITE_TIMEOUT_MS = 20; | static const uint32_t READ_WRITE_TIMEOUT_MS = 20; | ||||||
|  |  | ||||||
| static const uint32_t CONNECTION_TIMEOUT_MS = 5000; | static const uint32_t CONNECTION_TIMEOUT_MS = 5000; | ||||||
|  | static const uint8_t MAX_FETCHING_HEADER_ATTEMPTS = 6; | ||||||
| // The number of times the http read times out with no data before throwing an error |  | ||||||
| static const uint32_t ERROR_COUNT_NO_DATA_READ_TIMEOUT = 100; |  | ||||||
|  |  | ||||||
| static const size_t HTTP_STREAM_BUFFER_SIZE = 2048; | static const size_t HTTP_STREAM_BUFFER_SIZE = 2048; | ||||||
|  |  | ||||||
| static const uint8_t MAX_REDIRECTION = 5; | static const uint8_t MAX_REDIRECTIONS = 5; | ||||||
|  |  | ||||||
|  | static const char *const TAG = "audio_reader"; | ||||||
|  |  | ||||||
| // Some common HTTP status codes - borrowed from http_request component accessed 20241224 | // Some common HTTP status codes - borrowed from http_request component accessed 20241224 | ||||||
| enum HttpStatus { | enum HttpStatus { | ||||||
| @@ -94,7 +95,7 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) { | |||||||
|   client_config.url = uri.c_str(); |   client_config.url = uri.c_str(); | ||||||
|   client_config.cert_pem = nullptr; |   client_config.cert_pem = nullptr; | ||||||
|   client_config.disable_auto_redirect = false; |   client_config.disable_auto_redirect = false; | ||||||
|   client_config.max_redirection_count = 10; |   client_config.max_redirection_count = MAX_REDIRECTIONS; | ||||||
|   client_config.event_handler = http_event_handler; |   client_config.event_handler = http_event_handler; | ||||||
|   client_config.user_data = this; |   client_config.user_data = this; | ||||||
|   client_config.buffer_size = HTTP_STREAM_BUFFER_SIZE; |   client_config.buffer_size = HTTP_STREAM_BUFFER_SIZE; | ||||||
| @@ -116,12 +117,29 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) { | |||||||
|   esp_err_t err = esp_http_client_open(this->client_, 0); |   esp_err_t err = esp_http_client_open(this->client_, 0); | ||||||
|  |  | ||||||
|   if (err != ESP_OK) { |   if (err != ESP_OK) { | ||||||
|  |     ESP_LOGE(TAG, "Failed to open URL"); | ||||||
|     this->cleanup_connection_(); |     this->cleanup_connection_(); | ||||||
|     return err; |     return err; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   int64_t header_length = esp_http_client_fetch_headers(this->client_); |   int64_t header_length = esp_http_client_fetch_headers(this->client_); | ||||||
|  |   uint8_t reattempt_count = 0; | ||||||
|  |   while ((header_length < 0) && (reattempt_count < MAX_FETCHING_HEADER_ATTEMPTS)) { | ||||||
|  |     this->cleanup_connection_(); | ||||||
|  |     if (header_length != -ESP_ERR_HTTP_EAGAIN) { | ||||||
|  |       // Serious error, no recovery | ||||||
|  |       return ESP_FAIL; | ||||||
|  |     } else { | ||||||
|  |       // Reconnect from a fresh state to avoid a bug where it never reads the headers even if made available | ||||||
|  |       this->client_ = esp_http_client_init(&client_config); | ||||||
|  |       esp_http_client_open(this->client_, 0); | ||||||
|  |       header_length = esp_http_client_fetch_headers(this->client_); | ||||||
|  |       ++reattempt_count; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   if (header_length < 0) { |   if (header_length < 0) { | ||||||
|  |     ESP_LOGE(TAG, "Failed to fetch headers"); | ||||||
|     this->cleanup_connection_(); |     this->cleanup_connection_(); | ||||||
|     return ESP_FAIL; |     return ESP_FAIL; | ||||||
|   } |   } | ||||||
| @@ -135,7 +153,7 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) { | |||||||
|  |  | ||||||
|   ssize_t redirect_count = 0; |   ssize_t redirect_count = 0; | ||||||
|  |  | ||||||
|   while ((esp_http_client_set_redirection(this->client_) == ESP_OK) && (redirect_count < MAX_REDIRECTION)) { |   while ((esp_http_client_set_redirection(this->client_) == ESP_OK) && (redirect_count < MAX_REDIRECTIONS)) { | ||||||
|     err = esp_http_client_open(this->client_, 0); |     err = esp_http_client_open(this->client_, 0); | ||||||
|     if (err != ESP_OK) { |     if (err != ESP_OK) { | ||||||
|       this->cleanup_connection_(); |       this->cleanup_connection_(); | ||||||
| @@ -267,21 +285,24 @@ AudioReaderState AudioReader::http_read_() { | |||||||
|       return AudioReaderState::FINISHED; |       return AudioReaderState::FINISHED; | ||||||
|     } |     } | ||||||
|   } else if (this->output_transfer_buffer_->free() > 0) { |   } else if (this->output_transfer_buffer_->free() > 0) { | ||||||
|     size_t bytes_to_read = this->output_transfer_buffer_->free(); |     int received_len = esp_http_client_read(this->client_, (char *) this->output_transfer_buffer_->get_buffer_end(), | ||||||
|     int received_len = |                                             this->output_transfer_buffer_->free()); | ||||||
|         esp_http_client_read(this->client_, (char *) this->output_transfer_buffer_->get_buffer_end(), bytes_to_read); |  | ||||||
|  |  | ||||||
|     if (received_len > 0) { |     if (received_len > 0) { | ||||||
|       this->output_transfer_buffer_->increase_buffer_length(received_len); |       this->output_transfer_buffer_->increase_buffer_length(received_len); | ||||||
|       this->last_data_read_ms_ = millis(); |       this->last_data_read_ms_ = millis(); | ||||||
|     } else if (received_len < 0) { |       return AudioReaderState::READING; | ||||||
|  |     } else if (received_len <= 0) { | ||||||
|       // HTTP read error |       // HTTP read error | ||||||
|  |       if (received_len == -1) { | ||||||
|  |         // A true connection error occured, no chance at recovery | ||||||
|         this->cleanup_connection_(); |         this->cleanup_connection_(); | ||||||
|         return AudioReaderState::FAILED; |         return AudioReaderState::FAILED; | ||||||
|     } else { |       } | ||||||
|       if (bytes_to_read > 0) { |  | ||||||
|         // Read timed out |       // Read timed out, manually verify if it has been too long since the last successful read | ||||||
|         if ((millis() - this->last_data_read_ms_) > CONNECTION_TIMEOUT_MS) { |       if ((millis() - this->last_data_read_ms_) > MAX_FETCHING_HEADER_ATTEMPTS * CONNECTION_TIMEOUT_MS) { | ||||||
|  |         ESP_LOGE(TAG, "Timed out"); | ||||||
|         this->cleanup_connection_(); |         this->cleanup_connection_(); | ||||||
|         return AudioReaderState::FAILED; |         return AudioReaderState::FAILED; | ||||||
|       } |       } | ||||||
| @@ -289,7 +310,6 @@ AudioReaderState AudioReader::http_read_() { | |||||||
|       delay(READ_WRITE_TIMEOUT_MS); |       delay(READ_WRITE_TIMEOUT_MS); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return AudioReaderState::READING; |   return AudioReaderState::READING; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| #include "display.h" | #include "display.h" | ||||||
| #include <utility> | #include <utility> | ||||||
|  | #include <numbers> | ||||||
| #include "display_color_utils.h" | #include "display_color_utils.h" | ||||||
| #include "esphome/core/hal.h" | #include "esphome/core/hal.h" | ||||||
| #include "esphome/core/log.h" | #include "esphome/core/log.h" | ||||||
| @@ -424,15 +425,15 @@ void HOT Display::get_regular_polygon_vertex(int vertex_id, int *vertex_x, int * | |||||||
|     // hence we rotate the shape by 270° to orient the polygon up. |     // hence we rotate the shape by 270° to orient the polygon up. | ||||||
|     rotation_degrees += ROTATION_270_DEGREES; |     rotation_degrees += ROTATION_270_DEGREES; | ||||||
|     // Convert the rotation to radians, easier to use in trigonometrical calculations |     // Convert the rotation to radians, easier to use in trigonometrical calculations | ||||||
|     float rotation_radians = rotation_degrees * PI / 180; |     float rotation_radians = rotation_degrees * std::numbers::pi / 180; | ||||||
|     // A pointy top variation means the first vertex of the polygon is at the top center of the shape, this requires no |     // A pointy top variation means the first vertex of the polygon is at the top center of the shape, this requires no | ||||||
|     // additional rotation of the shape. |     // additional rotation of the shape. | ||||||
|     // A flat top variation means the first point of the polygon has to be rotated so that the first edge is horizontal, |     // A flat top variation means the first point of the polygon has to be rotated so that the first edge is horizontal, | ||||||
|     // this requires to rotate the shape by π/edges radians counter-clockwise so that the first point is located on the |     // this requires to rotate the shape by π/edges radians counter-clockwise so that the first point is located on the | ||||||
|     // left side of the first horizontal edge. |     // left side of the first horizontal edge. | ||||||
|     rotation_radians -= (variation == VARIATION_FLAT_TOP) ? PI / edges : 0.0; |     rotation_radians -= (variation == VARIATION_FLAT_TOP) ? std::numbers::pi / edges : 0.0; | ||||||
|  |  | ||||||
|     float vertex_angle = ((float) vertex_id) / edges * 2 * PI + rotation_radians; |     float vertex_angle = ((float) vertex_id) / edges * 2 * std::numbers::pi + rotation_radians; | ||||||
|     *vertex_x = (int) round(cos(vertex_angle) * radius) + center_x; |     *vertex_x = (int) round(cos(vertex_angle) * radius) + center_x; | ||||||
|     *vertex_y = (int) round(sin(vertex_angle) * radius) + center_y; |     *vertex_y = (int) round(sin(vertex_angle) * radius) + center_y; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -138,8 +138,6 @@ enum DisplayRotation { | |||||||
|   DISPLAY_ROTATION_270_DEGREES = 270, |   DISPLAY_ROTATION_270_DEGREES = 270, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| #define PI 3.1415926535897932384626433832795 |  | ||||||
|  |  | ||||||
| const int EDGES_TRIGON = 3; | const int EDGES_TRIGON = 3; | ||||||
| const int EDGES_TRIANGLE = 3; | const int EDGES_TRIANGLE = 3; | ||||||
| const int EDGES_TETRAGON = 4; | const int EDGES_TETRAGON = 4; | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ import logging | |||||||
| import os | import os | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
|  |  | ||||||
| from esphome import git | from esphome import yaml_util | ||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome.const import ( | from esphome.const import ( | ||||||
| @@ -23,7 +23,6 @@ from esphome.const import ( | |||||||
|     CONF_REFRESH, |     CONF_REFRESH, | ||||||
|     CONF_SOURCE, |     CONF_SOURCE, | ||||||
|     CONF_TYPE, |     CONF_TYPE, | ||||||
|     CONF_URL, |  | ||||||
|     CONF_VARIANT, |     CONF_VARIANT, | ||||||
|     CONF_VERSION, |     CONF_VERSION, | ||||||
|     KEY_CORE, |     KEY_CORE, | ||||||
| @@ -32,14 +31,13 @@ from esphome.const import ( | |||||||
|     KEY_TARGET_FRAMEWORK, |     KEY_TARGET_FRAMEWORK, | ||||||
|     KEY_TARGET_PLATFORM, |     KEY_TARGET_PLATFORM, | ||||||
|     PLATFORM_ESP32, |     PLATFORM_ESP32, | ||||||
|     TYPE_GIT, |  | ||||||
|     TYPE_LOCAL, |  | ||||||
|     __version__, |     __version__, | ||||||
| ) | ) | ||||||
| from esphome.core import CORE, HexInt, TimePeriod | from esphome.core import CORE, HexInt, TimePeriod | ||||||
| from esphome.cpp_generator import RawExpression | from esphome.cpp_generator import RawExpression | ||||||
| import esphome.final_validate as fv | import esphome.final_validate as fv | ||||||
| from esphome.helpers import copy_file_if_changed, mkdir_p, write_file_if_changed | from esphome.helpers import copy_file_if_changed, mkdir_p, write_file_if_changed | ||||||
|  | from esphome.types import ConfigType | ||||||
|  |  | ||||||
| from .boards import BOARDS | from .boards import BOARDS | ||||||
| from .const import (  # noqa | from .const import (  # noqa | ||||||
| @@ -49,10 +47,8 @@ from .const import (  # noqa | |||||||
|     KEY_EXTRA_BUILD_FILES, |     KEY_EXTRA_BUILD_FILES, | ||||||
|     KEY_PATH, |     KEY_PATH, | ||||||
|     KEY_REF, |     KEY_REF, | ||||||
|     KEY_REFRESH, |  | ||||||
|     KEY_REPO, |     KEY_REPO, | ||||||
|     KEY_SDKCONFIG_OPTIONS, |     KEY_SDKCONFIG_OPTIONS, | ||||||
|     KEY_SUBMODULES, |  | ||||||
|     KEY_VARIANT, |     KEY_VARIANT, | ||||||
|     VARIANT_ESP32, |     VARIANT_ESP32, | ||||||
|     VARIANT_ESP32C2, |     VARIANT_ESP32C2, | ||||||
| @@ -235,7 +231,7 @@ def add_idf_sdkconfig_option(name: str, value: SdkconfigValueType): | |||||||
| def add_idf_component( | def add_idf_component( | ||||||
|     *, |     *, | ||||||
|     name: str, |     name: str, | ||||||
|     repo: str, |     repo: str = None, | ||||||
|     ref: str = None, |     ref: str = None, | ||||||
|     path: str = None, |     path: str = None, | ||||||
|     refresh: TimePeriod = None, |     refresh: TimePeriod = None, | ||||||
| @@ -245,30 +241,27 @@ def add_idf_component( | |||||||
|     """Add an esp-idf component to the project.""" |     """Add an esp-idf component to the project.""" | ||||||
|     if not CORE.using_esp_idf: |     if not CORE.using_esp_idf: | ||||||
|         raise ValueError("Not an esp-idf project") |         raise ValueError("Not an esp-idf project") | ||||||
|     if components is None: |     if not repo and not ref and not path: | ||||||
|         components = [] |         raise ValueError("Requires at least one of repo, ref or path") | ||||||
|     if name not in CORE.data[KEY_ESP32][KEY_COMPONENTS]: |     if refresh or submodules or components: | ||||||
|  |         _LOGGER.warning( | ||||||
|  |             "The refresh, components and submodules parameters in add_idf_component() are " | ||||||
|  |             "deprecated and will be removed in ESPHome 2026.1. If you are seeing this, report " | ||||||
|  |             "an issue to the external_component author and ask them to update it." | ||||||
|  |         ) | ||||||
|  |     if components: | ||||||
|  |         for comp in components: | ||||||
|  |             CORE.data[KEY_ESP32][KEY_COMPONENTS][comp] = { | ||||||
|  |                 KEY_REPO: repo, | ||||||
|  |                 KEY_REF: ref, | ||||||
|  |                 KEY_PATH: f"{path}/{comp}" if path else comp, | ||||||
|  |             } | ||||||
|  |     else: | ||||||
|         CORE.data[KEY_ESP32][KEY_COMPONENTS][name] = { |         CORE.data[KEY_ESP32][KEY_COMPONENTS][name] = { | ||||||
|             KEY_REPO: repo, |             KEY_REPO: repo, | ||||||
|             KEY_REF: ref, |             KEY_REF: ref, | ||||||
|             KEY_PATH: path, |             KEY_PATH: path, | ||||||
|             KEY_REFRESH: refresh, |  | ||||||
|             KEY_COMPONENTS: components, |  | ||||||
|             KEY_SUBMODULES: submodules, |  | ||||||
|         } |         } | ||||||
|     else: |  | ||||||
|         component_config = CORE.data[KEY_ESP32][KEY_COMPONENTS][name] |  | ||||||
|         if components is not None: |  | ||||||
|             component_config[KEY_COMPONENTS] = list( |  | ||||||
|                 set(component_config[KEY_COMPONENTS] + components) |  | ||||||
|             ) |  | ||||||
|         if submodules is not None: |  | ||||||
|             if component_config[KEY_SUBMODULES] is None: |  | ||||||
|                 component_config[KEY_SUBMODULES] = submodules |  | ||||||
|             else: |  | ||||||
|                 component_config[KEY_SUBMODULES] = list( |  | ||||||
|                     set(component_config[KEY_SUBMODULES] + submodules) |  | ||||||
|                 ) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def add_extra_script(stage: str, filename: str, path: str): | def add_extra_script(stage: str, filename: str, path: str): | ||||||
| @@ -348,6 +341,7 @@ SUPPORTED_PLATFORMIO_ESP_IDF_5X = [ | |||||||
| # List based on https://github.com/pioarduino/esp-idf/releases | # List based on https://github.com/pioarduino/esp-idf/releases | ||||||
| SUPPORTED_PIOARDUINO_ESP_IDF_5X = [ | SUPPORTED_PIOARDUINO_ESP_IDF_5X = [ | ||||||
|     cv.Version(5, 5, 0), |     cv.Version(5, 5, 0), | ||||||
|  |     cv.Version(5, 4, 2), | ||||||
|     cv.Version(5, 4, 1), |     cv.Version(5, 4, 1), | ||||||
|     cv.Version(5, 4, 0), |     cv.Version(5, 4, 0), | ||||||
|     cv.Version(5, 3, 3), |     cv.Version(5, 3, 3), | ||||||
| @@ -575,6 +569,17 @@ CONF_ENABLE_LWIP_DHCP_SERVER = "enable_lwip_dhcp_server" | |||||||
| CONF_ENABLE_LWIP_MDNS_QUERIES = "enable_lwip_mdns_queries" | CONF_ENABLE_LWIP_MDNS_QUERIES = "enable_lwip_mdns_queries" | ||||||
| CONF_ENABLE_LWIP_BRIDGE_INTERFACE = "enable_lwip_bridge_interface" | CONF_ENABLE_LWIP_BRIDGE_INTERFACE = "enable_lwip_bridge_interface" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _validate_idf_component(config: ConfigType) -> ConfigType: | ||||||
|  |     """Validate IDF component config and warn about deprecated options.""" | ||||||
|  |     if CONF_REFRESH in config: | ||||||
|  |         _LOGGER.warning( | ||||||
|  |             "The 'refresh' option for IDF components is deprecated and has no effect. " | ||||||
|  |             "It will be removed in ESPHome 2026.1. Please remove it from your configuration." | ||||||
|  |         ) | ||||||
|  |     return config | ||||||
|  |  | ||||||
|  |  | ||||||
| ESP_IDF_FRAMEWORK_SCHEMA = cv.All( | ESP_IDF_FRAMEWORK_SCHEMA = cv.All( | ||||||
|     cv.Schema( |     cv.Schema( | ||||||
|         { |         { | ||||||
| @@ -606,7 +611,7 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All( | |||||||
|                         CONF_ENABLE_LWIP_DHCP_SERVER, "wifi", default=False |                         CONF_ENABLE_LWIP_DHCP_SERVER, "wifi", default=False | ||||||
|                     ): cv.boolean, |                     ): cv.boolean, | ||||||
|                     cv.Optional( |                     cv.Optional( | ||||||
|                         CONF_ENABLE_LWIP_MDNS_QUERIES, default=False |                         CONF_ENABLE_LWIP_MDNS_QUERIES, default=True | ||||||
|                     ): cv.boolean, |                     ): cv.boolean, | ||||||
|                     cv.Optional( |                     cv.Optional( | ||||||
|                         CONF_ENABLE_LWIP_BRIDGE_INTERFACE, default=False |                         CONF_ENABLE_LWIP_BRIDGE_INTERFACE, default=False | ||||||
| @@ -614,15 +619,19 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All( | |||||||
|                 } |                 } | ||||||
|             ), |             ), | ||||||
|             cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list( |             cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list( | ||||||
|  |                 cv.All( | ||||||
|                     cv.Schema( |                     cv.Schema( | ||||||
|                         { |                         { | ||||||
|                             cv.Required(CONF_NAME): cv.string_strict, |                             cv.Required(CONF_NAME): cv.string_strict, | ||||||
|                         cv.Required(CONF_SOURCE): cv.SOURCE_SCHEMA, |                             cv.Optional(CONF_SOURCE): cv.git_ref, | ||||||
|  |                             cv.Optional(CONF_REF): cv.string, | ||||||
|                             cv.Optional(CONF_PATH): cv.string, |                             cv.Optional(CONF_PATH): cv.string, | ||||||
|                         cv.Optional(CONF_REFRESH, default="1d"): cv.All( |                             cv.Optional(CONF_REFRESH): cv.All( | ||||||
|                                 cv.string, cv.source_refresh |                                 cv.string, cv.source_refresh | ||||||
|                             ), |                             ), | ||||||
|                         } |                         } | ||||||
|  |                     ), | ||||||
|  |                     _validate_idf_component, | ||||||
|                 ) |                 ) | ||||||
|             ), |             ), | ||||||
|         } |         } | ||||||
| @@ -696,7 +705,7 @@ FINAL_VALIDATE_SCHEMA = cv.Schema(final_validate) | |||||||
| async def to_code(config): | async def to_code(config): | ||||||
|     cg.add_platformio_option("board", config[CONF_BOARD]) |     cg.add_platformio_option("board", config[CONF_BOARD]) | ||||||
|     cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE]) |     cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE]) | ||||||
|     cg.set_cpp_standard("gnu++17") |     cg.set_cpp_standard("gnu++20") | ||||||
|     cg.add_build_flag("-DUSE_ESP32") |     cg.add_build_flag("-DUSE_ESP32") | ||||||
|     cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) |     cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) | ||||||
|     cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{config[CONF_VARIANT]}") |     cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{config[CONF_VARIANT]}") | ||||||
| @@ -750,6 +759,9 @@ async def to_code(config): | |||||||
|         add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0", False) |         add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0", False) | ||||||
|         add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1", False) |         add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1", False) | ||||||
|  |  | ||||||
|  |         # Disable dynamic log level control to save memory | ||||||
|  |         add_idf_sdkconfig_option("CONFIG_LOG_DYNAMIC_LEVEL_CONTROL", False) | ||||||
|  |  | ||||||
|         # Set default CPU frequency |         # Set default CPU frequency | ||||||
|         add_idf_sdkconfig_option(f"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_{freq}", True) |         add_idf_sdkconfig_option(f"CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_{freq}", True) | ||||||
|  |  | ||||||
| @@ -762,7 +774,7 @@ async def to_code(config): | |||||||
|             and not advanced[CONF_ENABLE_LWIP_DHCP_SERVER] |             and not advanced[CONF_ENABLE_LWIP_DHCP_SERVER] | ||||||
|         ): |         ): | ||||||
|             add_idf_sdkconfig_option("CONFIG_LWIP_DHCPS", False) |             add_idf_sdkconfig_option("CONFIG_LWIP_DHCPS", False) | ||||||
|         if not advanced.get(CONF_ENABLE_LWIP_MDNS_QUERIES, False): |         if not advanced.get(CONF_ENABLE_LWIP_MDNS_QUERIES, True): | ||||||
|             add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False) |             add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False) | ||||||
|         if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False): |         if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False): | ||||||
|             add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0) |             add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0) | ||||||
| @@ -814,18 +826,12 @@ async def to_code(config): | |||||||
|             add_idf_sdkconfig_option(name, RawSdkconfigValue(value)) |             add_idf_sdkconfig_option(name, RawSdkconfigValue(value)) | ||||||
|  |  | ||||||
|         for component in conf[CONF_COMPONENTS]: |         for component in conf[CONF_COMPONENTS]: | ||||||
|             source = component[CONF_SOURCE] |  | ||||||
|             if source[CONF_TYPE] == TYPE_GIT: |  | ||||||
|             add_idf_component( |             add_idf_component( | ||||||
|                 name=component[CONF_NAME], |                 name=component[CONF_NAME], | ||||||
|                     repo=source[CONF_URL], |                 repo=component.get(CONF_SOURCE), | ||||||
|                     ref=source.get(CONF_REF), |                 ref=component.get(CONF_REF), | ||||||
|                 path=component.get(CONF_PATH), |                 path=component.get(CONF_PATH), | ||||||
|                     refresh=component[CONF_REFRESH], |  | ||||||
|             ) |             ) | ||||||
|             elif source[CONF_TYPE] == TYPE_LOCAL: |  | ||||||
|                 _LOGGER.warning("Local components are not implemented yet.") |  | ||||||
|  |  | ||||||
|     elif conf[CONF_TYPE] == FRAMEWORK_ARDUINO: |     elif conf[CONF_TYPE] == FRAMEWORK_ARDUINO: | ||||||
|         cg.add_platformio_option("framework", "arduino") |         cg.add_platformio_option("framework", "arduino") | ||||||
|         cg.add_build_flag("-DUSE_ARDUINO") |         cg.add_build_flag("-DUSE_ARDUINO") | ||||||
| @@ -924,6 +930,26 @@ def _write_sdkconfig(): | |||||||
|         write_file_if_changed(sdk_path, contents) |         write_file_if_changed(sdk_path, contents) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _write_idf_component_yml(): | ||||||
|  |     yml_path = Path(CORE.relative_build_path("src/idf_component.yml")) | ||||||
|  |     if CORE.data[KEY_ESP32][KEY_COMPONENTS]: | ||||||
|  |         components: dict = CORE.data[KEY_ESP32][KEY_COMPONENTS] | ||||||
|  |         dependencies = {} | ||||||
|  |         for name, component in components.items(): | ||||||
|  |             dependency = {} | ||||||
|  |             if component[KEY_REF]: | ||||||
|  |                 dependency["version"] = component[KEY_REF] | ||||||
|  |             if component[KEY_REPO]: | ||||||
|  |                 dependency["git"] = component[KEY_REPO] | ||||||
|  |             if component[KEY_PATH]: | ||||||
|  |                 dependency["path"] = component[KEY_PATH] | ||||||
|  |             dependencies[name] = dependency | ||||||
|  |         contents = yaml_util.dump({"dependencies": dependencies}) | ||||||
|  |     else: | ||||||
|  |         contents = "" | ||||||
|  |     write_file_if_changed(yml_path, contents) | ||||||
|  |  | ||||||
|  |  | ||||||
| # Called by writer.py | # Called by writer.py | ||||||
| def copy_files(): | def copy_files(): | ||||||
|     if CORE.using_arduino: |     if CORE.using_arduino: | ||||||
| @@ -936,6 +962,7 @@ def copy_files(): | |||||||
|             ) |             ) | ||||||
|     if CORE.using_esp_idf: |     if CORE.using_esp_idf: | ||||||
|         _write_sdkconfig() |         _write_sdkconfig() | ||||||
|  |         _write_idf_component_yml() | ||||||
|         if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]: |         if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]: | ||||||
|             write_file_if_changed( |             write_file_if_changed( | ||||||
|                 CORE.relative_build_path("partitions.csv"), |                 CORE.relative_build_path("partitions.csv"), | ||||||
| @@ -952,55 +979,6 @@ def copy_files(): | |||||||
|             __version__, |             __version__, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         import shutil |  | ||||||
|  |  | ||||||
|         shutil.rmtree(CORE.relative_build_path("components"), ignore_errors=True) |  | ||||||
|  |  | ||||||
|         if CORE.data[KEY_ESP32][KEY_COMPONENTS]: |  | ||||||
|             components: dict = CORE.data[KEY_ESP32][KEY_COMPONENTS] |  | ||||||
|  |  | ||||||
|             for name, component in components.items(): |  | ||||||
|                 repo_dir, _ = git.clone_or_update( |  | ||||||
|                     url=component[KEY_REPO], |  | ||||||
|                     ref=component[KEY_REF], |  | ||||||
|                     refresh=component[KEY_REFRESH], |  | ||||||
|                     domain="idf_components", |  | ||||||
|                     submodules=component[KEY_SUBMODULES], |  | ||||||
|                 ) |  | ||||||
|                 mkdir_p(CORE.relative_build_path("components")) |  | ||||||
|                 component_dir = repo_dir |  | ||||||
|                 if component[KEY_PATH] is not None: |  | ||||||
|                     component_dir = component_dir / component[KEY_PATH] |  | ||||||
|  |  | ||||||
|                 if component[KEY_COMPONENTS] == ["*"]: |  | ||||||
|                     shutil.copytree( |  | ||||||
|                         component_dir, |  | ||||||
|                         CORE.relative_build_path("components"), |  | ||||||
|                         dirs_exist_ok=True, |  | ||||||
|                         ignore=shutil.ignore_patterns(".git*"), |  | ||||||
|                         symlinks=True, |  | ||||||
|                         ignore_dangling_symlinks=True, |  | ||||||
|                     ) |  | ||||||
|                 elif len(component[KEY_COMPONENTS]) > 0: |  | ||||||
|                     for comp in component[KEY_COMPONENTS]: |  | ||||||
|                         shutil.copytree( |  | ||||||
|                             component_dir / comp, |  | ||||||
|                             CORE.relative_build_path(f"components/{comp}"), |  | ||||||
|                             dirs_exist_ok=True, |  | ||||||
|                             ignore=shutil.ignore_patterns(".git*"), |  | ||||||
|                             symlinks=True, |  | ||||||
|                             ignore_dangling_symlinks=True, |  | ||||||
|                         ) |  | ||||||
|                 else: |  | ||||||
|                     shutil.copytree( |  | ||||||
|                         component_dir, |  | ||||||
|                         CORE.relative_build_path(f"components/{name}"), |  | ||||||
|                         dirs_exist_ok=True, |  | ||||||
|                         ignore=shutil.ignore_patterns(".git*"), |  | ||||||
|                         symlinks=True, |  | ||||||
|                         ignore_dangling_symlinks=True, |  | ||||||
|                     ) |  | ||||||
|  |  | ||||||
|     for _, file in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES].items(): |     for _, file in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES].items(): | ||||||
|         if file[KEY_PATH].startswith("http"): |         if file[KEY_PATH].startswith("http"): | ||||||
|             import requests |             import requests | ||||||
|   | |||||||
| @@ -29,9 +29,9 @@ class ESP32InternalGPIOPin : public InternalGPIOPin { | |||||||
|   void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; |   void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; | ||||||
|  |  | ||||||
|   gpio_num_t pin_; |   gpio_num_t pin_; | ||||||
|   bool inverted_; |  | ||||||
|   gpio_drive_cap_t drive_strength_; |   gpio_drive_cap_t drive_strength_; | ||||||
|   gpio::Flags flags_; |   gpio::Flags flags_; | ||||||
|  |   bool inverted_; | ||||||
|   // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) |   // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) | ||||||
|   static bool isr_service_installed; |   static bool isr_service_installed; | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -496,17 +496,17 @@ float BLEClientBase::parse_char_value(uint8_t *value, uint16_t length) { | |||||||
|       if (length > 2) { |       if (length > 2) { | ||||||
|         return (float) encode_uint16(value[1], value[2]); |         return (float) encode_uint16(value[1], value[2]); | ||||||
|       } |       } | ||||||
|       // fall through |       [[fallthrough]]; | ||||||
|     case 0x7:  // uint24. |     case 0x7:  // uint24. | ||||||
|       if (length > 3) { |       if (length > 3) { | ||||||
|         return (float) encode_uint24(value[1], value[2], value[3]); |         return (float) encode_uint24(value[1], value[2], value[3]); | ||||||
|       } |       } | ||||||
|       // fall through |       [[fallthrough]]; | ||||||
|     case 0x8:  // uint32. |     case 0x8:  // uint32. | ||||||
|       if (length > 4) { |       if (length > 4) { | ||||||
|         return (float) encode_uint32(value[1], value[2], value[3], value[4]); |         return (float) encode_uint32(value[1], value[2], value[3], value[4]); | ||||||
|       } |       } | ||||||
|       // fall through |       [[fallthrough]]; | ||||||
|     case 0xC:  // int8. |     case 0xC:  // int8. | ||||||
|       return (float) ((int8_t) value[1]); |       return (float) ((int8_t) value[1]); | ||||||
|     case 0xD:  // int12. |     case 0xD:  // int12. | ||||||
| @@ -514,12 +514,12 @@ float BLEClientBase::parse_char_value(uint8_t *value, uint16_t length) { | |||||||
|       if (length > 2) { |       if (length > 2) { | ||||||
|         return (float) ((int16_t) (value[1] << 8) + (int16_t) value[2]); |         return (float) ((int16_t) (value[1] << 8) + (int16_t) value[2]); | ||||||
|       } |       } | ||||||
|       // fall through |       [[fallthrough]]; | ||||||
|     case 0xF:  // int24. |     case 0xF:  // int24. | ||||||
|       if (length > 3) { |       if (length > 3) { | ||||||
|         return (float) ((int32_t) (value[1] << 16) + (int32_t) (value[2] << 8) + (int32_t) (value[3])); |         return (float) ((int32_t) (value[1] << 16) + (int32_t) (value[2] << 8) + (int32_t) (value[3])); | ||||||
|       } |       } | ||||||
|       // fall through |       [[fallthrough]]; | ||||||
|     case 0x10:  // int32. |     case 0x10:  // int32. | ||||||
|       if (length > 4) { |       if (length > 4) { | ||||||
|         return (float) ((int32_t) (value[1] << 24) + (int32_t) (value[2] << 16) + (int32_t) (value[3] << 8) + |         return (float) ((int32_t) (value[1] << 24) + (int32_t) (value[2] << 16) + (int32_t) (value[3] << 8) + | ||||||
|   | |||||||
| @@ -310,11 +310,7 @@ async def to_code(config): | |||||||
|     cg.add_define("USE_ESP32_CAMERA") |     cg.add_define("USE_ESP32_CAMERA") | ||||||
|  |  | ||||||
|     if CORE.using_esp_idf: |     if CORE.using_esp_idf: | ||||||
|         add_idf_component( |         add_idf_component(name="espressif/esp32-camera", ref="2.0.15") | ||||||
|             name="esp32-camera", |  | ||||||
|             repo="https://github.com/espressif/esp32-camera.git", |  | ||||||
|             ref="v2.0.15", |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|     for conf in config.get(CONF_ON_STREAM_START, []): |     for conf in config.get(CONF_ON_STREAM_START, []): | ||||||
|         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) |         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||||
|   | |||||||
							
								
								
									
										101
									
								
								esphome/components/esp32_hosted/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								esphome/components/esp32_hosted/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,101 @@ | |||||||
|  | import os | ||||||
|  |  | ||||||
|  | from esphome import pins | ||||||
|  | from esphome.components import esp32 | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.const import ( | ||||||
|  |     CONF_CLK_PIN, | ||||||
|  |     CONF_RESET_PIN, | ||||||
|  |     CONF_VARIANT, | ||||||
|  |     KEY_CORE, | ||||||
|  |     KEY_FRAMEWORK_VERSION, | ||||||
|  | ) | ||||||
|  | from esphome.core import CORE | ||||||
|  |  | ||||||
|  | CODEOWNERS = ["@swoboda1337"] | ||||||
|  |  | ||||||
|  | CONF_ACTIVE_HIGH = "active_high" | ||||||
|  | CONF_CMD_PIN = "cmd_pin" | ||||||
|  | CONF_D0_PIN = "d0_pin" | ||||||
|  | CONF_D1_PIN = "d1_pin" | ||||||
|  | CONF_D2_PIN = "d2_pin" | ||||||
|  | CONF_D3_PIN = "d3_pin" | ||||||
|  | CONF_SLOT = "slot" | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = cv.All( | ||||||
|  |     cv.Schema( | ||||||
|  |         { | ||||||
|  |             cv.Required(CONF_VARIANT): cv.one_of(*esp32.VARIANTS, upper=True), | ||||||
|  |             cv.Required(CONF_ACTIVE_HIGH): cv.boolean, | ||||||
|  |             cv.Required(CONF_CLK_PIN): pins.internal_gpio_output_pin_number, | ||||||
|  |             cv.Required(CONF_CMD_PIN): pins.internal_gpio_output_pin_number, | ||||||
|  |             cv.Required(CONF_D0_PIN): pins.internal_gpio_output_pin_number, | ||||||
|  |             cv.Required(CONF_D1_PIN): pins.internal_gpio_output_pin_number, | ||||||
|  |             cv.Required(CONF_D2_PIN): pins.internal_gpio_output_pin_number, | ||||||
|  |             cv.Required(CONF_D3_PIN): pins.internal_gpio_output_pin_number, | ||||||
|  |             cv.Required(CONF_RESET_PIN): pins.internal_gpio_output_pin_number, | ||||||
|  |             cv.Optional(CONF_SLOT, default=1): cv.int_range(min=0, max=1), | ||||||
|  |         } | ||||||
|  |     ), | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config): | ||||||
|  |     if config[CONF_ACTIVE_HIGH]: | ||||||
|  |         esp32.add_idf_sdkconfig_option( | ||||||
|  |             "CONFIG_ESP_HOSTED_SDIO_RESET_ACTIVE_HIGH", | ||||||
|  |             True, | ||||||
|  |         ) | ||||||
|  |     else: | ||||||
|  |         esp32.add_idf_sdkconfig_option( | ||||||
|  |             "CONFIG_ESP_HOSTED_SDIO_RESET_ACTIVE_LOW", | ||||||
|  |             True, | ||||||
|  |         ) | ||||||
|  |     esp32.add_idf_sdkconfig_option( | ||||||
|  |         "CONFIG_ESP_HOSTED_SDIO_GPIO_RESET_SLAVE",  # NOLINT | ||||||
|  |         config[CONF_RESET_PIN], | ||||||
|  |     ) | ||||||
|  |     esp32.add_idf_sdkconfig_option( | ||||||
|  |         f"CONFIG_SLAVE_IDF_TARGET_{config[CONF_VARIANT]}",  # NOLINT | ||||||
|  |         True, | ||||||
|  |     ) | ||||||
|  |     esp32.add_idf_sdkconfig_option( | ||||||
|  |         f"CONFIG_ESP_HOSTED_SDIO_SLOT_{config[CONF_SLOT]}", | ||||||
|  |         True, | ||||||
|  |     ) | ||||||
|  |     esp32.add_idf_sdkconfig_option( | ||||||
|  |         f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CLK_SLOT_{config[CONF_SLOT]}", | ||||||
|  |         config[CONF_CLK_PIN], | ||||||
|  |     ) | ||||||
|  |     esp32.add_idf_sdkconfig_option( | ||||||
|  |         f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CMD_SLOT_{config[CONF_SLOT]}", | ||||||
|  |         config[CONF_CMD_PIN], | ||||||
|  |     ) | ||||||
|  |     esp32.add_idf_sdkconfig_option( | ||||||
|  |         f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D0_SLOT_{config[CONF_SLOT]}", | ||||||
|  |         config[CONF_D0_PIN], | ||||||
|  |     ) | ||||||
|  |     esp32.add_idf_sdkconfig_option( | ||||||
|  |         f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D1_4BIT_BUS_SLOT_{config[CONF_SLOT]}", | ||||||
|  |         config[CONF_D1_PIN], | ||||||
|  |     ) | ||||||
|  |     esp32.add_idf_sdkconfig_option( | ||||||
|  |         f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D2_4BIT_BUS_SLOT_{config[CONF_SLOT]}", | ||||||
|  |         config[CONF_D2_PIN], | ||||||
|  |     ) | ||||||
|  |     esp32.add_idf_sdkconfig_option( | ||||||
|  |         f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D3_4BIT_BUS_SLOT_{config[CONF_SLOT]}", | ||||||
|  |         config[CONF_D3_PIN], | ||||||
|  |     ) | ||||||
|  |     esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_CUSTOM_SDIO_PINS", True) | ||||||
|  |  | ||||||
|  |     framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] | ||||||
|  |     os.environ["ESP_IDF_VERSION"] = f"{framework_ver.major}.{framework_ver.minor}" | ||||||
|  |     esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.10.2") | ||||||
|  |     esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0") | ||||||
|  |     esp32.add_idf_component(name="espressif/esp_hosted", ref="2.0.11") | ||||||
|  |     esp32.add_extra_script( | ||||||
|  |         "post", | ||||||
|  |         "esp32_hosted.py", | ||||||
|  |         os.path.join(os.path.dirname(__file__), "esp32_hosted.py.script"), | ||||||
|  |     ) | ||||||
							
								
								
									
										12
									
								
								esphome/components/esp32_hosted/esp32_hosted.py.script
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								esphome/components/esp32_hosted/esp32_hosted.py.script
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | # pylint: disable=E0602 | ||||||
|  | Import("env")  # noqa | ||||||
|  |  | ||||||
|  | # Workaround whole archive issue | ||||||
|  | if "__LIB_DEPS" in env and "libespressif__esp_hosted.a" in env["__LIB_DEPS"]: | ||||||
|  |     env.Append( | ||||||
|  |         LINKFLAGS=[ | ||||||
|  |             "-Wl,--whole-archive", | ||||||
|  |             env["BUILD_DIR"] + "/esp-idf/espressif__esp_hosted/libespressif__esp_hosted.a", | ||||||
|  |             "-Wl,--no-whole-archive", | ||||||
|  |         ] | ||||||
|  |     ) | ||||||
| @@ -183,7 +183,7 @@ async def to_code(config): | |||||||
|  |  | ||||||
|     cg.add_platformio_option("board", config[CONF_BOARD]) |     cg.add_platformio_option("board", config[CONF_BOARD]) | ||||||
|     cg.add_build_flag("-DUSE_ESP8266") |     cg.add_build_flag("-DUSE_ESP8266") | ||||||
|     cg.set_cpp_standard("gnu++17") |     cg.set_cpp_standard("gnu++20") | ||||||
|     cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) |     cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) | ||||||
|     cg.add_define("ESPHOME_VARIANT", "ESP8266") |     cg.add_define("ESPHOME_VARIANT", "ESP8266") | ||||||
|  |  | ||||||
|   | |||||||
| @@ -129,9 +129,9 @@ void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) { | |||||||
|     } |     } | ||||||
|   } else { |   } else { | ||||||
|     if (value != arg->inverted) { |     if (value != arg->inverted) { | ||||||
|       *arg->out_set_reg |= 1; |       *arg->out_set_reg = *arg->out_set_reg | 1; | ||||||
|     } else { |     } else { | ||||||
|       *arg->out_set_reg &= ~1; |       *arg->out_set_reg = *arg->out_set_reg & ~1; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @@ -147,7 +147,7 @@ void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) { | |||||||
|     if (flags & gpio::FLAG_OUTPUT) { |     if (flags & gpio::FLAG_OUTPUT) { | ||||||
|       *arg->mode_set_reg = arg->mask; |       *arg->mode_set_reg = arg->mask; | ||||||
|       if (flags & gpio::FLAG_OPEN_DRAIN) { |       if (flags & gpio::FLAG_OPEN_DRAIN) { | ||||||
|         *arg->control_reg |= 1 << GPCD; |         *arg->control_reg = *arg->control_reg | (1 << GPCD); | ||||||
|       } else { |       } else { | ||||||
|         *arg->control_reg &= ~(1 << GPCD); |         *arg->control_reg &= ~(1 << GPCD); | ||||||
|       } |       } | ||||||
| @@ -155,21 +155,21 @@ void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) { | |||||||
|       *arg->mode_clr_reg = arg->mask; |       *arg->mode_clr_reg = arg->mask; | ||||||
|     } |     } | ||||||
|     if (flags & gpio::FLAG_PULLUP) { |     if (flags & gpio::FLAG_PULLUP) { | ||||||
|       *arg->func_reg |= 1 << GPFPU; |       *arg->func_reg = *arg->func_reg | (1 << GPFPU); | ||||||
|       *arg->control_reg |= 1 << GPCD; |       *arg->control_reg = *arg->control_reg | (1 << GPCD); | ||||||
|     } else { |     } else { | ||||||
|       *arg->func_reg &= ~(1 << GPFPU); |       *arg->func_reg = *arg->func_reg & ~(1 << GPFPU); | ||||||
|     } |     } | ||||||
|   } else { |   } else { | ||||||
|     if (flags & gpio::FLAG_OUTPUT) { |     if (flags & gpio::FLAG_OUTPUT) { | ||||||
|       *arg->mode_set_reg |= 1; |       *arg->mode_set_reg = *arg->mode_set_reg | 1; | ||||||
|     } else { |     } else { | ||||||
|       *arg->mode_set_reg &= ~1; |       *arg->mode_set_reg = *arg->mode_set_reg & ~1; | ||||||
|     } |     } | ||||||
|     if (flags & gpio::FLAG_PULLDOWN) { |     if (flags & gpio::FLAG_PULLDOWN) { | ||||||
|       *arg->func_reg |= 1 << GP16FPD; |       *arg->func_reg = *arg->func_reg | (1 << GP16FPD); | ||||||
|     } else { |     } else { | ||||||
|       *arg->func_reg &= ~(1 << GP16FPD); |       *arg->func_reg = *arg->func_reg & ~(1 << GP16FPD); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ | |||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace ethernet { | namespace ethernet { | ||||||
|  |  | ||||||
| enum EthernetType { | enum EthernetType : uint8_t { | ||||||
|   ETHERNET_TYPE_UNKNOWN = 0, |   ETHERNET_TYPE_UNKNOWN = 0, | ||||||
|   ETHERNET_TYPE_LAN8720, |   ETHERNET_TYPE_LAN8720, | ||||||
|   ETHERNET_TYPE_RTL8201, |   ETHERNET_TYPE_RTL8201, | ||||||
| @@ -42,7 +42,7 @@ struct PHYRegister { | |||||||
|   uint32_t page; |   uint32_t page; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| enum class EthernetComponentState { | enum class EthernetComponentState : uint8_t { | ||||||
|   STOPPED, |   STOPPED, | ||||||
|   CONNECTING, |   CONNECTING, | ||||||
|   CONNECTED, |   CONNECTED, | ||||||
| @@ -119,25 +119,31 @@ class EthernetComponent : public Component { | |||||||
|   uint32_t polling_interval_{0}; |   uint32_t polling_interval_{0}; | ||||||
| #endif | #endif | ||||||
| #else | #else | ||||||
|   uint8_t phy_addr_{0}; |   // Group all 32-bit members first | ||||||
|   int power_pin_{-1}; |   int power_pin_{-1}; | ||||||
|   uint8_t mdc_pin_{23}; |  | ||||||
|   uint8_t mdio_pin_{18}; |  | ||||||
|   emac_rmii_clock_mode_t clk_mode_{EMAC_CLK_EXT_IN}; |   emac_rmii_clock_mode_t clk_mode_{EMAC_CLK_EXT_IN}; | ||||||
|   emac_rmii_clock_gpio_t clk_gpio_{EMAC_CLK_IN_GPIO}; |   emac_rmii_clock_gpio_t clk_gpio_{EMAC_CLK_IN_GPIO}; | ||||||
|   std::vector<PHYRegister> phy_registers_{}; |   std::vector<PHYRegister> phy_registers_{}; | ||||||
| #endif |  | ||||||
|   EthernetType type_{ETHERNET_TYPE_UNKNOWN}; |  | ||||||
|   optional<ManualIP> manual_ip_{}; |  | ||||||
|  |  | ||||||
|  |   // Group all 8-bit members together | ||||||
|  |   uint8_t phy_addr_{0}; | ||||||
|  |   uint8_t mdc_pin_{23}; | ||||||
|  |   uint8_t mdio_pin_{18}; | ||||||
|  | #endif | ||||||
|  |   optional<ManualIP> manual_ip_{}; | ||||||
|  |   uint32_t connect_begin_; | ||||||
|  |  | ||||||
|  |   // Group all uint8_t types together (enums and bools) | ||||||
|  |   EthernetType type_{ETHERNET_TYPE_UNKNOWN}; | ||||||
|  |   EthernetComponentState state_{EthernetComponentState::STOPPED}; | ||||||
|   bool started_{false}; |   bool started_{false}; | ||||||
|   bool connected_{false}; |   bool connected_{false}; | ||||||
|   bool got_ipv4_address_{false}; |   bool got_ipv4_address_{false}; | ||||||
| #if LWIP_IPV6 | #if LWIP_IPV6 | ||||||
|   uint8_t ipv6_count_{0}; |   uint8_t ipv6_count_{0}; | ||||||
| #endif /* LWIP_IPV6 */ | #endif /* LWIP_IPV6 */ | ||||||
|   EthernetComponentState state_{EthernetComponentState::STOPPED}; |  | ||||||
|   uint32_t connect_begin_; |   // Pointers at the end (naturally aligned) | ||||||
|   esp_netif_t *eth_netif_{nullptr}; |   esp_netif_t *eth_netif_{nullptr}; | ||||||
|   esp_eth_handle_t eth_handle_; |   esp_eth_handle_t eth_handle_; | ||||||
|   esp_eth_phy_t *phy_{nullptr}; |   esp_eth_phy_t *phy_{nullptr}; | ||||||
|   | |||||||
| @@ -41,6 +41,6 @@ CONFIG_SCHEMA = cv.All( | |||||||
| async def to_code(config): | async def to_code(config): | ||||||
|     cg.add_build_flag("-DUSE_HOST") |     cg.add_build_flag("-DUSE_HOST") | ||||||
|     cg.add_define("USE_ESPHOME_HOST_MAC_ADDRESS", config[CONF_MAC_ADDRESS].parts) |     cg.add_define("USE_ESPHOME_HOST_MAC_ADDRESS", config[CONF_MAC_ADDRESS].parts) | ||||||
|     cg.add_build_flag("-std=gnu++17") |     cg.add_build_flag("-std=gnu++20") | ||||||
|     cg.add_define("ESPHOME_BOARD", "host") |     cg.add_define("ESPHOME_BOARD", "host") | ||||||
|     cg.add_platformio_option("platform", "platformio/native") |     cg.add_platformio_option("platform", "platformio/native") | ||||||
|   | |||||||
| @@ -258,7 +258,7 @@ bool OtaHttpRequestComponent::http_get_md5_() { | |||||||
| } | } | ||||||
|  |  | ||||||
| bool OtaHttpRequestComponent::validate_url_(const std::string &url) { | bool OtaHttpRequestComponent::validate_url_(const std::string &url) { | ||||||
|   if ((url.length() < 8) || (url.find("http") != 0) || (url.find("://") == std::string::npos)) { |   if ((url.length() < 8) || !url.starts_with("http") || (url.find("://") == std::string::npos)) { | ||||||
|     ESP_LOGE(TAG, "URL is invalid and/or must be prefixed with 'http://' or 'https://'"); |     ESP_LOGE(TAG, "URL is invalid and/or must be prefixed with 'http://' or 'https://'"); | ||||||
|     return false; |     return false; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -159,12 +159,6 @@ void HydreonRGxxComponent::schedule_reboot_() { | |||||||
|   }); |   }); | ||||||
| } | } | ||||||
|  |  | ||||||
| bool HydreonRGxxComponent::buffer_starts_with_(const std::string &prefix) { |  | ||||||
|   return this->buffer_starts_with_(prefix.c_str()); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| bool HydreonRGxxComponent::buffer_starts_with_(const char *prefix) { return buffer_.rfind(prefix, 0) == 0; } |  | ||||||
|  |  | ||||||
| void HydreonRGxxComponent::process_line_() { | void HydreonRGxxComponent::process_line_() { | ||||||
|   ESP_LOGV(TAG, "Read from serial: %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str()); |   ESP_LOGV(TAG, "Read from serial: %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str()); | ||||||
|  |  | ||||||
| @@ -191,7 +185,7 @@ void HydreonRGxxComponent::process_line_() { | |||||||
|     ESP_LOGW(TAG, "Received EmSat!"); |     ESP_LOGW(TAG, "Received EmSat!"); | ||||||
|     this->em_sat_ = true; |     this->em_sat_ = true; | ||||||
|   } |   } | ||||||
|   if (this->buffer_starts_with_("PwrDays")) { |   if (buffer_.starts_with("PwrDays")) { | ||||||
|     if (this->boot_count_ <= 0) { |     if (this->boot_count_ <= 0) { | ||||||
|       this->boot_count_ = 1; |       this->boot_count_ = 1; | ||||||
|     } else { |     } else { | ||||||
| @@ -220,7 +214,7 @@ void HydreonRGxxComponent::process_line_() { | |||||||
|     } |     } | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   if (this->buffer_starts_with_("SW")) { |   if (buffer_.starts_with("SW")) { | ||||||
|     std::string::size_type majend = this->buffer_.find('.'); |     std::string::size_type majend = this->buffer_.find('.'); | ||||||
|     std::string::size_type endversion = this->buffer_.find(' ', 3); |     std::string::size_type endversion = this->buffer_.find(' ', 3); | ||||||
|     if (majend == std::string::npos || endversion == std::string::npos || majend > endversion) { |     if (majend == std::string::npos || endversion == std::string::npos || majend > endversion) { | ||||||
| @@ -282,7 +276,7 @@ void HydreonRGxxComponent::process_line_() { | |||||||
|     } |     } | ||||||
|   } else { |   } else { | ||||||
|     for (const auto *ignore : IGNORE_STRINGS) { |     for (const auto *ignore : IGNORE_STRINGS) { | ||||||
|       if (this->buffer_starts_with_(ignore)) { |       if (buffer_.starts_with(ignore)) { | ||||||
|         ESP_LOGI(TAG, "Ignoring %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str()); |         ESP_LOGI(TAG, "Ignoring %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str()); | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|   | |||||||
| @@ -1,5 +1,8 @@ | |||||||
|  | import logging | ||||||
|  |  | ||||||
| from esphome import pins | from esphome import pins | ||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
|  | from esphome.components import esp32 | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome.const import ( | from esphome.const import ( | ||||||
|     CONF_ADDRESS, |     CONF_ADDRESS, | ||||||
| @@ -12,6 +15,8 @@ from esphome.const import ( | |||||||
|     CONF_SCL, |     CONF_SCL, | ||||||
|     CONF_SDA, |     CONF_SDA, | ||||||
|     CONF_TIMEOUT, |     CONF_TIMEOUT, | ||||||
|  |     KEY_CORE, | ||||||
|  |     KEY_FRAMEWORK_VERSION, | ||||||
|     PLATFORM_ESP32, |     PLATFORM_ESP32, | ||||||
|     PLATFORM_ESP8266, |     PLATFORM_ESP8266, | ||||||
|     PLATFORM_RP2040, |     PLATFORM_RP2040, | ||||||
| @@ -19,6 +24,7 @@ from esphome.const import ( | |||||||
| from esphome.core import CORE, coroutine_with_priority | from esphome.core import CORE, coroutine_with_priority | ||||||
| import esphome.final_validate as fv | import esphome.final_validate as fv | ||||||
|  |  | ||||||
|  | LOGGER = logging.getLogger(__name__) | ||||||
| CODEOWNERS = ["@esphome/core"] | CODEOWNERS = ["@esphome/core"] | ||||||
| i2c_ns = cg.esphome_ns.namespace("i2c") | i2c_ns = cg.esphome_ns.namespace("i2c") | ||||||
| I2CBus = i2c_ns.class_("I2CBus") | I2CBus = i2c_ns.class_("I2CBus") | ||||||
| @@ -41,6 +47,32 @@ def _bus_declare_type(value): | |||||||
|     raise NotImplementedError |     raise NotImplementedError | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def validate_config(config): | ||||||
|  |     if ( | ||||||
|  |         config[CONF_SCAN] | ||||||
|  |         and CORE.is_esp32 | ||||||
|  |         and CORE.using_esp_idf | ||||||
|  |         and esp32.get_esp32_variant() | ||||||
|  |         in [ | ||||||
|  |             esp32.const.VARIANT_ESP32C5, | ||||||
|  |             esp32.const.VARIANT_ESP32C6, | ||||||
|  |             esp32.const.VARIANT_ESP32P4, | ||||||
|  |         ] | ||||||
|  |     ): | ||||||
|  |         version: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] | ||||||
|  |         if version.major == 5 and ( | ||||||
|  |             (version.minor == 3 and version.patch <= 3) | ||||||
|  |             or (version.minor == 4 and version.patch <= 1) | ||||||
|  |         ): | ||||||
|  |             LOGGER.warning( | ||||||
|  |                 "There is a bug in esp-idf version %s that breaks I2C scan, I2C scan " | ||||||
|  |                 "has been disabled, see https://github.com/esphome/issues/issues/7128", | ||||||
|  |                 str(version), | ||||||
|  |             ) | ||||||
|  |             config[CONF_SCAN] = False | ||||||
|  |     return config | ||||||
|  |  | ||||||
|  |  | ||||||
| pin_with_input_and_output_support = pins.internal_gpio_pin_number( | pin_with_input_and_output_support = pins.internal_gpio_pin_number( | ||||||
|     {CONF_OUTPUT: True, CONF_INPUT: True} |     {CONF_OUTPUT: True, CONF_INPUT: True} | ||||||
| ) | ) | ||||||
| @@ -66,6 +98,7 @@ CONFIG_SCHEMA = cv.All( | |||||||
|         } |         } | ||||||
|     ).extend(cv.COMPONENT_SCHEMA), |     ).extend(cv.COMPONENT_SCHEMA), | ||||||
|     cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040]), |     cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040]), | ||||||
|  |     validate_config, | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -10,6 +10,7 @@ | |||||||
|  |  | ||||||
| #include "esphome/core/application.h" | #include "esphome/core/application.h" | ||||||
|  |  | ||||||
|  | #define CHECK_BIT(var, pos) (((var) >> (pos)) & 1) | ||||||
| #define highbyte(val) (uint8_t)((val) >> 8) | #define highbyte(val) (uint8_t)((val) >> 8) | ||||||
| #define lowbyte(val) (uint8_t)((val) &0xff) | #define lowbyte(val) (uint8_t)((val) &0xff) | ||||||
|  |  | ||||||
| @@ -17,8 +18,162 @@ namespace esphome { | |||||||
| namespace ld2410 { | namespace ld2410 { | ||||||
|  |  | ||||||
| static const char *const TAG = "ld2410"; | static const char *const TAG = "ld2410"; | ||||||
|  | static const char *const NO_MAC = "08:05:04:03:02:01"; | ||||||
|  | static const char *const UNKNOWN_MAC = "unknown"; | ||||||
|  | static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; | ||||||
|  |  | ||||||
| LD2410Component::LD2410Component() {} | enum BaudRateStructure : uint8_t { | ||||||
|  |   BAUD_RATE_9600 = 1, | ||||||
|  |   BAUD_RATE_19200 = 2, | ||||||
|  |   BAUD_RATE_38400 = 3, | ||||||
|  |   BAUD_RATE_57600 = 4, | ||||||
|  |   BAUD_RATE_115200 = 5, | ||||||
|  |   BAUD_RATE_230400 = 6, | ||||||
|  |   BAUD_RATE_256000 = 7, | ||||||
|  |   BAUD_RATE_460800 = 8, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | enum DistanceResolutionStructure : uint8_t { | ||||||
|  |   DISTANCE_RESOLUTION_0_2 = 0x01, | ||||||
|  |   DISTANCE_RESOLUTION_0_75 = 0x00, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | enum LightFunctionStructure : uint8_t { | ||||||
|  |   LIGHT_FUNCTION_OFF = 0x00, | ||||||
|  |   LIGHT_FUNCTION_BELOW = 0x01, | ||||||
|  |   LIGHT_FUNCTION_ABOVE = 0x02, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | enum OutPinLevelStructure : uint8_t { | ||||||
|  |   OUT_PIN_LEVEL_LOW = 0x00, | ||||||
|  |   OUT_PIN_LEVEL_HIGH = 0x01, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | enum PeriodicDataStructure : uint8_t { | ||||||
|  |   DATA_TYPES = 6, | ||||||
|  |   TARGET_STATES = 8, | ||||||
|  |   MOVING_TARGET_LOW = 9, | ||||||
|  |   MOVING_TARGET_HIGH = 10, | ||||||
|  |   MOVING_ENERGY = 11, | ||||||
|  |   STILL_TARGET_LOW = 12, | ||||||
|  |   STILL_TARGET_HIGH = 13, | ||||||
|  |   STILL_ENERGY = 14, | ||||||
|  |   DETECT_DISTANCE_LOW = 15, | ||||||
|  |   DETECT_DISTANCE_HIGH = 16, | ||||||
|  |   MOVING_SENSOR_START = 19, | ||||||
|  |   STILL_SENSOR_START = 28, | ||||||
|  |   LIGHT_SENSOR = 37, | ||||||
|  |   OUT_PIN_SENSOR = 38, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | enum PeriodicDataValue : uint8_t { | ||||||
|  |   HEAD = 0xAA, | ||||||
|  |   END = 0x55, | ||||||
|  |   CHECK = 0x00, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | enum AckDataStructure : uint8_t { | ||||||
|  |   COMMAND = 6, | ||||||
|  |   COMMAND_STATUS = 7, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // Memory-efficient lookup tables | ||||||
|  | struct StringToUint8 { | ||||||
|  |   const char *str; | ||||||
|  |   uint8_t value; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | struct Uint8ToString { | ||||||
|  |   uint8_t value; | ||||||
|  |   const char *str; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | constexpr StringToUint8 BAUD_RATES_BY_STR[] = { | ||||||
|  |     {"9600", BAUD_RATE_9600},     {"19200", BAUD_RATE_19200},   {"38400", BAUD_RATE_38400}, | ||||||
|  |     {"57600", BAUD_RATE_57600},   {"115200", BAUD_RATE_115200}, {"230400", BAUD_RATE_230400}, | ||||||
|  |     {"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800}, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | constexpr StringToUint8 DISTANCE_RESOLUTIONS_BY_STR[] = { | ||||||
|  |     {"0.2m", DISTANCE_RESOLUTION_0_2}, | ||||||
|  |     {"0.75m", DISTANCE_RESOLUTION_0_75}, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | constexpr Uint8ToString DISTANCE_RESOLUTIONS_BY_UINT[] = { | ||||||
|  |     {DISTANCE_RESOLUTION_0_2, "0.2m"}, | ||||||
|  |     {DISTANCE_RESOLUTION_0_75, "0.75m"}, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | constexpr StringToUint8 LIGHT_FUNCTIONS_BY_STR[] = { | ||||||
|  |     {"off", LIGHT_FUNCTION_OFF}, | ||||||
|  |     {"below", LIGHT_FUNCTION_BELOW}, | ||||||
|  |     {"above", LIGHT_FUNCTION_ABOVE}, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | constexpr Uint8ToString LIGHT_FUNCTIONS_BY_UINT[] = { | ||||||
|  |     {LIGHT_FUNCTION_OFF, "off"}, | ||||||
|  |     {LIGHT_FUNCTION_BELOW, "below"}, | ||||||
|  |     {LIGHT_FUNCTION_ABOVE, "above"}, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | constexpr StringToUint8 OUT_PIN_LEVELS_BY_STR[] = { | ||||||
|  |     {"low", OUT_PIN_LEVEL_LOW}, | ||||||
|  |     {"high", OUT_PIN_LEVEL_HIGH}, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | constexpr Uint8ToString OUT_PIN_LEVELS_BY_UINT[] = { | ||||||
|  |     {OUT_PIN_LEVEL_LOW, "low"}, | ||||||
|  |     {OUT_PIN_LEVEL_HIGH, "high"}, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // Helper functions for lookups | ||||||
|  | template<size_t N> uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) { | ||||||
|  |   for (const auto &entry : arr) { | ||||||
|  |     if (str == entry.str) | ||||||
|  |       return entry.value; | ||||||
|  |   } | ||||||
|  |   return 0xFF;  // Not found | ||||||
|  | } | ||||||
|  |  | ||||||
|  | template<size_t N> const char *find_str(const Uint8ToString (&arr)[N], uint8_t value) { | ||||||
|  |   for (const auto &entry : arr) { | ||||||
|  |     if (value == entry.value) | ||||||
|  |       return entry.str; | ||||||
|  |   } | ||||||
|  |   return "";  // Not found | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Commands | ||||||
|  | static const uint8_t CMD_ENABLE_CONF = 0xFF; | ||||||
|  | static const uint8_t CMD_DISABLE_CONF = 0xFE; | ||||||
|  | static const uint8_t CMD_ENABLE_ENG = 0x62; | ||||||
|  | static const uint8_t CMD_DISABLE_ENG = 0x63; | ||||||
|  | static const uint8_t CMD_MAXDIST_DURATION = 0x60; | ||||||
|  | static const uint8_t CMD_QUERY = 0x61; | ||||||
|  | static const uint8_t CMD_GATE_SENS = 0x64; | ||||||
|  | static const uint8_t CMD_VERSION = 0xA0; | ||||||
|  | static const uint8_t CMD_QUERY_DISTANCE_RESOLUTION = 0xAB; | ||||||
|  | static const uint8_t CMD_SET_DISTANCE_RESOLUTION = 0xAA; | ||||||
|  | static const uint8_t CMD_QUERY_LIGHT_CONTROL = 0xAE; | ||||||
|  | static const uint8_t CMD_SET_LIGHT_CONTROL = 0xAD; | ||||||
|  | static const uint8_t CMD_SET_BAUD_RATE = 0xA1; | ||||||
|  | static const uint8_t CMD_BT_PASSWORD = 0xA9; | ||||||
|  | static const uint8_t CMD_MAC = 0xA5; | ||||||
|  | static const uint8_t CMD_RESET = 0xA2; | ||||||
|  | static const uint8_t CMD_RESTART = 0xA3; | ||||||
|  | static const uint8_t CMD_BLUETOOTH = 0xA4; | ||||||
|  | // Commands values | ||||||
|  | static const uint8_t CMD_MAX_MOVE_VALUE = 0x00; | ||||||
|  | static const uint8_t CMD_MAX_STILL_VALUE = 0x01; | ||||||
|  | static const uint8_t CMD_DURATION_VALUE = 0x02; | ||||||
|  | // Command Header & Footer | ||||||
|  | static const uint8_t CMD_FRAME_HEADER[4] = {0xFD, 0xFC, 0xFB, 0xFA}; | ||||||
|  | static const uint8_t CMD_FRAME_END[4] = {0x04, 0x03, 0x02, 0x01}; | ||||||
|  | // Data Header & Footer | ||||||
|  | static const uint8_t DATA_FRAME_HEADER[4] = {0xF4, 0xF3, 0xF2, 0xF1}; | ||||||
|  | static const uint8_t DATA_FRAME_END[4] = {0xF8, 0xF7, 0xF6, 0xF5}; | ||||||
|  |  | ||||||
|  | static inline int two_byte_to_int(char firstbyte, char secondbyte) { return (int16_t) (secondbyte << 8) + firstbyte; } | ||||||
|  |  | ||||||
| void LD2410Component::dump_config() { | void LD2410Component::dump_config() { | ||||||
|   ESP_LOGCONFIG(TAG, "LD2410:"); |   ESP_LOGCONFIG(TAG, "LD2410:"); | ||||||
| @@ -78,7 +233,7 @@ void LD2410Component::dump_config() { | |||||||
|                 "  Throttle: %ums\n" |                 "  Throttle: %ums\n" | ||||||
|                 "  MAC address: %s\n" |                 "  MAC address: %s\n" | ||||||
|                 "  Firmware version: %s", |                 "  Firmware version: %s", | ||||||
|                 this->throttle_, const_cast<char *>(this->mac_.c_str()), const_cast<char *>(this->version_.c_str())); |                 this->throttle_, this->mac_ == NO_MAC ? UNKNOWN_MAC : this->mac_.c_str(), this->version_.c_str()); | ||||||
| } | } | ||||||
|  |  | ||||||
| void LD2410Component::setup() { | void LD2410Component::setup() { | ||||||
| @@ -200,7 +355,7 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) { | |||||||
|   */ |   */ | ||||||
| #ifdef USE_SENSOR | #ifdef USE_SENSOR | ||||||
|   if (this->moving_target_distance_sensor_ != nullptr) { |   if (this->moving_target_distance_sensor_ != nullptr) { | ||||||
|     int new_moving_target_distance = this->two_byte_to_int_(buffer[MOVING_TARGET_LOW], buffer[MOVING_TARGET_HIGH]); |     int new_moving_target_distance = ld2410::two_byte_to_int(buffer[MOVING_TARGET_LOW], buffer[MOVING_TARGET_HIGH]); | ||||||
|     if (this->moving_target_distance_sensor_->get_state() != new_moving_target_distance) |     if (this->moving_target_distance_sensor_->get_state() != new_moving_target_distance) | ||||||
|       this->moving_target_distance_sensor_->publish_state(new_moving_target_distance); |       this->moving_target_distance_sensor_->publish_state(new_moving_target_distance); | ||||||
|   } |   } | ||||||
| @@ -210,7 +365,7 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) { | |||||||
|       this->moving_target_energy_sensor_->publish_state(new_moving_target_energy); |       this->moving_target_energy_sensor_->publish_state(new_moving_target_energy); | ||||||
|   } |   } | ||||||
|   if (this->still_target_distance_sensor_ != nullptr) { |   if (this->still_target_distance_sensor_ != nullptr) { | ||||||
|     int new_still_target_distance = this->two_byte_to_int_(buffer[STILL_TARGET_LOW], buffer[STILL_TARGET_HIGH]); |     int new_still_target_distance = ld2410::two_byte_to_int(buffer[STILL_TARGET_LOW], buffer[STILL_TARGET_HIGH]); | ||||||
|     if (this->still_target_distance_sensor_->get_state() != new_still_target_distance) |     if (this->still_target_distance_sensor_->get_state() != new_still_target_distance) | ||||||
|       this->still_target_distance_sensor_->publish_state(new_still_target_distance); |       this->still_target_distance_sensor_->publish_state(new_still_target_distance); | ||||||
|   } |   } | ||||||
| @@ -220,7 +375,7 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) { | |||||||
|       this->still_target_energy_sensor_->publish_state(new_still_target_energy); |       this->still_target_energy_sensor_->publish_state(new_still_target_energy); | ||||||
|   } |   } | ||||||
|   if (this->detection_distance_sensor_ != nullptr) { |   if (this->detection_distance_sensor_ != nullptr) { | ||||||
|     int new_detect_distance = this->two_byte_to_int_(buffer[DETECT_DISTANCE_LOW], buffer[DETECT_DISTANCE_HIGH]); |     int new_detect_distance = ld2410::two_byte_to_int(buffer[DETECT_DISTANCE_LOW], buffer[DETECT_DISTANCE_HIGH]); | ||||||
|     if (this->detection_distance_sensor_->get_state() != new_detect_distance) |     if (this->detection_distance_sensor_->get_state() != new_detect_distance) | ||||||
|       this->detection_distance_sensor_->publish_state(new_detect_distance); |       this->detection_distance_sensor_->publish_state(new_detect_distance); | ||||||
|   } |   } | ||||||
| @@ -282,25 +437,6 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) { | |||||||
| #endif | #endif | ||||||
| } | } | ||||||
|  |  | ||||||
| const char VERSION_FMT[] = "%u.%02X.%02X%02X%02X%02X"; |  | ||||||
|  |  | ||||||
| std::string format_version(uint8_t *buffer) { |  | ||||||
|   std::string::size_type version_size = 256; |  | ||||||
|   std::string version; |  | ||||||
|   do { |  | ||||||
|     version.resize(version_size + 1); |  | ||||||
|     version_size = std::snprintf(&version[0], version.size(), VERSION_FMT, buffer[13], buffer[12], buffer[17], |  | ||||||
|                                  buffer[16], buffer[15], buffer[14]); |  | ||||||
|   } while (version_size + 1 > version.size()); |  | ||||||
|   version.resize(version_size); |  | ||||||
|   return version; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const char MAC_FMT[] = "%02X:%02X:%02X:%02X:%02X:%02X"; |  | ||||||
|  |  | ||||||
| const std::string UNKNOWN_MAC("unknown"); |  | ||||||
| const std::string NO_MAC("08:05:04:03:02:01"); |  | ||||||
|  |  | ||||||
| #ifdef USE_NUMBER | #ifdef USE_NUMBER | ||||||
| std::function<void(void)> set_number_value(number::Number *n, float value) { | std::function<void(void)> set_number_value(number::Number *n, float value) { | ||||||
|   float normalized_value = value * 1.0; |   float normalized_value = value * 1.0; | ||||||
| @@ -326,7 +462,7 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { | |||||||
|     ESP_LOGE(TAG, "Invalid status"); |     ESP_LOGE(TAG, "Invalid status"); | ||||||
|     return true; |     return true; | ||||||
|   } |   } | ||||||
|   if (this->two_byte_to_int_(buffer[8], buffer[9]) != 0x00) { |   if (ld2410::two_byte_to_int(buffer[8], buffer[9]) != 0x00) { | ||||||
|     ESP_LOGE(TAG, "Invalid command: %u, %u", buffer[8], buffer[9]); |     ESP_LOGE(TAG, "Invalid command: %u, %u", buffer[8], buffer[9]); | ||||||
|     return true; |     return true; | ||||||
|   } |   } | ||||||
| @@ -347,8 +483,8 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { | |||||||
| #endif | #endif | ||||||
|       break; |       break; | ||||||
|     case lowbyte(CMD_VERSION): |     case lowbyte(CMD_VERSION): | ||||||
|       this->version_ = format_version(buffer); |       this->version_ = str_sprintf(VERSION_FMT, buffer[13], buffer[12], buffer[17], buffer[16], buffer[15], buffer[14]); | ||||||
|       ESP_LOGV(TAG, "Firmware version: %s", const_cast<char *>(this->version_.c_str())); |       ESP_LOGV(TAG, "Firmware version: %s", this->version_.c_str()); | ||||||
| #ifdef USE_TEXT_SENSOR | #ifdef USE_TEXT_SENSOR | ||||||
|       if (this->version_text_sensor_ != nullptr) { |       if (this->version_text_sensor_ != nullptr) { | ||||||
|         this->version_text_sensor_->publish_state(this->version_); |         this->version_text_sensor_->publish_state(this->version_); | ||||||
| @@ -357,8 +493,8 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { | |||||||
|       break; |       break; | ||||||
|     case lowbyte(CMD_QUERY_DISTANCE_RESOLUTION): { |     case lowbyte(CMD_QUERY_DISTANCE_RESOLUTION): { | ||||||
|       std::string distance_resolution = |       std::string distance_resolution = | ||||||
|           DISTANCE_RESOLUTION_INT_TO_ENUM.at(this->two_byte_to_int_(buffer[10], buffer[11])); |           find_str(DISTANCE_RESOLUTIONS_BY_UINT, ld2410::two_byte_to_int(buffer[10], buffer[11])); | ||||||
|       ESP_LOGV(TAG, "Distance resolution: %s", const_cast<char *>(distance_resolution.c_str())); |       ESP_LOGV(TAG, "Distance resolution: %s", distance_resolution.c_str()); | ||||||
| #ifdef USE_SELECT | #ifdef USE_SELECT | ||||||
|       if (this->distance_resolution_select_ != nullptr && |       if (this->distance_resolution_select_ != nullptr && | ||||||
|           this->distance_resolution_select_->state != distance_resolution) { |           this->distance_resolution_select_->state != distance_resolution) { | ||||||
| @@ -367,9 +503,9 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { | |||||||
| #endif | #endif | ||||||
|     } break; |     } break; | ||||||
|     case lowbyte(CMD_QUERY_LIGHT_CONTROL): { |     case lowbyte(CMD_QUERY_LIGHT_CONTROL): { | ||||||
|       this->light_function_ = LIGHT_FUNCTION_INT_TO_ENUM.at(buffer[10]); |       this->light_function_ = find_str(LIGHT_FUNCTIONS_BY_UINT, buffer[10]); | ||||||
|       this->light_threshold_ = buffer[11] * 1.0; |       this->light_threshold_ = buffer[11] * 1.0; | ||||||
|       this->out_pin_level_ = OUT_PIN_LEVEL_INT_TO_ENUM.at(buffer[12]); |       this->out_pin_level_ = find_str(OUT_PIN_LEVELS_BY_UINT, buffer[12]); | ||||||
|       ESP_LOGV(TAG, "Light function: %s", const_cast<char *>(this->light_function_.c_str())); |       ESP_LOGV(TAG, "Light function: %s", const_cast<char *>(this->light_function_.c_str())); | ||||||
|       ESP_LOGV(TAG, "Light threshold: %f", this->light_threshold_); |       ESP_LOGV(TAG, "Light threshold: %f", this->light_threshold_); | ||||||
|       ESP_LOGV(TAG, "Out pin level: %s", const_cast<char *>(this->out_pin_level_.c_str())); |       ESP_LOGV(TAG, "Out pin level: %s", const_cast<char *>(this->out_pin_level_.c_str())); | ||||||
| @@ -402,7 +538,7 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { | |||||||
| #endif | #endif | ||||||
| #ifdef USE_SWITCH | #ifdef USE_SWITCH | ||||||
|       if (this->bluetooth_switch_ != nullptr) { |       if (this->bluetooth_switch_ != nullptr) { | ||||||
|         this->bluetooth_switch_->publish_state(this->mac_ != UNKNOWN_MAC); |         this->bluetooth_switch_->publish_state(this->mac_ != NO_MAC); | ||||||
|       } |       } | ||||||
| #endif | #endif | ||||||
|       break; |       break; | ||||||
| @@ -448,7 +584,7 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { | |||||||
|       /* |       /* | ||||||
|         None Duration: 33~34th bytes |         None Duration: 33~34th bytes | ||||||
|       */ |       */ | ||||||
|       updates.push_back(set_number_value(this->timeout_number_, this->two_byte_to_int_(buffer[32], buffer[33]))); |       updates.push_back(set_number_value(this->timeout_number_, ld2410::two_byte_to_int(buffer[32], buffer[33]))); | ||||||
|       for (auto &update : updates) { |       for (auto &update : updates) { | ||||||
|         update(); |         update(); | ||||||
|       } |       } | ||||||
| @@ -505,14 +641,14 @@ void LD2410Component::set_bluetooth(bool enable) { | |||||||
|  |  | ||||||
| void LD2410Component::set_distance_resolution(const std::string &state) { | void LD2410Component::set_distance_resolution(const std::string &state) { | ||||||
|   this->set_config_mode_(true); |   this->set_config_mode_(true); | ||||||
|   uint8_t cmd_value[2] = {DISTANCE_RESOLUTION_ENUM_TO_INT.at(state), 0x00}; |   uint8_t cmd_value[2] = {find_uint8(DISTANCE_RESOLUTIONS_BY_STR, state), 0x00}; | ||||||
|   this->send_command_(CMD_SET_DISTANCE_RESOLUTION, cmd_value, 2); |   this->send_command_(CMD_SET_DISTANCE_RESOLUTION, cmd_value, 2); | ||||||
|   this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); |   this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); | ||||||
| } | } | ||||||
|  |  | ||||||
| void LD2410Component::set_baud_rate(const std::string &state) { | void LD2410Component::set_baud_rate(const std::string &state) { | ||||||
|   this->set_config_mode_(true); |   this->set_config_mode_(true); | ||||||
|   uint8_t cmd_value[2] = {BAUD_RATE_ENUM_TO_INT.at(state), 0x00}; |   uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00}; | ||||||
|   this->send_command_(CMD_SET_BAUD_RATE, cmd_value, 2); |   this->send_command_(CMD_SET_BAUD_RATE, cmd_value, 2); | ||||||
|   this->set_timeout(200, [this]() { this->restart_(); }); |   this->set_timeout(200, [this]() { this->restart_(); }); | ||||||
| } | } | ||||||
| @@ -646,9 +782,9 @@ void LD2410Component::set_light_out_control() { | |||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   this->set_config_mode_(true); |   this->set_config_mode_(true); | ||||||
|   uint8_t light_function = LIGHT_FUNCTION_ENUM_TO_INT.at(this->light_function_); |   uint8_t light_function = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_); | ||||||
|   uint8_t light_threshold = static_cast<uint8_t>(this->light_threshold_); |   uint8_t light_threshold = static_cast<uint8_t>(this->light_threshold_); | ||||||
|   uint8_t out_pin_level = OUT_PIN_LEVEL_ENUM_TO_INT.at(this->out_pin_level_); |   uint8_t out_pin_level = find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_); | ||||||
|   uint8_t value[4] = {light_function, light_threshold, out_pin_level, 0x00}; |   uint8_t value[4] = {light_function, light_threshold, out_pin_level, 0x00}; | ||||||
|   this->send_command_(CMD_SET_LIGHT_CONTROL, value, 4); |   this->send_command_(CMD_SET_LIGHT_CONTROL, value, 4); | ||||||
|   delay(50);  // NOLINT |   delay(50);  // NOLINT | ||||||
|   | |||||||
| @@ -26,114 +26,9 @@ | |||||||
| #include "esphome/core/automation.h" | #include "esphome/core/automation.h" | ||||||
| #include "esphome/core/helpers.h" | #include "esphome/core/helpers.h" | ||||||
|  |  | ||||||
| #include <map> |  | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace ld2410 { | namespace ld2410 { | ||||||
|  |  | ||||||
| #define CHECK_BIT(var, pos) (((var) >> (pos)) & 1) |  | ||||||
|  |  | ||||||
| // Commands |  | ||||||
| static const uint8_t CMD_ENABLE_CONF = 0x00FF; |  | ||||||
| static const uint8_t CMD_DISABLE_CONF = 0x00FE; |  | ||||||
| static const uint8_t CMD_ENABLE_ENG = 0x0062; |  | ||||||
| static const uint8_t CMD_DISABLE_ENG = 0x0063; |  | ||||||
| static const uint8_t CMD_MAXDIST_DURATION = 0x0060; |  | ||||||
| static const uint8_t CMD_QUERY = 0x0061; |  | ||||||
| static const uint8_t CMD_GATE_SENS = 0x0064; |  | ||||||
| static const uint8_t CMD_VERSION = 0x00A0; |  | ||||||
| static const uint8_t CMD_QUERY_DISTANCE_RESOLUTION = 0x00AB; |  | ||||||
| static const uint8_t CMD_SET_DISTANCE_RESOLUTION = 0x00AA; |  | ||||||
| static const uint8_t CMD_QUERY_LIGHT_CONTROL = 0x00AE; |  | ||||||
| static const uint8_t CMD_SET_LIGHT_CONTROL = 0x00AD; |  | ||||||
| static const uint8_t CMD_SET_BAUD_RATE = 0x00A1; |  | ||||||
| static const uint8_t CMD_BT_PASSWORD = 0x00A9; |  | ||||||
| static const uint8_t CMD_MAC = 0x00A5; |  | ||||||
| static const uint8_t CMD_RESET = 0x00A2; |  | ||||||
| static const uint8_t CMD_RESTART = 0x00A3; |  | ||||||
| static const uint8_t CMD_BLUETOOTH = 0x00A4; |  | ||||||
|  |  | ||||||
| enum BaudRateStructure : uint8_t { |  | ||||||
|   BAUD_RATE_9600 = 1, |  | ||||||
|   BAUD_RATE_19200 = 2, |  | ||||||
|   BAUD_RATE_38400 = 3, |  | ||||||
|   BAUD_RATE_57600 = 4, |  | ||||||
|   BAUD_RATE_115200 = 5, |  | ||||||
|   BAUD_RATE_230400 = 6, |  | ||||||
|   BAUD_RATE_256000 = 7, |  | ||||||
|   BAUD_RATE_460800 = 8 |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| static const std::map<std::string, uint8_t> BAUD_RATE_ENUM_TO_INT{ |  | ||||||
|     {"9600", BAUD_RATE_9600},     {"19200", BAUD_RATE_19200},   {"38400", BAUD_RATE_38400}, |  | ||||||
|     {"57600", BAUD_RATE_57600},   {"115200", BAUD_RATE_115200}, {"230400", BAUD_RATE_230400}, |  | ||||||
|     {"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800}}; |  | ||||||
|  |  | ||||||
| enum DistanceResolutionStructure : uint8_t { DISTANCE_RESOLUTION_0_2 = 0x01, DISTANCE_RESOLUTION_0_75 = 0x00 }; |  | ||||||
|  |  | ||||||
| static const std::map<std::string, uint8_t> DISTANCE_RESOLUTION_ENUM_TO_INT{{"0.2m", DISTANCE_RESOLUTION_0_2}, |  | ||||||
|                                                                             {"0.75m", DISTANCE_RESOLUTION_0_75}}; |  | ||||||
| static const std::map<uint8_t, std::string> DISTANCE_RESOLUTION_INT_TO_ENUM{{DISTANCE_RESOLUTION_0_2, "0.2m"}, |  | ||||||
|                                                                             {DISTANCE_RESOLUTION_0_75, "0.75m"}}; |  | ||||||
|  |  | ||||||
| enum LightFunctionStructure : uint8_t { |  | ||||||
|   LIGHT_FUNCTION_OFF = 0x00, |  | ||||||
|   LIGHT_FUNCTION_BELOW = 0x01, |  | ||||||
|   LIGHT_FUNCTION_ABOVE = 0x02 |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| static const std::map<std::string, uint8_t> LIGHT_FUNCTION_ENUM_TO_INT{ |  | ||||||
|     {"off", LIGHT_FUNCTION_OFF}, {"below", LIGHT_FUNCTION_BELOW}, {"above", LIGHT_FUNCTION_ABOVE}}; |  | ||||||
| static const std::map<uint8_t, std::string> LIGHT_FUNCTION_INT_TO_ENUM{ |  | ||||||
|     {LIGHT_FUNCTION_OFF, "off"}, {LIGHT_FUNCTION_BELOW, "below"}, {LIGHT_FUNCTION_ABOVE, "above"}}; |  | ||||||
|  |  | ||||||
| enum OutPinLevelStructure : uint8_t { OUT_PIN_LEVEL_LOW = 0x00, OUT_PIN_LEVEL_HIGH = 0x01 }; |  | ||||||
|  |  | ||||||
| static const std::map<std::string, uint8_t> OUT_PIN_LEVEL_ENUM_TO_INT{{"low", OUT_PIN_LEVEL_LOW}, |  | ||||||
|                                                                       {"high", OUT_PIN_LEVEL_HIGH}}; |  | ||||||
| static const std::map<uint8_t, std::string> OUT_PIN_LEVEL_INT_TO_ENUM{{OUT_PIN_LEVEL_LOW, "low"}, |  | ||||||
|                                                                       {OUT_PIN_LEVEL_HIGH, "high"}}; |  | ||||||
|  |  | ||||||
| // Commands values |  | ||||||
| static const uint8_t CMD_MAX_MOVE_VALUE = 0x0000; |  | ||||||
| static const uint8_t CMD_MAX_STILL_VALUE = 0x0001; |  | ||||||
| static const uint8_t CMD_DURATION_VALUE = 0x0002; |  | ||||||
| // Command Header & Footer |  | ||||||
| static const uint8_t CMD_FRAME_HEADER[4] = {0xFD, 0xFC, 0xFB, 0xFA}; |  | ||||||
| static const uint8_t CMD_FRAME_END[4] = {0x04, 0x03, 0x02, 0x01}; |  | ||||||
| // Data Header & Footer |  | ||||||
| static const uint8_t DATA_FRAME_HEADER[4] = {0xF4, 0xF3, 0xF2, 0xF1}; |  | ||||||
| static const uint8_t DATA_FRAME_END[4] = {0xF8, 0xF7, 0xF6, 0xF5}; |  | ||||||
| /* |  | ||||||
| Data Type: 6th byte |  | ||||||
| Target states: 9th byte |  | ||||||
|     Moving target distance: 10~11th bytes |  | ||||||
|     Moving target energy: 12th byte |  | ||||||
|     Still target distance: 13~14th bytes |  | ||||||
|     Still target energy: 15th byte |  | ||||||
|     Detect distance: 16~17th bytes |  | ||||||
| */ |  | ||||||
| enum PeriodicDataStructure : uint8_t { |  | ||||||
|   DATA_TYPES = 6, |  | ||||||
|   TARGET_STATES = 8, |  | ||||||
|   MOVING_TARGET_LOW = 9, |  | ||||||
|   MOVING_TARGET_HIGH = 10, |  | ||||||
|   MOVING_ENERGY = 11, |  | ||||||
|   STILL_TARGET_LOW = 12, |  | ||||||
|   STILL_TARGET_HIGH = 13, |  | ||||||
|   STILL_ENERGY = 14, |  | ||||||
|   DETECT_DISTANCE_LOW = 15, |  | ||||||
|   DETECT_DISTANCE_HIGH = 16, |  | ||||||
|   MOVING_SENSOR_START = 19, |  | ||||||
|   STILL_SENSOR_START = 28, |  | ||||||
|   LIGHT_SENSOR = 37, |  | ||||||
|   OUT_PIN_SENSOR = 38, |  | ||||||
| }; |  | ||||||
| enum PeriodicDataValue : uint8_t { HEAD = 0xAA, END = 0x55, CHECK = 0x00 }; |  | ||||||
|  |  | ||||||
| enum AckDataStructure : uint8_t { COMMAND = 6, COMMAND_STATUS = 7 }; |  | ||||||
|  |  | ||||||
| //  char cmd[2] = {enable ? 0xFF : 0xFE, 0x00}; |  | ||||||
| class LD2410Component : public Component, public uart::UARTDevice { | class LD2410Component : public Component, public uart::UARTDevice { | ||||||
| #ifdef USE_SENSOR | #ifdef USE_SENSOR | ||||||
|   SUB_SENSOR(moving_target_distance) |   SUB_SENSOR(moving_target_distance) | ||||||
| @@ -176,7 +71,6 @@ class LD2410Component : public Component, public uart::UARTDevice { | |||||||
| #endif | #endif | ||||||
|  |  | ||||||
|  public: |  public: | ||||||
|   LD2410Component(); |  | ||||||
|   void setup() override; |   void setup() override; | ||||||
|   void dump_config() override; |   void dump_config() override; | ||||||
|   void loop() override; |   void loop() override; | ||||||
| @@ -202,7 +96,6 @@ class LD2410Component : public Component, public uart::UARTDevice { | |||||||
|   void factory_reset(); |   void factory_reset(); | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   int two_byte_to_int_(char firstbyte, char secondbyte) { return (int16_t) (secondbyte << 8) + firstbyte; } |  | ||||||
|   void send_command_(uint8_t command_str, const uint8_t *command_value, int command_value_len); |   void send_command_(uint8_t command_str, const uint8_t *command_value, int command_value_len); | ||||||
|   void set_config_mode_(bool enable); |   void set_config_mode_(bool enable); | ||||||
|   void handle_periodic_data_(uint8_t *buffer, int len); |   void handle_periodic_data_(uint8_t *buffer, int len); | ||||||
| @@ -215,14 +108,14 @@ class LD2410Component : public Component, public uart::UARTDevice { | |||||||
|   void get_light_control_(); |   void get_light_control_(); | ||||||
|   void restart_(); |   void restart_(); | ||||||
|  |  | ||||||
|   int32_t last_periodic_millis_ = millis(); |   int32_t last_periodic_millis_ = 0; | ||||||
|   int32_t last_engineering_mode_change_millis_ = millis(); |   int32_t last_engineering_mode_change_millis_ = 0; | ||||||
|   uint16_t throttle_; |   uint16_t throttle_; | ||||||
|  |   float light_threshold_ = -1; | ||||||
|   std::string version_; |   std::string version_; | ||||||
|   std::string mac_; |   std::string mac_; | ||||||
|   std::string out_pin_level_; |   std::string out_pin_level_; | ||||||
|   std::string light_function_; |   std::string light_function_; | ||||||
|   float light_threshold_ = -1; |  | ||||||
| #ifdef USE_NUMBER | #ifdef USE_NUMBER | ||||||
|   std::vector<number::Number *> gate_still_threshold_numbers_ = std::vector<number::Number *>(9); |   std::vector<number::Number *> gate_still_threshold_numbers_ = std::vector<number::Number *>(9); | ||||||
|   std::vector<number::Number *> gate_move_threshold_numbers_ = std::vector<number::Number *>(9); |   std::vector<number::Number *> gate_move_threshold_numbers_ = std::vector<number::Number *>(9); | ||||||
|   | |||||||
| @@ -17,23 +17,109 @@ namespace esphome { | |||||||
| namespace ld2450 { | namespace ld2450 { | ||||||
|  |  | ||||||
| static const char *const TAG = "ld2450"; | static const char *const TAG = "ld2450"; | ||||||
| static const char *const NO_MAC("08:05:04:03:02:01"); | static const char *const NO_MAC = "08:05:04:03:02:01"; | ||||||
| static const char *const UNKNOWN_MAC("unknown"); | static const char *const UNKNOWN_MAC = "unknown"; | ||||||
|  | static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; | ||||||
|  |  | ||||||
|  | enum BaudRateStructure : uint8_t { | ||||||
|  |   BAUD_RATE_9600 = 1, | ||||||
|  |   BAUD_RATE_19200 = 2, | ||||||
|  |   BAUD_RATE_38400 = 3, | ||||||
|  |   BAUD_RATE_57600 = 4, | ||||||
|  |   BAUD_RATE_115200 = 5, | ||||||
|  |   BAUD_RATE_230400 = 6, | ||||||
|  |   BAUD_RATE_256000 = 7, | ||||||
|  |   BAUD_RATE_460800 = 8 | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // Zone type struct | ||||||
|  | enum ZoneTypeStructure : uint8_t { | ||||||
|  |   ZONE_DISABLED = 0, | ||||||
|  |   ZONE_DETECTION = 1, | ||||||
|  |   ZONE_FILTER = 2, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | enum PeriodicDataStructure : uint8_t { | ||||||
|  |   TARGET_X = 4, | ||||||
|  |   TARGET_Y = 6, | ||||||
|  |   TARGET_SPEED = 8, | ||||||
|  |   TARGET_RESOLUTION = 10, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | enum PeriodicDataValue : uint8_t { | ||||||
|  |   HEAD = 0xAA, | ||||||
|  |   END = 0x55, | ||||||
|  |   CHECK = 0x00, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | enum AckDataStructure : uint8_t { | ||||||
|  |   COMMAND = 6, | ||||||
|  |   COMMAND_STATUS = 7, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // Memory-efficient lookup tables | ||||||
|  | struct StringToUint8 { | ||||||
|  |   const char *str; | ||||||
|  |   uint8_t value; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | struct Uint8ToString { | ||||||
|  |   uint8_t value; | ||||||
|  |   const char *str; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | constexpr StringToUint8 BAUD_RATES_BY_STR[] = { | ||||||
|  |     {"9600", BAUD_RATE_9600},     {"19200", BAUD_RATE_19200},   {"38400", BAUD_RATE_38400}, | ||||||
|  |     {"57600", BAUD_RATE_57600},   {"115200", BAUD_RATE_115200}, {"230400", BAUD_RATE_230400}, | ||||||
|  |     {"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800}, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | constexpr Uint8ToString ZONE_TYPE_BY_UINT[] = { | ||||||
|  |     {ZONE_DISABLED, "Disabled"}, | ||||||
|  |     {ZONE_DETECTION, "Detection"}, | ||||||
|  |     {ZONE_FILTER, "Filter"}, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | constexpr StringToUint8 ZONE_TYPE_BY_STR[] = { | ||||||
|  |     {"Disabled", ZONE_DISABLED}, | ||||||
|  |     {"Detection", ZONE_DETECTION}, | ||||||
|  |     {"Filter", ZONE_FILTER}, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // Helper functions for lookups | ||||||
|  | template<size_t N> uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) { | ||||||
|  |   for (const auto &entry : arr) { | ||||||
|  |     if (str == entry.str) | ||||||
|  |       return entry.value; | ||||||
|  |   } | ||||||
|  |   return 0xFF;  // Not found | ||||||
|  | } | ||||||
|  |  | ||||||
|  | template<size_t N> const char *find_str(const Uint8ToString (&arr)[N], uint8_t value) { | ||||||
|  |   for (const auto &entry : arr) { | ||||||
|  |     if (value == entry.value) | ||||||
|  |       return entry.str; | ||||||
|  |   } | ||||||
|  |   return "";  // Not found | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // LD2450 serial command header & footer | ||||||
|  | static const uint8_t CMD_FRAME_HEADER[4] = {0xFD, 0xFC, 0xFB, 0xFA}; | ||||||
|  | static const uint8_t CMD_FRAME_END[4] = {0x04, 0x03, 0x02, 0x01}; | ||||||
| // LD2450 UART Serial Commands | // LD2450 UART Serial Commands | ||||||
| static const uint8_t CMD_ENABLE_CONF = 0x00FF; | static const uint8_t CMD_ENABLE_CONF = 0xFF; | ||||||
| static const uint8_t CMD_DISABLE_CONF = 0x00FE; | static const uint8_t CMD_DISABLE_CONF = 0xFE; | ||||||
| static const uint8_t CMD_VERSION = 0x00A0; | static const uint8_t CMD_VERSION = 0xA0; | ||||||
| static const uint8_t CMD_MAC = 0x00A5; | static const uint8_t CMD_MAC = 0xA5; | ||||||
| static const uint8_t CMD_RESET = 0x00A2; | static const uint8_t CMD_RESET = 0xA2; | ||||||
| static const uint8_t CMD_RESTART = 0x00A3; | static const uint8_t CMD_RESTART = 0xA3; | ||||||
| static const uint8_t CMD_BLUETOOTH = 0x00A4; | static const uint8_t CMD_BLUETOOTH = 0xA4; | ||||||
| static const uint8_t CMD_SINGLE_TARGET_MODE = 0x0080; | static const uint8_t CMD_SINGLE_TARGET_MODE = 0x80; | ||||||
| static const uint8_t CMD_MULTI_TARGET_MODE = 0x0090; | static const uint8_t CMD_MULTI_TARGET_MODE = 0x90; | ||||||
| static const uint8_t CMD_QUERY_TARGET_MODE = 0x0091; | static const uint8_t CMD_QUERY_TARGET_MODE = 0x91; | ||||||
| static const uint8_t CMD_SET_BAUD_RATE = 0x00A1; | static const uint8_t CMD_SET_BAUD_RATE = 0xA1; | ||||||
| static const uint8_t CMD_QUERY_ZONE = 0x00C1; | static const uint8_t CMD_QUERY_ZONE = 0xC1; | ||||||
| static const uint8_t CMD_SET_ZONE = 0x00C2; | static const uint8_t CMD_SET_ZONE = 0xC2; | ||||||
|  |  | ||||||
| static inline uint16_t convert_seconds_to_ms(uint16_t value) { return value * 1000; }; | static inline uint16_t convert_seconds_to_ms(uint16_t value) { return value * 1000; }; | ||||||
|  |  | ||||||
| @@ -98,13 +184,6 @@ static inline std::string get_direction(int16_t speed) { | |||||||
|   return STATIONARY; |   return STATIONARY; | ||||||
| } | } | ||||||
|  |  | ||||||
| static inline std::string format_version(uint8_t *buffer) { |  | ||||||
|   return str_sprintf("%u.%02X.%02X%02X%02X%02X", buffer[13], buffer[12], buffer[17], buffer[16], buffer[15], |  | ||||||
|                      buffer[14]); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| LD2450Component::LD2450Component() {} |  | ||||||
|  |  | ||||||
| void LD2450Component::setup() { | void LD2450Component::setup() { | ||||||
|   ESP_LOGCONFIG(TAG, "Running setup"); |   ESP_LOGCONFIG(TAG, "Running setup"); | ||||||
| #ifdef USE_NUMBER | #ifdef USE_NUMBER | ||||||
| @@ -189,7 +268,7 @@ void LD2450Component::dump_config() { | |||||||
|                 "  Throttle: %ums\n" |                 "  Throttle: %ums\n" | ||||||
|                 "  MAC Address: %s\n" |                 "  MAC Address: %s\n" | ||||||
|                 "  Firmware version: %s", |                 "  Firmware version: %s", | ||||||
|                 this->throttle_, const_cast<char *>(this->mac_.c_str()), const_cast<char *>(this->version_.c_str())); |                 this->throttle_, this->mac_ == NO_MAC ? UNKNOWN_MAC : this->mac_.c_str(), this->version_.c_str()); | ||||||
| } | } | ||||||
|  |  | ||||||
| void LD2450Component::loop() { | void LD2450Component::loop() { | ||||||
| @@ -596,7 +675,7 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { | |||||||
| #endif | #endif | ||||||
|       break; |       break; | ||||||
|     case lowbyte(CMD_VERSION): |     case lowbyte(CMD_VERSION): | ||||||
|       this->version_ = ld2450::format_version(buffer); |       this->version_ = str_sprintf(VERSION_FMT, buffer[13], buffer[12], buffer[17], buffer[16], buffer[15], buffer[14]); | ||||||
|       ESP_LOGV(TAG, "Firmware version: %s", this->version_.c_str()); |       ESP_LOGV(TAG, "Firmware version: %s", this->version_.c_str()); | ||||||
| #ifdef USE_TEXT_SENSOR | #ifdef USE_TEXT_SENSOR | ||||||
|       if (this->version_text_sensor_ != nullptr) { |       if (this->version_text_sensor_ != nullptr) { | ||||||
| @@ -617,7 +696,7 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { | |||||||
| #endif | #endif | ||||||
| #ifdef USE_SWITCH | #ifdef USE_SWITCH | ||||||
|       if (this->bluetooth_switch_ != nullptr) { |       if (this->bluetooth_switch_ != nullptr) { | ||||||
|         this->bluetooth_switch_->publish_state(this->mac_ != UNKNOWN_MAC); |         this->bluetooth_switch_->publish_state(this->mac_ != NO_MAC); | ||||||
|       } |       } | ||||||
| #endif | #endif | ||||||
|       break; |       break; | ||||||
| @@ -726,7 +805,7 @@ void LD2450Component::set_bluetooth(bool enable) { | |||||||
| // Set Baud rate | // Set Baud rate | ||||||
| void LD2450Component::set_baud_rate(const std::string &state) { | void LD2450Component::set_baud_rate(const std::string &state) { | ||||||
|   this->set_config_mode_(true); |   this->set_config_mode_(true); | ||||||
|   uint8_t cmd_value[2] = {BAUD_RATE_ENUM_TO_INT.at(state), 0x00}; |   uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00}; | ||||||
|   this->send_command_(CMD_SET_BAUD_RATE, cmd_value, 2); |   this->send_command_(CMD_SET_BAUD_RATE, cmd_value, 2); | ||||||
|   this->set_timeout(200, [this]() { this->restart_(); }); |   this->set_timeout(200, [this]() { this->restart_(); }); | ||||||
| } | } | ||||||
| @@ -734,7 +813,7 @@ void LD2450Component::set_baud_rate(const std::string &state) { | |||||||
| // Set Zone Type - one of: Disabled, Detection, Filter | // Set Zone Type - one of: Disabled, Detection, Filter | ||||||
| void LD2450Component::set_zone_type(const std::string &state) { | void LD2450Component::set_zone_type(const std::string &state) { | ||||||
|   ESP_LOGV(TAG, "Set zone type: %s", state.c_str()); |   ESP_LOGV(TAG, "Set zone type: %s", state.c_str()); | ||||||
|   uint8_t zone_type = ZONE_TYPE_ENUM_TO_INT.at(state); |   uint8_t zone_type = find_uint8(ZONE_TYPE_BY_STR, state); | ||||||
|   this->zone_type_ = zone_type; |   this->zone_type_ = zone_type; | ||||||
|   this->send_set_zone_command_(); |   this->send_set_zone_command_(); | ||||||
| } | } | ||||||
| @@ -742,7 +821,7 @@ void LD2450Component::set_zone_type(const std::string &state) { | |||||||
| // Publish Zone Type to Select component | // Publish Zone Type to Select component | ||||||
| void LD2450Component::publish_zone_type() { | void LD2450Component::publish_zone_type() { | ||||||
| #ifdef USE_SELECT | #ifdef USE_SELECT | ||||||
|   std::string zone_type = ZONE_TYPE_INT_TO_ENUM.at(static_cast<ZoneTypeStructure>(this->zone_type_)); |   std::string zone_type = find_str(ZONE_TYPE_BY_UINT, this->zone_type_); | ||||||
|   if (this->zone_type_select_ != nullptr) { |   if (this->zone_type_select_ != nullptr) { | ||||||
|     this->zone_type_select_->publish_state(zone_type); |     this->zone_type_select_->publish_state(zone_type); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -1,7 +1,5 @@ | |||||||
| #pragma once | #pragma once | ||||||
|  |  | ||||||
| #include <iomanip> |  | ||||||
| #include <map> |  | ||||||
| #include "esphome/components/uart/uart.h" | #include "esphome/components/uart/uart.h" | ||||||
| #include "esphome/core/component.h" | #include "esphome/core/component.h" | ||||||
| #include "esphome/core/defines.h" | #include "esphome/core/defines.h" | ||||||
| @@ -66,49 +64,6 @@ struct ZoneOfNumbers { | |||||||
| }; | }; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| enum BaudRateStructure : uint8_t { |  | ||||||
|   BAUD_RATE_9600 = 1, |  | ||||||
|   BAUD_RATE_19200 = 2, |  | ||||||
|   BAUD_RATE_38400 = 3, |  | ||||||
|   BAUD_RATE_57600 = 4, |  | ||||||
|   BAUD_RATE_115200 = 5, |  | ||||||
|   BAUD_RATE_230400 = 6, |  | ||||||
|   BAUD_RATE_256000 = 7, |  | ||||||
|   BAUD_RATE_460800 = 8 |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| // Convert baud rate enum to int |  | ||||||
| static const std::map<std::string, uint8_t> BAUD_RATE_ENUM_TO_INT{ |  | ||||||
|     {"9600", BAUD_RATE_9600},     {"19200", BAUD_RATE_19200},   {"38400", BAUD_RATE_38400}, |  | ||||||
|     {"57600", BAUD_RATE_57600},   {"115200", BAUD_RATE_115200}, {"230400", BAUD_RATE_230400}, |  | ||||||
|     {"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800}}; |  | ||||||
|  |  | ||||||
| // Zone type struct |  | ||||||
| enum ZoneTypeStructure : uint8_t { ZONE_DISABLED = 0, ZONE_DETECTION = 1, ZONE_FILTER = 2 }; |  | ||||||
|  |  | ||||||
| // Convert zone type int to enum |  | ||||||
| static const std::map<ZoneTypeStructure, std::string> ZONE_TYPE_INT_TO_ENUM{ |  | ||||||
|     {ZONE_DISABLED, "Disabled"}, {ZONE_DETECTION, "Detection"}, {ZONE_FILTER, "Filter"}}; |  | ||||||
|  |  | ||||||
| // Convert zone type enum to int |  | ||||||
| static const std::map<std::string, uint8_t> ZONE_TYPE_ENUM_TO_INT{ |  | ||||||
|     {"Disabled", ZONE_DISABLED}, {"Detection", ZONE_DETECTION}, {"Filter", ZONE_FILTER}}; |  | ||||||
|  |  | ||||||
| // LD2450 serial command header & footer |  | ||||||
| static const uint8_t CMD_FRAME_HEADER[4] = {0xFD, 0xFC, 0xFB, 0xFA}; |  | ||||||
| static const uint8_t CMD_FRAME_END[4] = {0x04, 0x03, 0x02, 0x01}; |  | ||||||
|  |  | ||||||
| enum PeriodicDataStructure : uint8_t { |  | ||||||
|   TARGET_X = 4, |  | ||||||
|   TARGET_Y = 6, |  | ||||||
|   TARGET_SPEED = 8, |  | ||||||
|   TARGET_RESOLUTION = 10, |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| enum PeriodicDataValue : uint8_t { HEAD = 0xAA, END = 0x55, CHECK = 0x00 }; |  | ||||||
|  |  | ||||||
| enum AckDataStructure : uint8_t { COMMAND = 6, COMMAND_STATUS = 7 }; |  | ||||||
|  |  | ||||||
| class LD2450Component : public Component, public uart::UARTDevice { | class LD2450Component : public Component, public uart::UARTDevice { | ||||||
| #ifdef USE_SENSOR | #ifdef USE_SENSOR | ||||||
|   SUB_SENSOR(target_count) |   SUB_SENSOR(target_count) | ||||||
| @@ -141,7 +96,6 @@ class LD2450Component : public Component, public uart::UARTDevice { | |||||||
| #endif | #endif | ||||||
|  |  | ||||||
|  public: |  public: | ||||||
|   LD2450Component(); |  | ||||||
|   void setup() override; |   void setup() override; | ||||||
|   void dump_config() override; |   void dump_config() override; | ||||||
|   void loop() override; |   void loop() override; | ||||||
| @@ -197,17 +151,17 @@ class LD2450Component : public Component, public uart::UARTDevice { | |||||||
|   bool get_timeout_status_(uint32_t check_millis); |   bool get_timeout_status_(uint32_t check_millis); | ||||||
|   uint8_t count_targets_in_zone_(const Zone &zone, bool is_moving); |   uint8_t count_targets_in_zone_(const Zone &zone, bool is_moving); | ||||||
|  |  | ||||||
|   Target target_info_[MAX_TARGETS]; |  | ||||||
|   Zone zone_config_[MAX_ZONES]; |  | ||||||
|   uint8_t buffer_pos_ = 0;  // where to resume processing/populating buffer |  | ||||||
|   uint8_t buffer_data_[MAX_LINE_LENGTH]; |  | ||||||
|   uint32_t last_periodic_millis_ = 0; |   uint32_t last_periodic_millis_ = 0; | ||||||
|   uint32_t presence_millis_ = 0; |   uint32_t presence_millis_ = 0; | ||||||
|   uint32_t still_presence_millis_ = 0; |   uint32_t still_presence_millis_ = 0; | ||||||
|   uint32_t moving_presence_millis_ = 0; |   uint32_t moving_presence_millis_ = 0; | ||||||
|   uint16_t throttle_ = 0; |   uint16_t throttle_ = 0; | ||||||
|   uint16_t timeout_ = 5; |   uint16_t timeout_ = 5; | ||||||
|  |   uint8_t buffer_pos_ = 0;  // where to resume processing/populating buffer | ||||||
|  |   uint8_t buffer_data_[MAX_LINE_LENGTH]; | ||||||
|   uint8_t zone_type_ = 0; |   uint8_t zone_type_ = 0; | ||||||
|  |   Target target_info_[MAX_TARGETS]; | ||||||
|  |   Zone zone_config_[MAX_ZONES]; | ||||||
|   std::string version_{}; |   std::string version_{}; | ||||||
|   std::string mac_{}; |   std::string mac_{}; | ||||||
| #ifdef USE_NUMBER | #ifdef USE_NUMBER | ||||||
|   | |||||||
| @@ -264,7 +264,7 @@ async def component_to_code(config): | |||||||
|     # force using arduino framework |     # force using arduino framework | ||||||
|     cg.add_platformio_option("framework", "arduino") |     cg.add_platformio_option("framework", "arduino") | ||||||
|     cg.add_build_flag("-DUSE_ARDUINO") |     cg.add_build_flag("-DUSE_ARDUINO") | ||||||
|     cg.set_cpp_standard("gnu++17") |     cg.set_cpp_standard("gnu++20") | ||||||
|  |  | ||||||
|     # disable library compatibility checks |     # disable library compatibility checks | ||||||
|     cg.add_platformio_option("lib_ldf_mode", "off") |     cg.add_platformio_option("lib_ldf_mode", "off") | ||||||
|   | |||||||
| @@ -10,9 +10,11 @@ namespace libretiny { | |||||||
| static const char *const TAG = "lt.component"; | static const char *const TAG = "lt.component"; | ||||||
|  |  | ||||||
| void LTComponent::dump_config() { | void LTComponent::dump_config() { | ||||||
|   ESP_LOGCONFIG(TAG, "LibreTiny:"); |   ESP_LOGCONFIG(TAG, | ||||||
|   ESP_LOGCONFIG(TAG, "  Version: %s", LT_BANNER_STR + 10); |                 "LibreTiny:\n" | ||||||
|   ESP_LOGCONFIG(TAG, "  Loglevel: %u", LT_LOGLEVEL); |                 "  Version: %s\n" | ||||||
|  |                 "  Loglevel: %u", | ||||||
|  |                 LT_BANNER_STR + 10, LT_LOGLEVEL); | ||||||
|  |  | ||||||
| #ifdef USE_TEXT_SENSOR | #ifdef USE_TEXT_SENSOR | ||||||
|   if (this->version_ != nullptr) { |   if (this->version_ != nullptr) { | ||||||
|   | |||||||
| @@ -48,6 +48,11 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch | |||||||
|   // For non-main tasks, queue the message for callbacks - but only if we have any callbacks registered |   // For non-main tasks, queue the message for callbacks - but only if we have any callbacks registered | ||||||
|   message_sent = |   message_sent = | ||||||
|       this->log_buffer_->send_message_thread_safe(level, tag, static_cast<uint16_t>(line), current_task, format, args); |       this->log_buffer_->send_message_thread_safe(level, tag, static_cast<uint16_t>(line), current_task, format, args); | ||||||
|  |   if (message_sent) { | ||||||
|  |     // Enable logger loop to process the buffered message | ||||||
|  |     // This is safe to call from any context including ISRs | ||||||
|  |     this->enable_loop_soon_any_context(); | ||||||
|  |   } | ||||||
| #endif  // USE_ESPHOME_TASK_LOG_BUFFER | #endif  // USE_ESPHOME_TASK_LOG_BUFFER | ||||||
|  |  | ||||||
|   // Emergency console logging for non-main tasks when ring buffer is full or disabled |   // Emergency console logging for non-main tasks when ring buffer is full or disabled | ||||||
| @@ -139,6 +144,10 @@ Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate | |||||||
| #ifdef USE_ESPHOME_TASK_LOG_BUFFER | #ifdef USE_ESPHOME_TASK_LOG_BUFFER | ||||||
| void Logger::init_log_buffer(size_t total_buffer_size) { | void Logger::init_log_buffer(size_t total_buffer_size) { | ||||||
|   this->log_buffer_ = esphome::make_unique<logger::TaskLogBuffer>(total_buffer_size); |   this->log_buffer_ = esphome::make_unique<logger::TaskLogBuffer>(total_buffer_size); | ||||||
|  |  | ||||||
|  |   // Start with loop disabled when using task buffer (unless using USB CDC) | ||||||
|  |   // The loop will be enabled automatically when messages arrive | ||||||
|  |   this->disable_loop_when_buffer_empty_(); | ||||||
| } | } | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| @@ -189,6 +198,10 @@ void Logger::loop() { | |||||||
|         this->write_msg_(this->tx_buffer_); |         this->write_msg_(this->tx_buffer_); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |   } else { | ||||||
|  |     // No messages to process, disable loop if appropriate | ||||||
|  |     // This reduces overhead when there's no async logging activity | ||||||
|  |     this->disable_loop_when_buffer_empty_(); | ||||||
|   } |   } | ||||||
| #endif | #endif | ||||||
| } | } | ||||||
|   | |||||||
| @@ -358,6 +358,26 @@ class Logger : public Component { | |||||||
|     static const uint16_t RESET_COLOR_LEN = strlen(ESPHOME_LOG_RESET_COLOR); |     static const uint16_t RESET_COLOR_LEN = strlen(ESPHOME_LOG_RESET_COLOR); | ||||||
|     this->write_body_to_buffer_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN, buffer, buffer_at, buffer_size); |     this->write_body_to_buffer_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN, buffer, buffer_at, buffer_size); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  | #ifdef USE_ESP32 | ||||||
|  |   // Disable loop when task buffer is empty (with USB CDC check) | ||||||
|  |   inline void disable_loop_when_buffer_empty_() { | ||||||
|  |     // Thread safety note: This is safe even if another task calls enable_loop_soon_any_context() | ||||||
|  |     // concurrently. If that happens between our check and disable_loop(), the enable request | ||||||
|  |     // will be processed on the next main loop iteration since: | ||||||
|  |     // - disable_loop() takes effect immediately | ||||||
|  |     // - enable_loop_soon_any_context() sets a pending flag that's checked at loop start | ||||||
|  | #if defined(USE_LOGGER_USB_CDC) && defined(USE_ARDUINO) | ||||||
|  |     // Only disable if not using USB CDC (which needs loop for connection detection) | ||||||
|  |     if (this->uart_ != UART_SELECTION_USB_CDC) { | ||||||
|  |       this->disable_loop(); | ||||||
|  |     } | ||||||
|  | #else | ||||||
|  |     // No USB CDC support, always safe to disable | ||||||
|  |     this->disable_loop(); | ||||||
|  | #endif | ||||||
|  |   } | ||||||
|  | #endif | ||||||
| }; | }; | ||||||
| extern Logger *global_logger;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | extern Logger *global_logger;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -466,7 +466,7 @@ LVGL_SCHEMA = cv.All( | |||||||
|                 ): lvalid.lv_color, |                 ): lvalid.lv_color, | ||||||
|                 cv.Optional(df.CONF_THEME): cv.Schema( |                 cv.Optional(df.CONF_THEME): cv.Schema( | ||||||
|                     { |                     { | ||||||
|                         cv.Optional(name): obj_schema(w) |                         cv.Optional(name): obj_schema(w).extend(FULL_STYLE_SCHEMA) | ||||||
|                         for name, w in WIDGET_TYPES.items() |                         for name, w in WIDGET_TYPES.items() | ||||||
|                     } |                     } | ||||||
|                 ), |                 ), | ||||||
|   | |||||||
| @@ -21,7 +21,7 @@ from esphome.core.config import StartupTrigger | |||||||
| from esphome.schema_extractors import SCHEMA_EXTRACT | from esphome.schema_extractors import SCHEMA_EXTRACT | ||||||
|  |  | ||||||
| from . import defines as df, lv_validation as lvalid | from . import defines as df, lv_validation as lvalid | ||||||
| from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR | from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR, TYPE_GRID | ||||||
| from .helpers import add_lv_use, requires_component, validate_printf | from .helpers import add_lv_use, requires_component, validate_printf | ||||||
| from .lv_validation import lv_color, lv_font, lv_gradient, lv_image, opacity | from .lv_validation import lv_color, lv_font, lv_gradient, lv_image, opacity | ||||||
| from .lvcode import LvglComponent, lv_event_t_ptr | from .lvcode import LvglComponent, lv_event_t_ptr | ||||||
| @@ -349,7 +349,60 @@ def obj_schema(widget_type: WidgetType): | |||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _validate_grid_layout(config): | ||||||
|  |     layout = config[df.CONF_LAYOUT] | ||||||
|  |     rows = len(layout[df.CONF_GRID_ROWS]) | ||||||
|  |     columns = len(layout[df.CONF_GRID_COLUMNS]) | ||||||
|  |     used_cells = [[None] * columns for _ in range(rows)] | ||||||
|  |     for index, widget in enumerate(config[df.CONF_WIDGETS]): | ||||||
|  |         _, w = next(iter(widget.items())) | ||||||
|  |         if (df.CONF_GRID_CELL_COLUMN_POS in w) != (df.CONF_GRID_CELL_ROW_POS in w): | ||||||
|  |             # pylint: disable=raise-missing-from | ||||||
|  |             raise cv.Invalid( | ||||||
|  |                 "Both row and column positions must be specified, or both omitted", | ||||||
|  |                 [df.CONF_WIDGETS, index], | ||||||
|  |             ) | ||||||
|  |         if df.CONF_GRID_CELL_ROW_POS in w: | ||||||
|  |             row = w[df.CONF_GRID_CELL_ROW_POS] | ||||||
|  |             column = w[df.CONF_GRID_CELL_COLUMN_POS] | ||||||
|  |         else: | ||||||
|  |             try: | ||||||
|  |                 row, column = next( | ||||||
|  |                     (r_idx, c_idx) | ||||||
|  |                     for r_idx, row in enumerate(used_cells) | ||||||
|  |                     for c_idx, value in enumerate(row) | ||||||
|  |                     if value is None | ||||||
|  |                 ) | ||||||
|  |             except StopIteration: | ||||||
|  |                 # pylint: disable=raise-missing-from | ||||||
|  |                 raise cv.Invalid( | ||||||
|  |                     "No free cells available in grid layout", [df.CONF_WIDGETS, index] | ||||||
|  |                 ) | ||||||
|  |             w[df.CONF_GRID_CELL_ROW_POS] = row | ||||||
|  |             w[df.CONF_GRID_CELL_COLUMN_POS] = column | ||||||
|  |  | ||||||
|  |         for i in range(w[df.CONF_GRID_CELL_ROW_SPAN]): | ||||||
|  |             for j in range(w[df.CONF_GRID_CELL_COLUMN_SPAN]): | ||||||
|  |                 if row + i >= rows or column + j >= columns: | ||||||
|  |                     # pylint: disable=raise-missing-from | ||||||
|  |                     raise cv.Invalid( | ||||||
|  |                         f"Cell at {row}/{column} span {w[df.CONF_GRID_CELL_ROW_SPAN]}x{w[df.CONF_GRID_CELL_COLUMN_SPAN]} " | ||||||
|  |                         f"exceeds grid size {rows}x{columns}", | ||||||
|  |                         [df.CONF_WIDGETS, index], | ||||||
|  |                     ) | ||||||
|  |                 if used_cells[row + i][column + j] is not None: | ||||||
|  |                     # pylint: disable=raise-missing-from | ||||||
|  |                     raise cv.Invalid( | ||||||
|  |                         f"Cell span {row + i}/{column + j} already occupied by widget at index {used_cells[row + i][column + j]}", | ||||||
|  |                         [df.CONF_WIDGETS, index], | ||||||
|  |                     ) | ||||||
|  |                 used_cells[row + i][column + j] = index | ||||||
|  |  | ||||||
|  |     return config | ||||||
|  |  | ||||||
|  |  | ||||||
| LAYOUT_SCHEMAS = {} | LAYOUT_SCHEMAS = {} | ||||||
|  | LAYOUT_VALIDATORS = {TYPE_GRID: _validate_grid_layout} | ||||||
|  |  | ||||||
| ALIGN_TO_SCHEMA = { | ALIGN_TO_SCHEMA = { | ||||||
|     cv.Optional(df.CONF_ALIGN_TO): cv.Schema( |     cv.Optional(df.CONF_ALIGN_TO): cv.Schema( | ||||||
| @@ -402,8 +455,8 @@ LAYOUT_SCHEMA = { | |||||||
| } | } | ||||||
|  |  | ||||||
| GRID_CELL_SCHEMA = { | GRID_CELL_SCHEMA = { | ||||||
|     cv.Required(df.CONF_GRID_CELL_ROW_POS): cv.positive_int, |     cv.Optional(df.CONF_GRID_CELL_ROW_POS): cv.positive_int, | ||||||
|     cv.Required(df.CONF_GRID_CELL_COLUMN_POS): cv.positive_int, |     cv.Optional(df.CONF_GRID_CELL_COLUMN_POS): cv.positive_int, | ||||||
|     cv.Optional(df.CONF_GRID_CELL_ROW_SPAN, default=1): cv.positive_int, |     cv.Optional(df.CONF_GRID_CELL_ROW_SPAN, default=1): cv.positive_int, | ||||||
|     cv.Optional(df.CONF_GRID_CELL_COLUMN_SPAN, default=1): cv.positive_int, |     cv.Optional(df.CONF_GRID_CELL_COLUMN_SPAN, default=1): cv.positive_int, | ||||||
|     cv.Optional(df.CONF_GRID_CELL_X_ALIGN): grid_alignments, |     cv.Optional(df.CONF_GRID_CELL_X_ALIGN): grid_alignments, | ||||||
| @@ -474,7 +527,10 @@ def container_validator(schema, widget_type: WidgetType): | |||||||
|         result = result.extend( |         result = result.extend( | ||||||
|             LAYOUT_SCHEMAS.get(ltype.lower(), LAYOUT_SCHEMAS[df.TYPE_NONE]) |             LAYOUT_SCHEMAS.get(ltype.lower(), LAYOUT_SCHEMAS[df.TYPE_NONE]) | ||||||
|         ) |         ) | ||||||
|         return result(value) |         value = result(value) | ||||||
|  |         if layout_validator := LAYOUT_VALIDATORS.get(ltype): | ||||||
|  |             value = layout_validator(value) | ||||||
|  |         return value | ||||||
|  |  | ||||||
|     return validator |     return validator | ||||||
|  |  | ||||||
|   | |||||||
| @@ -6,7 +6,11 @@ namespace mcp23xxx_base { | |||||||
|  |  | ||||||
| float MCP23XXXBase::get_setup_priority() const { return setup_priority::IO; } | float MCP23XXXBase::get_setup_priority() const { return setup_priority::IO; } | ||||||
|  |  | ||||||
| void MCP23XXXGPIOPin::setup() { pin_mode(flags_); } | void MCP23XXXGPIOPin::setup() { | ||||||
|  |   pin_mode(flags_); | ||||||
|  |   this->parent_->pin_interrupt_mode(this->pin_, this->interrupt_mode_); | ||||||
|  | } | ||||||
|  |  | ||||||
| void MCP23XXXGPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } | void MCP23XXXGPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } | ||||||
| bool MCP23XXXGPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } | bool MCP23XXXGPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } | ||||||
| void MCP23XXXGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } | void MCP23XXXGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } | ||||||
|   | |||||||
| @@ -88,12 +88,7 @@ async def to_code(config): | |||||||
|     if CORE.using_esp_idf and CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] >= cv.Version( |     if CORE.using_esp_idf and CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] >= cv.Version( | ||||||
|         5, 0, 0 |         5, 0, 0 | ||||||
|     ): |     ): | ||||||
|         add_idf_component( |         add_idf_component(name="espressif/mdns", ref="1.8.2") | ||||||
|             name="mdns", |  | ||||||
|             repo="https://github.com/espressif/esp-protocols.git", |  | ||||||
|             ref="mdns-v1.8.2", |  | ||||||
|             path="components/mdns", |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|     cg.add_define("USE_MDNS") |     cg.add_define("USE_MDNS") | ||||||
|  |  | ||||||
|   | |||||||
| @@ -449,11 +449,7 @@ async def to_code(config): | |||||||
|     cg.add_define("USE_MICRO_WAKE_WORD") |     cg.add_define("USE_MICRO_WAKE_WORD") | ||||||
|     cg.add_define("USE_OTA_STATE_CALLBACK") |     cg.add_define("USE_OTA_STATE_CALLBACK") | ||||||
|  |  | ||||||
|     esp32.add_idf_component( |     esp32.add_idf_component(name="espressif/esp-tflite-micro", ref="1.3.3~1") | ||||||
|         name="esp-tflite-micro", |  | ||||||
|         repo="https://github.com/espressif/esp-tflite-micro", |  | ||||||
|         ref="v1.3.3.1", |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     cg.add_build_flag("-DTF_LITE_STATIC_MEMORY") |     cg.add_build_flag("-DTF_LITE_STATIC_MEMORY") | ||||||
|     cg.add_build_flag("-DTF_LITE_DISABLE_X86_NEON") |     cg.add_build_flag("-DTF_LITE_DISABLE_X86_NEON") | ||||||
|   | |||||||
| @@ -344,7 +344,7 @@ void OnlineImage::end_connection_() { | |||||||
| } | } | ||||||
|  |  | ||||||
| bool OnlineImage::validate_url_(const std::string &url) { | bool OnlineImage::validate_url_(const std::string &url) { | ||||||
|   if ((url.length() < 8) || (url.find("http") != 0) || (url.find("://") == std::string::npos)) { |   if ((url.length() < 8) || !url.starts_with("http") || (url.find("://") == std::string::npos)) { | ||||||
|     ESP_LOGE(TAG, "URL is invalid and/or must be prefixed with 'http://' or 'https://'"); |     ESP_LOGE(TAG, "URL is invalid and/or must be prefixed with 'http://' or 'https://'"); | ||||||
|     return false; |     return false; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -584,7 +584,7 @@ void PN7150::nci_fsm_transition_() { | |||||||
|       } else { |       } else { | ||||||
|         this->nci_fsm_set_state_(NCIState::NFCC_INIT); |         this->nci_fsm_set_state_(NCIState::NFCC_INIT); | ||||||
|       } |       } | ||||||
|       // fall through |       [[fallthrough]]; | ||||||
|  |  | ||||||
|     case NCIState::NFCC_INIT: |     case NCIState::NFCC_INIT: | ||||||
|       if (this->init_core_() != nfc::STATUS_OK) { |       if (this->init_core_() != nfc::STATUS_OK) { | ||||||
| @@ -594,7 +594,7 @@ void PN7150::nci_fsm_transition_() { | |||||||
|       } else { |       } else { | ||||||
|         this->nci_fsm_set_state_(NCIState::NFCC_CONFIG); |         this->nci_fsm_set_state_(NCIState::NFCC_CONFIG); | ||||||
|       } |       } | ||||||
|       // fall through |       [[fallthrough]]; | ||||||
|  |  | ||||||
|     case NCIState::NFCC_CONFIG: |     case NCIState::NFCC_CONFIG: | ||||||
|       if (this->send_init_config_() != nfc::STATUS_OK) { |       if (this->send_init_config_() != nfc::STATUS_OK) { | ||||||
| @@ -605,7 +605,7 @@ void PN7150::nci_fsm_transition_() { | |||||||
|         this->config_refresh_pending_ = false; |         this->config_refresh_pending_ = false; | ||||||
|         this->nci_fsm_set_state_(NCIState::NFCC_SET_DISCOVER_MAP); |         this->nci_fsm_set_state_(NCIState::NFCC_SET_DISCOVER_MAP); | ||||||
|       } |       } | ||||||
|       // fall through |       [[fallthrough]]; | ||||||
|  |  | ||||||
|     case NCIState::NFCC_SET_DISCOVER_MAP: |     case NCIState::NFCC_SET_DISCOVER_MAP: | ||||||
|       if (this->set_discover_map_() != nfc::STATUS_OK) { |       if (this->set_discover_map_() != nfc::STATUS_OK) { | ||||||
| @@ -615,7 +615,7 @@ void PN7150::nci_fsm_transition_() { | |||||||
|       } else { |       } else { | ||||||
|         this->nci_fsm_set_state_(NCIState::NFCC_SET_LISTEN_MODE_ROUTING); |         this->nci_fsm_set_state_(NCIState::NFCC_SET_LISTEN_MODE_ROUTING); | ||||||
|       } |       } | ||||||
|       // fall through |       [[fallthrough]]; | ||||||
|  |  | ||||||
|     case NCIState::NFCC_SET_LISTEN_MODE_ROUTING: |     case NCIState::NFCC_SET_LISTEN_MODE_ROUTING: | ||||||
|       if (this->set_listen_mode_routing_() != nfc::STATUS_OK) { |       if (this->set_listen_mode_routing_() != nfc::STATUS_OK) { | ||||||
| @@ -625,7 +625,7 @@ void PN7150::nci_fsm_transition_() { | |||||||
|       } else { |       } else { | ||||||
|         this->nci_fsm_set_state_(NCIState::RFST_IDLE); |         this->nci_fsm_set_state_(NCIState::RFST_IDLE); | ||||||
|       } |       } | ||||||
|       // fall through |       [[fallthrough]]; | ||||||
|  |  | ||||||
|     case NCIState::RFST_IDLE: |     case NCIState::RFST_IDLE: | ||||||
|       if (this->nci_state_error_ == NCIState::RFST_DISCOVERY) { |       if (this->nci_state_error_ == NCIState::RFST_DISCOVERY) { | ||||||
| @@ -650,14 +650,14 @@ void PN7150::nci_fsm_transition_() { | |||||||
|  |  | ||||||
|     case NCIState::RFST_W4_HOST_SELECT: |     case NCIState::RFST_W4_HOST_SELECT: | ||||||
|       select_endpoint_(); |       select_endpoint_(); | ||||||
|       // fall through |       [[fallthrough]]; | ||||||
|  |  | ||||||
|     // All cases below are waiting for NOTIFICATION messages |     // All cases below are waiting for NOTIFICATION messages | ||||||
|     case NCIState::RFST_DISCOVERY: |     case NCIState::RFST_DISCOVERY: | ||||||
|       if (this->config_refresh_pending_) { |       if (this->config_refresh_pending_) { | ||||||
|         this->refresh_core_config_(); |         this->refresh_core_config_(); | ||||||
|       } |       } | ||||||
|       // fall through |       [[fallthrough]]; | ||||||
|  |  | ||||||
|     case NCIState::RFST_LISTEN_ACTIVE: |     case NCIState::RFST_LISTEN_ACTIVE: | ||||||
|     case NCIState::RFST_LISTEN_SLEEP: |     case NCIState::RFST_LISTEN_SLEEP: | ||||||
|   | |||||||
| @@ -609,7 +609,7 @@ void PN7160::nci_fsm_transition_() { | |||||||
|       } else { |       } else { | ||||||
|         this->nci_fsm_set_state_(NCIState::NFCC_INIT); |         this->nci_fsm_set_state_(NCIState::NFCC_INIT); | ||||||
|       } |       } | ||||||
|       // fall through |       [[fallthrough]]; | ||||||
|  |  | ||||||
|     case NCIState::NFCC_INIT: |     case NCIState::NFCC_INIT: | ||||||
|       if (this->init_core_() != nfc::STATUS_OK) { |       if (this->init_core_() != nfc::STATUS_OK) { | ||||||
| @@ -619,7 +619,7 @@ void PN7160::nci_fsm_transition_() { | |||||||
|       } else { |       } else { | ||||||
|         this->nci_fsm_set_state_(NCIState::NFCC_CONFIG); |         this->nci_fsm_set_state_(NCIState::NFCC_CONFIG); | ||||||
|       } |       } | ||||||
|       // fall through |       [[fallthrough]]; | ||||||
|  |  | ||||||
|     case NCIState::NFCC_CONFIG: |     case NCIState::NFCC_CONFIG: | ||||||
|       if (this->send_init_config_() != nfc::STATUS_OK) { |       if (this->send_init_config_() != nfc::STATUS_OK) { | ||||||
| @@ -630,7 +630,7 @@ void PN7160::nci_fsm_transition_() { | |||||||
|         this->config_refresh_pending_ = false; |         this->config_refresh_pending_ = false; | ||||||
|         this->nci_fsm_set_state_(NCIState::NFCC_SET_DISCOVER_MAP); |         this->nci_fsm_set_state_(NCIState::NFCC_SET_DISCOVER_MAP); | ||||||
|       } |       } | ||||||
|       // fall through |       [[fallthrough]]; | ||||||
|  |  | ||||||
|     case NCIState::NFCC_SET_DISCOVER_MAP: |     case NCIState::NFCC_SET_DISCOVER_MAP: | ||||||
|       if (this->set_discover_map_() != nfc::STATUS_OK) { |       if (this->set_discover_map_() != nfc::STATUS_OK) { | ||||||
| @@ -640,7 +640,7 @@ void PN7160::nci_fsm_transition_() { | |||||||
|       } else { |       } else { | ||||||
|         this->nci_fsm_set_state_(NCIState::NFCC_SET_LISTEN_MODE_ROUTING); |         this->nci_fsm_set_state_(NCIState::NFCC_SET_LISTEN_MODE_ROUTING); | ||||||
|       } |       } | ||||||
|       // fall through |       [[fallthrough]]; | ||||||
|  |  | ||||||
|     case NCIState::NFCC_SET_LISTEN_MODE_ROUTING: |     case NCIState::NFCC_SET_LISTEN_MODE_ROUTING: | ||||||
|       if (this->set_listen_mode_routing_() != nfc::STATUS_OK) { |       if (this->set_listen_mode_routing_() != nfc::STATUS_OK) { | ||||||
| @@ -650,7 +650,7 @@ void PN7160::nci_fsm_transition_() { | |||||||
|       } else { |       } else { | ||||||
|         this->nci_fsm_set_state_(NCIState::RFST_IDLE); |         this->nci_fsm_set_state_(NCIState::RFST_IDLE); | ||||||
|       } |       } | ||||||
|       // fall through |       [[fallthrough]]; | ||||||
|  |  | ||||||
|     case NCIState::RFST_IDLE: |     case NCIState::RFST_IDLE: | ||||||
|       if (this->nci_state_error_ == NCIState::RFST_DISCOVERY) { |       if (this->nci_state_error_ == NCIState::RFST_DISCOVERY) { | ||||||
| @@ -675,14 +675,14 @@ void PN7160::nci_fsm_transition_() { | |||||||
|  |  | ||||||
|     case NCIState::RFST_W4_HOST_SELECT: |     case NCIState::RFST_W4_HOST_SELECT: | ||||||
|       select_endpoint_(); |       select_endpoint_(); | ||||||
|       // fall through |       [[fallthrough]]; | ||||||
|  |  | ||||||
|     // All cases below are waiting for NOTIFICATION messages |     // All cases below are waiting for NOTIFICATION messages | ||||||
|     case NCIState::RFST_DISCOVERY: |     case NCIState::RFST_DISCOVERY: | ||||||
|       if (this->config_refresh_pending_) { |       if (this->config_refresh_pending_) { | ||||||
|         this->refresh_core_config_(); |         this->refresh_core_config_(); | ||||||
|       } |       } | ||||||
|       // fall through |       [[fallthrough]]; | ||||||
|  |  | ||||||
|     case NCIState::RFST_LISTEN_ACTIVE: |     case NCIState::RFST_LISTEN_ACTIVE: | ||||||
|     case NCIState::RFST_LISTEN_SLEEP: |     case NCIState::RFST_LISTEN_SLEEP: | ||||||
|   | |||||||
| @@ -63,7 +63,7 @@ void PulseMeterSensor::loop() { | |||||||
|   // If an edge was peeked, repay the debt |   // If an edge was peeked, repay the debt | ||||||
|   if (this->peeked_edge_ && this->get_->count_ > 0) { |   if (this->peeked_edge_ && this->get_->count_ > 0) { | ||||||
|     this->peeked_edge_ = false; |     this->peeked_edge_ = false; | ||||||
|     this->get_->count_--; |     this->get_->count_--;  // NOLINT(clang-diagnostic-deprecated-volatile) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // If there is an unprocessed edge, and filter_us_ has passed since, count this edge early |   // If there is an unprocessed edge, and filter_us_ has passed since, count this edge early | ||||||
| @@ -71,7 +71,7 @@ void PulseMeterSensor::loop() { | |||||||
|       now - this->get_->last_rising_edge_us_ >= this->filter_us_) { |       now - this->get_->last_rising_edge_us_ >= this->filter_us_) { | ||||||
|     this->peeked_edge_ = true; |     this->peeked_edge_ = true; | ||||||
|     this->get_->last_detected_edge_us_ = this->get_->last_rising_edge_us_; |     this->get_->last_detected_edge_us_ = this->get_->last_rising_edge_us_; | ||||||
|     this->get_->count_++; |     this->get_->count_++;  // NOLINT(clang-diagnostic-deprecated-volatile) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // Check if we detected a pulse this loop |   // Check if we detected a pulse this loop | ||||||
| @@ -146,7 +146,7 @@ void IRAM_ATTR PulseMeterSensor::edge_intr(PulseMeterSensor *sensor) { | |||||||
|     state.last_sent_edge_us_ = now; |     state.last_sent_edge_us_ = now; | ||||||
|     set.last_detected_edge_us_ = now; |     set.last_detected_edge_us_ = now; | ||||||
|     set.last_rising_edge_us_ = now; |     set.last_rising_edge_us_ = now; | ||||||
|     set.count_++; |     set.count_++;  // NOLINT(clang-diagnostic-deprecated-volatile) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // This ISR is bound to rising edges, so the pin is high |   // This ISR is bound to rising edges, so the pin is high | ||||||
| @@ -169,7 +169,7 @@ void IRAM_ATTR PulseMeterSensor::pulse_intr(PulseMeterSensor *sensor) { | |||||||
|   } else if (length && !state.latched_ && sensor->last_pin_val_) {  // Long enough high edge |   } else if (length && !state.latched_ && sensor->last_pin_val_) {  // Long enough high edge | ||||||
|     state.latched_ = true; |     state.latched_ = true; | ||||||
|     set.last_detected_edge_us_ = state.last_intr_; |     set.last_detected_edge_us_ = state.last_intr_; | ||||||
|     set.count_++; |     set.count_++;  // NOLINT(clang-diagnostic-deprecated-volatile) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // Due to order of operations this includes |   // Due to order of operations this includes | ||||||
|   | |||||||
| @@ -17,7 +17,7 @@ bool RadonEyeListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device | |||||||
|  |  | ||||||
|     // Check if the device name starts with any of the prefixes |     // Check if the device name starts with any of the prefixes | ||||||
|     if (std::any_of(prefixes.begin(), prefixes.end(), |     if (std::any_of(prefixes.begin(), prefixes.end(), | ||||||
|                     [&](const std::string &prefix) { return device.get_name().rfind(prefix, 0) == 0; })) { |                     [&](const std::string &prefix) { return device.get_name().starts_with(prefix); })) { | ||||||
|       // Device found |       // Device found | ||||||
|       ESP_LOGD(TAG, "Found Radon Eye device Name: %s (MAC: %s)", device.get_name().c_str(), |       ESP_LOGD(TAG, "Found Radon Eye device Name: %s (MAC: %s)", device.get_name().c_str(), | ||||||
|                device.address_str().c_str()); |                device.address_str().c_str()); | ||||||
|   | |||||||
| @@ -27,7 +27,7 @@ void IRAM_ATTR HOT RemoteReceiverComponentStore::gpio_intr(RemoteReceiverCompone | |||||||
|   if (time_since_change <= arg->filter_us) |   if (time_since_change <= arg->filter_us) | ||||||
|     return; |     return; | ||||||
|  |  | ||||||
|   arg->buffer[arg->buffer_write_at = next] = now; |   arg->buffer[arg->buffer_write_at = next] = now;  // NOLINT(clang-diagnostic-deprecated-volatile) | ||||||
| } | } | ||||||
|  |  | ||||||
| void RemoteReceiverComponent::setup() { | void RemoteReceiverComponent::setup() { | ||||||
|   | |||||||
| @@ -167,7 +167,7 @@ async def to_code(config): | |||||||
|     cg.add_platformio_option("lib_ldf_mode", "chain+") |     cg.add_platformio_option("lib_ldf_mode", "chain+") | ||||||
|     cg.add_platformio_option("board", config[CONF_BOARD]) |     cg.add_platformio_option("board", config[CONF_BOARD]) | ||||||
|     cg.add_build_flag("-DUSE_RP2040") |     cg.add_build_flag("-DUSE_RP2040") | ||||||
|     cg.set_cpp_standard("gnu++17") |     cg.set_cpp_standard("gnu++20") | ||||||
|     cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) |     cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) | ||||||
|     cg.add_define("ESPHOME_VARIANT", "RP2040") |     cg.add_define("ESPHOME_VARIANT", "RP2040") | ||||||
|  |  | ||||||
|   | |||||||
| @@ -33,12 +33,15 @@ class SafeModeComponent : public Component { | |||||||
|   void write_rtc_(uint32_t val); |   void write_rtc_(uint32_t val); | ||||||
|   uint32_t read_rtc_(); |   uint32_t read_rtc_(); | ||||||
|  |  | ||||||
|   bool boot_successful_{false};                   ///< set to true after boot is considered successful |   // Group all 4-byte aligned members together to avoid padding | ||||||
|   uint32_t safe_mode_boot_is_good_after_{60000};  ///< The amount of time after which the boot is considered successful |   uint32_t safe_mode_boot_is_good_after_{60000};  ///< The amount of time after which the boot is considered successful | ||||||
|   uint32_t safe_mode_enable_time_{60000};         ///< The time safe mode should remain active for |   uint32_t safe_mode_enable_time_{60000};         ///< The time safe mode should remain active for | ||||||
|   uint32_t safe_mode_rtc_value_{0}; |   uint32_t safe_mode_rtc_value_{0}; | ||||||
|   uint32_t safe_mode_start_time_{0};  ///< stores when safe mode was enabled |   uint32_t safe_mode_start_time_{0};  ///< stores when safe mode was enabled | ||||||
|  |   // Group 1-byte members together to minimize padding | ||||||
|  |   bool boot_successful_{false};  ///< set to true after boot is considered successful | ||||||
|   uint8_t safe_mode_num_attempts_{0}; |   uint8_t safe_mode_num_attempts_{0}; | ||||||
|  |   // Larger objects at the end | ||||||
|   ESPPreferenceObject rtc_; |   ESPPreferenceObject rtc_; | ||||||
|   CallbackManager<void()> safe_mode_callback_{}; |   CallbackManager<void()> safe_mode_callback_{}; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -23,16 +23,22 @@ std::string state_class_to_string(StateClass state_class) { | |||||||
| Sensor::Sensor() : state(NAN), raw_state(NAN) {} | Sensor::Sensor() : state(NAN), raw_state(NAN) {} | ||||||
|  |  | ||||||
| int8_t Sensor::get_accuracy_decimals() { | int8_t Sensor::get_accuracy_decimals() { | ||||||
|   if (this->accuracy_decimals_.has_value()) |   if (this->sensor_flags_.has_accuracy_override) | ||||||
|     return *this->accuracy_decimals_; |     return this->accuracy_decimals_; | ||||||
|   return 0; |   return 0; | ||||||
| } | } | ||||||
| void Sensor::set_accuracy_decimals(int8_t accuracy_decimals) { this->accuracy_decimals_ = accuracy_decimals; } | void Sensor::set_accuracy_decimals(int8_t accuracy_decimals) { | ||||||
|  |   this->accuracy_decimals_ = accuracy_decimals; | ||||||
|  |   this->sensor_flags_.has_accuracy_override = true; | ||||||
|  | } | ||||||
|  |  | ||||||
| void Sensor::set_state_class(StateClass state_class) { this->state_class_ = state_class; } | void Sensor::set_state_class(StateClass state_class) { | ||||||
|  |   this->state_class_ = state_class; | ||||||
|  |   this->sensor_flags_.has_state_class_override = true; | ||||||
|  | } | ||||||
| StateClass Sensor::get_state_class() { | StateClass Sensor::get_state_class() { | ||||||
|   if (this->state_class_.has_value()) |   if (this->sensor_flags_.has_state_class_override) | ||||||
|     return *this->state_class_; |     return this->state_class_; | ||||||
|   return StateClass::STATE_CLASS_NONE; |   return StateClass::STATE_CLASS_NONE; | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -80,9 +80,9 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa | |||||||
|    * state changes to the database when they are published, even if the state is the |    * state changes to the database when they are published, even if the state is the | ||||||
|    * same as before. |    * same as before. | ||||||
|    */ |    */ | ||||||
|   bool get_force_update() const { return force_update_; } |   bool get_force_update() const { return sensor_flags_.force_update; } | ||||||
|   /// Set force update mode. |   /// Set force update mode. | ||||||
|   void set_force_update(bool force_update) { force_update_ = force_update; } |   void set_force_update(bool force_update) { sensor_flags_.force_update = force_update; } | ||||||
|  |  | ||||||
|   /// Add a filter to the filter chain. Will be appended to the back. |   /// Add a filter to the filter chain. Will be appended to the back. | ||||||
|   void add_filter(Filter *filter); |   void add_filter(Filter *filter); | ||||||
| @@ -155,9 +155,17 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa | |||||||
|  |  | ||||||
|   Filter *filter_list_{nullptr};  ///< Store all active filters. |   Filter *filter_list_{nullptr};  ///< Store all active filters. | ||||||
|  |  | ||||||
|   optional<int8_t> accuracy_decimals_;                  ///< Accuracy in decimals override |   // Group small members together to avoid padding | ||||||
|   optional<StateClass> state_class_{STATE_CLASS_NONE};  ///< State class override |   int8_t accuracy_decimals_{-1};              ///< Accuracy in decimals (-1 = not set) | ||||||
|   bool force_update_{false};                            ///< Force update mode |   StateClass state_class_{STATE_CLASS_NONE};  ///< State class (STATE_CLASS_NONE = not set) | ||||||
|  |  | ||||||
|  |   // Bit-packed flags for sensor-specific settings | ||||||
|  |   struct SensorFlags { | ||||||
|  |     uint8_t has_accuracy_override : 1; | ||||||
|  |     uint8_t has_state_class_override : 1; | ||||||
|  |     uint8_t force_update : 1; | ||||||
|  |     uint8_t reserved : 5;  // Reserved for future use | ||||||
|  |   } sensor_flags_{}; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| }  // namespace sensor | }  // namespace sensor | ||||||
|   | |||||||
| @@ -445,8 +445,7 @@ template<typename T> stm32_err_t stm32_check_ack_timeout(const stm32_err_t s_err | |||||||
|       return STM32_ERR_OK; |       return STM32_ERR_OK; | ||||||
|     case STM32_ERR_NACK: |     case STM32_ERR_NACK: | ||||||
|       log(); |       log(); | ||||||
|       // TODO: c++17 [[fallthrough]] |       [[fallthrough]]; | ||||||
|       /* fallthrough */ |  | ||||||
|     default: |     default: | ||||||
|       return STM32_ERR_UNKNOWN; |       return STM32_ERR_UNKNOWN; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| #include "sn74hc595.h" | #include "sn74hc595.h" | ||||||
| #include "esphome/core/log.h" | #include "esphome/core/log.h" | ||||||
|  | #include <ranges> | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace sn74hc595 { | namespace sn74hc595 { | ||||||
| @@ -55,9 +56,9 @@ void SN74HC595Component::digital_write_(uint16_t pin, bool value) { | |||||||
| } | } | ||||||
|  |  | ||||||
| void SN74HC595GPIOComponent::write_gpio() { | void SN74HC595GPIOComponent::write_gpio() { | ||||||
|   for (auto byte = this->output_bytes_.rbegin(); byte != this->output_bytes_.rend(); byte++) { |   for (uint8_t &output_byte : std::ranges::reverse_view(this->output_bytes_)) { | ||||||
|     for (int8_t i = 7; i >= 0; i--) { |     for (int8_t i = 7; i >= 0; i--) { | ||||||
|       bool bit = (*byte >> i) & 1; |       bool bit = (output_byte >> i) & 1; | ||||||
|       this->data_pin_->digital_write(bit); |       this->data_pin_->digital_write(bit); | ||||||
|       this->clock_pin_->digital_write(true); |       this->clock_pin_->digital_write(true); | ||||||
|       this->clock_pin_->digital_write(false); |       this->clock_pin_->digital_write(false); | ||||||
| @@ -68,9 +69,9 @@ void SN74HC595GPIOComponent::write_gpio() { | |||||||
|  |  | ||||||
| #ifdef USE_SPI | #ifdef USE_SPI | ||||||
| void SN74HC595SPIComponent::write_gpio() { | void SN74HC595SPIComponent::write_gpio() { | ||||||
|   for (auto byte = this->output_bytes_.rbegin(); byte != this->output_bytes_.rend(); byte++) { |   for (uint8_t &output_byte : std::ranges::reverse_view(this->output_bytes_)) { | ||||||
|     this->enable(); |     this->enable(); | ||||||
|     this->transfer_byte(*byte); |     this->transfer_byte(output_byte); | ||||||
|     this->disable(); |     this->disable(); | ||||||
|   } |   } | ||||||
|   SN74HC595Component::write_gpio(); |   SN74HC595Component::write_gpio(); | ||||||
|   | |||||||
| @@ -343,10 +343,9 @@ void AudioPipeline::read_task(void *params) { | |||||||
|     xEventGroupSetBits(this_pipeline->event_group_, EventGroupBits::READER_MESSAGE_FINISHED); |     xEventGroupSetBits(this_pipeline->event_group_, EventGroupBits::READER_MESSAGE_FINISHED); | ||||||
|  |  | ||||||
|     // Wait until the pipeline notifies us the source of the media file |     // Wait until the pipeline notifies us the source of the media file | ||||||
|     EventBits_t event_bits = |     EventBits_t event_bits = xEventGroupWaitBits( | ||||||
|         xEventGroupWaitBits(this_pipeline->event_group_, |         this_pipeline->event_group_, | ||||||
|                             EventGroupBits::READER_COMMAND_INIT_FILE | EventGroupBits::READER_COMMAND_INIT_HTTP | |         EventGroupBits::READER_COMMAND_INIT_FILE | EventGroupBits::READER_COMMAND_INIT_HTTP,  // Bit message to read | ||||||
|                                 EventGroupBits::PIPELINE_COMMAND_STOP,  // Bit message to read |  | ||||||
|         pdFALSE,                                                                              // Clear the bit on exit |         pdFALSE,                                                                              // Clear the bit on exit | ||||||
|         pdFALSE,                                                                              // Wait for all the bits, |         pdFALSE,                                                                              // Wait for all the bits, | ||||||
|         portMAX_DELAY);  // Block indefinitely until bit is set |         portMAX_DELAY);  // Block indefinitely until bit is set | ||||||
| @@ -434,9 +433,9 @@ void AudioPipeline::decode_task(void *params) { | |||||||
|     xEventGroupSetBits(this_pipeline->event_group_, EventGroupBits::DECODER_MESSAGE_FINISHED); |     xEventGroupSetBits(this_pipeline->event_group_, EventGroupBits::DECODER_MESSAGE_FINISHED); | ||||||
|  |  | ||||||
|     // Wait until the reader notifies us that the media type is available |     // Wait until the reader notifies us that the media type is available | ||||||
|     EventBits_t event_bits = xEventGroupWaitBits(this_pipeline->event_group_, |     EventBits_t event_bits = | ||||||
|                                                  EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE | |         xEventGroupWaitBits(this_pipeline->event_group_, | ||||||
|                                                      EventGroupBits::PIPELINE_COMMAND_STOP,  // Bit message to read |                             EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE,  // Bit message to read | ||||||
|                             pdFALSE,                                           // Clear the bit on exit |                             pdFALSE,                                           // Clear the bit on exit | ||||||
|                             pdFALSE,                                           // Wait for all the bits, |                             pdFALSE,                                           // Wait for all the bits, | ||||||
|                             portMAX_DELAY);                                    // Block indefinitely until bit is set |                             portMAX_DELAY);                                    // Block indefinitely until bit is set | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| #include "sun.h" | #include "sun.h" | ||||||
| #include "esphome/core/log.h" | #include "esphome/core/log.h" | ||||||
|  | #include <numbers> | ||||||
|  |  | ||||||
| /* | /* | ||||||
| The formulas/algorithms in this module are based on the book | The formulas/algorithms in this module are based on the book | ||||||
| @@ -18,14 +19,12 @@ using namespace esphome::sun::internal; | |||||||
|  |  | ||||||
| static const char *const TAG = "sun"; | static const char *const TAG = "sun"; | ||||||
|  |  | ||||||
| #undef PI |  | ||||||
| #undef degrees | #undef degrees | ||||||
| #undef radians | #undef radians | ||||||
| #undef sq | #undef sq | ||||||
|  |  | ||||||
| static const num_t PI = 3.141592653589793; | inline num_t degrees(num_t rad) { return rad * 180 / std::numbers::pi; } | ||||||
| inline num_t degrees(num_t rad) { return rad * 180 / PI; } | inline num_t radians(num_t deg) { return deg * std::numbers::pi / 180; } | ||||||
| inline num_t radians(num_t deg) { return deg * PI / 180; } |  | ||||||
| inline num_t arcdeg(num_t deg, num_t minutes, num_t seconds) { return deg + minutes / 60 + seconds / 3600; } | inline num_t arcdeg(num_t deg, num_t minutes, num_t seconds) { return deg + minutes / 60 + seconds / 3600; } | ||||||
| inline num_t sq(num_t x) { return x * x; } | inline num_t sq(num_t x) { return x * x; } | ||||||
| inline num_t cb(num_t x) { return x * x * x; } | inline num_t cb(num_t x) { return x * x * x; } | ||||||
|   | |||||||
| @@ -152,7 +152,7 @@ void IRAM_ATTR Tx20ComponentStore::gpio_intr(Tx20ComponentStore *arg) { | |||||||
|     } |     } | ||||||
|     arg->buffer[arg->buffer_index] = 1; |     arg->buffer[arg->buffer_index] = 1; | ||||||
|     arg->start_time = now; |     arg->start_time = now; | ||||||
|     arg->buffer_index++; |     arg->buffer_index++;  // NOLINT(clang-diagnostic-deprecated-volatile) | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   const uint32_t delay = now - arg->start_time; |   const uint32_t delay = now - arg->start_time; | ||||||
| @@ -183,7 +183,7 @@ void IRAM_ATTR Tx20ComponentStore::gpio_intr(Tx20ComponentStore *arg) { | |||||||
|   } |   } | ||||||
|   arg->spent_time += delay; |   arg->spent_time += delay; | ||||||
|   arg->start_time = now; |   arg->start_time = now; | ||||||
|   arg->buffer_index++; |   arg->buffer_index++;  // NOLINT(clang-diagnostic-deprecated-volatile) | ||||||
| } | } | ||||||
| void IRAM_ATTR Tx20ComponentStore::reset() { | void IRAM_ATTR Tx20ComponentStore::reset() { | ||||||
|   tx20_available = false; |   tx20_available = false; | ||||||
|   | |||||||
| @@ -17,10 +17,11 @@ from esphome.const import ( | |||||||
| AUTO_LOAD = ["socket"] | AUTO_LOAD = ["socket"] | ||||||
| DEPENDENCIES = ["api", "microphone"] | DEPENDENCIES = ["api", "microphone"] | ||||||
|  |  | ||||||
| CODEOWNERS = ["@jesserockz"] | CODEOWNERS = ["@jesserockz", "@kahrendt"] | ||||||
|  |  | ||||||
| CONF_ON_END = "on_end" | CONF_ON_END = "on_end" | ||||||
| CONF_ON_INTENT_END = "on_intent_end" | CONF_ON_INTENT_END = "on_intent_end" | ||||||
|  | CONF_ON_INTENT_PROGRESS = "on_intent_progress" | ||||||
| CONF_ON_INTENT_START = "on_intent_start" | CONF_ON_INTENT_START = "on_intent_start" | ||||||
| CONF_ON_LISTENING = "on_listening" | CONF_ON_LISTENING = "on_listening" | ||||||
| CONF_ON_START = "on_start" | CONF_ON_START = "on_start" | ||||||
| @@ -136,6 +137,9 @@ CONFIG_SCHEMA = cv.All( | |||||||
|             cv.Optional(CONF_ON_INTENT_START): automation.validate_automation( |             cv.Optional(CONF_ON_INTENT_START): automation.validate_automation( | ||||||
|                 single=True |                 single=True | ||||||
|             ), |             ), | ||||||
|  |             cv.Optional(CONF_ON_INTENT_PROGRESS): automation.validate_automation( | ||||||
|  |                 single=True | ||||||
|  |             ), | ||||||
|             cv.Optional(CONF_ON_INTENT_END): automation.validate_automation( |             cv.Optional(CONF_ON_INTENT_END): automation.validate_automation( | ||||||
|                 single=True |                 single=True | ||||||
|             ), |             ), | ||||||
| @@ -282,6 +286,13 @@ async def to_code(config): | |||||||
|             config[CONF_ON_INTENT_START], |             config[CONF_ON_INTENT_START], | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |     if CONF_ON_INTENT_PROGRESS in config: | ||||||
|  |         await automation.build_automation( | ||||||
|  |             var.get_intent_progress_trigger(), | ||||||
|  |             [(cg.std_string, "x")], | ||||||
|  |             config[CONF_ON_INTENT_PROGRESS], | ||||||
|  |         ) | ||||||
|  |  | ||||||
|     if CONF_ON_INTENT_END in config: |     if CONF_ON_INTENT_END in config: | ||||||
|         await automation.build_automation( |         await automation.build_automation( | ||||||
|             var.get_intent_end_trigger(), |             var.get_intent_end_trigger(), | ||||||
|   | |||||||
| @@ -555,7 +555,7 @@ void VoiceAssistant::request_stop() { | |||||||
|       break; |       break; | ||||||
|     case State::AWAITING_RESPONSE: |     case State::AWAITING_RESPONSE: | ||||||
|       this->signal_stop_(); |       this->signal_stop_(); | ||||||
|       break; |       // Fallthrough intended to stop a streaming TTS announcement that has potentially started | ||||||
|     case State::STREAMING_RESPONSE: |     case State::STREAMING_RESPONSE: | ||||||
| #ifdef USE_MEDIA_PLAYER | #ifdef USE_MEDIA_PLAYER | ||||||
|       // Stop any ongoing media player announcement |       // Stop any ongoing media player announcement | ||||||
| @@ -599,6 +599,14 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { | |||||||
|   switch (msg.event_type) { |   switch (msg.event_type) { | ||||||
|     case api::enums::VOICE_ASSISTANT_RUN_START: |     case api::enums::VOICE_ASSISTANT_RUN_START: | ||||||
|       ESP_LOGD(TAG, "Assist Pipeline running"); |       ESP_LOGD(TAG, "Assist Pipeline running"); | ||||||
|  | #ifdef USE_MEDIA_PLAYER | ||||||
|  |       this->started_streaming_tts_ = false; | ||||||
|  |       for (auto arg : msg.data) { | ||||||
|  |         if (arg.name == "url") { | ||||||
|  |           this->tts_response_url_ = std::move(arg.value); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  | #endif | ||||||
|       this->defer([this]() { this->start_trigger_->trigger(); }); |       this->defer([this]() { this->start_trigger_->trigger(); }); | ||||||
|       break; |       break; | ||||||
|     case api::enums::VOICE_ASSISTANT_WAKE_WORD_START: |     case api::enums::VOICE_ASSISTANT_WAKE_WORD_START: | ||||||
| @@ -622,6 +630,8 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { | |||||||
|       if (text.empty()) { |       if (text.empty()) { | ||||||
|         ESP_LOGW(TAG, "No text in STT_END event"); |         ESP_LOGW(TAG, "No text in STT_END event"); | ||||||
|         return; |         return; | ||||||
|  |       } else if (text.length() > 500) { | ||||||
|  |         text = text.substr(0, 497) + "..."; | ||||||
|       } |       } | ||||||
|       ESP_LOGD(TAG, "Speech recognised as: \"%s\"", text.c_str()); |       ESP_LOGD(TAG, "Speech recognised as: \"%s\"", text.c_str()); | ||||||
|       this->defer([this, text]() { this->stt_end_trigger_->trigger(text); }); |       this->defer([this, text]() { this->stt_end_trigger_->trigger(text); }); | ||||||
| @@ -631,6 +641,27 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { | |||||||
|       ESP_LOGD(TAG, "Intent started"); |       ESP_LOGD(TAG, "Intent started"); | ||||||
|       this->defer([this]() { this->intent_start_trigger_->trigger(); }); |       this->defer([this]() { this->intent_start_trigger_->trigger(); }); | ||||||
|       break; |       break; | ||||||
|  |     case api::enums::VOICE_ASSISTANT_INTENT_PROGRESS: { | ||||||
|  |       ESP_LOGD(TAG, "Intent progress"); | ||||||
|  |       std::string tts_url_for_trigger = ""; | ||||||
|  | #ifdef USE_MEDIA_PLAYER | ||||||
|  |       if (this->media_player_ != nullptr) { | ||||||
|  |         for (const auto &arg : msg.data) { | ||||||
|  |           if ((arg.name == "tts_start_streaming") && (arg.value == "1") && !this->tts_response_url_.empty()) { | ||||||
|  |             this->media_player_->make_call().set_media_url(this->tts_response_url_).set_announcement(true).perform(); | ||||||
|  |  | ||||||
|  |             this->media_player_wait_for_announcement_start_ = true; | ||||||
|  |             this->media_player_wait_for_announcement_end_ = false; | ||||||
|  |             this->started_streaming_tts_ = true; | ||||||
|  |             tts_url_for_trigger = this->tts_response_url_; | ||||||
|  |             this->tts_response_url_.clear();  // Reset streaming URL | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  | #endif | ||||||
|  |       this->defer([this, tts_url_for_trigger]() { this->intent_progress_trigger_->trigger(tts_url_for_trigger); }); | ||||||
|  |       break; | ||||||
|  |     } | ||||||
|     case api::enums::VOICE_ASSISTANT_INTENT_END: { |     case api::enums::VOICE_ASSISTANT_INTENT_END: { | ||||||
|       for (auto arg : msg.data) { |       for (auto arg : msg.data) { | ||||||
|         if (arg.name == "conversation_id") { |         if (arg.name == "conversation_id") { | ||||||
| @@ -653,6 +684,9 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { | |||||||
|         ESP_LOGW(TAG, "No text in TTS_START event"); |         ESP_LOGW(TAG, "No text in TTS_START event"); | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|  |       if (text.length() > 500) { | ||||||
|  |         text = text.substr(0, 497) + "..."; | ||||||
|  |       } | ||||||
|       ESP_LOGD(TAG, "Response: \"%s\"", text.c_str()); |       ESP_LOGD(TAG, "Response: \"%s\"", text.c_str()); | ||||||
|       this->defer([this, text]() { |       this->defer([this, text]() { | ||||||
|         this->tts_start_trigger_->trigger(text); |         this->tts_start_trigger_->trigger(text); | ||||||
| @@ -678,7 +712,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { | |||||||
|       ESP_LOGD(TAG, "Response URL: \"%s\"", url.c_str()); |       ESP_LOGD(TAG, "Response URL: \"%s\"", url.c_str()); | ||||||
|       this->defer([this, url]() { |       this->defer([this, url]() { | ||||||
| #ifdef USE_MEDIA_PLAYER | #ifdef USE_MEDIA_PLAYER | ||||||
|         if (this->media_player_ != nullptr) { |         if ((this->media_player_ != nullptr) && (!this->started_streaming_tts_)) { | ||||||
|           this->media_player_->make_call().set_media_url(url).set_announcement(true).perform(); |           this->media_player_->make_call().set_media_url(url).set_announcement(true).perform(); | ||||||
|  |  | ||||||
|           this->media_player_wait_for_announcement_start_ = true; |           this->media_player_wait_for_announcement_start_ = true; | ||||||
|   | |||||||
| @@ -177,6 +177,7 @@ class VoiceAssistant : public Component { | |||||||
|  |  | ||||||
|   Trigger<> *get_intent_end_trigger() const { return this->intent_end_trigger_; } |   Trigger<> *get_intent_end_trigger() const { return this->intent_end_trigger_; } | ||||||
|   Trigger<> *get_intent_start_trigger() const { return this->intent_start_trigger_; } |   Trigger<> *get_intent_start_trigger() const { return this->intent_start_trigger_; } | ||||||
|  |   Trigger<std::string> *get_intent_progress_trigger() const { return this->intent_progress_trigger_; } | ||||||
|   Trigger<> *get_listening_trigger() const { return this->listening_trigger_; } |   Trigger<> *get_listening_trigger() const { return this->listening_trigger_; } | ||||||
|   Trigger<> *get_end_trigger() const { return this->end_trigger_; } |   Trigger<> *get_end_trigger() const { return this->end_trigger_; } | ||||||
|   Trigger<> *get_start_trigger() const { return this->start_trigger_; } |   Trigger<> *get_start_trigger() const { return this->start_trigger_; } | ||||||
| @@ -233,6 +234,7 @@ class VoiceAssistant : public Component { | |||||||
|   Trigger<> *tts_stream_start_trigger_ = new Trigger<>(); |   Trigger<> *tts_stream_start_trigger_ = new Trigger<>(); | ||||||
|   Trigger<> *tts_stream_end_trigger_ = new Trigger<>(); |   Trigger<> *tts_stream_end_trigger_ = new Trigger<>(); | ||||||
| #endif | #endif | ||||||
|  |   Trigger<std::string> *intent_progress_trigger_ = new Trigger<std::string>(); | ||||||
|   Trigger<> *wake_word_detected_trigger_ = new Trigger<>(); |   Trigger<> *wake_word_detected_trigger_ = new Trigger<>(); | ||||||
|   Trigger<std::string> *stt_end_trigger_ = new Trigger<std::string>(); |   Trigger<std::string> *stt_end_trigger_ = new Trigger<std::string>(); | ||||||
|   Trigger<std::string> *tts_end_trigger_ = new Trigger<std::string>(); |   Trigger<std::string> *tts_end_trigger_ = new Trigger<std::string>(); | ||||||
| @@ -268,6 +270,8 @@ class VoiceAssistant : public Component { | |||||||
| #endif | #endif | ||||||
| #ifdef USE_MEDIA_PLAYER | #ifdef USE_MEDIA_PLAYER | ||||||
|   media_player::MediaPlayer *media_player_{nullptr}; |   media_player::MediaPlayer *media_player_{nullptr}; | ||||||
|  |   std::string tts_response_url_{""}; | ||||||
|  |   bool started_streaming_tts_{false}; | ||||||
|   bool media_player_wait_for_announcement_start_{false}; |   bool media_player_wait_for_announcement_start_{false}; | ||||||
|   bool media_player_wait_for_announcement_end_{false}; |   bool media_player_wait_for_announcement_end_{false}; | ||||||
| #endif | #endif | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ static const char *const KEYS = "0123456789*#"; | |||||||
| void IRAM_ATTR HOT WiegandStore::d0_gpio_intr(WiegandStore *arg) { | void IRAM_ATTR HOT WiegandStore::d0_gpio_intr(WiegandStore *arg) { | ||||||
|   if (arg->d0.digital_read()) |   if (arg->d0.digital_read()) | ||||||
|     return; |     return; | ||||||
|   arg->count++; |   arg->count++;  // NOLINT(clang-diagnostic-deprecated-volatile) | ||||||
|   arg->value <<= 1; |   arg->value <<= 1; | ||||||
|   arg->last_bit_time = millis(); |   arg->last_bit_time = millis(); | ||||||
|   arg->done = false; |   arg->done = false; | ||||||
| @@ -20,7 +20,7 @@ void IRAM_ATTR HOT WiegandStore::d0_gpio_intr(WiegandStore *arg) { | |||||||
| void IRAM_ATTR HOT WiegandStore::d1_gpio_intr(WiegandStore *arg) { | void IRAM_ATTR HOT WiegandStore::d1_gpio_intr(WiegandStore *arg) { | ||||||
|   if (arg->d1.digital_read()) |   if (arg->d1.digital_read()) | ||||||
|     return; |     return; | ||||||
|   arg->count++; |   arg->count++;  // NOLINT(clang-diagnostic-deprecated-volatile) | ||||||
|   arg->value = (arg->value << 1) | 1; |   arg->value = (arg->value << 1) | 1; | ||||||
|   arg->last_bit_time = millis(); |   arg->last_bit_time = millis(); | ||||||
|   arg->done = false; |   arg->done = false; | ||||||
|   | |||||||
| @@ -62,7 +62,7 @@ struct SavedWifiFastConnectSettings { | |||||||
|   uint8_t channel; |   uint8_t channel; | ||||||
| } PACKED;  // NOLINT | } PACKED;  // NOLINT | ||||||
|  |  | ||||||
| enum WiFiComponentState { | enum WiFiComponentState : uint8_t { | ||||||
|   /** Nothing has been initialized yet. Internal AP, if configured, is disabled at this point. */ |   /** Nothing has been initialized yet. Internal AP, if configured, is disabled at this point. */ | ||||||
|   WIFI_COMPONENT_STATE_OFF = 0, |   WIFI_COMPONENT_STATE_OFF = 0, | ||||||
|   /** WiFi is disabled. */ |   /** WiFi is disabled. */ | ||||||
| @@ -146,14 +146,14 @@ class WiFiAP { | |||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   std::string ssid_; |   std::string ssid_; | ||||||
|   optional<bssid_t> bssid_; |  | ||||||
|   std::string password_; |   std::string password_; | ||||||
|  |   optional<bssid_t> bssid_; | ||||||
| #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 | ||||||
|   optional<uint8_t> channel_; |  | ||||||
|   float priority_{0}; |  | ||||||
|   optional<ManualIP> manual_ip_; |   optional<ManualIP> manual_ip_; | ||||||
|  |   float priority_{0}; | ||||||
|  |   optional<uint8_t> channel_; | ||||||
|   bool hidden_{false}; |   bool hidden_{false}; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| @@ -177,14 +177,14 @@ class WiFiScanResult { | |||||||
|   bool operator==(const WiFiScanResult &rhs) const; |   bool operator==(const WiFiScanResult &rhs) const; | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   bool matches_{false}; |  | ||||||
|   bssid_t bssid_; |   bssid_t bssid_; | ||||||
|   std::string ssid_; |   std::string ssid_; | ||||||
|  |   float priority_{0.0f}; | ||||||
|   uint8_t channel_; |   uint8_t channel_; | ||||||
|   int8_t rssi_; |   int8_t rssi_; | ||||||
|  |   bool matches_{false}; | ||||||
|   bool with_auth_; |   bool with_auth_; | ||||||
|   bool is_hidden_; |   bool is_hidden_; | ||||||
|   float priority_{0.0f}; |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| struct WiFiSTAPriority { | struct WiFiSTAPriority { | ||||||
| @@ -192,7 +192,7 @@ struct WiFiSTAPriority { | |||||||
|   float priority; |   float priority; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| enum WiFiPowerSaveMode { | enum WiFiPowerSaveMode : uint8_t { | ||||||
|   WIFI_POWER_SAVE_NONE = 0, |   WIFI_POWER_SAVE_NONE = 0, | ||||||
|   WIFI_POWER_SAVE_LIGHT, |   WIFI_POWER_SAVE_LIGHT, | ||||||
|   WIFI_POWER_SAVE_HIGH, |   WIFI_POWER_SAVE_HIGH, | ||||||
| @@ -383,28 +383,36 @@ class WiFiComponent : public Component { | |||||||
|   std::string use_address_; |   std::string use_address_; | ||||||
|   std::vector<WiFiAP> sta_; |   std::vector<WiFiAP> sta_; | ||||||
|   std::vector<WiFiSTAPriority> sta_priorities_; |   std::vector<WiFiSTAPriority> sta_priorities_; | ||||||
|  |   std::vector<WiFiScanResult> scan_result_; | ||||||
|   WiFiAP selected_ap_; |   WiFiAP selected_ap_; | ||||||
|   bool fast_connect_{false}; |  | ||||||
|   bool retry_hidden_{false}; |  | ||||||
|  |  | ||||||
|   bool has_ap_{false}; |  | ||||||
|   WiFiAP ap_; |   WiFiAP ap_; | ||||||
|   WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF}; |   optional<float> output_power_; | ||||||
|   bool handled_connected_state_{false}; |   ESPPreferenceObject pref_; | ||||||
|  |   ESPPreferenceObject fast_connect_pref_; | ||||||
|  |  | ||||||
|  |   // Group all 32-bit integers together | ||||||
|   uint32_t action_started_; |   uint32_t action_started_; | ||||||
|   uint8_t num_retried_{0}; |  | ||||||
|   uint32_t last_connected_{0}; |   uint32_t last_connected_{0}; | ||||||
|   uint32_t reboot_timeout_{}; |   uint32_t reboot_timeout_{}; | ||||||
|   uint32_t ap_timeout_{}; |   uint32_t ap_timeout_{}; | ||||||
|  |  | ||||||
|  |   // Group all 8-bit values together | ||||||
|  |   WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF}; | ||||||
|   WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE}; |   WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE}; | ||||||
|  |   uint8_t num_retried_{0}; | ||||||
|  | #if USE_NETWORK_IPV6 | ||||||
|  |   uint8_t num_ipv6_addresses_{0}; | ||||||
|  | #endif /* USE_NETWORK_IPV6 */ | ||||||
|  |  | ||||||
|  |   // Group all boolean values together | ||||||
|  |   bool fast_connect_{false}; | ||||||
|  |   bool retry_hidden_{false}; | ||||||
|  |   bool has_ap_{false}; | ||||||
|  |   bool handled_connected_state_{false}; | ||||||
|   bool error_from_callback_{false}; |   bool error_from_callback_{false}; | ||||||
|   std::vector<WiFiScanResult> scan_result_; |  | ||||||
|   bool scan_done_{false}; |   bool scan_done_{false}; | ||||||
|   bool ap_setup_{false}; |   bool ap_setup_{false}; | ||||||
|   optional<float> output_power_; |  | ||||||
|   bool passive_scan_{false}; |   bool passive_scan_{false}; | ||||||
|   ESPPreferenceObject pref_; |  | ||||||
|   ESPPreferenceObject fast_connect_pref_; |  | ||||||
|   bool has_saved_wifi_settings_{false}; |   bool has_saved_wifi_settings_{false}; | ||||||
| #ifdef USE_WIFI_11KV_SUPPORT | #ifdef USE_WIFI_11KV_SUPPORT | ||||||
|   bool btm_{false}; |   bool btm_{false}; | ||||||
| @@ -412,10 +420,8 @@ class WiFiComponent : public Component { | |||||||
| #endif | #endif | ||||||
|   bool enable_on_boot_; |   bool enable_on_boot_; | ||||||
|   bool got_ipv4_address_{false}; |   bool got_ipv4_address_{false}; | ||||||
| #if USE_NETWORK_IPV6 |  | ||||||
|   uint8_t num_ipv6_addresses_{0}; |  | ||||||
| #endif /* USE_NETWORK_IPV6 */ |  | ||||||
|  |  | ||||||
|  |   // Pointers at the end (naturally aligned) | ||||||
|   Trigger<> *connect_trigger_{new Trigger<>()}; |   Trigger<> *connect_trigger_{new Trigger<>()}; | ||||||
|   Trigger<> *disconnect_trigger_{new Trigger<>()}; |   Trigger<> *disconnect_trigger_{new Trigger<>()}; | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ | |||||||
| #include "esphome/core/version.h" | #include "esphome/core/version.h" | ||||||
| #include "esphome/core/hal.h" | #include "esphome/core/hal.h" | ||||||
| #include <algorithm> | #include <algorithm> | ||||||
|  | #include <ranges> | ||||||
|  |  | ||||||
| #ifdef USE_STATUS_LED | #ifdef USE_STATUS_LED | ||||||
| #include "esphome/components/status_led/status_led.h" | #include "esphome/components/status_led/status_led.h" | ||||||
| @@ -184,8 +185,8 @@ void IRAM_ATTR HOT Application::feed_wdt(uint32_t time) { | |||||||
| } | } | ||||||
| void Application::reboot() { | void Application::reboot() { | ||||||
|   ESP_LOGI(TAG, "Forcing a reboot"); |   ESP_LOGI(TAG, "Forcing a reboot"); | ||||||
|   for (auto it = this->components_.rbegin(); it != this->components_.rend(); ++it) { |   for (auto &component : std::ranges::reverse_view(this->components_)) { | ||||||
|     (*it)->on_shutdown(); |     component->on_shutdown(); | ||||||
|   } |   } | ||||||
|   arch_restart(); |   arch_restart(); | ||||||
| } | } | ||||||
| @@ -198,17 +199,17 @@ void Application::safe_reboot() { | |||||||
| } | } | ||||||
|  |  | ||||||
| void Application::run_safe_shutdown_hooks() { | void Application::run_safe_shutdown_hooks() { | ||||||
|   for (auto it = this->components_.rbegin(); it != this->components_.rend(); ++it) { |   for (auto &component : std::ranges::reverse_view(this->components_)) { | ||||||
|     (*it)->on_safe_shutdown(); |     component->on_safe_shutdown(); | ||||||
|   } |   } | ||||||
|   for (auto it = this->components_.rbegin(); it != this->components_.rend(); ++it) { |   for (auto &component : std::ranges::reverse_view(this->components_)) { | ||||||
|     (*it)->on_shutdown(); |     component->on_shutdown(); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| void Application::run_powerdown_hooks() { | void Application::run_powerdown_hooks() { | ||||||
|   for (auto it = this->components_.rbegin(); it != this->components_.rend(); ++it) { |   for (auto &component : std::ranges::reverse_view(this->components_)) { | ||||||
|     (*it)->on_powerdown(); |     component->on_powerdown(); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,5 +1,7 @@ | |||||||
| #pragma once | #pragma once | ||||||
|  |  | ||||||
|  | #include <algorithm> | ||||||
|  | #include <limits> | ||||||
| #include <string> | #include <string> | ||||||
| #include <vector> | #include <vector> | ||||||
| #include "esphome/core/component.h" | #include "esphome/core/component.h" | ||||||
| @@ -335,11 +337,16 @@ class Application { | |||||||
|    * Each component can request a high frequency loop execution by using the HighFrequencyLoopRequester |    * Each component can request a high frequency loop execution by using the HighFrequencyLoopRequester | ||||||
|    * helper in helpers.h |    * helper in helpers.h | ||||||
|    * |    * | ||||||
|  |    * Note: This method is not called by ESPHome core code. It is only used by lambda functions | ||||||
|  |    * in YAML configurations or by external components. | ||||||
|  |    * | ||||||
|    * @param loop_interval The interval in milliseconds to run the core loop at. Defaults to 16 milliseconds. |    * @param loop_interval The interval in milliseconds to run the core loop at. Defaults to 16 milliseconds. | ||||||
|    */ |    */ | ||||||
|   void set_loop_interval(uint32_t loop_interval) { this->loop_interval_ = loop_interval; } |   void set_loop_interval(uint32_t loop_interval) { | ||||||
|  |     this->loop_interval_ = std::min(loop_interval, static_cast<uint32_t>(std::numeric_limits<uint16_t>::max())); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   uint32_t get_loop_interval() const { return this->loop_interval_; } |   uint32_t get_loop_interval() const { return static_cast<uint32_t>(this->loop_interval_); } | ||||||
|  |  | ||||||
|   void schedule_dump_config() { this->dump_config_at_ = 0; } |   void schedule_dump_config() { this->dump_config_at_ = 0; } | ||||||
|  |  | ||||||
| @@ -618,6 +625,17 @@ class Application { | |||||||
|   /// Perform a delay while also monitoring socket file descriptors for readiness |   /// Perform a delay while also monitoring socket file descriptors for readiness | ||||||
|   void yield_with_select_(uint32_t delay_ms); |   void yield_with_select_(uint32_t delay_ms); | ||||||
|  |  | ||||||
|  |   // === Member variables ordered by size to minimize padding === | ||||||
|  |  | ||||||
|  |   // Pointer-sized members first | ||||||
|  |   Component *current_component_{nullptr}; | ||||||
|  |   const char *comment_{nullptr}; | ||||||
|  |   const char *compilation_time_{nullptr}; | ||||||
|  |  | ||||||
|  |   // size_t members | ||||||
|  |   size_t dump_config_at_{SIZE_MAX}; | ||||||
|  |  | ||||||
|  |   // Vectors (largest members) | ||||||
|   std::vector<Component *> components_{}; |   std::vector<Component *> components_{}; | ||||||
|  |  | ||||||
|   // Partitioned vector design for looping components |   // Partitioned vector design for looping components | ||||||
| @@ -637,11 +655,6 @@ class Application { | |||||||
|   //   and active_end_ is incremented |   //   and active_end_ is incremented | ||||||
|   // - This eliminates branch mispredictions from flag checking in the hot loop |   // - This eliminates branch mispredictions from flag checking in the hot loop | ||||||
|   std::vector<Component *> looping_components_{}; |   std::vector<Component *> looping_components_{}; | ||||||
|   uint16_t looping_components_active_end_{0}; |  | ||||||
|  |  | ||||||
|   // For safe reentrant modifications during iteration |  | ||||||
|   uint16_t current_loop_index_{0}; |  | ||||||
|   bool in_loop_{false}; |  | ||||||
|  |  | ||||||
| #ifdef USE_DEVICES | #ifdef USE_DEVICES | ||||||
|   std::vector<Device *> devices_{}; |   std::vector<Device *> devices_{}; | ||||||
| @@ -713,24 +726,37 @@ class Application { | |||||||
|   std::vector<update::UpdateEntity *> updates_{}; |   std::vector<update::UpdateEntity *> updates_{}; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|  | #ifdef USE_SOCKET_SELECT_SUPPORT | ||||||
|  |   std::vector<int> socket_fds_;  // Vector of all monitored socket file descriptors | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  |   // String members | ||||||
|   std::string name_; |   std::string name_; | ||||||
|   std::string friendly_name_; |   std::string friendly_name_; | ||||||
|   const char *comment_{nullptr}; |  | ||||||
|   const char *compilation_time_{nullptr}; |   // 4-byte members | ||||||
|   bool name_add_mac_suffix_; |  | ||||||
|   uint32_t last_loop_{0}; |   uint32_t last_loop_{0}; | ||||||
|   uint32_t loop_interval_{16}; |  | ||||||
|   size_t dump_config_at_{SIZE_MAX}; |  | ||||||
|   uint8_t app_state_{0}; |  | ||||||
|   volatile bool has_pending_enable_loop_requests_{false}; |  | ||||||
|   Component *current_component_{nullptr}; |  | ||||||
|   uint32_t loop_component_start_time_{0}; |   uint32_t loop_component_start_time_{0}; | ||||||
|  |  | ||||||
| #ifdef USE_SOCKET_SELECT_SUPPORT | #ifdef USE_SOCKET_SELECT_SUPPORT | ||||||
|   // Socket select management |  | ||||||
|   std::vector<int> socket_fds_;     // Vector of all monitored socket file descriptors |  | ||||||
|   bool socket_fds_changed_{false};  // Flag to rebuild base_read_fds_ when socket_fds_ changes |  | ||||||
|   int max_fd_{-1};  // Highest file descriptor number for select() |   int max_fd_{-1};  // Highest file descriptor number for select() | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  |   // 2-byte members (grouped together for alignment) | ||||||
|  |   uint16_t loop_interval_{16};  // Loop interval in ms (max 65535ms = 65.5 seconds) | ||||||
|  |   uint16_t looping_components_active_end_{0}; | ||||||
|  |   uint16_t current_loop_index_{0};  // For safe reentrant modifications during iteration | ||||||
|  |  | ||||||
|  |   // 1-byte members (grouped together to minimize padding) | ||||||
|  |   uint8_t app_state_{0}; | ||||||
|  |   bool name_add_mac_suffix_; | ||||||
|  |   bool in_loop_{false}; | ||||||
|  |   volatile bool has_pending_enable_loop_requests_{false}; | ||||||
|  |  | ||||||
|  | #ifdef USE_SOCKET_SELECT_SUPPORT | ||||||
|  |   bool socket_fds_changed_{false};  // Flag to rebuild base_read_fds_ when socket_fds_ changes | ||||||
|  |  | ||||||
|  |   // Variable-sized members at end | ||||||
|   fd_set base_read_fds_{};  // Cached fd_set rebuilt only when socket_fds_ changes |   fd_set base_read_fds_{};  // Cached fd_set rebuilt only when socket_fds_ changes | ||||||
|   fd_set read_fds_{};       // Working fd_set for select(), copied from base_read_fds_ |   fd_set read_fds_{};       // Working fd_set for select(), copied from base_read_fds_ | ||||||
| #endif | #endif | ||||||
|   | |||||||
| @@ -27,20 +27,67 @@ template<typename T, typename... X> class TemplatableValue { | |||||||
|  public: |  public: | ||||||
|   TemplatableValue() : type_(NONE) {} |   TemplatableValue() : type_(NONE) {} | ||||||
|  |  | ||||||
|   template<typename F, enable_if_t<!is_invocable<F, X...>::value, int> = 0> |   template<typename F, enable_if_t<!is_invocable<F, X...>::value, int> = 0> TemplatableValue(F value) : type_(VALUE) { | ||||||
|   TemplatableValue(F value) : type_(VALUE), value_(std::move(value)) {} |     new (&this->value_) T(std::move(value)); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   template<typename F, enable_if_t<is_invocable<F, X...>::value, int> = 0> |   template<typename F, enable_if_t<is_invocable<F, X...>::value, int> = 0> TemplatableValue(F f) : type_(LAMBDA) { | ||||||
|   TemplatableValue(F f) : type_(LAMBDA), f_(f) {} |     this->f_ = new std::function<T(X...)>(std::move(f)); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Copy constructor | ||||||
|  |   TemplatableValue(const TemplatableValue &other) : type_(other.type_) { | ||||||
|  |     if (type_ == VALUE) { | ||||||
|  |       new (&this->value_) T(other.value_); | ||||||
|  |     } else if (type_ == LAMBDA) { | ||||||
|  |       this->f_ = new std::function<T(X...)>(*other.f_); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Move constructor | ||||||
|  |   TemplatableValue(TemplatableValue &&other) noexcept : type_(other.type_) { | ||||||
|  |     if (type_ == VALUE) { | ||||||
|  |       new (&this->value_) T(std::move(other.value_)); | ||||||
|  |     } else if (type_ == LAMBDA) { | ||||||
|  |       this->f_ = other.f_; | ||||||
|  |       other.f_ = nullptr; | ||||||
|  |     } | ||||||
|  |     other.type_ = NONE; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Assignment operators | ||||||
|  |   TemplatableValue &operator=(const TemplatableValue &other) { | ||||||
|  |     if (this != &other) { | ||||||
|  |       this->~TemplatableValue(); | ||||||
|  |       new (this) TemplatableValue(other); | ||||||
|  |     } | ||||||
|  |     return *this; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   TemplatableValue &operator=(TemplatableValue &&other) noexcept { | ||||||
|  |     if (this != &other) { | ||||||
|  |       this->~TemplatableValue(); | ||||||
|  |       new (this) TemplatableValue(std::move(other)); | ||||||
|  |     } | ||||||
|  |     return *this; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   ~TemplatableValue() { | ||||||
|  |     if (type_ == VALUE) { | ||||||
|  |       this->value_.~T(); | ||||||
|  |     } else if (type_ == LAMBDA) { | ||||||
|  |       delete this->f_; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   bool has_value() { return this->type_ != NONE; } |   bool has_value() { return this->type_ != NONE; } | ||||||
|  |  | ||||||
|   T value(X... x) { |   T value(X... x) { | ||||||
|     if (this->type_ == LAMBDA) { |     if (this->type_ == LAMBDA) { | ||||||
|       return this->f_(x...); |       return (*this->f_)(x...); | ||||||
|     } |     } | ||||||
|     // return value also when none |     // return value also when none | ||||||
|     return this->value_; |     return this->type_ == VALUE ? this->value_ : T{}; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   optional<T> optional_value(X... x) { |   optional<T> optional_value(X... x) { | ||||||
| @@ -58,14 +105,16 @@ template<typename T, typename... X> class TemplatableValue { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   enum { |   enum : uint8_t { | ||||||
|     NONE, |     NONE, | ||||||
|     VALUE, |     VALUE, | ||||||
|     LAMBDA, |     LAMBDA, | ||||||
|   } type_; |   } type_; | ||||||
|  |  | ||||||
|   T value_{}; |   union { | ||||||
|   std::function<T(X...)> f_{}; |     T value_; | ||||||
|  |     std::function<T(X...)> *f_; | ||||||
|  |   }; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| /** Base class for all automation conditions. | /** Base class for all automation conditions. | ||||||
|   | |||||||
| @@ -375,7 +375,7 @@ void ComponentIterator::advance() { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (advance_platform) { |   if (advance_platform) { | ||||||
|     this->state_ = static_cast<IteratorState>(static_cast<uint32_t>(this->state_) + 1); |     this->state_ = static_cast<IteratorState>(static_cast<uint8_t>(this->state_) + 1); | ||||||
|     this->at_ = 0; |     this->at_ = 0; | ||||||
|   } else if (success) { |   } else if (success) { | ||||||
|     this->at_++; |     this->at_++; | ||||||
|   | |||||||
| @@ -93,7 +93,9 @@ class ComponentIterator { | |||||||
|   virtual bool on_end(); |   virtual bool on_end(); | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   enum class IteratorState { |   // Iterates over all ESPHome entities (sensors, switches, lights, etc.) | ||||||
|  |   // Supports up to 256 entity types and up to 65,535 entities of each type | ||||||
|  |   enum class IteratorState : uint8_t { | ||||||
|     NONE = 0, |     NONE = 0, | ||||||
|     BEGIN, |     BEGIN, | ||||||
| #ifdef USE_BINARY_SENSOR | #ifdef USE_BINARY_SENSOR | ||||||
| @@ -167,7 +169,7 @@ class ComponentIterator { | |||||||
| #endif | #endif | ||||||
|     MAX, |     MAX, | ||||||
|   } state_{IteratorState::NONE}; |   } state_{IteratorState::NONE}; | ||||||
|   size_t at_{0}; |   uint16_t at_{0};  // Supports up to 65,535 entities per type | ||||||
|   bool include_internal_{false}; |   bool include_internal_{false}; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -132,6 +132,8 @@ | |||||||
|  |  | ||||||
| // ESP32-specific feature flags | // ESP32-specific feature flags | ||||||
| #ifdef USE_ESP32 | #ifdef USE_ESP32 | ||||||
|  | #define USE_ESPHOME_TASK_LOG_BUFFER | ||||||
|  |  | ||||||
| #define USE_BLUETOOTH_PROXY | #define USE_BLUETOOTH_PROXY | ||||||
| #define USE_CAPTIVE_PORTAL | #define USE_CAPTIVE_PORTAL | ||||||
| #define USE_ESP32_BLE | #define USE_ESP32_BLE | ||||||
|   | |||||||
| @@ -617,7 +617,7 @@ def set_cpp_standard(standard: str) -> None: | |||||||
|     """Set C++ standard with compiler flag `-std={standard}`.""" |     """Set C++ standard with compiler flag `-std={standard}`.""" | ||||||
|     CORE.add_build_unflag("-std=gnu++11") |     CORE.add_build_unflag("-std=gnu++11") | ||||||
|     CORE.add_build_unflag("-std=gnu++14") |     CORE.add_build_unflag("-std=gnu++14") | ||||||
|     CORE.add_build_unflag("-std=gnu++20") |     CORE.add_build_unflag("-std=gnu++17") | ||||||
|     CORE.add_build_unflag("-std=gnu++23") |     CORE.add_build_unflag("-std=gnu++23") | ||||||
|     CORE.add_build_unflag("-std=gnu++2a") |     CORE.add_build_unflag("-std=gnu++2a") | ||||||
|     CORE.add_build_unflag("-std=gnu++2b") |     CORE.add_build_unflag("-std=gnu++2b") | ||||||
|   | |||||||
| @@ -1,13 +1,19 @@ | |||||||
| dependencies: | dependencies: | ||||||
|   esp-tflite-micro: |   espressif/esp-tflite-micro: | ||||||
|     git: https://github.com/espressif/esp-tflite-micro.git |     version: 1.3.3~1 | ||||||
|     version: v1.3.1 |   espressif/esp32-camera: | ||||||
|   esp32_camera: |     version: 2.0.15 | ||||||
|     git: https://github.com/espressif/esp32-camera.git |   espressif/mdns: | ||||||
|     version: v2.0.15 |     version: 1.8.2 | ||||||
|   mdns: |   espressif/esp_wifi_remote: | ||||||
|     git: https://github.com/espressif/esp-protocols.git |     version: 0.10.2 | ||||||
|     version: mdns-v1.8.2 |  | ||||||
|     path: components/mdns |  | ||||||
|     rules: |     rules: | ||||||
|       - if: "idf_version >=5.0" |       - if: "target in [esp32h2, esp32p4]" | ||||||
|  |   espressif/eppp_link: | ||||||
|  |     version: 0.2.0 | ||||||
|  |     rules: | ||||||
|  |       - if: "target in [esp32h2, esp32p4]" | ||||||
|  |   espressif/esp_hosted: | ||||||
|  |     version: 2.0.11 | ||||||
|  |     rules: | ||||||
|  |       - if: "target in [esp32h2, esp32p4]" | ||||||
|   | |||||||
| @@ -47,11 +47,11 @@ lib_deps = | |||||||
|     lvgl/lvgl@8.4.0                                       ; lvgl |     lvgl/lvgl@8.4.0                                       ; lvgl | ||||||
| build_flags = | build_flags = | ||||||
|     -DESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_VERY_VERBOSE |     -DESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_VERY_VERBOSE | ||||||
|     -std=gnu++17 |     -std=gnu++20 | ||||||
| build_unflags = | build_unflags = | ||||||
|     -std=gnu++11 |     -std=gnu++11 | ||||||
|     -std=gnu++14 |     -std=gnu++14 | ||||||
|     -std=gnu++20 |     -std=gnu++17 | ||||||
|     -std=gnu++23 |     -std=gnu++23 | ||||||
|     -std=gnu++2a |     -std=gnu++2a | ||||||
|     -std=gnu++2b |     -std=gnu++2b | ||||||
| @@ -560,7 +560,7 @@ lib_deps = | |||||||
| build_flags = | build_flags = | ||||||
|     ${common.build_flags} |     ${common.build_flags} | ||||||
|     -DUSE_HOST |     -DUSE_HOST | ||||||
|     -std=c++17 |     -std=c++20 | ||||||
| build_unflags = | build_unflags = | ||||||
|     ${common.build_unflags} |     ${common.build_unflags} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| pylint==3.3.7 | pylint==3.3.7 | ||||||
| flake8==7.3.0  # also change in .pre-commit-config.yaml when updating | flake8==7.3.0  # also change in .pre-commit-config.yaml when updating | ||||||
| ruff==0.12.0  # also change in .pre-commit-config.yaml when updating | ruff==0.12.1  # also change in .pre-commit-config.yaml when updating | ||||||
| pyupgrade==3.20.0  # also change in .pre-commit-config.yaml when updating | pyupgrade==3.20.0  # also change in .pre-commit-config.yaml when updating | ||||||
| pre-commit | pre-commit | ||||||
|  |  | ||||||
|   | |||||||
| @@ -886,7 +886,7 @@ def build_message_type( | |||||||
|         public_content.append("#ifdef HAS_PROTO_MESSAGE_DUMP") |         public_content.append("#ifdef HAS_PROTO_MESSAGE_DUMP") | ||||||
|         snake_name = camel_to_snake(desc.name) |         snake_name = camel_to_snake(desc.name) | ||||||
|         public_content.append( |         public_content.append( | ||||||
|             f'static constexpr const char *message_name() {{ return "{snake_name}"; }}' |             f'const char *message_name() const override {{ return "{snake_name}"; }}' | ||||||
|         ) |         ) | ||||||
|         public_content.append("#endif") |         public_content.append("#endif") | ||||||
|  |  | ||||||
| @@ -1356,7 +1356,7 @@ def main() -> None: | |||||||
|     hpp += "  template<typename T>\n" |     hpp += "  template<typename T>\n" | ||||||
|     hpp += "  bool send_message(const T &msg) {\n" |     hpp += "  bool send_message(const T &msg) {\n" | ||||||
|     hpp += "#ifdef HAS_PROTO_MESSAGE_DUMP\n" |     hpp += "#ifdef HAS_PROTO_MESSAGE_DUMP\n" | ||||||
|     hpp += "    this->log_send_message_(T::message_name(), msg.dump());\n" |     hpp += "    this->log_send_message_(msg.message_name(), msg.dump());\n" | ||||||
|     hpp += "#endif\n" |     hpp += "#endif\n" | ||||||
|     hpp += "    return this->send_message_(msg, T::MESSAGE_TYPE);\n" |     hpp += "    return this->send_message_(msg, T::MESSAGE_TYPE);\n" | ||||||
|     hpp += "  }\n\n" |     hpp += "  }\n\n" | ||||||
|   | |||||||
							
								
								
									
										0
									
								
								script/run-in-env.py
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										0
									
								
								script/run-in-env.py
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
								
								
									
										15
									
								
								tests/components/esp32_hosted/common.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								tests/components/esp32_hosted/common.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | esp32_hosted: | ||||||
|  |   variant: ESP32C6 | ||||||
|  |   slot: 1 | ||||||
|  |   active_high: true | ||||||
|  |   reset_pin: GPIO15 | ||||||
|  |   cmd_pin: GPIO13 | ||||||
|  |   clk_pin: GPIO12 | ||||||
|  |   d0_pin: GPIO11 | ||||||
|  |   d1_pin: GPIO10 | ||||||
|  |   d2_pin: GPIO9 | ||||||
|  |   d3_pin: GPIO8 | ||||||
|  |  | ||||||
|  | wifi: | ||||||
|  |   ssid: MySSID | ||||||
|  |   password: password1 | ||||||
							
								
								
									
										1
									
								
								tests/components/esp32_hosted/test.esp32-p4-idf.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tests/components/esp32_hosted/test.esp32-p4-idf.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <<: !include common.yaml | ||||||
| @@ -839,9 +839,7 @@ lvgl: | |||||||
|                       styles: bdr_style |                       styles: bdr_style | ||||||
|                       grid_cell_x_align: center |                       grid_cell_x_align: center | ||||||
|                       grid_cell_y_align: stretch |                       grid_cell_y_align: stretch | ||||||
|                       grid_cell_row_pos: 0 |                       grid_cell_column_span: 2 | ||||||
|                       grid_cell_column_pos: 1 |  | ||||||
|                       grid_cell_column_span: 1 |  | ||||||
|                       text: "Grid cell 0/1" |                       text: "Grid cell 0/1" | ||||||
|                   - label: |                   - label: | ||||||
|                       grid_cell_x_align: end |                       grid_cell_x_align: end | ||||||
|   | |||||||
							
								
								
									
										7
									
								
								tests/integration/fixtures/api_reboot_timeout.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								tests/integration/fixtures/api_reboot_timeout.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | esphome: | ||||||
|  |   name: api-reboot-test | ||||||
|  | host: | ||||||
|  | api: | ||||||
|  |   reboot_timeout: 0.5s  # Very short timeout for fast testing | ||||||
|  | logger: | ||||||
|  |   level: DEBUG | ||||||
| @@ -8,5 +8,8 @@ sensor: | |||||||
|     name: Test Sensor |     name: Test Sensor | ||||||
|     id: test_sensor |     id: test_sensor | ||||||
|     unit_of_measurement: °C |     unit_of_measurement: °C | ||||||
|  |     accuracy_decimals: 2 | ||||||
|  |     state_class: measurement | ||||||
|  |     force_update: true | ||||||
|     lambda: return 42.0; |     lambda: return 42.0; | ||||||
|     update_interval: 0.1s |     update_interval: 0.1s | ||||||
|   | |||||||
							
								
								
									
										35
									
								
								tests/integration/test_api_reboot_timeout.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								tests/integration/test_api_reboot_timeout.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | |||||||
|  | """Test API server reboot timeout functionality.""" | ||||||
|  |  | ||||||
|  | import asyncio | ||||||
|  | import re | ||||||
|  |  | ||||||
|  | import pytest | ||||||
|  |  | ||||||
|  | from .types import RunCompiledFunction | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.asyncio | ||||||
|  | async def test_api_reboot_timeout( | ||||||
|  |     yaml_config: str, | ||||||
|  |     run_compiled: RunCompiledFunction, | ||||||
|  | ) -> None: | ||||||
|  |     """Test that the device reboots when no API clients connect within the timeout.""" | ||||||
|  |     loop = asyncio.get_running_loop() | ||||||
|  |     reboot_future = loop.create_future() | ||||||
|  |     reboot_pattern = re.compile(r"No clients; rebooting") | ||||||
|  |  | ||||||
|  |     def check_output(line: str) -> None: | ||||||
|  |         """Check output for reboot message.""" | ||||||
|  |         if not reboot_future.done() and reboot_pattern.search(line): | ||||||
|  |             reboot_future.set_result(True) | ||||||
|  |  | ||||||
|  |     # Run the device without connecting any API client | ||||||
|  |     async with run_compiled(yaml_config, line_callback=check_output): | ||||||
|  |         # Wait for reboot with timeout | ||||||
|  |         # (0.5s reboot timeout + some margin for processing) | ||||||
|  |         try: | ||||||
|  |             await asyncio.wait_for(reboot_future, timeout=2.0) | ||||||
|  |         except asyncio.TimeoutError: | ||||||
|  |             pytest.fail("Device did not reboot within expected timeout") | ||||||
|  |  | ||||||
|  |     # Test passes if we get here - reboot was detected | ||||||
| @@ -4,6 +4,7 @@ from __future__ import annotations | |||||||
|  |  | ||||||
| import asyncio | import asyncio | ||||||
|  |  | ||||||
|  | import aioesphomeapi | ||||||
| from aioesphomeapi import EntityState | from aioesphomeapi import EntityState | ||||||
| import pytest | import pytest | ||||||
|  |  | ||||||
| @@ -47,3 +48,23 @@ async def test_host_mode_with_sensor( | |||||||
|         # Verify the sensor state |         # Verify the sensor state | ||||||
|         assert test_sensor_state.state == 42.0 |         assert test_sensor_state.state == 42.0 | ||||||
|         assert len(states) > 0, "No states received" |         assert len(states) > 0, "No states received" | ||||||
|  |  | ||||||
|  |         # Verify the optimized fields are working correctly | ||||||
|  |         # Get entity info to check accuracy_decimals, state_class, etc. | ||||||
|  |         entities, _ = await client.list_entities_services() | ||||||
|  |         sensor_info: aioesphomeapi.SensorInfo | None = None | ||||||
|  |         for entity in entities: | ||||||
|  |             if isinstance(entity, aioesphomeapi.SensorInfo): | ||||||
|  |                 sensor_info = entity | ||||||
|  |                 break | ||||||
|  |  | ||||||
|  |         assert sensor_info is not None, "Sensor entity info not found" | ||||||
|  |         assert sensor_info.accuracy_decimals == 2, ( | ||||||
|  |             f"Expected accuracy_decimals=2, got {sensor_info.accuracy_decimals}" | ||||||
|  |         ) | ||||||
|  |         assert sensor_info.state_class == 1, ( | ||||||
|  |             f"Expected state_class=1 (measurement), got {sensor_info.state_class}" | ||||||
|  |         ) | ||||||
|  |         assert sensor_info.force_update is True, ( | ||||||
|  |             f"Expected force_update=True, got {sensor_info.force_update}" | ||||||
|  |         ) | ||||||
|   | |||||||
| @@ -15,4 +15,3 @@ packages: | |||||||
|     file: $component_test_file |     file: $component_test_file | ||||||
|     vars: |     vars: | ||||||
|       component_test_file: $component_test_file |       component_test_file: $component_test_file | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user