mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +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: | ||||
|   schedule: | ||||
|     - cron: "30 0 * * *" | ||||
|     - cron: "30 0 * * *"  # Run daily at 00:30 UTC | ||||
|   workflow_dispatch: | ||||
|  | ||||
| permissions: | ||||
|   issues: write | ||||
|   pull-requests: write | ||||
|  | ||||
| concurrency: | ||||
|   group: lock | ||||
|  | ||||
| jobs: | ||||
|   lock: | ||||
|     runs-on: ubuntu-latest | ||||
|     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 | ||||
|     uses: esphome/workflows/.github/workflows/lock.yml@main | ||||
|   | ||||
| @@ -4,7 +4,7 @@ | ||||
| repos: | ||||
|   - repo: https://github.com/astral-sh/ruff-pre-commit | ||||
|     # Ruff version. | ||||
|     rev: v0.12.0 | ||||
|     rev: v0.12.1 | ||||
|     hooks: | ||||
|       # Run the linter. | ||||
|       - id: ruff | ||||
|   | ||||
| @@ -146,6 +146,7 @@ esphome/components/esp32_ble_client/* @jesserockz | ||||
| esphome/components/esp32_ble_server/* @Rapsssito @clydebarrow @jesserockz | ||||
| esphome/components/esp32_camera_web_server/* @ayufan | ||||
| esphome/components/esp32_can/* @Sympatron | ||||
| esphome/components/esp32_hosted/* @swoboda1337 | ||||
| esphome/components/esp32_improv/* @jesserockz | ||||
| esphome/components/esp32_rmt/* @jesserockz | ||||
| esphome/components/esp32_rmt_led_strip/* @jesserockz | ||||
| @@ -491,7 +492,7 @@ esphome/components/vbus/* @ssieb | ||||
| esphome/components/veml3235/* @kbx81 | ||||
| esphome/components/veml7700/* @latonita | ||||
| 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/watchdog/* @oarcher | ||||
| esphome/components/waveshare_epaper/* @clydebarrow | ||||
|   | ||||
| @@ -4,6 +4,7 @@ | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include <cmath> | ||||
| #include <numbers> | ||||
|  | ||||
| #ifdef USE_ESP8266 | ||||
| #include <core_esp8266_waveform.h> | ||||
| @@ -203,7 +204,7 @@ void AcDimmer::setup() { | ||||
| #endif | ||||
| } | ||||
| 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)); | ||||
|   if (new_value != 0 && this->store_.value == 0) | ||||
|     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, | ||||
| // 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_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"; | ||||
| #ifdef USE_ESP32_CAMERA | ||||
| static const int ESP32_CAMERA_STOP_STREAM = 5000; | ||||
| #endif | ||||
|  | ||||
| APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent) | ||||
|     : parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) { | ||||
| @@ -90,16 +95,6 @@ APIConnection::~APIConnection() { | ||||
| } | ||||
|  | ||||
| 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_) { | ||||
|     // requested a disconnect | ||||
|     this->helper_->close(); | ||||
| @@ -152,20 +147,19 @@ void APIConnection::loop() { | ||||
|  | ||||
|   // Process deferred batch if 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_(); | ||||
|   } | ||||
|  | ||||
|   if (!this->list_entities_iterator_.completed()) | ||||
|   if (!this->list_entities_iterator_.completed()) { | ||||
|     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(); | ||||
|   } | ||||
|  | ||||
|   static uint8_t max_ping_retries = 60; | ||||
|   static uint16_t ping_retry_interval = 1000; | ||||
|   if (this->sent_ping_) { | ||||
|     // 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(); | ||||
|       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"); | ||||
|     this->sent_ping_ = this->send_message(PingRequest()); | ||||
|     if (!this->sent_ping_) { | ||||
|       this->next_ping_retry_ = now + ping_retry_interval; | ||||
|       this->next_ping_retry_ = now + PING_RETRY_INTERVAL; | ||||
|       this->ping_retries_++; | ||||
|       std::string warn_str = str_sprintf("%s: Sending keepalive failed %u time(s);", | ||||
|                                          this->get_client_combined_info().c_str(), this->ping_retries_); | ||||
|       if (this->ping_retries_ >= max_ping_retries) { | ||||
|       if (this->ping_retries_ >= MAX_PING_RETRIES) { | ||||
|         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) { | ||||
|         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 { | ||||
|         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; | ||||
|     buffer.encode_bool(3, done); | ||||
|  | ||||
|     bool success = this->send_buffer(buffer, 44); | ||||
|     bool success = this->send_buffer(buffer, CameraImageResponse::MESSAGE_TYPE); | ||||
|  | ||||
|     if (success) { | ||||
|       this->image_reader_.consume_data(to_send); | ||||
|     } | ||||
|     if (success && done) { | ||||
|       if (done) { | ||||
|         this->image_reader_.return_image(); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| #endif | ||||
|  | ||||
|   if (state_subs_at_ != -1) { | ||||
|   if (state_subs_at_ >= 0) { | ||||
|     const auto &subs = this->parent_->get_state_subs(); | ||||
|     if (state_subs_at_ >= (int) subs.size()) { | ||||
|       state_subs_at_ = -1; | ||||
|     } else { | ||||
|     if (state_subs_at_ < static_cast<int>(subs.size())) { | ||||
|       auto &it = subs[state_subs_at_]; | ||||
|       SubscribeHomeAssistantStateResponse resp; | ||||
|       resp.entity_id = it.entity_id; | ||||
| @@ -231,6 +221,8 @@ void APIConnection::loop() { | ||||
|       if (this->send_message(resp)) { | ||||
|         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 | ||||
|   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) | ||||
|   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 | ||||
| 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) { | ||||
|   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]; | ||||
|  | ||||
|     // 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 && | ||||
|         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) { | ||||
|     // Try to encode message | ||||
|     // 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) { | ||||
|       // 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, | ||||
|                                                    bool is_single) const { | ||||
|   switch (message_type_) { | ||||
|     case 0:  // Function pointer | ||||
|       return data_.ptr(entity, conn, remaining_size, is_single); | ||||
|  | ||||
|                                                    bool is_single, uint16_t message_type) const { | ||||
|   if (has_tagged_string_ptr_()) { | ||||
|     // Handle string-based messages | ||||
|     switch (message_type) { | ||||
| #ifdef USE_EVENT | ||||
|       case EventResponse::MESSAGE_TYPE: { | ||||
|         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 | ||||
|  | ||||
|       default: | ||||
|         // Should not happen, return 0 to indicate no message | ||||
|         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, | ||||
|   | ||||
| @@ -483,55 +483,57 @@ class APIConnection : public APIServerConnection { | ||||
|   // Function pointer type for message encoding | ||||
|   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 { | ||||
|     // Ensure pointer alignment allows LSB tagging | ||||
|     static_assert(alignof(std::string *) > 1, "String pointer alignment must be > 1 for LSB tagging"); | ||||
|  | ||||
|    public: | ||||
|     // Constructor for function pointer (message_type = 0) | ||||
|     MessageCreator(MessageCreatorPtr ptr) : message_type_(0) { data_.ptr = ptr; } | ||||
|     // Constructor for function pointer | ||||
|     MessageCreator(MessageCreatorPtr ptr) { | ||||
|       // Function pointers are always aligned, so LSB is 0 | ||||
|       data_.ptr = ptr; | ||||
|     } | ||||
|  | ||||
|     // Constructor for string state capture | ||||
|     MessageCreator(const std::string &value, uint16_t msg_type) : message_type_(msg_type) { | ||||
|       data_.string_ptr = new std::string(value); | ||||
|     explicit MessageCreator(const std::string &str_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 | ||||
|     ~MessageCreator() { | ||||
|       // Clean up string data for string-based message types | ||||
|       if (uses_string_data_()) { | ||||
|         delete data_.string_ptr; | ||||
|       if (has_tagged_string_ptr_()) { | ||||
|         delete get_string_ptr_(); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // Copy constructor | ||||
|     MessageCreator(const MessageCreator &other) : message_type_(other.message_type_) { | ||||
|       if (message_type_ == 0) { | ||||
|         data_.ptr = other.data_.ptr; | ||||
|       } else if (uses_string_data_()) { | ||||
|         data_.string_ptr = new std::string(*other.data_.string_ptr); | ||||
|     MessageCreator(const MessageCreator &other) { | ||||
|       if (other.has_tagged_string_ptr_()) { | ||||
|         auto *str = new std::string(*other.get_string_ptr_()); | ||||
|         data_.tagged = reinterpret_cast<uintptr_t>(str) | 1; | ||||
|       } else { | ||||
|         data_ = other.data_;  // For POD types | ||||
|         data_ = other.data_; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // Move constructor | ||||
|     MessageCreator(MessageCreator &&other) noexcept : data_(other.data_), message_type_(other.message_type_) { | ||||
|       other.message_type_ = 0;  // Reset other to function pointer type | ||||
|       other.data_.ptr = nullptr; | ||||
|     } | ||||
|     MessageCreator(MessageCreator &&other) noexcept : data_(other.data_) { other.data_.ptr = nullptr; } | ||||
|  | ||||
|     // Assignment operators (needed for batch deduplication) | ||||
|     MessageCreator &operator=(const MessageCreator &other) { | ||||
|       if (this != &other) { | ||||
|         // Clean up current string data if needed | ||||
|         if (uses_string_data_()) { | ||||
|           delete data_.string_ptr; | ||||
|         if (has_tagged_string_ptr_()) { | ||||
|           delete get_string_ptr_(); | ||||
|         } | ||||
|         // Copy new data | ||||
|         message_type_ = other.message_type_; | ||||
|         if (other.message_type_ == 0) { | ||||
|           data_.ptr = other.data_.ptr; | ||||
|         } else if (other.uses_string_data_()) { | ||||
|           data_.string_ptr = new std::string(*other.data_.string_ptr); | ||||
|         if (other.has_tagged_string_ptr_()) { | ||||
|           auto *str = new std::string(*other.get_string_ptr_()); | ||||
|           data_.tagged = reinterpret_cast<uintptr_t>(str) | 1; | ||||
|         } else { | ||||
|           data_ = other.data_; | ||||
|         } | ||||
| @@ -542,30 +544,35 @@ class APIConnection : public APIServerConnection { | ||||
|     MessageCreator &operator=(MessageCreator &&other) noexcept { | ||||
|       if (this != &other) { | ||||
|         // Clean up current string data if needed | ||||
|         if (uses_string_data_()) { | ||||
|           delete data_.string_ptr; | ||||
|         if (has_tagged_string_ptr_()) { | ||||
|           delete get_string_ptr_(); | ||||
|         } | ||||
|         // Move data | ||||
|         message_type_ = other.message_type_; | ||||
|         data_ = other.data_; | ||||
|         // Reset other to safe state | ||||
|         other.message_type_ = 0; | ||||
|         other.data_.ptr = nullptr; | ||||
|       } | ||||
|       return *this; | ||||
|     } | ||||
|  | ||||
|     // Call operator | ||||
|     uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) const; | ||||
|     // Call operator - now accepts message_type as parameter | ||||
|     uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single, | ||||
|                         uint16_t message_type) const; | ||||
|  | ||||
|    private: | ||||
|     // Helper to check if this message type uses heap-allocated strings | ||||
|     bool uses_string_data_() const { return message_type_ == EventResponse::MESSAGE_TYPE; } | ||||
|     union CreatorData { | ||||
|       MessageCreatorPtr ptr;    // 8 bytes | ||||
|       std::string *string_ptr;  // 8 bytes | ||||
|     } data_;                    // 8 bytes | ||||
|     uint16_t message_type_;     // 2 bytes (0 = function ptr, >0 = state capture) | ||||
|     // Check if this contains a string pointer | ||||
|     bool has_tagged_string_ptr_() const { return (data_.tagged & 1) != 0; } | ||||
|  | ||||
|     // Get the actual string pointer (clears the tag bit) | ||||
|     std::string *get_string_ptr_() const { | ||||
|       // NOLINTNEXTLINE(performance-no-int-to-ptr) | ||||
|       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 | ||||
|   | ||||
| @@ -66,6 +66,17 @@ const char *api_error_to_str(APIError err) { | ||||
|   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 | ||||
| void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len) { | ||||
|   SendBuffer buffer; | ||||
| @@ -287,13 +298,8 @@ APIError APINoiseFrameHelper::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 | ||||
|   // Use base class implementation for buffer sending | ||||
|   return APIFrameHelper::loop(); | ||||
| } | ||||
|  | ||||
| /** 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; | ||||
|     } | ||||
|  | ||||
|     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 | ||||
|   } | ||||
|  | ||||
|   // 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]; | ||||
|  | ||||
|   if (state_ != State::DATA && msg_size > 128) { | ||||
| @@ -595,10 +599,6 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { | ||||
|     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 data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3]; | ||||
|   if (data_len > msg_size - 4) { | ||||
| @@ -831,18 +831,12 @@ APIError APIPlaintextFrameHelper::init() { | ||||
|   state_ = State::DATA; | ||||
|   return APIError::OK; | ||||
| } | ||||
| /// Not used for plaintext | ||||
| APIError APIPlaintextFrameHelper::loop() { | ||||
|   if (state_ != State::DATA) { | ||||
|     return APIError::BAD_STATE; | ||||
|   } | ||||
|   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 | ||||
|   // Use base class implementation for buffer sending | ||||
|   return APIFrameHelper::loop(); | ||||
| } | ||||
|  | ||||
| /** 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) {} | ||||
| }; | ||||
|  | ||||
| enum class APIError : int { | ||||
| enum class APIError : uint16_t { | ||||
|   OK = 0, | ||||
|   WOULD_BLOCK = 1001, | ||||
|   BAD_HANDSHAKE_PACKET_LEN = 1002, | ||||
| @@ -74,7 +74,7 @@ class APIFrameHelper { | ||||
|   } | ||||
|   virtual ~APIFrameHelper() = default; | ||||
|   virtual APIError init() = 0; | ||||
|   virtual APIError loop() = 0; | ||||
|   virtual APIError loop(); | ||||
|   virtual APIError read_packet(ReadPacketBuffer *buffer) = 0; | ||||
|   bool can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); } | ||||
|   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) { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|     this->log_send_message_(T::message_name(), msg.dump()); | ||||
|     this->log_send_message_(msg.message_name(), msg.dump()); | ||||
| #endif | ||||
|     return this->send_message_(msg, T::MESSAGE_TYPE); | ||||
|   } | ||||
|   | ||||
| @@ -47,6 +47,11 @@ void APIServer::setup() { | ||||
|   } | ||||
| #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 | ||||
|   if (this->socket_ == nullptr) { | ||||
|     ESP_LOGW(TAG, "Could not create socket"); | ||||
| @@ -106,8 +111,6 @@ void APIServer::setup() { | ||||
|   } | ||||
| #endif | ||||
|  | ||||
|   this->last_connected_ = App.get_loop_component_start_time(); | ||||
|  | ||||
| #ifdef USE_ESP32_CAMERA | ||||
|   if (esp32_camera::global_esp32_camera != nullptr && !esp32_camera::global_esp32_camera->is_internal()) { | ||||
|     esp32_camera::global_esp32_camera->add_image_callback( | ||||
| @@ -121,6 +124,16 @@ void APIServer::setup() { | ||||
| #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() { | ||||
|   // Accept new clients only if the socket exists and has incoming connections | ||||
|   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); | ||||
|       if (!sock) | ||||
|         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); | ||||
|       this->clients_.emplace_back(conn); | ||||
|       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 | ||||
|   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; | ||||
|   while (client_index < this->clients_.size()) { | ||||
|     auto &client = this->clients_[client_index]; | ||||
|  | ||||
|       if (client->remove_) { | ||||
|         // Handle disconnection | ||||
|     if (!client->remove_) { | ||||
|       // 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_); | ||||
|         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) | ||||
|     if (client_index < this->clients_.size() - 1) { | ||||
|       std::swap(this->clients_[client_index], this->clients_.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) { | ||||
|     const uint32_t now = App.get_loop_component_start_time(); | ||||
|     if (!this->is_connected()) { | ||||
|       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(); | ||||
|     // Schedule reboot when last client disconnects | ||||
|     if (this->clients_.empty() && this->reboot_timeout_ != 0) { | ||||
|       this->schedule_reboot_timeout_(); | ||||
|     } | ||||
|     // Don't increment client_index since we need to process the swapped element | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -142,6 +142,7 @@ class APIServer : public Component, public Controller { | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
|   void schedule_reboot_timeout_(); | ||||
|   // Pointers and pointer-like types first (4 bytes each) | ||||
|   std::unique_ptr<socket::Socket> socket_ = nullptr; | ||||
|   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 | ||||
|   uint32_t reboot_timeout_{300000}; | ||||
|   uint32_t batch_delay_{100}; | ||||
|   uint32_t last_connected_{0}; | ||||
|  | ||||
|   // Vectors and strings (12 bytes each on 32-bit) | ||||
|   std::vector<std::unique_ptr<APIConnection>> clients_; | ||||
|   | ||||
| @@ -4,7 +4,13 @@ import asyncio | ||||
| from datetime import datetime | ||||
| import logging | ||||
| from typing import TYPE_CHECKING, Any | ||||
| import warnings | ||||
|  | ||||
| # Suppress protobuf version warnings | ||||
| 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 | ||||
|  | ||||
|   | ||||
| @@ -335,6 +335,7 @@ class ProtoMessage { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   std::string dump() const; | ||||
|   virtual void dump_to(std::string &out) const = 0; | ||||
|   virtual const char *message_name() const { return "unknown"; } | ||||
| #endif | ||||
|  | ||||
|  protected: | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| #include "atm90e32.h" | ||||
| #include <cinttypes> | ||||
| #include <cmath> | ||||
| #include <numbers> | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| 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 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 threshold = peak_01v / divider; | ||||
|   | ||||
| @@ -312,7 +312,7 @@ FileDecoderState AudioDecoder::decode_mp3_() { | ||||
|   if (err) { | ||||
|     switch (err) { | ||||
|       case esp_audio_libs::helix_decoder::ERR_MP3_OUT_OF_MEMORY: | ||||
|         // Intentional fallthrough | ||||
|         [[fallthrough]]; | ||||
|       case esp_audio_libs::helix_decoder::ERR_MP3_NULL_POINTER: | ||||
|         return FileDecoderState::FAILED; | ||||
|         break; | ||||
|   | ||||
| @@ -5,6 +5,7 @@ | ||||
| #include "esphome/core/defines.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| #if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE | ||||
| #include "esp_crt_bundle.h" | ||||
| @@ -16,13 +17,13 @@ namespace audio { | ||||
| static const uint32_t READ_WRITE_TIMEOUT_MS = 20; | ||||
|  | ||||
| static const uint32_t CONNECTION_TIMEOUT_MS = 5000; | ||||
|  | ||||
| // 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 uint8_t MAX_FETCHING_HEADER_ATTEMPTS = 6; | ||||
|  | ||||
| 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 | ||||
| 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.cert_pem = nullptr; | ||||
|   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.user_data = this; | ||||
|   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); | ||||
|  | ||||
|   if (err != ESP_OK) { | ||||
|     ESP_LOGE(TAG, "Failed to open URL"); | ||||
|     this->cleanup_connection_(); | ||||
|     return err; | ||||
|   } | ||||
|  | ||||
|   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) { | ||||
|     ESP_LOGE(TAG, "Failed to fetch headers"); | ||||
|     this->cleanup_connection_(); | ||||
|     return ESP_FAIL; | ||||
|   } | ||||
| @@ -135,7 +153,7 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) { | ||||
|  | ||||
|   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); | ||||
|     if (err != ESP_OK) { | ||||
|       this->cleanup_connection_(); | ||||
| @@ -267,21 +285,24 @@ AudioReaderState AudioReader::http_read_() { | ||||
|       return AudioReaderState::FINISHED; | ||||
|     } | ||||
|   } 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(), bytes_to_read); | ||||
|     int received_len = esp_http_client_read(this->client_, (char *) this->output_transfer_buffer_->get_buffer_end(), | ||||
|                                             this->output_transfer_buffer_->free()); | ||||
|  | ||||
|     if (received_len > 0) { | ||||
|       this->output_transfer_buffer_->increase_buffer_length(received_len); | ||||
|       this->last_data_read_ms_ = millis(); | ||||
|     } else if (received_len < 0) { | ||||
|       return AudioReaderState::READING; | ||||
|     } else if (received_len <= 0) { | ||||
|       // HTTP read error | ||||
|       if (received_len == -1) { | ||||
|         // A true connection error occured, no chance at recovery | ||||
|         this->cleanup_connection_(); | ||||
|         return AudioReaderState::FAILED; | ||||
|     } else { | ||||
|       if (bytes_to_read > 0) { | ||||
|         // Read timed out | ||||
|         if ((millis() - this->last_data_read_ms_) > CONNECTION_TIMEOUT_MS) { | ||||
|       } | ||||
|  | ||||
|       // Read timed out, manually verify if it has been too long since the last successful read | ||||
|       if ((millis() - this->last_data_read_ms_) > MAX_FETCHING_HEADER_ATTEMPTS * CONNECTION_TIMEOUT_MS) { | ||||
|         ESP_LOGE(TAG, "Timed out"); | ||||
|         this->cleanup_connection_(); | ||||
|         return AudioReaderState::FAILED; | ||||
|       } | ||||
| @@ -289,7 +310,6 @@ AudioReaderState AudioReader::http_read_() { | ||||
|       delay(READ_WRITE_TIMEOUT_MS); | ||||
|     } | ||||
|   } | ||||
|   } | ||||
|  | ||||
|   return AudioReaderState::READING; | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| #include "display.h" | ||||
| #include <utility> | ||||
| #include <numbers> | ||||
| #include "display_color_utils.h" | ||||
| #include "esphome/core/hal.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. | ||||
|     rotation_degrees += ROTATION_270_DEGREES; | ||||
|     // 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 | ||||
|     // 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, | ||||
|     // 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. | ||||
|     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_y = (int) round(sin(vertex_angle) * radius) + center_y; | ||||
|   } | ||||
|   | ||||
| @@ -138,8 +138,6 @@ enum DisplayRotation { | ||||
|   DISPLAY_ROTATION_270_DEGREES = 270, | ||||
| }; | ||||
|  | ||||
| #define PI 3.1415926535897932384626433832795 | ||||
|  | ||||
| const int EDGES_TRIGON = 3; | ||||
| const int EDGES_TRIANGLE = 3; | ||||
| const int EDGES_TETRAGON = 4; | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import logging | ||||
| import os | ||||
| from pathlib import Path | ||||
|  | ||||
| from esphome import git | ||||
| from esphome import yaml_util | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
| @@ -23,7 +23,6 @@ from esphome.const import ( | ||||
|     CONF_REFRESH, | ||||
|     CONF_SOURCE, | ||||
|     CONF_TYPE, | ||||
|     CONF_URL, | ||||
|     CONF_VARIANT, | ||||
|     CONF_VERSION, | ||||
|     KEY_CORE, | ||||
| @@ -32,14 +31,13 @@ from esphome.const import ( | ||||
|     KEY_TARGET_FRAMEWORK, | ||||
|     KEY_TARGET_PLATFORM, | ||||
|     PLATFORM_ESP32, | ||||
|     TYPE_GIT, | ||||
|     TYPE_LOCAL, | ||||
|     __version__, | ||||
| ) | ||||
| from esphome.core import CORE, HexInt, TimePeriod | ||||
| from esphome.cpp_generator import RawExpression | ||||
| import esphome.final_validate as fv | ||||
| from esphome.helpers import copy_file_if_changed, mkdir_p, write_file_if_changed | ||||
| from esphome.types import ConfigType | ||||
|  | ||||
| from .boards import BOARDS | ||||
| from .const import (  # noqa | ||||
| @@ -49,10 +47,8 @@ from .const import (  # noqa | ||||
|     KEY_EXTRA_BUILD_FILES, | ||||
|     KEY_PATH, | ||||
|     KEY_REF, | ||||
|     KEY_REFRESH, | ||||
|     KEY_REPO, | ||||
|     KEY_SDKCONFIG_OPTIONS, | ||||
|     KEY_SUBMODULES, | ||||
|     KEY_VARIANT, | ||||
|     VARIANT_ESP32, | ||||
|     VARIANT_ESP32C2, | ||||
| @@ -235,7 +231,7 @@ def add_idf_sdkconfig_option(name: str, value: SdkconfigValueType): | ||||
| def add_idf_component( | ||||
|     *, | ||||
|     name: str, | ||||
|     repo: str, | ||||
|     repo: str = None, | ||||
|     ref: str = None, | ||||
|     path: str = None, | ||||
|     refresh: TimePeriod = None, | ||||
| @@ -245,30 +241,27 @@ def add_idf_component( | ||||
|     """Add an esp-idf component to the project.""" | ||||
|     if not CORE.using_esp_idf: | ||||
|         raise ValueError("Not an esp-idf project") | ||||
|     if components is None: | ||||
|         components = [] | ||||
|     if name not in CORE.data[KEY_ESP32][KEY_COMPONENTS]: | ||||
|     if not repo and not ref and not path: | ||||
|         raise ValueError("Requires at least one of repo, ref or path") | ||||
|     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] = { | ||||
|             KEY_REPO: repo, | ||||
|             KEY_REF: ref, | ||||
|             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): | ||||
| @@ -348,6 +341,7 @@ SUPPORTED_PLATFORMIO_ESP_IDF_5X = [ | ||||
| # List based on https://github.com/pioarduino/esp-idf/releases | ||||
| SUPPORTED_PIOARDUINO_ESP_IDF_5X = [ | ||||
|     cv.Version(5, 5, 0), | ||||
|     cv.Version(5, 4, 2), | ||||
|     cv.Version(5, 4, 1), | ||||
|     cv.Version(5, 4, 0), | ||||
|     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_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( | ||||
|     cv.Schema( | ||||
|         { | ||||
| @@ -606,7 +611,7 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All( | ||||
|                         CONF_ENABLE_LWIP_DHCP_SERVER, "wifi", default=False | ||||
|                     ): cv.boolean, | ||||
|                     cv.Optional( | ||||
|                         CONF_ENABLE_LWIP_MDNS_QUERIES, default=False | ||||
|                         CONF_ENABLE_LWIP_MDNS_QUERIES, default=True | ||||
|                     ): cv.boolean, | ||||
|                     cv.Optional( | ||||
|                         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.All( | ||||
|                     cv.Schema( | ||||
|                         { | ||||
|                             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_REFRESH, default="1d"): cv.All( | ||||
|                             cv.Optional(CONF_REFRESH): cv.All( | ||||
|                                 cv.string, cv.source_refresh | ||||
|                             ), | ||||
|                         } | ||||
|                     ), | ||||
|                     _validate_idf_component, | ||||
|                 ) | ||||
|             ), | ||||
|         } | ||||
| @@ -696,7 +705,7 @@ FINAL_VALIDATE_SCHEMA = cv.Schema(final_validate) | ||||
| async def to_code(config): | ||||
|     cg.add_platformio_option("board", config[CONF_BOARD]) | ||||
|     cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE]) | ||||
|     cg.set_cpp_standard("gnu++17") | ||||
|     cg.set_cpp_standard("gnu++20") | ||||
|     cg.add_build_flag("-DUSE_ESP32") | ||||
|     cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) | ||||
|     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_CPU1", False) | ||||
|  | ||||
|         # Disable dynamic log level control to save memory | ||||
|         add_idf_sdkconfig_option("CONFIG_LOG_DYNAMIC_LEVEL_CONTROL", False) | ||||
|  | ||||
|         # Set default CPU frequency | ||||
|         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] | ||||
|         ): | ||||
|             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) | ||||
|         if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False): | ||||
|             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)) | ||||
|  | ||||
|         for component in conf[CONF_COMPONENTS]: | ||||
|             source = component[CONF_SOURCE] | ||||
|             if source[CONF_TYPE] == TYPE_GIT: | ||||
|             add_idf_component( | ||||
|                 name=component[CONF_NAME], | ||||
|                     repo=source[CONF_URL], | ||||
|                     ref=source.get(CONF_REF), | ||||
|                 repo=component.get(CONF_SOURCE), | ||||
|                 ref=component.get(CONF_REF), | ||||
|                 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: | ||||
|         cg.add_platformio_option("framework", "arduino") | ||||
|         cg.add_build_flag("-DUSE_ARDUINO") | ||||
| @@ -924,6 +930,26 @@ def _write_sdkconfig(): | ||||
|         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 | ||||
| def copy_files(): | ||||
|     if CORE.using_arduino: | ||||
| @@ -936,6 +962,7 @@ def copy_files(): | ||||
|             ) | ||||
|     if CORE.using_esp_idf: | ||||
|         _write_sdkconfig() | ||||
|         _write_idf_component_yml() | ||||
|         if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]: | ||||
|             write_file_if_changed( | ||||
|                 CORE.relative_build_path("partitions.csv"), | ||||
| @@ -952,55 +979,6 @@ def copy_files(): | ||||
|             __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(): | ||||
|         if file[KEY_PATH].startswith("http"): | ||||
|             import requests | ||||
|   | ||||
| @@ -29,9 +29,9 @@ class ESP32InternalGPIOPin : public InternalGPIOPin { | ||||
|   void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; | ||||
|  | ||||
|   gpio_num_t pin_; | ||||
|   bool inverted_; | ||||
|   gpio_drive_cap_t drive_strength_; | ||||
|   gpio::Flags flags_; | ||||
|   bool inverted_; | ||||
|   // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) | ||||
|   static bool isr_service_installed; | ||||
| }; | ||||
|   | ||||
| @@ -496,17 +496,17 @@ float BLEClientBase::parse_char_value(uint8_t *value, uint16_t length) { | ||||
|       if (length > 2) { | ||||
|         return (float) encode_uint16(value[1], value[2]); | ||||
|       } | ||||
|       // fall through | ||||
|       [[fallthrough]]; | ||||
|     case 0x7:  // uint24. | ||||
|       if (length > 3) { | ||||
|         return (float) encode_uint24(value[1], value[2], value[3]); | ||||
|       } | ||||
|       // fall through | ||||
|       [[fallthrough]]; | ||||
|     case 0x8:  // uint32. | ||||
|       if (length > 4) { | ||||
|         return (float) encode_uint32(value[1], value[2], value[3], value[4]); | ||||
|       } | ||||
|       // fall through | ||||
|       [[fallthrough]]; | ||||
|     case 0xC:  // int8. | ||||
|       return (float) ((int8_t) value[1]); | ||||
|     case 0xD:  // int12. | ||||
| @@ -514,12 +514,12 @@ float BLEClientBase::parse_char_value(uint8_t *value, uint16_t length) { | ||||
|       if (length > 2) { | ||||
|         return (float) ((int16_t) (value[1] << 8) + (int16_t) value[2]); | ||||
|       } | ||||
|       // fall through | ||||
|       [[fallthrough]]; | ||||
|     case 0xF:  // int24. | ||||
|       if (length > 3) { | ||||
|         return (float) ((int32_t) (value[1] << 16) + (int32_t) (value[2] << 8) + (int32_t) (value[3])); | ||||
|       } | ||||
|       // fall through | ||||
|       [[fallthrough]]; | ||||
|     case 0x10:  // int32. | ||||
|       if (length > 4) { | ||||
|         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") | ||||
|  | ||||
|     if CORE.using_esp_idf: | ||||
|         add_idf_component( | ||||
|             name="esp32-camera", | ||||
|             repo="https://github.com/espressif/esp32-camera.git", | ||||
|             ref="v2.0.15", | ||||
|         ) | ||||
|         add_idf_component(name="espressif/esp32-camera", ref="2.0.15") | ||||
|  | ||||
|     for conf in config.get(CONF_ON_STREAM_START, []): | ||||
|         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_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_VARIANT", "ESP8266") | ||||
|  | ||||
|   | ||||
| @@ -129,9 +129,9 @@ void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) { | ||||
|     } | ||||
|   } else { | ||||
|     if (value != arg->inverted) { | ||||
|       *arg->out_set_reg |= 1; | ||||
|       *arg->out_set_reg = *arg->out_set_reg | 1; | ||||
|     } 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) { | ||||
|       *arg->mode_set_reg = arg->mask; | ||||
|       if (flags & gpio::FLAG_OPEN_DRAIN) { | ||||
|         *arg->control_reg |= 1 << GPCD; | ||||
|         *arg->control_reg = *arg->control_reg | (1 << GPCD); | ||||
|       } else { | ||||
|         *arg->control_reg &= ~(1 << GPCD); | ||||
|       } | ||||
| @@ -155,21 +155,21 @@ void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) { | ||||
|       *arg->mode_clr_reg = arg->mask; | ||||
|     } | ||||
|     if (flags & gpio::FLAG_PULLUP) { | ||||
|       *arg->func_reg |= 1 << GPFPU; | ||||
|       *arg->control_reg |= 1 << GPCD; | ||||
|       *arg->func_reg = *arg->func_reg | (1 << GPFPU); | ||||
|       *arg->control_reg = *arg->control_reg | (1 << GPCD); | ||||
|     } else { | ||||
|       *arg->func_reg &= ~(1 << GPFPU); | ||||
|       *arg->func_reg = *arg->func_reg & ~(1 << GPFPU); | ||||
|     } | ||||
|   } else { | ||||
|     if (flags & gpio::FLAG_OUTPUT) { | ||||
|       *arg->mode_set_reg |= 1; | ||||
|       *arg->mode_set_reg = *arg->mode_set_reg | 1; | ||||
|     } else { | ||||
|       *arg->mode_set_reg &= ~1; | ||||
|       *arg->mode_set_reg = *arg->mode_set_reg & ~1; | ||||
|     } | ||||
|     if (flags & gpio::FLAG_PULLDOWN) { | ||||
|       *arg->func_reg |= 1 << GP16FPD; | ||||
|       *arg->func_reg = *arg->func_reg | (1 << GP16FPD); | ||||
|     } else { | ||||
|       *arg->func_reg &= ~(1 << GP16FPD); | ||||
|       *arg->func_reg = *arg->func_reg & ~(1 << GP16FPD); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -15,7 +15,7 @@ | ||||
| namespace esphome { | ||||
| namespace ethernet { | ||||
|  | ||||
| enum EthernetType { | ||||
| enum EthernetType : uint8_t { | ||||
|   ETHERNET_TYPE_UNKNOWN = 0, | ||||
|   ETHERNET_TYPE_LAN8720, | ||||
|   ETHERNET_TYPE_RTL8201, | ||||
| @@ -42,7 +42,7 @@ struct PHYRegister { | ||||
|   uint32_t page; | ||||
| }; | ||||
|  | ||||
| enum class EthernetComponentState { | ||||
| enum class EthernetComponentState : uint8_t { | ||||
|   STOPPED, | ||||
|   CONNECTING, | ||||
|   CONNECTED, | ||||
| @@ -119,25 +119,31 @@ class EthernetComponent : public Component { | ||||
|   uint32_t polling_interval_{0}; | ||||
| #endif | ||||
| #else | ||||
|   uint8_t phy_addr_{0}; | ||||
|   // Group all 32-bit members first | ||||
|   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_gpio_t clk_gpio_{EMAC_CLK_IN_GPIO}; | ||||
|   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 connected_{false}; | ||||
|   bool got_ipv4_address_{false}; | ||||
| #if LWIP_IPV6 | ||||
|   uint8_t ipv6_count_{0}; | ||||
| #endif /* LWIP_IPV6 */ | ||||
|   EthernetComponentState state_{EthernetComponentState::STOPPED}; | ||||
|   uint32_t connect_begin_; | ||||
|  | ||||
|   // Pointers at the end (naturally aligned) | ||||
|   esp_netif_t *eth_netif_{nullptr}; | ||||
|   esp_eth_handle_t eth_handle_; | ||||
|   esp_eth_phy_t *phy_{nullptr}; | ||||
|   | ||||
| @@ -41,6 +41,6 @@ CONFIG_SCHEMA = cv.All( | ||||
| async def to_code(config): | ||||
|     cg.add_build_flag("-DUSE_HOST") | ||||
|     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_platformio_option("platform", "platformio/native") | ||||
|   | ||||
| @@ -258,7 +258,7 @@ bool OtaHttpRequestComponent::http_get_md5_() { | ||||
| } | ||||
|  | ||||
| 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://'"); | ||||
|     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_() { | ||||
|   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!"); | ||||
|     this->em_sat_ = true; | ||||
|   } | ||||
|   if (this->buffer_starts_with_("PwrDays")) { | ||||
|   if (buffer_.starts_with("PwrDays")) { | ||||
|     if (this->boot_count_ <= 0) { | ||||
|       this->boot_count_ = 1; | ||||
|     } else { | ||||
| @@ -220,7 +214,7 @@ void HydreonRGxxComponent::process_line_() { | ||||
|     } | ||||
|     return; | ||||
|   } | ||||
|   if (this->buffer_starts_with_("SW")) { | ||||
|   if (buffer_.starts_with("SW")) { | ||||
|     std::string::size_type majend = this->buffer_.find('.'); | ||||
|     std::string::size_type endversion = this->buffer_.find(' ', 3); | ||||
|     if (majend == std::string::npos || endversion == std::string::npos || majend > endversion) { | ||||
| @@ -282,7 +276,7 @@ void HydreonRGxxComponent::process_line_() { | ||||
|     } | ||||
|   } else { | ||||
|     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()); | ||||
|         return; | ||||
|       } | ||||
|   | ||||
| @@ -1,5 +1,8 @@ | ||||
| import logging | ||||
|  | ||||
| from esphome import pins | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import esp32 | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_ADDRESS, | ||||
| @@ -12,6 +15,8 @@ from esphome.const import ( | ||||
|     CONF_SCL, | ||||
|     CONF_SDA, | ||||
|     CONF_TIMEOUT, | ||||
|     KEY_CORE, | ||||
|     KEY_FRAMEWORK_VERSION, | ||||
|     PLATFORM_ESP32, | ||||
|     PLATFORM_ESP8266, | ||||
|     PLATFORM_RP2040, | ||||
| @@ -19,6 +24,7 @@ from esphome.const import ( | ||||
| from esphome.core import CORE, coroutine_with_priority | ||||
| import esphome.final_validate as fv | ||||
|  | ||||
| LOGGER = logging.getLogger(__name__) | ||||
| CODEOWNERS = ["@esphome/core"] | ||||
| i2c_ns = cg.esphome_ns.namespace("i2c") | ||||
| I2CBus = i2c_ns.class_("I2CBus") | ||||
| @@ -41,6 +47,32 @@ def _bus_declare_type(value): | ||||
|     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( | ||||
|     {CONF_OUTPUT: True, CONF_INPUT: True} | ||||
| ) | ||||
| @@ -66,6 +98,7 @@ CONFIG_SCHEMA = cv.All( | ||||
|         } | ||||
|     ).extend(cv.COMPONENT_SCHEMA), | ||||
|     cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040]), | ||||
|     validate_config, | ||||
| ) | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -10,6 +10,7 @@ | ||||
|  | ||||
| #include "esphome/core/application.h" | ||||
|  | ||||
| #define CHECK_BIT(var, pos) (((var) >> (pos)) & 1) | ||||
| #define highbyte(val) (uint8_t)((val) >> 8) | ||||
| #define lowbyte(val) (uint8_t)((val) &0xff) | ||||
|  | ||||
| @@ -17,8 +18,162 @@ namespace esphome { | ||||
| namespace 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() { | ||||
|   ESP_LOGCONFIG(TAG, "LD2410:"); | ||||
| @@ -78,7 +233,7 @@ void LD2410Component::dump_config() { | ||||
|                 "  Throttle: %ums\n" | ||||
|                 "  MAC address: %s\n" | ||||
|                 "  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() { | ||||
| @@ -200,7 +355,7 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) { | ||||
|   */ | ||||
| #ifdef USE_SENSOR | ||||
|   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) | ||||
|       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); | ||||
|   } | ||||
|   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) | ||||
|       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); | ||||
|   } | ||||
|   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) | ||||
|       this->detection_distance_sensor_->publish_state(new_detect_distance); | ||||
|   } | ||||
| @@ -282,25 +437,6 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) { | ||||
| #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 | ||||
| std::function<void(void)> set_number_value(number::Number *n, float value) { | ||||
|   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"); | ||||
|     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]); | ||||
|     return true; | ||||
|   } | ||||
| @@ -347,8 +483,8 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { | ||||
| #endif | ||||
|       break; | ||||
|     case lowbyte(CMD_VERSION): | ||||
|       this->version_ = format_version(buffer); | ||||
|       ESP_LOGV(TAG, "Firmware version: %s", const_cast<char *>(this->version_.c_str())); | ||||
|       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()); | ||||
| #ifdef USE_TEXT_SENSOR | ||||
|       if (this->version_text_sensor_ != nullptr) { | ||||
|         this->version_text_sensor_->publish_state(this->version_); | ||||
| @@ -357,8 +493,8 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { | ||||
|       break; | ||||
|     case lowbyte(CMD_QUERY_DISTANCE_RESOLUTION): { | ||||
|       std::string distance_resolution = | ||||
|           DISTANCE_RESOLUTION_INT_TO_ENUM.at(this->two_byte_to_int_(buffer[10], buffer[11])); | ||||
|       ESP_LOGV(TAG, "Distance resolution: %s", const_cast<char *>(distance_resolution.c_str())); | ||||
|           find_str(DISTANCE_RESOLUTIONS_BY_UINT, ld2410::two_byte_to_int(buffer[10], buffer[11])); | ||||
|       ESP_LOGV(TAG, "Distance resolution: %s", distance_resolution.c_str()); | ||||
| #ifdef USE_SELECT | ||||
|       if (this->distance_resolution_select_ != nullptr && | ||||
|           this->distance_resolution_select_->state != distance_resolution) { | ||||
| @@ -367,9 +503,9 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { | ||||
| #endif | ||||
|     } break; | ||||
|     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->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 threshold: %f", this->light_threshold_); | ||||
|       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 | ||||
| #ifdef USE_SWITCH | ||||
|       if (this->bluetooth_switch_ != nullptr) { | ||||
|         this->bluetooth_switch_->publish_state(this->mac_ != UNKNOWN_MAC); | ||||
|         this->bluetooth_switch_->publish_state(this->mac_ != NO_MAC); | ||||
|       } | ||||
| #endif | ||||
|       break; | ||||
| @@ -448,7 +584,7 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { | ||||
|       /* | ||||
|         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) { | ||||
|         update(); | ||||
|       } | ||||
| @@ -505,14 +641,14 @@ void LD2410Component::set_bluetooth(bool enable) { | ||||
|  | ||||
| void LD2410Component::set_distance_resolution(const std::string &state) { | ||||
|   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->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); | ||||
| } | ||||
|  | ||||
| void LD2410Component::set_baud_rate(const std::string &state) { | ||||
|   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->set_timeout(200, [this]() { this->restart_(); }); | ||||
| } | ||||
| @@ -646,9 +782,9 @@ void LD2410Component::set_light_out_control() { | ||||
|     return; | ||||
|   } | ||||
|   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 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}; | ||||
|   this->send_command_(CMD_SET_LIGHT_CONTROL, value, 4); | ||||
|   delay(50);  // NOLINT | ||||
|   | ||||
| @@ -26,114 +26,9 @@ | ||||
| #include "esphome/core/automation.h" | ||||
| #include "esphome/core/helpers.h" | ||||
|  | ||||
| #include <map> | ||||
|  | ||||
| namespace esphome { | ||||
| 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 { | ||||
| #ifdef USE_SENSOR | ||||
|   SUB_SENSOR(moving_target_distance) | ||||
| @@ -176,7 +71,6 @@ class LD2410Component : public Component, public uart::UARTDevice { | ||||
| #endif | ||||
|  | ||||
|  public: | ||||
|   LD2410Component(); | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
|   void loop() override; | ||||
| @@ -202,7 +96,6 @@ class LD2410Component : public Component, public uart::UARTDevice { | ||||
|   void factory_reset(); | ||||
|  | ||||
|  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 set_config_mode_(bool enable); | ||||
|   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 restart_(); | ||||
|  | ||||
|   int32_t last_periodic_millis_ = millis(); | ||||
|   int32_t last_engineering_mode_change_millis_ = millis(); | ||||
|   int32_t last_periodic_millis_ = 0; | ||||
|   int32_t last_engineering_mode_change_millis_ = 0; | ||||
|   uint16_t throttle_; | ||||
|   float light_threshold_ = -1; | ||||
|   std::string version_; | ||||
|   std::string mac_; | ||||
|   std::string out_pin_level_; | ||||
|   std::string light_function_; | ||||
|   float light_threshold_ = -1; | ||||
| #ifdef USE_NUMBER | ||||
|   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); | ||||
|   | ||||
| @@ -17,23 +17,109 @@ namespace esphome { | ||||
| namespace ld2450 { | ||||
|  | ||||
| static const char *const TAG = "ld2450"; | ||||
| static const char *const NO_MAC("08:05:04:03:02:01"); | ||||
| static const char *const UNKNOWN_MAC("unknown"); | ||||
| 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"; | ||||
|  | ||||
| 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 | ||||
| static const uint8_t CMD_ENABLE_CONF = 0x00FF; | ||||
| static const uint8_t CMD_DISABLE_CONF = 0x00FE; | ||||
| static const uint8_t CMD_VERSION = 0x00A0; | ||||
| 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; | ||||
| static const uint8_t CMD_SINGLE_TARGET_MODE = 0x0080; | ||||
| static const uint8_t CMD_MULTI_TARGET_MODE = 0x0090; | ||||
| static const uint8_t CMD_QUERY_TARGET_MODE = 0x0091; | ||||
| static const uint8_t CMD_SET_BAUD_RATE = 0x00A1; | ||||
| static const uint8_t CMD_QUERY_ZONE = 0x00C1; | ||||
| static const uint8_t CMD_SET_ZONE = 0x00C2; | ||||
| static const uint8_t CMD_ENABLE_CONF = 0xFF; | ||||
| static const uint8_t CMD_DISABLE_CONF = 0xFE; | ||||
| static const uint8_t CMD_VERSION = 0xA0; | ||||
| 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; | ||||
| static const uint8_t CMD_SINGLE_TARGET_MODE = 0x80; | ||||
| static const uint8_t CMD_MULTI_TARGET_MODE = 0x90; | ||||
| static const uint8_t CMD_QUERY_TARGET_MODE = 0x91; | ||||
| static const uint8_t CMD_SET_BAUD_RATE = 0xA1; | ||||
| static const uint8_t CMD_QUERY_ZONE = 0xC1; | ||||
| static const uint8_t CMD_SET_ZONE = 0xC2; | ||||
|  | ||||
| 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; | ||||
| } | ||||
|  | ||||
| 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() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
| #ifdef USE_NUMBER | ||||
| @@ -189,7 +268,7 @@ void LD2450Component::dump_config() { | ||||
|                 "  Throttle: %ums\n" | ||||
|                 "  MAC Address: %s\n" | ||||
|                 "  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() { | ||||
| @@ -596,7 +675,7 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { | ||||
| #endif | ||||
|       break; | ||||
|     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()); | ||||
| #ifdef USE_TEXT_SENSOR | ||||
|       if (this->version_text_sensor_ != nullptr) { | ||||
| @@ -617,7 +696,7 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) { | ||||
| #endif | ||||
| #ifdef USE_SWITCH | ||||
|       if (this->bluetooth_switch_ != nullptr) { | ||||
|         this->bluetooth_switch_->publish_state(this->mac_ != UNKNOWN_MAC); | ||||
|         this->bluetooth_switch_->publish_state(this->mac_ != NO_MAC); | ||||
|       } | ||||
| #endif | ||||
|       break; | ||||
| @@ -726,7 +805,7 @@ void LD2450Component::set_bluetooth(bool enable) { | ||||
| // Set Baud rate | ||||
| void LD2450Component::set_baud_rate(const std::string &state) { | ||||
|   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->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 | ||||
| void LD2450Component::set_zone_type(const std::string &state) { | ||||
|   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->send_set_zone_command_(); | ||||
| } | ||||
| @@ -742,7 +821,7 @@ void LD2450Component::set_zone_type(const std::string &state) { | ||||
| // Publish Zone Type to Select component | ||||
| void LD2450Component::publish_zone_type() { | ||||
| #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) { | ||||
|     this->zone_type_select_->publish_state(zone_type); | ||||
|   } | ||||
|   | ||||
| @@ -1,7 +1,5 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <iomanip> | ||||
| #include <map> | ||||
| #include "esphome/components/uart/uart.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/defines.h" | ||||
| @@ -66,49 +64,6 @@ struct ZoneOfNumbers { | ||||
| }; | ||||
| #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 { | ||||
| #ifdef USE_SENSOR | ||||
|   SUB_SENSOR(target_count) | ||||
| @@ -141,7 +96,6 @@ class LD2450Component : public Component, public uart::UARTDevice { | ||||
| #endif | ||||
|  | ||||
|  public: | ||||
|   LD2450Component(); | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
|   void loop() override; | ||||
| @@ -197,17 +151,17 @@ class LD2450Component : public Component, public uart::UARTDevice { | ||||
|   bool get_timeout_status_(uint32_t check_millis); | ||||
|   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 presence_millis_ = 0; | ||||
|   uint32_t still_presence_millis_ = 0; | ||||
|   uint32_t moving_presence_millis_ = 0; | ||||
|   uint16_t throttle_ = 0; | ||||
|   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; | ||||
|   Target target_info_[MAX_TARGETS]; | ||||
|   Zone zone_config_[MAX_ZONES]; | ||||
|   std::string version_{}; | ||||
|   std::string mac_{}; | ||||
| #ifdef USE_NUMBER | ||||
|   | ||||
| @@ -264,7 +264,7 @@ async def component_to_code(config): | ||||
|     # force using arduino framework | ||||
|     cg.add_platformio_option("framework", "arduino") | ||||
|     cg.add_build_flag("-DUSE_ARDUINO") | ||||
|     cg.set_cpp_standard("gnu++17") | ||||
|     cg.set_cpp_standard("gnu++20") | ||||
|  | ||||
|     # disable library compatibility checks | ||||
|     cg.add_platformio_option("lib_ldf_mode", "off") | ||||
|   | ||||
| @@ -10,9 +10,11 @@ namespace libretiny { | ||||
| static const char *const TAG = "lt.component"; | ||||
|  | ||||
| void LTComponent::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "LibreTiny:"); | ||||
|   ESP_LOGCONFIG(TAG, "  Version: %s", LT_BANNER_STR + 10); | ||||
|   ESP_LOGCONFIG(TAG, "  Loglevel: %u", LT_LOGLEVEL); | ||||
|   ESP_LOGCONFIG(TAG, | ||||
|                 "LibreTiny:\n" | ||||
|                 "  Version: %s\n" | ||||
|                 "  Loglevel: %u", | ||||
|                 LT_BANNER_STR + 10, LT_LOGLEVEL); | ||||
|  | ||||
| #ifdef USE_TEXT_SENSOR | ||||
|   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 | ||||
|   message_sent = | ||||
|       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 | ||||
|  | ||||
|   // 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 | ||||
| void Logger::init_log_buffer(size_t 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 | ||||
|  | ||||
| @@ -189,6 +198,10 @@ void Logger::loop() { | ||||
|         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 | ||||
| } | ||||
|   | ||||
| @@ -358,6 +358,26 @@ class Logger : public Component { | ||||
|     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); | ||||
|   } | ||||
|  | ||||
| #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) | ||||
|  | ||||
|   | ||||
| @@ -466,7 +466,7 @@ LVGL_SCHEMA = cv.All( | ||||
|                 ): lvalid.lv_color, | ||||
|                 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() | ||||
|                     } | ||||
|                 ), | ||||
|   | ||||
| @@ -21,7 +21,7 @@ from esphome.core.config import StartupTrigger | ||||
| from esphome.schema_extractors import SCHEMA_EXTRACT | ||||
|  | ||||
| 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 .lv_validation import lv_color, lv_font, lv_gradient, lv_image, opacity | ||||
| 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_VALIDATORS = {TYPE_GRID: _validate_grid_layout} | ||||
|  | ||||
| ALIGN_TO_SCHEMA = { | ||||
|     cv.Optional(df.CONF_ALIGN_TO): cv.Schema( | ||||
| @@ -402,8 +455,8 @@ LAYOUT_SCHEMA = { | ||||
| } | ||||
|  | ||||
| GRID_CELL_SCHEMA = { | ||||
|     cv.Required(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_ROW_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_COLUMN_SPAN, default=1): cv.positive_int, | ||||
|     cv.Optional(df.CONF_GRID_CELL_X_ALIGN): grid_alignments, | ||||
| @@ -474,7 +527,10 @@ def container_validator(schema, widget_type: WidgetType): | ||||
|         result = result.extend( | ||||
|             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 | ||||
|  | ||||
|   | ||||
| @@ -6,7 +6,11 @@ namespace mcp23xxx_base { | ||||
|  | ||||
| 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); } | ||||
| 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_); } | ||||
|   | ||||
| @@ -88,12 +88,7 @@ async def to_code(config): | ||||
|     if CORE.using_esp_idf and CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] >= cv.Version( | ||||
|         5, 0, 0 | ||||
|     ): | ||||
|         add_idf_component( | ||||
|             name="mdns", | ||||
|             repo="https://github.com/espressif/esp-protocols.git", | ||||
|             ref="mdns-v1.8.2", | ||||
|             path="components/mdns", | ||||
|         ) | ||||
|         add_idf_component(name="espressif/mdns", ref="1.8.2") | ||||
|  | ||||
|     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_OTA_STATE_CALLBACK") | ||||
|  | ||||
|     esp32.add_idf_component( | ||||
|         name="esp-tflite-micro", | ||||
|         repo="https://github.com/espressif/esp-tflite-micro", | ||||
|         ref="v1.3.3.1", | ||||
|     ) | ||||
|     esp32.add_idf_component(name="espressif/esp-tflite-micro", ref="1.3.3~1") | ||||
|  | ||||
|     cg.add_build_flag("-DTF_LITE_STATIC_MEMORY") | ||||
|     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) { | ||||
|   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://'"); | ||||
|     return false; | ||||
|   } | ||||
|   | ||||
| @@ -584,7 +584,7 @@ void PN7150::nci_fsm_transition_() { | ||||
|       } else { | ||||
|         this->nci_fsm_set_state_(NCIState::NFCC_INIT); | ||||
|       } | ||||
|       // fall through | ||||
|       [[fallthrough]]; | ||||
|  | ||||
|     case NCIState::NFCC_INIT: | ||||
|       if (this->init_core_() != nfc::STATUS_OK) { | ||||
| @@ -594,7 +594,7 @@ void PN7150::nci_fsm_transition_() { | ||||
|       } else { | ||||
|         this->nci_fsm_set_state_(NCIState::NFCC_CONFIG); | ||||
|       } | ||||
|       // fall through | ||||
|       [[fallthrough]]; | ||||
|  | ||||
|     case NCIState::NFCC_CONFIG: | ||||
|       if (this->send_init_config_() != nfc::STATUS_OK) { | ||||
| @@ -605,7 +605,7 @@ void PN7150::nci_fsm_transition_() { | ||||
|         this->config_refresh_pending_ = false; | ||||
|         this->nci_fsm_set_state_(NCIState::NFCC_SET_DISCOVER_MAP); | ||||
|       } | ||||
|       // fall through | ||||
|       [[fallthrough]]; | ||||
|  | ||||
|     case NCIState::NFCC_SET_DISCOVER_MAP: | ||||
|       if (this->set_discover_map_() != nfc::STATUS_OK) { | ||||
| @@ -615,7 +615,7 @@ void PN7150::nci_fsm_transition_() { | ||||
|       } else { | ||||
|         this->nci_fsm_set_state_(NCIState::NFCC_SET_LISTEN_MODE_ROUTING); | ||||
|       } | ||||
|       // fall through | ||||
|       [[fallthrough]]; | ||||
|  | ||||
|     case NCIState::NFCC_SET_LISTEN_MODE_ROUTING: | ||||
|       if (this->set_listen_mode_routing_() != nfc::STATUS_OK) { | ||||
| @@ -625,7 +625,7 @@ void PN7150::nci_fsm_transition_() { | ||||
|       } else { | ||||
|         this->nci_fsm_set_state_(NCIState::RFST_IDLE); | ||||
|       } | ||||
|       // fall through | ||||
|       [[fallthrough]]; | ||||
|  | ||||
|     case NCIState::RFST_IDLE: | ||||
|       if (this->nci_state_error_ == NCIState::RFST_DISCOVERY) { | ||||
| @@ -650,14 +650,14 @@ void PN7150::nci_fsm_transition_() { | ||||
|  | ||||
|     case NCIState::RFST_W4_HOST_SELECT: | ||||
|       select_endpoint_(); | ||||
|       // fall through | ||||
|       [[fallthrough]]; | ||||
|  | ||||
|     // All cases below are waiting for NOTIFICATION messages | ||||
|     case NCIState::RFST_DISCOVERY: | ||||
|       if (this->config_refresh_pending_) { | ||||
|         this->refresh_core_config_(); | ||||
|       } | ||||
|       // fall through | ||||
|       [[fallthrough]]; | ||||
|  | ||||
|     case NCIState::RFST_LISTEN_ACTIVE: | ||||
|     case NCIState::RFST_LISTEN_SLEEP: | ||||
|   | ||||
| @@ -609,7 +609,7 @@ void PN7160::nci_fsm_transition_() { | ||||
|       } else { | ||||
|         this->nci_fsm_set_state_(NCIState::NFCC_INIT); | ||||
|       } | ||||
|       // fall through | ||||
|       [[fallthrough]]; | ||||
|  | ||||
|     case NCIState::NFCC_INIT: | ||||
|       if (this->init_core_() != nfc::STATUS_OK) { | ||||
| @@ -619,7 +619,7 @@ void PN7160::nci_fsm_transition_() { | ||||
|       } else { | ||||
|         this->nci_fsm_set_state_(NCIState::NFCC_CONFIG); | ||||
|       } | ||||
|       // fall through | ||||
|       [[fallthrough]]; | ||||
|  | ||||
|     case NCIState::NFCC_CONFIG: | ||||
|       if (this->send_init_config_() != nfc::STATUS_OK) { | ||||
| @@ -630,7 +630,7 @@ void PN7160::nci_fsm_transition_() { | ||||
|         this->config_refresh_pending_ = false; | ||||
|         this->nci_fsm_set_state_(NCIState::NFCC_SET_DISCOVER_MAP); | ||||
|       } | ||||
|       // fall through | ||||
|       [[fallthrough]]; | ||||
|  | ||||
|     case NCIState::NFCC_SET_DISCOVER_MAP: | ||||
|       if (this->set_discover_map_() != nfc::STATUS_OK) { | ||||
| @@ -640,7 +640,7 @@ void PN7160::nci_fsm_transition_() { | ||||
|       } else { | ||||
|         this->nci_fsm_set_state_(NCIState::NFCC_SET_LISTEN_MODE_ROUTING); | ||||
|       } | ||||
|       // fall through | ||||
|       [[fallthrough]]; | ||||
|  | ||||
|     case NCIState::NFCC_SET_LISTEN_MODE_ROUTING: | ||||
|       if (this->set_listen_mode_routing_() != nfc::STATUS_OK) { | ||||
| @@ -650,7 +650,7 @@ void PN7160::nci_fsm_transition_() { | ||||
|       } else { | ||||
|         this->nci_fsm_set_state_(NCIState::RFST_IDLE); | ||||
|       } | ||||
|       // fall through | ||||
|       [[fallthrough]]; | ||||
|  | ||||
|     case NCIState::RFST_IDLE: | ||||
|       if (this->nci_state_error_ == NCIState::RFST_DISCOVERY) { | ||||
| @@ -675,14 +675,14 @@ void PN7160::nci_fsm_transition_() { | ||||
|  | ||||
|     case NCIState::RFST_W4_HOST_SELECT: | ||||
|       select_endpoint_(); | ||||
|       // fall through | ||||
|       [[fallthrough]]; | ||||
|  | ||||
|     // All cases below are waiting for NOTIFICATION messages | ||||
|     case NCIState::RFST_DISCOVERY: | ||||
|       if (this->config_refresh_pending_) { | ||||
|         this->refresh_core_config_(); | ||||
|       } | ||||
|       // fall through | ||||
|       [[fallthrough]]; | ||||
|  | ||||
|     case NCIState::RFST_LISTEN_ACTIVE: | ||||
|     case NCIState::RFST_LISTEN_SLEEP: | ||||
|   | ||||
| @@ -63,7 +63,7 @@ void PulseMeterSensor::loop() { | ||||
|   // If an edge was peeked, repay the debt | ||||
|   if (this->peeked_edge_ && this->get_->count_ > 0) { | ||||
|     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 | ||||
| @@ -71,7 +71,7 @@ void PulseMeterSensor::loop() { | ||||
|       now - this->get_->last_rising_edge_us_ >= this->filter_us_) { | ||||
|     this->peeked_edge_ = true; | ||||
|     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 | ||||
| @@ -146,7 +146,7 @@ void IRAM_ATTR PulseMeterSensor::edge_intr(PulseMeterSensor *sensor) { | ||||
|     state.last_sent_edge_us_ = now; | ||||
|     set.last_detected_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 | ||||
| @@ -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 | ||||
|     state.latched_ = true; | ||||
|     set.last_detected_edge_us_ = state.last_intr_; | ||||
|     set.count_++; | ||||
|     set.count_++;  // NOLINT(clang-diagnostic-deprecated-volatile) | ||||
|   } | ||||
|  | ||||
|   // 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 | ||||
|     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 | ||||
|       ESP_LOGD(TAG, "Found Radon Eye device Name: %s (MAC: %s)", device.get_name().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) | ||||
|     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() { | ||||
|   | ||||
| @@ -167,7 +167,7 @@ async def to_code(config): | ||||
|     cg.add_platformio_option("lib_ldf_mode", "chain+") | ||||
|     cg.add_platformio_option("board", config[CONF_BOARD]) | ||||
|     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_VARIANT", "RP2040") | ||||
|  | ||||
|   | ||||
| @@ -33,12 +33,15 @@ class SafeModeComponent : public Component { | ||||
|   void write_rtc_(uint32_t val); | ||||
|   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_enable_time_{60000};         ///< The time safe mode should remain active for | ||||
|   uint32_t safe_mode_rtc_value_{0}; | ||||
|   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}; | ||||
|   // Larger objects at the end | ||||
|   ESPPreferenceObject rtc_; | ||||
|   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) {} | ||||
|  | ||||
| int8_t Sensor::get_accuracy_decimals() { | ||||
|   if (this->accuracy_decimals_.has_value()) | ||||
|     return *this->accuracy_decimals_; | ||||
|   if (this->sensor_flags_.has_accuracy_override) | ||||
|     return this->accuracy_decimals_; | ||||
|   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() { | ||||
|   if (this->state_class_.has_value()) | ||||
|     return *this->state_class_; | ||||
|   if (this->sensor_flags_.has_state_class_override) | ||||
|     return this->state_class_; | ||||
|   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 | ||||
|    * 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. | ||||
|   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. | ||||
|   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. | ||||
|  | ||||
|   optional<int8_t> accuracy_decimals_;                  ///< Accuracy in decimals override | ||||
|   optional<StateClass> state_class_{STATE_CLASS_NONE};  ///< State class override | ||||
|   bool force_update_{false};                            ///< Force update mode | ||||
|   // Group small members together to avoid padding | ||||
|   int8_t accuracy_decimals_{-1};              ///< Accuracy in decimals (-1 = not set) | ||||
|   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 | ||||
|   | ||||
| @@ -445,8 +445,7 @@ template<typename T> stm32_err_t stm32_check_ack_timeout(const stm32_err_t s_err | ||||
|       return STM32_ERR_OK; | ||||
|     case STM32_ERR_NACK: | ||||
|       log(); | ||||
|       // TODO: c++17 [[fallthrough]] | ||||
|       /* fallthrough */ | ||||
|       [[fallthrough]]; | ||||
|     default: | ||||
|       return STM32_ERR_UNKNOWN; | ||||
|   } | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| #include "sn74hc595.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include <ranges> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace sn74hc595 { | ||||
| @@ -55,9 +56,9 @@ void SN74HC595Component::digital_write_(uint16_t pin, bool value) { | ||||
| } | ||||
|  | ||||
| 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--) { | ||||
|       bool bit = (*byte >> i) & 1; | ||||
|       bool bit = (output_byte >> i) & 1; | ||||
|       this->data_pin_->digital_write(bit); | ||||
|       this->clock_pin_->digital_write(true); | ||||
|       this->clock_pin_->digital_write(false); | ||||
| @@ -68,9 +69,9 @@ void SN74HC595GPIOComponent::write_gpio() { | ||||
|  | ||||
| #ifdef USE_SPI | ||||
| 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->transfer_byte(*byte); | ||||
|     this->transfer_byte(output_byte); | ||||
|     this->disable(); | ||||
|   } | ||||
|   SN74HC595Component::write_gpio(); | ||||
|   | ||||
| @@ -343,10 +343,9 @@ void AudioPipeline::read_task(void *params) { | ||||
|     xEventGroupSetBits(this_pipeline->event_group_, EventGroupBits::READER_MESSAGE_FINISHED); | ||||
|  | ||||
|     // Wait until the pipeline notifies us the source of the media file | ||||
|     EventBits_t event_bits = | ||||
|         xEventGroupWaitBits(this_pipeline->event_group_, | ||||
|                             EventGroupBits::READER_COMMAND_INIT_FILE | EventGroupBits::READER_COMMAND_INIT_HTTP | | ||||
|                                 EventGroupBits::PIPELINE_COMMAND_STOP,  // Bit message to read | ||||
|     EventBits_t event_bits = xEventGroupWaitBits( | ||||
|         this_pipeline->event_group_, | ||||
|         EventGroupBits::READER_COMMAND_INIT_FILE | EventGroupBits::READER_COMMAND_INIT_HTTP,  // Bit message to read | ||||
|         pdFALSE,                                                                              // Clear the bit on exit | ||||
|         pdFALSE,                                                                              // Wait for all the bits, | ||||
|         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); | ||||
|  | ||||
|     // Wait until the reader notifies us that the media type is available | ||||
|     EventBits_t event_bits = xEventGroupWaitBits(this_pipeline->event_group_, | ||||
|                                                  EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE | | ||||
|                                                      EventGroupBits::PIPELINE_COMMAND_STOP,  // Bit message to read | ||||
|     EventBits_t event_bits = | ||||
|         xEventGroupWaitBits(this_pipeline->event_group_, | ||||
|                             EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE,  // Bit message to read | ||||
|                             pdFALSE,                                           // Clear the bit on exit | ||||
|                             pdFALSE,                                           // Wait for all the bits, | ||||
|                             portMAX_DELAY);                                    // Block indefinitely until bit is set | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| #include "sun.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include <numbers> | ||||
|  | ||||
| /* | ||||
| 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"; | ||||
|  | ||||
| #undef PI | ||||
| #undef degrees | ||||
| #undef radians | ||||
| #undef sq | ||||
|  | ||||
| static const num_t PI = 3.141592653589793; | ||||
| inline num_t degrees(num_t rad) { return rad * 180 / PI; } | ||||
| inline num_t radians(num_t deg) { return deg * PI / 180; } | ||||
| inline num_t degrees(num_t rad) { return rad * 180 / std::numbers::pi; } | ||||
| inline num_t radians(num_t deg) { return deg * std::numbers::pi / 180; } | ||||
| 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 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->start_time = now; | ||||
|     arg->buffer_index++; | ||||
|     arg->buffer_index++;  // NOLINT(clang-diagnostic-deprecated-volatile) | ||||
|     return; | ||||
|   } | ||||
|   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->start_time = now; | ||||
|   arg->buffer_index++; | ||||
|   arg->buffer_index++;  // NOLINT(clang-diagnostic-deprecated-volatile) | ||||
| } | ||||
| void IRAM_ATTR Tx20ComponentStore::reset() { | ||||
|   tx20_available = false; | ||||
|   | ||||
| @@ -17,10 +17,11 @@ from esphome.const import ( | ||||
| AUTO_LOAD = ["socket"] | ||||
| DEPENDENCIES = ["api", "microphone"] | ||||
|  | ||||
| CODEOWNERS = ["@jesserockz"] | ||||
| CODEOWNERS = ["@jesserockz", "@kahrendt"] | ||||
|  | ||||
| CONF_ON_END = "on_end" | ||||
| CONF_ON_INTENT_END = "on_intent_end" | ||||
| CONF_ON_INTENT_PROGRESS = "on_intent_progress" | ||||
| CONF_ON_INTENT_START = "on_intent_start" | ||||
| CONF_ON_LISTENING = "on_listening" | ||||
| CONF_ON_START = "on_start" | ||||
| @@ -136,6 +137,9 @@ CONFIG_SCHEMA = cv.All( | ||||
|             cv.Optional(CONF_ON_INTENT_START): automation.validate_automation( | ||||
|                 single=True | ||||
|             ), | ||||
|             cv.Optional(CONF_ON_INTENT_PROGRESS): automation.validate_automation( | ||||
|                 single=True | ||||
|             ), | ||||
|             cv.Optional(CONF_ON_INTENT_END): automation.validate_automation( | ||||
|                 single=True | ||||
|             ), | ||||
| @@ -282,6 +286,13 @@ async def to_code(config): | ||||
|             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: | ||||
|         await automation.build_automation( | ||||
|             var.get_intent_end_trigger(), | ||||
|   | ||||
| @@ -555,7 +555,7 @@ void VoiceAssistant::request_stop() { | ||||
|       break; | ||||
|     case State::AWAITING_RESPONSE: | ||||
|       this->signal_stop_(); | ||||
|       break; | ||||
|       // Fallthrough intended to stop a streaming TTS announcement that has potentially started | ||||
|     case State::STREAMING_RESPONSE: | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
|       // Stop any ongoing media player announcement | ||||
| @@ -599,6 +599,14 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { | ||||
|   switch (msg.event_type) { | ||||
|     case api::enums::VOICE_ASSISTANT_RUN_START: | ||||
|       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(); }); | ||||
|       break; | ||||
|     case api::enums::VOICE_ASSISTANT_WAKE_WORD_START: | ||||
| @@ -622,6 +630,8 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { | ||||
|       if (text.empty()) { | ||||
|         ESP_LOGW(TAG, "No text in STT_END event"); | ||||
|         return; | ||||
|       } else if (text.length() > 500) { | ||||
|         text = text.substr(0, 497) + "..."; | ||||
|       } | ||||
|       ESP_LOGD(TAG, "Speech recognised as: \"%s\"", text.c_str()); | ||||
|       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"); | ||||
|       this->defer([this]() { this->intent_start_trigger_->trigger(); }); | ||||
|       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: { | ||||
|       for (auto arg : msg.data) { | ||||
|         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"); | ||||
|         return; | ||||
|       } | ||||
|       if (text.length() > 500) { | ||||
|         text = text.substr(0, 497) + "..."; | ||||
|       } | ||||
|       ESP_LOGD(TAG, "Response: \"%s\"", text.c_str()); | ||||
|       this->defer([this, 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()); | ||||
|       this->defer([this, url]() { | ||||
| #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_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_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_end_trigger() const { return this->end_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_end_trigger_ = new Trigger<>(); | ||||
| #endif | ||||
|   Trigger<std::string> *intent_progress_trigger_ = new Trigger<std::string>(); | ||||
|   Trigger<> *wake_word_detected_trigger_ = new Trigger<>(); | ||||
|   Trigger<std::string> *stt_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 | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
|   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_end_{false}; | ||||
| #endif | ||||
|   | ||||
| @@ -11,7 +11,7 @@ static const char *const KEYS = "0123456789*#"; | ||||
| void IRAM_ATTR HOT WiegandStore::d0_gpio_intr(WiegandStore *arg) { | ||||
|   if (arg->d0.digital_read()) | ||||
|     return; | ||||
|   arg->count++; | ||||
|   arg->count++;  // NOLINT(clang-diagnostic-deprecated-volatile) | ||||
|   arg->value <<= 1; | ||||
|   arg->last_bit_time = millis(); | ||||
|   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) { | ||||
|   if (arg->d1.digital_read()) | ||||
|     return; | ||||
|   arg->count++; | ||||
|   arg->count++;  // NOLINT(clang-diagnostic-deprecated-volatile) | ||||
|   arg->value = (arg->value << 1) | 1; | ||||
|   arg->last_bit_time = millis(); | ||||
|   arg->done = false; | ||||
|   | ||||
| @@ -62,7 +62,7 @@ struct SavedWifiFastConnectSettings { | ||||
|   uint8_t channel; | ||||
| } PACKED;  // NOLINT | ||||
|  | ||||
| enum WiFiComponentState { | ||||
| enum WiFiComponentState : uint8_t { | ||||
|   /** Nothing has been initialized yet. Internal AP, if configured, is disabled at this point. */ | ||||
|   WIFI_COMPONENT_STATE_OFF = 0, | ||||
|   /** WiFi is disabled. */ | ||||
| @@ -146,14 +146,14 @@ class WiFiAP { | ||||
|  | ||||
|  protected: | ||||
|   std::string ssid_; | ||||
|   optional<bssid_t> bssid_; | ||||
|   std::string password_; | ||||
|   optional<bssid_t> bssid_; | ||||
| #ifdef USE_WIFI_WPA2_EAP | ||||
|   optional<EAPAuth> eap_; | ||||
| #endif  // USE_WIFI_WPA2_EAP | ||||
|   optional<uint8_t> channel_; | ||||
|   float priority_{0}; | ||||
|   optional<ManualIP> manual_ip_; | ||||
|   float priority_{0}; | ||||
|   optional<uint8_t> channel_; | ||||
|   bool hidden_{false}; | ||||
| }; | ||||
|  | ||||
| @@ -177,14 +177,14 @@ class WiFiScanResult { | ||||
|   bool operator==(const WiFiScanResult &rhs) const; | ||||
|  | ||||
|  protected: | ||||
|   bool matches_{false}; | ||||
|   bssid_t bssid_; | ||||
|   std::string ssid_; | ||||
|   float priority_{0.0f}; | ||||
|   uint8_t channel_; | ||||
|   int8_t rssi_; | ||||
|   bool matches_{false}; | ||||
|   bool with_auth_; | ||||
|   bool is_hidden_; | ||||
|   float priority_{0.0f}; | ||||
| }; | ||||
|  | ||||
| struct WiFiSTAPriority { | ||||
| @@ -192,7 +192,7 @@ struct WiFiSTAPriority { | ||||
|   float priority; | ||||
| }; | ||||
|  | ||||
| enum WiFiPowerSaveMode { | ||||
| enum WiFiPowerSaveMode : uint8_t { | ||||
|   WIFI_POWER_SAVE_NONE = 0, | ||||
|   WIFI_POWER_SAVE_LIGHT, | ||||
|   WIFI_POWER_SAVE_HIGH, | ||||
| @@ -383,28 +383,36 @@ class WiFiComponent : public Component { | ||||
|   std::string use_address_; | ||||
|   std::vector<WiFiAP> sta_; | ||||
|   std::vector<WiFiSTAPriority> sta_priorities_; | ||||
|   std::vector<WiFiScanResult> scan_result_; | ||||
|   WiFiAP selected_ap_; | ||||
|   bool fast_connect_{false}; | ||||
|   bool retry_hidden_{false}; | ||||
|  | ||||
|   bool has_ap_{false}; | ||||
|   WiFiAP ap_; | ||||
|   WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF}; | ||||
|   bool handled_connected_state_{false}; | ||||
|   optional<float> output_power_; | ||||
|   ESPPreferenceObject pref_; | ||||
|   ESPPreferenceObject fast_connect_pref_; | ||||
|  | ||||
|   // Group all 32-bit integers together | ||||
|   uint32_t action_started_; | ||||
|   uint8_t num_retried_{0}; | ||||
|   uint32_t last_connected_{0}; | ||||
|   uint32_t reboot_timeout_{}; | ||||
|   uint32_t ap_timeout_{}; | ||||
|  | ||||
|   // Group all 8-bit values together | ||||
|   WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF}; | ||||
|   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}; | ||||
|   std::vector<WiFiScanResult> scan_result_; | ||||
|   bool scan_done_{false}; | ||||
|   bool ap_setup_{false}; | ||||
|   optional<float> output_power_; | ||||
|   bool passive_scan_{false}; | ||||
|   ESPPreferenceObject pref_; | ||||
|   ESPPreferenceObject fast_connect_pref_; | ||||
|   bool has_saved_wifi_settings_{false}; | ||||
| #ifdef USE_WIFI_11KV_SUPPORT | ||||
|   bool btm_{false}; | ||||
| @@ -412,10 +420,8 @@ class WiFiComponent : public Component { | ||||
| #endif | ||||
|   bool enable_on_boot_; | ||||
|   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<> *disconnect_trigger_{new Trigger<>()}; | ||||
| }; | ||||
|   | ||||
| @@ -3,6 +3,7 @@ | ||||
| #include "esphome/core/version.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include <algorithm> | ||||
| #include <ranges> | ||||
|  | ||||
| #ifdef USE_STATUS_LED | ||||
| #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() { | ||||
|   ESP_LOGI(TAG, "Forcing a reboot"); | ||||
|   for (auto it = this->components_.rbegin(); it != this->components_.rend(); ++it) { | ||||
|     (*it)->on_shutdown(); | ||||
|   for (auto &component : std::ranges::reverse_view(this->components_)) { | ||||
|     component->on_shutdown(); | ||||
|   } | ||||
|   arch_restart(); | ||||
| } | ||||
| @@ -198,17 +199,17 @@ void Application::safe_reboot() { | ||||
| } | ||||
|  | ||||
| void Application::run_safe_shutdown_hooks() { | ||||
|   for (auto it = this->components_.rbegin(); it != this->components_.rend(); ++it) { | ||||
|     (*it)->on_safe_shutdown(); | ||||
|   for (auto &component : std::ranges::reverse_view(this->components_)) { | ||||
|     component->on_safe_shutdown(); | ||||
|   } | ||||
|   for (auto it = this->components_.rbegin(); it != this->components_.rend(); ++it) { | ||||
|     (*it)->on_shutdown(); | ||||
|   for (auto &component : std::ranges::reverse_view(this->components_)) { | ||||
|     component->on_shutdown(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void Application::run_powerdown_hooks() { | ||||
|   for (auto it = this->components_.rbegin(); it != this->components_.rend(); ++it) { | ||||
|     (*it)->on_powerdown(); | ||||
|   for (auto &component : std::ranges::reverse_view(this->components_)) { | ||||
|     component->on_powerdown(); | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <algorithm> | ||||
| #include <limits> | ||||
| #include <string> | ||||
| #include <vector> | ||||
| #include "esphome/core/component.h" | ||||
| @@ -335,11 +337,16 @@ class Application { | ||||
|    * Each component can request a high frequency loop execution by using the HighFrequencyLoopRequester | ||||
|    * 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. | ||||
|    */ | ||||
|   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; } | ||||
|  | ||||
| @@ -618,6 +625,17 @@ class Application { | ||||
|   /// Perform a delay while also monitoring socket file descriptors for readiness | ||||
|   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_{}; | ||||
|  | ||||
|   // Partitioned vector design for looping components | ||||
| @@ -637,11 +655,6 @@ class Application { | ||||
|   //   and active_end_ is incremented | ||||
|   // - This eliminates branch mispredictions from flag checking in the hot loop | ||||
|   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 | ||||
|   std::vector<Device *> devices_{}; | ||||
| @@ -713,24 +726,37 @@ class Application { | ||||
|   std::vector<update::UpdateEntity *> updates_{}; | ||||
| #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 friendly_name_; | ||||
|   const char *comment_{nullptr}; | ||||
|   const char *compilation_time_{nullptr}; | ||||
|   bool name_add_mac_suffix_; | ||||
|  | ||||
|   // 4-byte members | ||||
|   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}; | ||||
|  | ||||
| #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() | ||||
| #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 read_fds_{};       // Working fd_set for select(), copied from base_read_fds_ | ||||
| #endif | ||||
|   | ||||
| @@ -27,20 +27,67 @@ template<typename T, typename... X> class TemplatableValue { | ||||
|  public: | ||||
|   TemplatableValue() : type_(NONE) {} | ||||
|  | ||||
|   template<typename F, enable_if_t<!is_invocable<F, X...>::value, int> = 0> | ||||
|   TemplatableValue(F value) : type_(VALUE), value_(std::move(value)) {} | ||||
|   template<typename F, enable_if_t<!is_invocable<F, X...>::value, int> = 0> TemplatableValue(F value) : type_(VALUE) { | ||||
|     new (&this->value_) T(std::move(value)); | ||||
|   } | ||||
|  | ||||
|   template<typename F, enable_if_t<is_invocable<F, X...>::value, int> = 0> | ||||
|   TemplatableValue(F f) : type_(LAMBDA), f_(f) {} | ||||
|   template<typename F, enable_if_t<is_invocable<F, X...>::value, int> = 0> TemplatableValue(F f) : type_(LAMBDA) { | ||||
|     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; } | ||||
|  | ||||
|   T value(X... x) { | ||||
|     if (this->type_ == LAMBDA) { | ||||
|       return this->f_(x...); | ||||
|       return (*this->f_)(x...); | ||||
|     } | ||||
|     // return value also when none | ||||
|     return this->value_; | ||||
|     return this->type_ == VALUE ? this->value_ : T{}; | ||||
|   } | ||||
|  | ||||
|   optional<T> optional_value(X... x) { | ||||
| @@ -58,14 +105,16 @@ template<typename T, typename... X> class TemplatableValue { | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
|   enum { | ||||
|   enum : uint8_t { | ||||
|     NONE, | ||||
|     VALUE, | ||||
|     LAMBDA, | ||||
|   } type_; | ||||
|  | ||||
|   T value_{}; | ||||
|   std::function<T(X...)> f_{}; | ||||
|   union { | ||||
|     T value_; | ||||
|     std::function<T(X...)> *f_; | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| /** Base class for all automation conditions. | ||||
|   | ||||
| @@ -375,7 +375,7 @@ void ComponentIterator::advance() { | ||||
|   } | ||||
|  | ||||
|   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; | ||||
|   } else if (success) { | ||||
|     this->at_++; | ||||
|   | ||||
| @@ -93,7 +93,9 @@ class ComponentIterator { | ||||
|   virtual bool on_end(); | ||||
|  | ||||
|  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, | ||||
|     BEGIN, | ||||
| #ifdef USE_BINARY_SENSOR | ||||
| @@ -167,7 +169,7 @@ class ComponentIterator { | ||||
| #endif | ||||
|     MAX, | ||||
|   } state_{IteratorState::NONE}; | ||||
|   size_t at_{0}; | ||||
|   uint16_t at_{0};  // Supports up to 65,535 entities per type | ||||
|   bool include_internal_{false}; | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -132,6 +132,8 @@ | ||||
|  | ||||
| // ESP32-specific feature flags | ||||
| #ifdef USE_ESP32 | ||||
| #define USE_ESPHOME_TASK_LOG_BUFFER | ||||
|  | ||||
| #define USE_BLUETOOTH_PROXY | ||||
| #define USE_CAPTIVE_PORTAL | ||||
| #define USE_ESP32_BLE | ||||
|   | ||||
| @@ -617,7 +617,7 @@ def set_cpp_standard(standard: str) -> None: | ||||
|     """Set C++ standard with compiler flag `-std={standard}`.""" | ||||
|     CORE.add_build_unflag("-std=gnu++11") | ||||
|     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++2a") | ||||
|     CORE.add_build_unflag("-std=gnu++2b") | ||||
|   | ||||
| @@ -1,13 +1,19 @@ | ||||
| dependencies: | ||||
|   esp-tflite-micro: | ||||
|     git: https://github.com/espressif/esp-tflite-micro.git | ||||
|     version: v1.3.1 | ||||
|   esp32_camera: | ||||
|     git: https://github.com/espressif/esp32-camera.git | ||||
|     version: v2.0.15 | ||||
|   mdns: | ||||
|     git: https://github.com/espressif/esp-protocols.git | ||||
|     version: mdns-v1.8.2 | ||||
|     path: components/mdns | ||||
|   espressif/esp-tflite-micro: | ||||
|     version: 1.3.3~1 | ||||
|   espressif/esp32-camera: | ||||
|     version: 2.0.15 | ||||
|   espressif/mdns: | ||||
|     version: 1.8.2 | ||||
|   espressif/esp_wifi_remote: | ||||
|     version: 0.10.2 | ||||
|     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 | ||||
| build_flags = | ||||
|     -DESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_VERY_VERBOSE | ||||
|     -std=gnu++17 | ||||
|     -std=gnu++20 | ||||
| build_unflags = | ||||
|     -std=gnu++11 | ||||
|     -std=gnu++14 | ||||
|     -std=gnu++20 | ||||
|     -std=gnu++17 | ||||
|     -std=gnu++23 | ||||
|     -std=gnu++2a | ||||
|     -std=gnu++2b | ||||
| @@ -560,7 +560,7 @@ lib_deps = | ||||
| build_flags = | ||||
|     ${common.build_flags} | ||||
|     -DUSE_HOST | ||||
|     -std=c++17 | ||||
|     -std=c++20 | ||||
| build_unflags = | ||||
|     ${common.build_unflags} | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| pylint==3.3.7 | ||||
| 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 | ||||
| pre-commit | ||||
|  | ||||
|   | ||||
| @@ -886,7 +886,7 @@ def build_message_type( | ||||
|         public_content.append("#ifdef HAS_PROTO_MESSAGE_DUMP") | ||||
|         snake_name = camel_to_snake(desc.name) | ||||
|         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") | ||||
|  | ||||
| @@ -1356,7 +1356,7 @@ def main() -> None: | ||||
|     hpp += "  template<typename T>\n" | ||||
|     hpp += "  bool send_message(const T &msg) {\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 += "    return this->send_message_(msg, T::MESSAGE_TYPE);\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 | ||||
|                       grid_cell_x_align: center | ||||
|                       grid_cell_y_align: stretch | ||||
|                       grid_cell_row_pos: 0 | ||||
|                       grid_cell_column_pos: 1 | ||||
|                       grid_cell_column_span: 1 | ||||
|                       grid_cell_column_span: 2 | ||||
|                       text: "Grid cell 0/1" | ||||
|                   - label: | ||||
|                       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 | ||||
|     id: test_sensor | ||||
|     unit_of_measurement: °C | ||||
|     accuracy_decimals: 2 | ||||
|     state_class: measurement | ||||
|     force_update: true | ||||
|     lambda: return 42.0; | ||||
|     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 aioesphomeapi | ||||
| from aioesphomeapi import EntityState | ||||
| import pytest | ||||
|  | ||||
| @@ -47,3 +48,23 @@ async def test_host_mode_with_sensor( | ||||
|         # Verify the sensor state | ||||
|         assert test_sensor_state.state == 42.0 | ||||
|         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 | ||||
|     vars: | ||||
|       component_test_file: $component_test_file | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user