mirror of
				https://github.com/esphome/esphome.git
				synced 2025-11-04 00:51:49 +00:00 
			
		
		
		
	Compare commits
	
		
			7 Commits
		
	
	
		
			select_opt
			...
			mqtt_wake
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					a6d21eeb38 | ||
| 
						 | 
					cbd29d8e2e | ||
| 
						 | 
					fb7dbc9910 | ||
| 
						 | 
					3f12630a6b | ||
| 
						 | 
					06d0787ee0 | ||
| 
						 | 
					cb039b42aa | ||
| 
						 | 
					f05f45af74 | 
@@ -877,7 +877,7 @@ uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection
 | 
			
		||||
                                              bool is_single) {
 | 
			
		||||
  auto *select = static_cast<select::Select *>(entity);
 | 
			
		||||
  SelectStateResponse resp;
 | 
			
		||||
  resp.set_state(StringRef(select->current_option()));
 | 
			
		||||
  resp.set_state(StringRef(select->state));
 | 
			
		||||
  resp.missing_state = !select->has_state();
 | 
			
		||||
  return fill_and_encode_entity_state(select, resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -7,19 +7,19 @@ namespace copy {
 | 
			
		||||
static const char *const TAG = "copy.select";
 | 
			
		||||
 | 
			
		||||
void CopySelect::setup() {
 | 
			
		||||
  source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(index); });
 | 
			
		||||
  source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(value); });
 | 
			
		||||
 | 
			
		||||
  traits.set_options(source_->traits.get_options());
 | 
			
		||||
 | 
			
		||||
  if (source_->has_state())
 | 
			
		||||
    this->publish_state(source_->active_index().value());
 | 
			
		||||
    this->publish_state(source_->state);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void CopySelect::dump_config() { LOG_SELECT("", "Copy Select", this); }
 | 
			
		||||
 | 
			
		||||
void CopySelect::control(size_t index) {
 | 
			
		||||
void CopySelect::control(const std::string &value) {
 | 
			
		||||
  auto call = source_->make_call();
 | 
			
		||||
  call.set_index(index);
 | 
			
		||||
  call.set_option(value);
 | 
			
		||||
  call.perform();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,7 @@ class CopySelect : public select::Select, public Component {
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void control(size_t index) override;
 | 
			
		||||
  void control(const std::string &value) override;
 | 
			
		||||
 | 
			
		||||
  select::Select *source_;
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ namespace demo {
 | 
			
		||||
 | 
			
		||||
class DemoSelect : public select::Select, public Component {
 | 
			
		||||
 protected:
 | 
			
		||||
  void control(size_t index) override { this->publish_state(index); }
 | 
			
		||||
  void control(const std::string &value) override { this->publish_state(value); }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace demo
 | 
			
		||||
 
 | 
			
		||||
@@ -42,7 +42,7 @@ std::string MenuItemSelect::get_value_text() const {
 | 
			
		||||
    result = this->value_getter_.value()(this);
 | 
			
		||||
  } else {
 | 
			
		||||
    if (this->select_var_ != nullptr) {
 | 
			
		||||
      result = this->select_var_->current_option();
 | 
			
		||||
      result = this->select_var_->state;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,9 +3,9 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace es8388 {
 | 
			
		||||
 | 
			
		||||
void ADCInputMicSelect::control(size_t index) {
 | 
			
		||||
  this->publish_state(index);
 | 
			
		||||
  this->parent_->set_adc_input_mic(static_cast<AdcInputMicLine>(index));
 | 
			
		||||
void ADCInputMicSelect::control(const std::string &value) {
 | 
			
		||||
  this->publish_state(value);
 | 
			
		||||
  this->parent_->set_adc_input_mic(static_cast<AdcInputMicLine>(this->index_of(value).value()));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace es8388
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ namespace es8388 {
 | 
			
		||||
 | 
			
		||||
class ADCInputMicSelect : public select::Select, public Parented<ES8388> {
 | 
			
		||||
 protected:
 | 
			
		||||
  void control(size_t index) override;
 | 
			
		||||
  void control(const std::string &value) override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace es8388
 | 
			
		||||
 
 | 
			
		||||
@@ -3,9 +3,9 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace es8388 {
 | 
			
		||||
 | 
			
		||||
void DacOutputSelect::control(size_t index) {
 | 
			
		||||
  this->publish_state(index);
 | 
			
		||||
  this->parent_->set_dac_output(static_cast<DacOutputLine>(index));
 | 
			
		||||
void DacOutputSelect::control(const std::string &value) {
 | 
			
		||||
  this->publish_state(value);
 | 
			
		||||
  this->parent_->set_dac_output(static_cast<DacOutputLine>(this->index_of(value).value()));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace es8388
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ namespace es8388 {
 | 
			
		||||
 | 
			
		||||
class DacOutputSelect : public select::Select, public Parented<ES8388> {
 | 
			
		||||
 protected:
 | 
			
		||||
  void control(size_t index) override;
 | 
			
		||||
  void control(const std::string &value) override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace es8388
 | 
			
		||||
 
 | 
			
		||||
@@ -558,6 +558,7 @@ CONF_DISABLE_LIBC_LOCKS_IN_IRAM = "disable_libc_locks_in_iram"
 | 
			
		||||
CONF_DISABLE_VFS_SUPPORT_TERMIOS = "disable_vfs_support_termios"
 | 
			
		||||
CONF_DISABLE_VFS_SUPPORT_SELECT = "disable_vfs_support_select"
 | 
			
		||||
CONF_DISABLE_VFS_SUPPORT_DIR = "disable_vfs_support_dir"
 | 
			
		||||
CONF_LOOP_TASK_STACK_SIZE = "loop_task_stack_size"
 | 
			
		||||
 | 
			
		||||
# VFS requirement tracking
 | 
			
		||||
# Components that need VFS features can call require_vfs_select() or require_vfs_dir()
 | 
			
		||||
@@ -654,6 +655,9 @@ FRAMEWORK_SCHEMA = cv.All(
 | 
			
		||||
                    ): cv.boolean,
 | 
			
		||||
                    cv.Optional(CONF_DISABLE_VFS_SUPPORT_DIR, default=True): cv.boolean,
 | 
			
		||||
                    cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean,
 | 
			
		||||
                    cv.Optional(CONF_LOOP_TASK_STACK_SIZE, default=8192): cv.int_range(
 | 
			
		||||
                        min=8192, max=32768
 | 
			
		||||
                    ),
 | 
			
		||||
                }
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list(
 | 
			
		||||
@@ -926,6 +930,10 @@ async def to_code(config):
 | 
			
		||||
                f"VERSION_CODE({framework_ver.major}, {framework_ver.minor}, {framework_ver.patch})"
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        add_idf_sdkconfig_option(
 | 
			
		||||
            "CONFIG_ARDUINO_LOOP_STACK_SIZE",
 | 
			
		||||
            conf[CONF_ADVANCED][CONF_LOOP_TASK_STACK_SIZE],
 | 
			
		||||
        )
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_AUTOSTART_ARDUINO", True)
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_MBEDTLS_PSK_MODES", True)
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True)
 | 
			
		||||
@@ -1071,6 +1079,10 @@ async def to_code(config):
 | 
			
		||||
        )
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_IDF_EXPERIMENTAL_FEATURES", True)
 | 
			
		||||
 | 
			
		||||
    cg.add_define(
 | 
			
		||||
        "ESPHOME_LOOP_TASK_STACK_SIZE", advanced.get(CONF_LOOP_TASK_STACK_SIZE)
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    cg.add_define(
 | 
			
		||||
        "USE_ESP_IDF_VERSION_CODE",
 | 
			
		||||
        cg.RawExpression(
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "preferences.h"
 | 
			
		||||
@@ -97,9 +98,9 @@ void loop_task(void *pv_params) {
 | 
			
		||||
extern "C" void app_main() {
 | 
			
		||||
  esp32::setup_preferences();
 | 
			
		||||
#if CONFIG_FREERTOS_UNICORE
 | 
			
		||||
  xTaskCreate(loop_task, "loopTask", 8192, nullptr, 1, &loop_task_handle);
 | 
			
		||||
  xTaskCreate(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, &loop_task_handle);
 | 
			
		||||
#else
 | 
			
		||||
  xTaskCreatePinnedToCore(loop_task, "loopTask", 8192, nullptr, 1, &loop_task_handle, 1);
 | 
			
		||||
  xTaskCreatePinnedToCore(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, &loop_task_handle, 1);
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
#endif  // USE_ESP_IDF
 | 
			
		||||
 
 | 
			
		||||
@@ -22,6 +22,7 @@ from esphome.core import CORE, CoroPriority, TimePeriod, coroutine_with_priority
 | 
			
		||||
import esphome.final_validate as fv
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["esp32"]
 | 
			
		||||
AUTO_LOAD = ["socket"]
 | 
			
		||||
CODEOWNERS = ["@jesserockz", "@Rapsssito", "@bdraco"]
 | 
			
		||||
DOMAIN = "esp32_ble"
 | 
			
		||||
 | 
			
		||||
@@ -482,13 +483,10 @@ async def to_code(config):
 | 
			
		||||
        cg.add(var.set_name(name))
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
 | 
			
		||||
    # BLE uses 1 UDP socket for event notification to wake up main loop from select()
 | 
			
		||||
    # BLE uses the socket wake_loop_threadsafe() mechanism to wake the main loop from BLE tasks
 | 
			
		||||
    # This enables low-latency (~12μs) BLE event processing instead of waiting for
 | 
			
		||||
    # select() timeout (0-16ms). The socket is created in ble_setup_() and used to
 | 
			
		||||
    # wake lwip_select() when BLE events arrive from the BLE thread.
 | 
			
		||||
    # Note: Called during config generation, socket is created at runtime. In practice,
 | 
			
		||||
    # always used since esp32_ble only runs on ESP32 which always has USE_SOCKET_SELECT_SUPPORT.
 | 
			
		||||
    socket.consume_sockets(1, "esp32_ble")(config)
 | 
			
		||||
    # select() timeout (0-16ms). The wake socket is shared across all components.
 | 
			
		||||
    socket.require_wake_loop_threadsafe()
 | 
			
		||||
 | 
			
		||||
    # Define max connections for use in C++ code (e.g., ble_server.h)
 | 
			
		||||
    max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS)
 | 
			
		||||
 
 | 
			
		||||
@@ -297,21 +297,10 @@ bool ESP32BLE::ble_setup_() {
 | 
			
		||||
  // BLE takes some time to be fully set up, 200ms should be more than enough
 | 
			
		||||
  delay(200);  // NOLINT
 | 
			
		||||
 | 
			
		||||
  // Set up notification socket to wake main loop for BLE events
 | 
			
		||||
  // This enables low-latency (~12μs) event processing instead of waiting for select() timeout
 | 
			
		||||
#ifdef USE_SOCKET_SELECT_SUPPORT
 | 
			
		||||
  this->setup_event_notification_();
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool ESP32BLE::ble_dismantle_() {
 | 
			
		||||
  // Clean up notification socket first before dismantling BLE stack
 | 
			
		||||
#ifdef USE_SOCKET_SELECT_SUPPORT
 | 
			
		||||
  this->cleanup_event_notification_();
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  esp_err_t err = esp_bluedroid_disable();
 | 
			
		||||
  if (err != ESP_OK) {
 | 
			
		||||
    ESP_LOGE(TAG, "esp_bluedroid_disable failed: %d", err);
 | 
			
		||||
@@ -409,12 +398,6 @@ void ESP32BLE::loop() {
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
#ifdef USE_SOCKET_SELECT_SUPPORT
 | 
			
		||||
  // Drain any notification socket events first
 | 
			
		||||
  // This clears the socket so it doesn't stay "ready" in subsequent select() calls
 | 
			
		||||
  this->drain_event_notifications_();
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  BLEEvent *ble_event = this->ble_events_.pop();
 | 
			
		||||
  while (ble_event != nullptr) {
 | 
			
		||||
    switch (ble_event->type_) {
 | 
			
		||||
@@ -589,8 +572,8 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa
 | 
			
		||||
    GAP_SECURITY_EVENTS:
 | 
			
		||||
      enqueue_ble_event(event, param);
 | 
			
		||||
      // Wake up main loop to process security event immediately
 | 
			
		||||
#ifdef USE_SOCKET_SELECT_SUPPORT
 | 
			
		||||
      global_ble->notify_main_loop_();
 | 
			
		||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
 | 
			
		||||
      App.wake_loop_threadsafe();
 | 
			
		||||
#endif
 | 
			
		||||
      return;
 | 
			
		||||
 | 
			
		||||
@@ -612,8 +595,8 @@ void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gat
 | 
			
		||||
                                   esp_ble_gatts_cb_param_t *param) {
 | 
			
		||||
  enqueue_ble_event(event, gatts_if, param);
 | 
			
		||||
  // Wake up main loop to process GATT event immediately
 | 
			
		||||
#ifdef USE_SOCKET_SELECT_SUPPORT
 | 
			
		||||
  global_ble->notify_main_loop_();
 | 
			
		||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
 | 
			
		||||
  App.wake_loop_threadsafe();
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
@@ -623,8 +606,8 @@ void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gat
 | 
			
		||||
                                   esp_ble_gattc_cb_param_t *param) {
 | 
			
		||||
  enqueue_ble_event(event, gattc_if, param);
 | 
			
		||||
  // Wake up main loop to process GATT event immediately
 | 
			
		||||
#ifdef USE_SOCKET_SELECT_SUPPORT
 | 
			
		||||
  global_ble->notify_main_loop_();
 | 
			
		||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
 | 
			
		||||
  App.wake_loop_threadsafe();
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
@@ -665,89 +648,6 @@ void ESP32BLE::dump_config() {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#ifdef USE_SOCKET_SELECT_SUPPORT
 | 
			
		||||
void ESP32BLE::setup_event_notification_() {
 | 
			
		||||
  // Create UDP socket for event notifications
 | 
			
		||||
  this->notify_fd_ = lwip_socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
 | 
			
		||||
  if (this->notify_fd_ < 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "Event socket create failed: %d", errno);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Bind to loopback with auto-assigned port
 | 
			
		||||
  struct sockaddr_in addr = {};
 | 
			
		||||
  addr.sin_family = AF_INET;
 | 
			
		||||
  addr.sin_addr.s_addr = lwip_htonl(INADDR_LOOPBACK);
 | 
			
		||||
  addr.sin_port = 0;  // Auto-assign port
 | 
			
		||||
 | 
			
		||||
  if (lwip_bind(this->notify_fd_, (struct sockaddr *) &addr, sizeof(addr)) < 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "Event socket bind failed: %d", errno);
 | 
			
		||||
    lwip_close(this->notify_fd_);
 | 
			
		||||
    this->notify_fd_ = -1;
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Get the assigned address and connect to it
 | 
			
		||||
  // Connecting a UDP socket allows using send() instead of sendto() for better performance
 | 
			
		||||
  struct sockaddr_in notify_addr;
 | 
			
		||||
  socklen_t len = sizeof(notify_addr);
 | 
			
		||||
  if (lwip_getsockname(this->notify_fd_, (struct sockaddr *) ¬ify_addr, &len) < 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "Event socket address failed: %d", errno);
 | 
			
		||||
    lwip_close(this->notify_fd_);
 | 
			
		||||
    this->notify_fd_ = -1;
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Connect to self (loopback) - allows using send() instead of sendto()
 | 
			
		||||
  // After connect(), no need to store notify_addr - the socket remembers it
 | 
			
		||||
  if (lwip_connect(this->notify_fd_, (struct sockaddr *) ¬ify_addr, sizeof(notify_addr)) < 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "Event socket connect failed: %d", errno);
 | 
			
		||||
    lwip_close(this->notify_fd_);
 | 
			
		||||
    this->notify_fd_ = -1;
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Set non-blocking mode
 | 
			
		||||
  int flags = lwip_fcntl(this->notify_fd_, F_GETFL, 0);
 | 
			
		||||
  lwip_fcntl(this->notify_fd_, F_SETFL, flags | O_NONBLOCK);
 | 
			
		||||
 | 
			
		||||
  // Register with application's select() loop
 | 
			
		||||
  if (!App.register_socket_fd(this->notify_fd_)) {
 | 
			
		||||
    ESP_LOGW(TAG, "Event socket register failed");
 | 
			
		||||
    lwip_close(this->notify_fd_);
 | 
			
		||||
    this->notify_fd_ = -1;
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGD(TAG, "Event socket ready");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESP32BLE::cleanup_event_notification_() {
 | 
			
		||||
  if (this->notify_fd_ >= 0) {
 | 
			
		||||
    App.unregister_socket_fd(this->notify_fd_);
 | 
			
		||||
    lwip_close(this->notify_fd_);
 | 
			
		||||
    this->notify_fd_ = -1;
 | 
			
		||||
    ESP_LOGD(TAG, "Event socket closed");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESP32BLE::drain_event_notifications_() {
 | 
			
		||||
  // Called from main loop to drain any pending notifications
 | 
			
		||||
  // Must check is_socket_ready() to avoid blocking on empty socket
 | 
			
		||||
  if (this->notify_fd_ >= 0 && App.is_socket_ready(this->notify_fd_)) {
 | 
			
		||||
    char buffer[BLE_EVENT_NOTIFY_DRAIN_BUFFER_SIZE];
 | 
			
		||||
    // Drain all pending notifications with non-blocking reads
 | 
			
		||||
    // Multiple BLE events may have triggered multiple writes, so drain until EWOULDBLOCK
 | 
			
		||||
    // We control both ends of this loopback socket (always write 1 byte per event),
 | 
			
		||||
    // so no error checking needed - any errors indicate catastrophic system failure
 | 
			
		||||
    while (lwip_recvfrom(this->notify_fd_, buffer, sizeof(buffer), 0, nullptr, nullptr) > 0) {
 | 
			
		||||
      // Just draining, no action needed - actual BLE events are already queued
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#endif  // USE_SOCKET_SELECT_SUPPORT
 | 
			
		||||
 | 
			
		||||
uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) {
 | 
			
		||||
  uint64_t u = 0;
 | 
			
		||||
  u |= uint64_t(address[0] & 0xFF) << 40;
 | 
			
		||||
 
 | 
			
		||||
@@ -166,12 +166,10 @@ class ESP32BLE : public Component {
 | 
			
		||||
  void advertising_init_();
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_SOCKET_SELECT_SUPPORT
 | 
			
		||||
  void setup_event_notification_();    // Create notification socket
 | 
			
		||||
  void cleanup_event_notification_();  // Close and unregister socket
 | 
			
		||||
  inline void notify_main_loop_();     // Wake up select() from BLE thread (hot path - inlined)
 | 
			
		||||
  void drain_event_notifications_();   // Read pending notifications in main loop
 | 
			
		||||
#endif
 | 
			
		||||
  // BLE uses the core wake_loop_threadsafe() mechanism to wake the main event loop
 | 
			
		||||
  // from BLE tasks. This enables low-latency (~12μs) event processing instead of
 | 
			
		||||
  // waiting for select() timeout (0-16ms). The wake socket is shared with other
 | 
			
		||||
  // components that need this functionality.
 | 
			
		||||
 | 
			
		||||
 private:
 | 
			
		||||
  template<typename... Args> friend void enqueue_ble_event(Args... args);
 | 
			
		||||
@@ -207,13 +205,6 @@ class ESP32BLE : public Component {
 | 
			
		||||
  esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE};  // 4 bytes (enum)
 | 
			
		||||
  uint32_t advertising_cycle_time_{};         // 4 bytes
 | 
			
		||||
 | 
			
		||||
#ifdef USE_SOCKET_SELECT_SUPPORT
 | 
			
		||||
  // Event notification socket for waking up main loop from BLE thread
 | 
			
		||||
  // Uses connected UDP loopback socket to wake lwip_select() with ~12μs latency vs 0-16ms timeout
 | 
			
		||||
  // Socket is connected during setup, allowing use of send() instead of sendto() for efficiency
 | 
			
		||||
  int notify_fd_{-1};  // 4 bytes (file descriptor)
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  // 2-byte aligned members
 | 
			
		||||
  uint16_t appearance_{0};  // 2 bytes
 | 
			
		||||
 | 
			
		||||
@@ -225,29 +216,6 @@ class ESP32BLE : public Component {
 | 
			
		||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
 | 
			
		||||
extern ESP32BLE *global_ble;
 | 
			
		||||
 | 
			
		||||
#ifdef USE_SOCKET_SELECT_SUPPORT
 | 
			
		||||
// Inline implementations for hot-path functions
 | 
			
		||||
// These are called from BLE thread (notify) and main loop (drain) on every event
 | 
			
		||||
 | 
			
		||||
// Small buffer for draining notification bytes (1 byte sent per BLE event)
 | 
			
		||||
// Size allows draining multiple notifications per recvfrom() without wasting stack
 | 
			
		||||
static constexpr size_t BLE_EVENT_NOTIFY_DRAIN_BUFFER_SIZE = 16;
 | 
			
		||||
 | 
			
		||||
inline void ESP32BLE::notify_main_loop_() {
 | 
			
		||||
  // Called from BLE thread context when events are queued
 | 
			
		||||
  // Wakes up lwip_select() in main loop by writing to connected loopback socket
 | 
			
		||||
  if (this->notify_fd_ >= 0) {
 | 
			
		||||
    const char dummy = 1;
 | 
			
		||||
    // Non-blocking send - if it fails (unlikely), select() will wake on timeout anyway
 | 
			
		||||
    // No error checking needed: we control both ends of this loopback socket, and the
 | 
			
		||||
    // BLE event is already queued. Notification is best-effort to reduce latency.
 | 
			
		||||
    // This is safe to call from BLE thread - send() is thread-safe in lwip
 | 
			
		||||
    // Socket is already connected to loopback address, so send() is faster than sendto()
 | 
			
		||||
    lwip_send(this->notify_fd_, &dummy, 1, 0);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif  // USE_SOCKET_SELECT_SUPPORT
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class BLEEnabledCondition : public Condition<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  bool check(Ts... x) override { return global_ble->is_active(); }
 | 
			
		||||
 
 | 
			
		||||
@@ -2,11 +2,18 @@ import logging
 | 
			
		||||
 | 
			
		||||
from esphome import pins
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components.zephyr import (
 | 
			
		||||
    zephyr_add_overlay,
 | 
			
		||||
    zephyr_add_prj_conf,
 | 
			
		||||
    zephyr_data,
 | 
			
		||||
)
 | 
			
		||||
from esphome.components.zephyr.const import KEY_BOARD
 | 
			
		||||
from esphome.config_helpers import filter_source_files_from_platform
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ADDRESS,
 | 
			
		||||
    CONF_FREQUENCY,
 | 
			
		||||
    CONF_I2C,
 | 
			
		||||
    CONF_I2C_ID,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_SCAN,
 | 
			
		||||
@@ -15,10 +22,12 @@ from esphome.const import (
 | 
			
		||||
    CONF_TIMEOUT,
 | 
			
		||||
    PLATFORM_ESP32,
 | 
			
		||||
    PLATFORM_ESP8266,
 | 
			
		||||
    PLATFORM_NRF52,
 | 
			
		||||
    PLATFORM_RP2040,
 | 
			
		||||
    PlatformFramework,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
 | 
			
		||||
from esphome.cpp_generator import MockObj
 | 
			
		||||
import esphome.final_validate as fv
 | 
			
		||||
 | 
			
		||||
LOGGER = logging.getLogger(__name__)
 | 
			
		||||
@@ -28,6 +37,7 @@ I2CBus = i2c_ns.class_("I2CBus")
 | 
			
		||||
InternalI2CBus = i2c_ns.class_("InternalI2CBus", I2CBus)
 | 
			
		||||
ArduinoI2CBus = i2c_ns.class_("ArduinoI2CBus", InternalI2CBus, cg.Component)
 | 
			
		||||
IDFI2CBus = i2c_ns.class_("IDFI2CBus", InternalI2CBus, cg.Component)
 | 
			
		||||
ZephyrI2CBus = i2c_ns.class_("ZephyrI2CBus", I2CBus, cg.Component)
 | 
			
		||||
I2CDevice = i2c_ns.class_("I2CDevice")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -41,6 +51,8 @@ def _bus_declare_type(value):
 | 
			
		||||
        return cv.declare_id(ArduinoI2CBus)(value)
 | 
			
		||||
    if CORE.using_esp_idf:
 | 
			
		||||
        return cv.declare_id(IDFI2CBus)(value)
 | 
			
		||||
    if CORE.using_zephyr:
 | 
			
		||||
        return cv.declare_id(ZephyrI2CBus)(value)
 | 
			
		||||
    raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -62,23 +74,70 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
            cv.SplitDefault(CONF_SCL_PULLUP_ENABLED, esp32_idf=True): cv.All(
 | 
			
		||||
                cv.only_with_esp_idf, cv.boolean
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_FREQUENCY, default="50kHz"): cv.All(
 | 
			
		||||
                cv.frequency, cv.Range(min=0, min_included=False)
 | 
			
		||||
            cv.SplitDefault(
 | 
			
		||||
                CONF_FREQUENCY,
 | 
			
		||||
                esp32="50kHz",
 | 
			
		||||
                esp8266="50kHz",
 | 
			
		||||
                rp2040="50kHz",
 | 
			
		||||
                nrf52="100kHz",
 | 
			
		||||
            ): cv.All(
 | 
			
		||||
                cv.frequency,
 | 
			
		||||
                cv.Range(min=0, min_included=False),
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_TIMEOUT): cv.All(
 | 
			
		||||
                cv.only_with_framework(["arduino", "esp-idf"]),
 | 
			
		||||
                cv.positive_time_period,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_TIMEOUT): cv.positive_time_period,
 | 
			
		||||
            cv.Optional(CONF_SCAN, default=True): cv.boolean,
 | 
			
		||||
        }
 | 
			
		||||
    ).extend(cv.COMPONENT_SCHEMA),
 | 
			
		||||
    cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040]),
 | 
			
		||||
    cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, PLATFORM_NRF52]),
 | 
			
		||||
    validate_config,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _final_validate(config):
 | 
			
		||||
    full_config = fv.full_config.get()[CONF_I2C]
 | 
			
		||||
    if CORE.using_zephyr and len(full_config) > 1:
 | 
			
		||||
        raise cv.Invalid("Second i2c is not implemented on Zephyr yet")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = _final_validate
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@coroutine_with_priority(CoroPriority.BUS)
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    cg.add_global(i2c_ns.using)
 | 
			
		||||
    cg.add_define("USE_I2C")
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    if CORE.using_zephyr:
 | 
			
		||||
        zephyr_add_prj_conf("I2C", True)
 | 
			
		||||
        i2c = "i2c0"
 | 
			
		||||
        if zephyr_data()[KEY_BOARD] in ["xiao_ble"]:
 | 
			
		||||
            i2c = "i2c1"
 | 
			
		||||
        zephyr_add_overlay(
 | 
			
		||||
            f"""
 | 
			
		||||
                &pinctrl {{
 | 
			
		||||
                    {i2c}_default: {i2c}_default {{
 | 
			
		||||
                        group1 {{
 | 
			
		||||
                            psels = <NRF_PSEL(TWIM_SDA, {config[CONF_SDA] // 32}, {config[CONF_SDA] % 32})>,
 | 
			
		||||
                                <NRF_PSEL(TWIM_SCL, {config[CONF_SCL] // 32}, {config[CONF_SCL] % 32})>;
 | 
			
		||||
                        }};
 | 
			
		||||
                    }};
 | 
			
		||||
                    {i2c}_sleep: {i2c}_sleep {{
 | 
			
		||||
                        group1 {{
 | 
			
		||||
                            psels = <NRF_PSEL(TWIM_SDA, {config[CONF_SDA] // 32}, {config[CONF_SDA] % 32})>,
 | 
			
		||||
                                <NRF_PSEL(TWIM_SCL, {config[CONF_SCL] // 32}, {config[CONF_SCL] % 32})>;
 | 
			
		||||
                            low-power-enable;
 | 
			
		||||
                        }};
 | 
			
		||||
                    }};
 | 
			
		||||
                }};
 | 
			
		||||
            """
 | 
			
		||||
        )
 | 
			
		||||
        var = cg.new_Pvariable(
 | 
			
		||||
            config[CONF_ID], MockObj(f"DEVICE_DT_GET(DT_NODELABEL({i2c}))")
 | 
			
		||||
        )
 | 
			
		||||
    else:
 | 
			
		||||
        var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
 | 
			
		||||
    cg.add(var.set_sda_pin(config[CONF_SDA]))
 | 
			
		||||
@@ -197,5 +256,6 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
 | 
			
		||||
            PlatformFramework.LN882X_ARDUINO,
 | 
			
		||||
        },
 | 
			
		||||
        "i2c_bus_esp_idf.cpp": {PlatformFramework.ESP32_IDF},
 | 
			
		||||
        "i2c_bus_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR},
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
#include "i2c.h"
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include <memory>
 | 
			
		||||
 | 
			
		||||
@@ -23,6 +24,8 @@ void I2CBus::i2c_scan_() {
 | 
			
		||||
    } else if (err == ERROR_UNKNOWN) {
 | 
			
		||||
      scan_results_.emplace_back(address, false);
 | 
			
		||||
    }
 | 
			
		||||
    // it takes 16sec to scan on nrf52. It prevents board reset.
 | 
			
		||||
    arch_feed_wdt();
 | 
			
		||||
  }
 | 
			
		||||
#if defined(USE_ESP32) && defined(USE_LOGGER)
 | 
			
		||||
  esp_log_level_set("*", previous);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										133
									
								
								esphome/components/i2c/i2c_bus_zephyr.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								esphome/components/i2c/i2c_bus_zephyr.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,133 @@
 | 
			
		||||
#ifdef USE_ZEPHYR
 | 
			
		||||
 | 
			
		||||
#include "i2c_bus_zephyr.h"
 | 
			
		||||
#include <zephyr/drivers/i2c.h>
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome::i2c {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "i2c.zephyr";
 | 
			
		||||
 | 
			
		||||
void ZephyrI2CBus::setup() {
 | 
			
		||||
  if (!device_is_ready(this->i2c_dev_)) {
 | 
			
		||||
    ESP_LOGE(TAG, "I2C dev is not ready.");
 | 
			
		||||
    mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  int ret = i2c_configure(this->i2c_dev_, this->dev_config_);
 | 
			
		||||
  if (ret < 0) {
 | 
			
		||||
    ESP_LOGE(TAG, "I2C: Failed to configure device");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->recovery_result_ = i2c_recover_bus(this->i2c_dev_);
 | 
			
		||||
  if (this->recovery_result_ != 0) {
 | 
			
		||||
    ESP_LOGE(TAG, "I2C recover bus failed, err %d", this->recovery_result_);
 | 
			
		||||
  }
 | 
			
		||||
  if (this->scan_) {
 | 
			
		||||
    ESP_LOGV(TAG, "Scanning I2C bus for active devices...");
 | 
			
		||||
    this->i2c_scan_();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ZephyrI2CBus::dump_config() {
 | 
			
		||||
  auto get_speed = [](uint32_t dev_config) {
 | 
			
		||||
    switch (I2C_SPEED_GET(dev_config)) {
 | 
			
		||||
      case I2C_SPEED_STANDARD:
 | 
			
		||||
        return "100 kHz";
 | 
			
		||||
      case I2C_SPEED_FAST:
 | 
			
		||||
        return "400 kHz";
 | 
			
		||||
      case I2C_SPEED_FAST_PLUS:
 | 
			
		||||
        return "1 MHz";
 | 
			
		||||
      case I2C_SPEED_HIGH:
 | 
			
		||||
        return "3.4 MHz";
 | 
			
		||||
      case I2C_SPEED_ULTRA:
 | 
			
		||||
        return "5 MHz";
 | 
			
		||||
    }
 | 
			
		||||
    return "unknown";
 | 
			
		||||
  };
 | 
			
		||||
  ESP_LOGCONFIG(TAG,
 | 
			
		||||
                "I2C Bus:\n"
 | 
			
		||||
                "  SDA Pin: GPIO%u\n"
 | 
			
		||||
                "  SCL Pin: GPIO%u\n"
 | 
			
		||||
                "  Frequency: %s\n"
 | 
			
		||||
                "  Name: %s",
 | 
			
		||||
                this->sda_pin_, this->scl_pin_, get_speed(this->dev_config_), this->i2c_dev_->name);
 | 
			
		||||
 | 
			
		||||
  if (this->recovery_result_ != 0) {
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Recovery: failed, err %d", this->recovery_result_);
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Recovery: bus successfully recovered");
 | 
			
		||||
  }
 | 
			
		||||
  if (this->scan_) {
 | 
			
		||||
    ESP_LOGI(TAG, "Results from I2C bus scan:");
 | 
			
		||||
    if (scan_results_.empty()) {
 | 
			
		||||
      ESP_LOGI(TAG, "Found no I2C devices!");
 | 
			
		||||
    } else {
 | 
			
		||||
      for (const auto &s : scan_results_) {
 | 
			
		||||
        if (s.second) {
 | 
			
		||||
          ESP_LOGI(TAG, "Found I2C device at address 0x%02X", s.first);
 | 
			
		||||
        } else {
 | 
			
		||||
          ESP_LOGE(TAG, "Unknown error at address 0x%02X", s.first);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ErrorCode ZephyrI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count,
 | 
			
		||||
                                    uint8_t *read_buffer, size_t read_count) {
 | 
			
		||||
  if (!device_is_ready(this->i2c_dev_)) {
 | 
			
		||||
    return ERROR_NOT_INITIALIZED;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  i2c_msg msgs[2]{};
 | 
			
		||||
  size_t cnt = 0;
 | 
			
		||||
  uint8_t dst = 0x00;  // dummy data to not use random value
 | 
			
		||||
 | 
			
		||||
  if (read_count == 0 && write_count == 0) {
 | 
			
		||||
    msgs[cnt].buf = &dst;
 | 
			
		||||
    msgs[cnt].len = 0U;
 | 
			
		||||
    msgs[cnt++].flags = I2C_MSG_WRITE;
 | 
			
		||||
  } else {
 | 
			
		||||
    if (write_count) {
 | 
			
		||||
      // the same struct is used for read/write — const cast is fine; data isn't modified
 | 
			
		||||
      msgs[cnt].buf = const_cast<uint8_t *>(write_buffer);
 | 
			
		||||
      msgs[cnt].len = write_count;
 | 
			
		||||
      msgs[cnt++].flags = I2C_MSG_WRITE;
 | 
			
		||||
    }
 | 
			
		||||
    if (read_count) {
 | 
			
		||||
      msgs[cnt].buf = const_cast<uint8_t *>(read_buffer);
 | 
			
		||||
      msgs[cnt].len = read_count;
 | 
			
		||||
      msgs[cnt++].flags = I2C_MSG_READ | I2C_MSG_RESTART;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  msgs[cnt - 1].flags |= I2C_MSG_STOP;
 | 
			
		||||
 | 
			
		||||
  auto err = i2c_transfer(this->i2c_dev_, msgs, cnt, address);
 | 
			
		||||
 | 
			
		||||
  if (err == -EIO) {
 | 
			
		||||
    return ERROR_NOT_ACKNOWLEDGED;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    ESP_LOGE(TAG, "i2c transfer error %d", err);
 | 
			
		||||
    return ERROR_UNKNOWN;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return ERROR_OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ZephyrI2CBus::set_frequency(uint32_t frequency) {
 | 
			
		||||
  this->dev_config_ &= ~I2C_SPEED_MASK;
 | 
			
		||||
  if (frequency >= 400000) {
 | 
			
		||||
    this->dev_config_ |= I2C_SPEED_SET(I2C_SPEED_FAST);
 | 
			
		||||
  } else {
 | 
			
		||||
    this->dev_config_ |= I2C_SPEED_SET(I2C_SPEED_STANDARD);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::i2c
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
							
								
								
									
										38
									
								
								esphome/components/i2c/i2c_bus_zephyr.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								esphome/components/i2c/i2c_bus_zephyr.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ZEPHYR
 | 
			
		||||
 | 
			
		||||
#include "i2c_bus.h"
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
 | 
			
		||||
struct device;
 | 
			
		||||
 | 
			
		||||
namespace esphome::i2c {
 | 
			
		||||
 | 
			
		||||
class ZephyrI2CBus : public InternalI2CBus, public Component {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit ZephyrI2CBus(const device *i2c_dev) : i2c_dev_(i2c_dev) {}
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  ErrorCode write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, uint8_t *read_buffer,
 | 
			
		||||
                        size_t read_count) override;
 | 
			
		||||
  float get_setup_priority() const override { return setup_priority::BUS; }
 | 
			
		||||
 | 
			
		||||
  void set_scan(bool scan) { scan_ = scan; }
 | 
			
		||||
  void set_sda_pin(uint8_t sda_pin) { this->sda_pin_ = sda_pin; }
 | 
			
		||||
  void set_scl_pin(uint8_t scl_pin) { this->scl_pin_ = scl_pin; }
 | 
			
		||||
  void set_frequency(uint32_t frequency);
 | 
			
		||||
 | 
			
		||||
  int get_port() const override { return 0; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  const device *i2c_dev_;
 | 
			
		||||
  int recovery_result_ = 0;
 | 
			
		||||
  uint8_t sda_pin_{};
 | 
			
		||||
  uint8_t scl_pin_{};
 | 
			
		||||
  uint32_t dev_config_{};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::i2c
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
@@ -121,9 +121,9 @@ constexpr Uint8ToString OUT_PIN_LEVELS_BY_UINT[] = {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Helper functions for lookups
 | 
			
		||||
template<size_t N> uint8_t find_uint8(const StringToUint8 (&arr)[N], const char *str) {
 | 
			
		||||
template<size_t N> uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) {
 | 
			
		||||
  for (const auto &entry : arr) {
 | 
			
		||||
    if (strcmp(str, entry.str) == 0)
 | 
			
		||||
    if (str == entry.str)
 | 
			
		||||
      return entry.value;
 | 
			
		||||
  }
 | 
			
		||||
  return 0xFF;  // Not found
 | 
			
		||||
@@ -441,7 +441,7 @@ bool LD2410Component::handle_ack_data_() {
 | 
			
		||||
      ESP_LOGV(TAG, "Baud rate change");
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
      if (this->baud_rate_select_ != nullptr) {
 | 
			
		||||
        ESP_LOGE(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->current_option());
 | 
			
		||||
        ESP_LOGE(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->state.c_str());
 | 
			
		||||
      }
 | 
			
		||||
#endif
 | 
			
		||||
      break;
 | 
			
		||||
@@ -626,14 +626,14 @@ void LD2410Component::set_bluetooth(bool enable) {
 | 
			
		||||
  this->set_timeout(200, [this]() { this->restart_and_read_all_info(); });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void LD2410Component::set_distance_resolution(const char *state) {
 | 
			
		||||
void LD2410Component::set_distance_resolution(const std::string &state) {
 | 
			
		||||
  this->set_config_mode_(true);
 | 
			
		||||
  const uint8_t cmd_value[2] = {find_uint8(DISTANCE_RESOLUTIONS_BY_STR, state), 0x00};
 | 
			
		||||
  this->send_command_(CMD_SET_DISTANCE_RESOLUTION, cmd_value, sizeof(cmd_value));
 | 
			
		||||
  this->set_timeout(200, [this]() { this->restart_and_read_all_info(); });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void LD2410Component::set_baud_rate(const char *state) {
 | 
			
		||||
void LD2410Component::set_baud_rate(const std::string &state) {
 | 
			
		||||
  this->set_config_mode_(true);
 | 
			
		||||
  const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00};
 | 
			
		||||
  this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value));
 | 
			
		||||
@@ -759,10 +759,10 @@ void LD2410Component::set_light_out_control() {
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
  if (this->light_function_select_ != nullptr && this->light_function_select_->has_state()) {
 | 
			
		||||
    this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->current_option());
 | 
			
		||||
    this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->state);
 | 
			
		||||
  }
 | 
			
		||||
  if (this->out_pin_level_select_ != nullptr && this->out_pin_level_select_->has_state()) {
 | 
			
		||||
    this->out_pin_level_ = find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option());
 | 
			
		||||
    this->out_pin_level_ = find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->state);
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
  this->set_config_mode_(true);
 | 
			
		||||
 
 | 
			
		||||
@@ -98,8 +98,8 @@ class LD2410Component : public Component, public uart::UARTDevice {
 | 
			
		||||
  void read_all_info();
 | 
			
		||||
  void restart_and_read_all_info();
 | 
			
		||||
  void set_bluetooth(bool enable);
 | 
			
		||||
  void set_distance_resolution(const char *state);
 | 
			
		||||
  void set_baud_rate(const char *state);
 | 
			
		||||
  void set_distance_resolution(const std::string &state);
 | 
			
		||||
  void set_baud_rate(const std::string &state);
 | 
			
		||||
  void factory_reset();
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
 
 | 
			
		||||
@@ -3,9 +3,9 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace ld2410 {
 | 
			
		||||
 | 
			
		||||
void BaudRateSelect::control(size_t index) {
 | 
			
		||||
  this->publish_state(index);
 | 
			
		||||
  this->parent_->set_baud_rate(this->option_at(index));
 | 
			
		||||
void BaudRateSelect::control(const std::string &value) {
 | 
			
		||||
  this->publish_state(value);
 | 
			
		||||
  this->parent_->set_baud_rate(state);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace ld2410
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ class BaudRateSelect : public select::Select, public Parented<LD2410Component> {
 | 
			
		||||
  BaudRateSelect() = default;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void control(size_t index) override;
 | 
			
		||||
  void control(const std::string &value) override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace ld2410
 | 
			
		||||
 
 | 
			
		||||
@@ -3,9 +3,9 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace ld2410 {
 | 
			
		||||
 | 
			
		||||
void DistanceResolutionSelect::control(size_t index) {
 | 
			
		||||
  this->publish_state(index);
 | 
			
		||||
  this->parent_->set_distance_resolution(this->option_at(index));
 | 
			
		||||
void DistanceResolutionSelect::control(const std::string &value) {
 | 
			
		||||
  this->publish_state(value);
 | 
			
		||||
  this->parent_->set_distance_resolution(state);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace ld2410
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ class DistanceResolutionSelect : public select::Select, public Parented<LD2410Co
 | 
			
		||||
  DistanceResolutionSelect() = default;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void control(size_t index) override;
 | 
			
		||||
  void control(const std::string &value) override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace ld2410
 | 
			
		||||
 
 | 
			
		||||
@@ -3,8 +3,8 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace ld2410 {
 | 
			
		||||
 | 
			
		||||
void LightOutControlSelect::control(size_t index) {
 | 
			
		||||
  this->publish_state(index);
 | 
			
		||||
void LightOutControlSelect::control(const std::string &value) {
 | 
			
		||||
  this->publish_state(value);
 | 
			
		||||
  this->parent_->set_light_out_control();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ class LightOutControlSelect : public select::Select, public Parented<LD2410Compo
 | 
			
		||||
  LightOutControlSelect() = default;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void control(size_t index) override;
 | 
			
		||||
  void control(const std::string &value) override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace ld2410
 | 
			
		||||
 
 | 
			
		||||
@@ -132,9 +132,9 @@ constexpr Uint8ToString OUT_PIN_LEVELS_BY_UINT[] = {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Helper functions for lookups
 | 
			
		||||
template<size_t N> uint8_t find_uint8(const StringToUint8 (&arr)[N], const char *str) {
 | 
			
		||||
template<size_t N> uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) {
 | 
			
		||||
  for (const auto &entry : arr) {
 | 
			
		||||
    if (strcmp(str, entry.str) == 0) {
 | 
			
		||||
    if (str == entry.str) {
 | 
			
		||||
      return entry.value;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -485,7 +485,7 @@ bool LD2412Component::handle_ack_data_() {
 | 
			
		||||
      ESP_LOGV(TAG, "Baud rate change");
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
      if (this->baud_rate_select_ != nullptr) {
 | 
			
		||||
        ESP_LOGW(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->current_option());
 | 
			
		||||
        ESP_LOGW(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->state.c_str());
 | 
			
		||||
      }
 | 
			
		||||
#endif
 | 
			
		||||
      break;
 | 
			
		||||
@@ -699,14 +699,14 @@ void LD2412Component::set_bluetooth(bool enable) {
 | 
			
		||||
  this->set_timeout(200, [this]() { this->restart_and_read_all_info(); });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void LD2412Component::set_distance_resolution(const char *state) {
 | 
			
		||||
void LD2412Component::set_distance_resolution(const std::string &state) {
 | 
			
		||||
  this->set_config_mode_(true);
 | 
			
		||||
  const uint8_t cmd_value[6] = {find_uint8(DISTANCE_RESOLUTIONS_BY_STR, state), 0x00, 0x00, 0x00, 0x00, 0x00};
 | 
			
		||||
  this->send_command_(CMD_SET_DISTANCE_RESOLUTION, cmd_value, sizeof(cmd_value));
 | 
			
		||||
  this->set_timeout(200, [this]() { this->restart_and_read_all_info(); });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void LD2412Component::set_baud_rate(const char *state) {
 | 
			
		||||
void LD2412Component::set_baud_rate(const std::string &state) {
 | 
			
		||||
  this->set_config_mode_(true);
 | 
			
		||||
  const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00};
 | 
			
		||||
  this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value));
 | 
			
		||||
@@ -783,7 +783,7 @@ void LD2412Component::set_basic_config() {
 | 
			
		||||
      1,    TOTAL_GATES, DEFAULT_PRESENCE_TIMEOUT, 0,
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
      find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option()),
 | 
			
		||||
      find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->state),
 | 
			
		||||
#else
 | 
			
		||||
      0x01,  // Default value if not using select
 | 
			
		||||
#endif
 | 
			
		||||
@@ -837,7 +837,7 @@ void LD2412Component::set_light_out_control() {
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
  if (this->light_function_select_ != nullptr && this->light_function_select_->has_state()) {
 | 
			
		||||
    this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->current_option());
 | 
			
		||||
    this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->state);
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
  uint8_t value[2] = {this->light_function_, this->light_threshold_};
 | 
			
		||||
 
 | 
			
		||||
@@ -99,8 +99,8 @@ class LD2412Component : public Component, public uart::UARTDevice {
 | 
			
		||||
  void read_all_info();
 | 
			
		||||
  void restart_and_read_all_info();
 | 
			
		||||
  void set_bluetooth(bool enable);
 | 
			
		||||
  void set_distance_resolution(const char *state);
 | 
			
		||||
  void set_baud_rate(const char *state);
 | 
			
		||||
  void set_distance_resolution(const std::string &state);
 | 
			
		||||
  void set_baud_rate(const std::string &state);
 | 
			
		||||
  void factory_reset();
 | 
			
		||||
  void start_dynamic_background_correction();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,9 +3,9 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace ld2412 {
 | 
			
		||||
 | 
			
		||||
void BaudRateSelect::control(size_t index) {
 | 
			
		||||
  this->publish_state(index);
 | 
			
		||||
  this->parent_->set_baud_rate(this->option_at(index));
 | 
			
		||||
void BaudRateSelect::control(const std::string &value) {
 | 
			
		||||
  this->publish_state(value);
 | 
			
		||||
  this->parent_->set_baud_rate(state);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace ld2412
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ class BaudRateSelect : public select::Select, public Parented<LD2412Component> {
 | 
			
		||||
  BaudRateSelect() = default;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void control(size_t index) override;
 | 
			
		||||
  void control(const std::string &value) override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace ld2412
 | 
			
		||||
 
 | 
			
		||||
@@ -3,9 +3,9 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace ld2412 {
 | 
			
		||||
 | 
			
		||||
void DistanceResolutionSelect::control(size_t index) {
 | 
			
		||||
  this->publish_state(index);
 | 
			
		||||
  this->parent_->set_distance_resolution(this->option_at(index));
 | 
			
		||||
void DistanceResolutionSelect::control(const std::string &value) {
 | 
			
		||||
  this->publish_state(value);
 | 
			
		||||
  this->parent_->set_distance_resolution(state);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace ld2412
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ class DistanceResolutionSelect : public select::Select, public Parented<LD2412Co
 | 
			
		||||
  DistanceResolutionSelect() = default;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void control(size_t index) override;
 | 
			
		||||
  void control(const std::string &value) override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace ld2412
 | 
			
		||||
 
 | 
			
		||||
@@ -3,8 +3,8 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace ld2412 {
 | 
			
		||||
 | 
			
		||||
void LightOutControlSelect::control(size_t index) {
 | 
			
		||||
  this->publish_state(index);
 | 
			
		||||
void LightOutControlSelect::control(const std::string &value) {
 | 
			
		||||
  this->publish_state(value);
 | 
			
		||||
  this->parent_->set_light_out_control();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ class LightOutControlSelect : public select::Select, public Parented<LD2412Compo
 | 
			
		||||
  LightOutControlSelect() = default;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void control(size_t index) override;
 | 
			
		||||
  void control(const std::string &value) override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace ld2412
 | 
			
		||||
 
 | 
			
		||||
@@ -131,8 +131,8 @@ static const uint8_t CMD_FRAME_STATUS = 7;
 | 
			
		||||
static const uint8_t CMD_ERROR_WORD = 8;
 | 
			
		||||
static const uint8_t ENERGY_SENSOR_START = 9;
 | 
			
		||||
static const uint8_t CALIBRATE_REPORT_INTERVAL = 4;
 | 
			
		||||
static const char *const OP_NORMAL_MODE_STRING = "Normal";
 | 
			
		||||
static const char *const OP_SIMPLE_MODE_STRING = "Simple";
 | 
			
		||||
static const std::string OP_NORMAL_MODE_STRING = "Normal";
 | 
			
		||||
static const std::string OP_SIMPLE_MODE_STRING = "Simple";
 | 
			
		||||
 | 
			
		||||
// Memory-efficient lookup tables
 | 
			
		||||
struct StringToUint8 {
 | 
			
		||||
@@ -379,7 +379,7 @@ void LD2420Component::report_gate_data() {
 | 
			
		||||
  ESP_LOGI(TAG, "Total samples: %d", this->total_sample_number_counter);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void LD2420Component::set_operating_mode(const char *state) {
 | 
			
		||||
void LD2420Component::set_operating_mode(const std::string &state) {
 | 
			
		||||
  // If unsupported firmware ignore mode select
 | 
			
		||||
  if (ld2420::get_firmware_int(firmware_ver_) >= CALIBRATE_VERSION_MIN) {
 | 
			
		||||
    this->current_operating_mode = find_uint8(OP_MODE_BY_STR, state);
 | 
			
		||||
 
 | 
			
		||||
@@ -107,7 +107,7 @@ class LD2420Component : public Component, public uart::UARTDevice {
 | 
			
		||||
  int send_cmd_from_array(CmdFrameT cmd_frame);
 | 
			
		||||
  void report_gate_data();
 | 
			
		||||
  void handle_cmd_error(uint8_t error);
 | 
			
		||||
  void set_operating_mode(const char *state);
 | 
			
		||||
  void set_operating_mode(const std::string &state);
 | 
			
		||||
  void auto_calibrate_sensitivity();
 | 
			
		||||
  void update_radar_data(uint16_t const *gate_energy, uint8_t sample_number);
 | 
			
		||||
  uint8_t set_config_mode(bool enable);
 | 
			
		||||
 
 | 
			
		||||
@@ -7,9 +7,9 @@ namespace ld2420 {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "ld2420.select";
 | 
			
		||||
 | 
			
		||||
void LD2420Select::control(size_t index) {
 | 
			
		||||
  this->publish_state(index);
 | 
			
		||||
  this->parent_->set_operating_mode(this->option_at(index));
 | 
			
		||||
void LD2420Select::control(const std::string &value) {
 | 
			
		||||
  this->publish_state(value);
 | 
			
		||||
  this->parent_->set_operating_mode(value);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace ld2420
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ class LD2420Select : public Component, public select::Select, public Parented<LD
 | 
			
		||||
  LD2420Select() = default;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void control(size_t index) override;
 | 
			
		||||
  void control(const std::string &value) override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace ld2420
 | 
			
		||||
 
 | 
			
		||||
@@ -380,7 +380,7 @@ void LD2450Component::read_all_info() {
 | 
			
		||||
  this->set_config_mode_(false);
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
  const auto baud_rate = std::to_string(this->parent_->get_baud_rate());
 | 
			
		||||
  if (this->baud_rate_select_ != nullptr && strcmp(this->baud_rate_select_->current_option(), baud_rate.c_str()) != 0) {
 | 
			
		||||
  if (this->baud_rate_select_ != nullptr && this->baud_rate_select_->state != baud_rate) {
 | 
			
		||||
    this->baud_rate_select_->publish_state(baud_rate);
 | 
			
		||||
  }
 | 
			
		||||
  this->publish_zone_type();
 | 
			
		||||
@@ -635,7 +635,7 @@ bool LD2450Component::handle_ack_data_() {
 | 
			
		||||
      ESP_LOGV(TAG, "Baud rate change");
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
      if (this->baud_rate_select_ != nullptr) {
 | 
			
		||||
        ESP_LOGE(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->current_option());
 | 
			
		||||
        ESP_LOGE(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->state.c_str());
 | 
			
		||||
      }
 | 
			
		||||
#endif
 | 
			
		||||
      break;
 | 
			
		||||
@@ -716,7 +716,7 @@ bool LD2450Component::handle_ack_data_() {
 | 
			
		||||
      this->publish_zone_type();
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
      if (this->zone_type_select_ != nullptr) {
 | 
			
		||||
        ESP_LOGV(TAG, "Change zone type to: %s", this->zone_type_select_->current_option());
 | 
			
		||||
        ESP_LOGV(TAG, "Change zone type to: %s", this->zone_type_select_->state.c_str());
 | 
			
		||||
      }
 | 
			
		||||
#endif
 | 
			
		||||
      if (this->buffer_data_[10] == 0x00) {
 | 
			
		||||
@@ -790,7 +790,7 @@ void LD2450Component::set_bluetooth(bool enable) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Set Baud rate
 | 
			
		||||
void LD2450Component::set_baud_rate(const char *state) {
 | 
			
		||||
void LD2450Component::set_baud_rate(const std::string &state) {
 | 
			
		||||
  this->set_config_mode_(true);
 | 
			
		||||
  const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00};
 | 
			
		||||
  this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value));
 | 
			
		||||
@@ -798,8 +798,8 @@ void LD2450Component::set_baud_rate(const char *state) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Set Zone Type - one of: Disabled, Detection, Filter
 | 
			
		||||
void LD2450Component::set_zone_type(const char *state) {
 | 
			
		||||
  ESP_LOGV(TAG, "Set zone type: %s", state);
 | 
			
		||||
void LD2450Component::set_zone_type(const std::string &state) {
 | 
			
		||||
  ESP_LOGV(TAG, "Set zone type: %s", state.c_str());
 | 
			
		||||
  uint8_t zone_type = find_uint8(ZONE_TYPE_BY_STR, state);
 | 
			
		||||
  this->zone_type_ = zone_type;
 | 
			
		||||
  this->send_set_zone_command_();
 | 
			
		||||
 
 | 
			
		||||
@@ -115,8 +115,8 @@ class LD2450Component : public Component, public uart::UARTDevice {
 | 
			
		||||
  void restart_and_read_all_info();
 | 
			
		||||
  void set_bluetooth(bool enable);
 | 
			
		||||
  void set_multi_target(bool enable);
 | 
			
		||||
  void set_baud_rate(const char *state);
 | 
			
		||||
  void set_zone_type(const char *state);
 | 
			
		||||
  void set_baud_rate(const std::string &state);
 | 
			
		||||
  void set_zone_type(const std::string &state);
 | 
			
		||||
  void publish_zone_type();
 | 
			
		||||
  void factory_reset();
 | 
			
		||||
#ifdef USE_TEXT_SENSOR
 | 
			
		||||
 
 | 
			
		||||
@@ -3,9 +3,9 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace ld2450 {
 | 
			
		||||
 | 
			
		||||
void BaudRateSelect::control(size_t index) {
 | 
			
		||||
  this->publish_state(index);
 | 
			
		||||
  this->parent_->set_baud_rate(this->option_at(index));
 | 
			
		||||
void BaudRateSelect::control(const std::string &value) {
 | 
			
		||||
  this->publish_state(value);
 | 
			
		||||
  this->parent_->set_baud_rate(state);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace ld2450
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ class BaudRateSelect : public select::Select, public Parented<LD2450Component> {
 | 
			
		||||
  BaudRateSelect() = default;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void control(size_t index) override;
 | 
			
		||||
  void control(const std::string &value) override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace ld2450
 | 
			
		||||
 
 | 
			
		||||
@@ -3,9 +3,9 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace ld2450 {
 | 
			
		||||
 | 
			
		||||
void ZoneTypeSelect::control(size_t index) {
 | 
			
		||||
  this->publish_state(index);
 | 
			
		||||
  this->parent_->set_zone_type(this->option_at(index));
 | 
			
		||||
void ZoneTypeSelect::control(const std::string &value) {
 | 
			
		||||
  this->publish_state(value);
 | 
			
		||||
  this->parent_->set_zone_type(state);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace ld2450
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ class ZoneTypeSelect : public select::Select, public Parented<LD2450Component> {
 | 
			
		||||
  ZoneTypeSelect() = default;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void control(size_t index) override;
 | 
			
		||||
  void control(const std::string &value) override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace ld2450
 | 
			
		||||
 
 | 
			
		||||
@@ -3,10 +3,10 @@
 | 
			
		||||
namespace esphome::logger {
 | 
			
		||||
 | 
			
		||||
void LoggerLevelSelect::publish_state(int level) {
 | 
			
		||||
  auto index = level_to_index(level);
 | 
			
		||||
  if (!this->has_index(index))
 | 
			
		||||
  const auto &option = this->at(level_to_index(level));
 | 
			
		||||
  if (!option)
 | 
			
		||||
    return;
 | 
			
		||||
  Select::publish_state(index);
 | 
			
		||||
  Select::publish_state(option.value());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void LoggerLevelSelect::setup() {
 | 
			
		||||
@@ -14,6 +14,11 @@ void LoggerLevelSelect::setup() {
 | 
			
		||||
  this->publish_state(this->parent_->get_log_level());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void LoggerLevelSelect::control(size_t index) { this->parent_->set_log_level(index_to_level(index)); }
 | 
			
		||||
void LoggerLevelSelect::control(const std::string &value) {
 | 
			
		||||
  const auto index = this->index_of(value);
 | 
			
		||||
  if (!index)
 | 
			
		||||
    return;
 | 
			
		||||
  this->parent_->set_log_level(index_to_level(index.value()));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::logger
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ class LoggerLevelSelect : public Component, public select::Select, public Parent
 | 
			
		||||
 public:
 | 
			
		||||
  void publish_state(int level);
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void control(size_t index) override;
 | 
			
		||||
  void control(const std::string &value) override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  // Convert log level to option index (skip CONFIG at level 4)
 | 
			
		||||
 
 | 
			
		||||
@@ -41,16 +41,16 @@ class LVGLSelect : public select::Select, public Component {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void publish() {
 | 
			
		||||
    auto index = this->widget_->get_selected_index();
 | 
			
		||||
    this->publish_state(index);
 | 
			
		||||
    this->publish_state(this->widget_->get_selected_text());
 | 
			
		||||
    if (this->restore_) {
 | 
			
		||||
      auto index = this->widget_->get_selected_index();
 | 
			
		||||
      this->pref_.save(&index);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void control(size_t index) override {
 | 
			
		||||
    this->widget_->set_selected_index(index, this->anim_);
 | 
			
		||||
  void control(const std::string &value) override {
 | 
			
		||||
    this->widget_->set_selected_text(value, this->anim_);
 | 
			
		||||
    this->publish();
 | 
			
		||||
  }
 | 
			
		||||
  void set_options_() {
 | 
			
		||||
 
 | 
			
		||||
@@ -28,9 +28,8 @@ void ModbusSelect::parse_and_publish(const std::vector<uint8_t> &data) {
 | 
			
		||||
 | 
			
		||||
    if (map_it != this->mapping_.cend()) {
 | 
			
		||||
      size_t idx = std::distance(this->mapping_.cbegin(), map_it);
 | 
			
		||||
      ESP_LOGV(TAG, "Found option %s for value %lld", this->option_at(idx), value);
 | 
			
		||||
      this->publish_state(idx);
 | 
			
		||||
      return;
 | 
			
		||||
      new_state = std::string(this->option_at(idx));
 | 
			
		||||
      ESP_LOGV(TAG, "Found option %s for value %lld", new_state->c_str(), value);
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGE(TAG, "No option found for mapping %lld", value);
 | 
			
		||||
    }
 | 
			
		||||
@@ -41,16 +40,19 @@ void ModbusSelect::parse_and_publish(const std::vector<uint8_t> &data) {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ModbusSelect::control(size_t index) {
 | 
			
		||||
  optional<int64_t> mapval = this->mapping_[index];
 | 
			
		||||
  const char *option = this->option_at(index);
 | 
			
		||||
  ESP_LOGD(TAG, "Found value %lld for option '%s'", *mapval, option);
 | 
			
		||||
void ModbusSelect::control(const std::string &value) {
 | 
			
		||||
  auto idx = this->index_of(value);
 | 
			
		||||
  if (!idx.has_value()) {
 | 
			
		||||
    ESP_LOGW(TAG, "Invalid option '%s'", value.c_str());
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  optional<int64_t> mapval = this->mapping_[idx.value()];
 | 
			
		||||
  ESP_LOGD(TAG, "Found value %lld for option '%s'", *mapval, value.c_str());
 | 
			
		||||
 | 
			
		||||
  std::vector<uint16_t> data;
 | 
			
		||||
 | 
			
		||||
  if (this->write_transform_func_.has_value()) {
 | 
			
		||||
    // Transform func requires string parameter for backward compatibility
 | 
			
		||||
    auto val = (*this->write_transform_func_)(this, std::string(option), *mapval, data);
 | 
			
		||||
    auto val = (*this->write_transform_func_)(this, value, *mapval, data);
 | 
			
		||||
    if (val.has_value()) {
 | 
			
		||||
      mapval = *val;
 | 
			
		||||
      ESP_LOGV(TAG, "write_lambda returned mapping value %lld", *mapval);
 | 
			
		||||
@@ -83,7 +85,7 @@ void ModbusSelect::control(size_t index) {
 | 
			
		||||
  this->parent_->queue_command(write_cmd);
 | 
			
		||||
 | 
			
		||||
  if (this->optimistic_)
 | 
			
		||||
    this->publish_state(index);
 | 
			
		||||
    this->publish_state(value);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace modbus_controller
 | 
			
		||||
 
 | 
			
		||||
@@ -38,7 +38,7 @@ class ModbusSelect : public Component, public select::Select, public SensorItem
 | 
			
		||||
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  void parse_and_publish(const std::vector<uint8_t> &data) override;
 | 
			
		||||
  void control(size_t index) override;
 | 
			
		||||
  void control(const std::string &value) override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  std::vector<int64_t> mapping_{};
 | 
			
		||||
 
 | 
			
		||||
@@ -72,7 +72,7 @@ bool MopekaStdCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device)
 | 
			
		||||
 | 
			
		||||
  const u_int8_t hardware_id = mopeka_data->data_1 & 0xCF;
 | 
			
		||||
  if (static_cast<SensorType>(hardware_id) != STANDARD && static_cast<SensorType>(hardware_id) != XL &&
 | 
			
		||||
      static_cast<SensorType>(hardware_id) != ETRAILER) {
 | 
			
		||||
      static_cast<SensorType>(hardware_id) != ETRAILER && static_cast<SensorType>(hardware_id) != STANDARD_ALT) {
 | 
			
		||||
    ESP_LOGE(TAG, "[%s] Unsupported Sensor Type (0x%X)", device.address_str().c_str(), hardware_id);
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,7 @@ namespace mopeka_std_check {
 | 
			
		||||
enum SensorType {
 | 
			
		||||
  STANDARD = 0x02,
 | 
			
		||||
  XL = 0x03,
 | 
			
		||||
  STANDARD_ALT = 0x44,
 | 
			
		||||
  ETRAILER = 0x46,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@ import re
 | 
			
		||||
from esphome import automation
 | 
			
		||||
from esphome.automation import Condition
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import logger
 | 
			
		||||
from esphome.components import logger, socket
 | 
			
		||||
from esphome.components.esp32 import add_idf_sdkconfig_option
 | 
			
		||||
from esphome.config_helpers import filter_source_files_from_platform
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
@@ -66,6 +66,9 @@ DEPENDENCIES = ["network"]
 | 
			
		||||
def AUTO_LOAD():
 | 
			
		||||
    if CORE.is_esp8266 or CORE.is_libretiny:
 | 
			
		||||
        return ["async_tcp", "json"]
 | 
			
		||||
    # ESP32 needs socket for wake_loop_threadsafe()
 | 
			
		||||
    if CORE.is_esp32:
 | 
			
		||||
        return ["json", "socket"]
 | 
			
		||||
    return ["json"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -213,8 +216,6 @@ def validate_fingerprint(value):
 | 
			
		||||
 | 
			
		||||
def _consume_mqtt_sockets(config: ConfigType) -> ConfigType:
 | 
			
		||||
    """Register socket needs for MQTT component."""
 | 
			
		||||
    from esphome.components import socket
 | 
			
		||||
 | 
			
		||||
    # MQTT needs 1 socket for the broker connection
 | 
			
		||||
    socket.consume_sockets(1, "mqtt")(config)
 | 
			
		||||
    return config
 | 
			
		||||
@@ -341,6 +342,11 @@ async def to_code(config):
 | 
			
		||||
        # https://github.com/heman/async-mqtt-client/blob/master/library.json
 | 
			
		||||
        cg.add_library("heman/AsyncMqttClient-esphome", "2.0.0")
 | 
			
		||||
 | 
			
		||||
    # MQTT on ESP32 uses wake_loop_threadsafe() to wake the main loop from the MQTT event handler
 | 
			
		||||
    # This enables low-latency MQTT event processing instead of waiting for select() timeout
 | 
			
		||||
    if CORE.is_esp32:
 | 
			
		||||
        socket.require_wake_loop_threadsafe()
 | 
			
		||||
 | 
			
		||||
    cg.add_define("USE_MQTT")
 | 
			
		||||
    cg.add_global(mqtt_ns.using)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -190,6 +190,11 @@ void MQTTBackendESP32::mqtt_event_handler(void *handler_args, esp_event_base_t b
 | 
			
		||||
  if (instance) {
 | 
			
		||||
    auto event = *static_cast<esp_mqtt_event_t *>(event_data);
 | 
			
		||||
    instance->mqtt_events_.emplace(event);
 | 
			
		||||
 | 
			
		||||
    // Wake main loop immediately to process MQTT event instead of waiting for select() timeout
 | 
			
		||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
 | 
			
		||||
    App.wake_loop_threadsafe();
 | 
			
		||||
#endif
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -21,8 +21,7 @@ void MQTTSelectComponent::setup() {
 | 
			
		||||
    call.set_option(state);
 | 
			
		||||
    call.perform();
 | 
			
		||||
  });
 | 
			
		||||
  this->select_->add_on_state_callback(
 | 
			
		||||
      [this](const std::string &state, size_t index) { this->publish_state(this->select_->option_at(index)); });
 | 
			
		||||
  this->select_->add_on_state_callback([this](const std::string &state, size_t index) { this->publish_state(state); });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void MQTTSelectComponent::dump_config() {
 | 
			
		||||
@@ -45,7 +44,7 @@ void MQTTSelectComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon
 | 
			
		||||
}
 | 
			
		||||
bool MQTTSelectComponent::send_initial_state() {
 | 
			
		||||
  if (this->select_->has_state()) {
 | 
			
		||||
    return this->publish_state(this->select_->current_option());
 | 
			
		||||
    return this->publish_state(this->select_->state);
 | 
			
		||||
  } else {
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -435,12 +435,12 @@ void MR24HPC1Component::r24_frame_parse_open_underlying_information_(uint8_t *da
 | 
			
		||||
  } else if ((this->existence_boundary_select_ != nullptr) &&
 | 
			
		||||
             ((data[FRAME_COMMAND_WORD_INDEX] == 0x0a) || (data[FRAME_COMMAND_WORD_INDEX] == 0x8a))) {
 | 
			
		||||
    if (this->existence_boundary_select_->has_index(data[FRAME_DATA_INDEX] - 1)) {
 | 
			
		||||
      this->existence_boundary_select_->publish_state(data[FRAME_DATA_INDEX] - 1);
 | 
			
		||||
      this->existence_boundary_select_->publish_state(S_BOUNDARY_STR[data[FRAME_DATA_INDEX] - 1]);
 | 
			
		||||
    }
 | 
			
		||||
  } else if ((this->motion_boundary_select_ != nullptr) &&
 | 
			
		||||
             ((data[FRAME_COMMAND_WORD_INDEX] == 0x0b) || (data[FRAME_COMMAND_WORD_INDEX] == 0x8b))) {
 | 
			
		||||
    if (this->motion_boundary_select_->has_index(data[FRAME_DATA_INDEX] - 1)) {
 | 
			
		||||
      this->motion_boundary_select_->publish_state(data[FRAME_DATA_INDEX] - 1);
 | 
			
		||||
      this->motion_boundary_select_->publish_state(S_BOUNDARY_STR[data[FRAME_DATA_INDEX] - 1]);
 | 
			
		||||
    }
 | 
			
		||||
  } else if ((this->motion_trigger_number_ != nullptr) &&
 | 
			
		||||
             ((data[FRAME_COMMAND_WORD_INDEX] == 0x0c) || (data[FRAME_COMMAND_WORD_INDEX] == 0x8c))) {
 | 
			
		||||
@@ -515,7 +515,7 @@ void MR24HPC1Component::r24_frame_parse_work_status_(uint8_t *data) {
 | 
			
		||||
    ESP_LOGD(TAG, "Reply: get radar init status 0x%02X", data[FRAME_DATA_INDEX]);
 | 
			
		||||
  } else if (data[FRAME_COMMAND_WORD_INDEX] == 0x07) {
 | 
			
		||||
    if ((this->scene_mode_select_ != nullptr) && (this->scene_mode_select_->has_index(data[FRAME_DATA_INDEX]))) {
 | 
			
		||||
      this->scene_mode_select_->publish_state(data[FRAME_DATA_INDEX]);
 | 
			
		||||
      this->scene_mode_select_->publish_state(S_SCENE_STR[data[FRAME_DATA_INDEX]]);
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGD(TAG, "Select has index offset %d Error", data[FRAME_DATA_INDEX]);
 | 
			
		||||
    }
 | 
			
		||||
@@ -538,7 +538,7 @@ void MR24HPC1Component::r24_frame_parse_work_status_(uint8_t *data) {
 | 
			
		||||
    ESP_LOGD(TAG, "Reply: get radar init status 0x%02X", data[FRAME_DATA_INDEX]);
 | 
			
		||||
  } else if (data[FRAME_COMMAND_WORD_INDEX] == 0x87) {
 | 
			
		||||
    if ((this->scene_mode_select_ != nullptr) && (this->scene_mode_select_->has_index(data[FRAME_DATA_INDEX]))) {
 | 
			
		||||
      this->scene_mode_select_->publish_state(data[FRAME_DATA_INDEX]);
 | 
			
		||||
      this->scene_mode_select_->publish_state(S_SCENE_STR[data[FRAME_DATA_INDEX]]);
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGD(TAG, "Select has index offset %d Error", data[FRAME_DATA_INDEX]);
 | 
			
		||||
    }
 | 
			
		||||
@@ -581,7 +581,7 @@ void MR24HPC1Component::r24_frame_parse_human_information_(uint8_t *data) {
 | 
			
		||||
             ((data[FRAME_COMMAND_WORD_INDEX] == 0x0A) || (data[FRAME_COMMAND_WORD_INDEX] == 0x8A))) {
 | 
			
		||||
    // none:0x00  1s:0x01 30s:0x02 1min:0x03 2min:0x04 5min:0x05 10min:0x06 30min:0x07 1hour:0x08
 | 
			
		||||
    if (data[FRAME_DATA_INDEX] < 9) {
 | 
			
		||||
      this->unman_time_select_->publish_state(data[FRAME_DATA_INDEX]);
 | 
			
		||||
      this->unman_time_select_->publish_state(S_UNMANNED_TIME_STR[data[FRAME_DATA_INDEX]]);
 | 
			
		||||
    }
 | 
			
		||||
  } else if ((this->keep_away_text_sensor_ != nullptr) &&
 | 
			
		||||
             ((data[FRAME_COMMAND_WORD_INDEX] == 0x0B) || (data[FRAME_COMMAND_WORD_INDEX] == 0x8B))) {
 | 
			
		||||
 
 | 
			
		||||
@@ -3,9 +3,12 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace seeed_mr24hpc1 {
 | 
			
		||||
 | 
			
		||||
void ExistenceBoundarySelect::control(size_t index) {
 | 
			
		||||
  this->publish_state(index);
 | 
			
		||||
  this->parent_->set_existence_boundary(index);
 | 
			
		||||
void ExistenceBoundarySelect::control(const std::string &value) {
 | 
			
		||||
  this->publish_state(value);
 | 
			
		||||
  auto index = this->index_of(value);
 | 
			
		||||
  if (index.has_value()) {
 | 
			
		||||
    this->parent_->set_existence_boundary(index.value());
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace seeed_mr24hpc1
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ class ExistenceBoundarySelect : public select::Select, public Parented<MR24HPC1C
 | 
			
		||||
  ExistenceBoundarySelect() = default;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void control(size_t index) override;
 | 
			
		||||
  void control(const std::string &value) override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace seeed_mr24hpc1
 | 
			
		||||
 
 | 
			
		||||
@@ -3,9 +3,12 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace seeed_mr24hpc1 {
 | 
			
		||||
 | 
			
		||||
void MotionBoundarySelect::control(size_t index) {
 | 
			
		||||
  this->publish_state(index);
 | 
			
		||||
  this->parent_->set_motion_boundary(index);
 | 
			
		||||
void MotionBoundarySelect::control(const std::string &value) {
 | 
			
		||||
  this->publish_state(value);
 | 
			
		||||
  auto index = this->index_of(value);
 | 
			
		||||
  if (index.has_value()) {
 | 
			
		||||
    this->parent_->set_motion_boundary(index.value());
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace seeed_mr24hpc1
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ class MotionBoundarySelect : public select::Select, public Parented<MR24HPC1Comp
 | 
			
		||||
  MotionBoundarySelect() = default;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void control(size_t index) override;
 | 
			
		||||
  void control(const std::string &value) override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace seeed_mr24hpc1
 | 
			
		||||
 
 | 
			
		||||
@@ -3,9 +3,12 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace seeed_mr24hpc1 {
 | 
			
		||||
 | 
			
		||||
void SceneModeSelect::control(size_t index) {
 | 
			
		||||
  this->publish_state(index);
 | 
			
		||||
  this->parent_->set_scene_mode(index);
 | 
			
		||||
void SceneModeSelect::control(const std::string &value) {
 | 
			
		||||
  this->publish_state(value);
 | 
			
		||||
  auto index = this->index_of(value);
 | 
			
		||||
  if (index.has_value()) {
 | 
			
		||||
    this->parent_->set_scene_mode(index.value());
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace seeed_mr24hpc1
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ class SceneModeSelect : public select::Select, public Parented<MR24HPC1Component
 | 
			
		||||
  SceneModeSelect() = default;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void control(size_t index) override;
 | 
			
		||||
  void control(const std::string &value) override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace seeed_mr24hpc1
 | 
			
		||||
 
 | 
			
		||||
@@ -3,9 +3,12 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace seeed_mr24hpc1 {
 | 
			
		||||
 | 
			
		||||
void UnmanTimeSelect::control(size_t index) {
 | 
			
		||||
  this->publish_state(index);
 | 
			
		||||
  this->parent_->set_unman_time(index);
 | 
			
		||||
void UnmanTimeSelect::control(const std::string &value) {
 | 
			
		||||
  this->publish_state(value);
 | 
			
		||||
  auto index = this->index_of(value);
 | 
			
		||||
  if (index.has_value()) {
 | 
			
		||||
    this->parent_->set_unman_time(index.value());
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace seeed_mr24hpc1
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ class UnmanTimeSelect : public select::Select, public Parented<MR24HPC1Component
 | 
			
		||||
  UnmanTimeSelect() = default;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void control(size_t index) override;
 | 
			
		||||
  void control(const std::string &value) override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace seeed_mr24hpc1
 | 
			
		||||
 
 | 
			
		||||
@@ -292,7 +292,7 @@ void MR60FDA2Component::process_frame_() {
 | 
			
		||||
 | 
			
		||||
        install_height_float = bit_cast<float>(current_install_height_int);
 | 
			
		||||
        uint32_t select_index = find_nearest_index(install_height_float, INSTALL_HEIGHT, 7);
 | 
			
		||||
        this->install_height_select_->publish_state(select_index);
 | 
			
		||||
        this->install_height_select_->publish_state(this->install_height_select_->at(select_index).value());
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (this->height_threshold_select_ != nullptr) {
 | 
			
		||||
@@ -301,7 +301,7 @@ void MR60FDA2Component::process_frame_() {
 | 
			
		||||
 | 
			
		||||
        height_threshold_float = bit_cast<float>(current_height_threshold_int);
 | 
			
		||||
        size_t select_index = find_nearest_index(height_threshold_float, HEIGHT_THRESHOLD, 7);
 | 
			
		||||
        this->height_threshold_select_->publish_state(select_index);
 | 
			
		||||
        this->height_threshold_select_->publish_state(this->height_threshold_select_->at(select_index).value());
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (this->sensitivity_select_ != nullptr) {
 | 
			
		||||
@@ -309,7 +309,7 @@ void MR60FDA2Component::process_frame_() {
 | 
			
		||||
            encode_uint32(current_data_buf_[11], current_data_buf_[10], current_data_buf_[9], current_data_buf_[8]);
 | 
			
		||||
 | 
			
		||||
        uint32_t select_index = find_nearest_index(current_sensitivity, SENSITIVITY, 3);
 | 
			
		||||
        this->sensitivity_select_->publish_state(select_index);
 | 
			
		||||
        this->sensitivity_select_->publish_state(this->sensitivity_select_->at(select_index).value());
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      ESP_LOGD(TAG, "Mounting height: %.2f, Height threshold: %.2f, Sensitivity: %" PRIu32, install_height_float,
 | 
			
		||||
 
 | 
			
		||||
@@ -3,9 +3,12 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace seeed_mr60fda2 {
 | 
			
		||||
 | 
			
		||||
void HeightThresholdSelect::control(size_t index) {
 | 
			
		||||
  this->publish_state(index);
 | 
			
		||||
  this->parent_->set_height_threshold(index);
 | 
			
		||||
void HeightThresholdSelect::control(const std::string &value) {
 | 
			
		||||
  this->publish_state(value);
 | 
			
		||||
  auto index = this->index_of(value);
 | 
			
		||||
  if (index.has_value()) {
 | 
			
		||||
    this->parent_->set_height_threshold(index.value());
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace seeed_mr60fda2
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ class HeightThresholdSelect : public select::Select, public Parented<MR60FDA2Com
 | 
			
		||||
  HeightThresholdSelect() = default;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void control(size_t index) override;
 | 
			
		||||
  void control(const std::string &value) override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace seeed_mr60fda2
 | 
			
		||||
 
 | 
			
		||||
@@ -3,9 +3,12 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace seeed_mr60fda2 {
 | 
			
		||||
 | 
			
		||||
void InstallHeightSelect::control(size_t index) {
 | 
			
		||||
  this->publish_state(index);
 | 
			
		||||
  this->parent_->set_install_height(index);
 | 
			
		||||
void InstallHeightSelect::control(const std::string &value) {
 | 
			
		||||
  this->publish_state(value);
 | 
			
		||||
  auto index = this->index_of(value);
 | 
			
		||||
  if (index.has_value()) {
 | 
			
		||||
    this->parent_->set_install_height(index.value());
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace seeed_mr60fda2
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ class InstallHeightSelect : public select::Select, public Parented<MR60FDA2Compo
 | 
			
		||||
  InstallHeightSelect() = default;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void control(size_t index) override;
 | 
			
		||||
  void control(const std::string &value) override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace seeed_mr60fda2
 | 
			
		||||
 
 | 
			
		||||
@@ -3,9 +3,12 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace seeed_mr60fda2 {
 | 
			
		||||
 | 
			
		||||
void SensitivitySelect::control(size_t index) {
 | 
			
		||||
  this->publish_state(index);
 | 
			
		||||
  this->parent_->set_sensitivity(index);
 | 
			
		||||
void SensitivitySelect::control(const std::string &value) {
 | 
			
		||||
  this->publish_state(value);
 | 
			
		||||
  auto index = this->index_of(value);
 | 
			
		||||
  if (index.has_value()) {
 | 
			
		||||
    this->parent_->set_sensitivity(index.value());
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace seeed_mr60fda2
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ class SensitivitySelect : public select::Select, public Parented<MR60FDA2Compone
 | 
			
		||||
  SensitivitySelect() = default;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void control(size_t index) override;
 | 
			
		||||
  void control(const std::string &value) override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace seeed_mr60fda2
 | 
			
		||||
 
 | 
			
		||||
@@ -7,43 +7,24 @@ namespace select {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "select";
 | 
			
		||||
 | 
			
		||||
void Select::publish_state(const std::string &state) { this->publish_state(state.c_str()); }
 | 
			
		||||
 | 
			
		||||
void Select::publish_state(const char *state) {
 | 
			
		||||
void Select::publish_state(const std::string &state) {
 | 
			
		||||
  auto index = this->index_of(state);
 | 
			
		||||
  const auto *name = this->get_name().c_str();
 | 
			
		||||
  if (index.has_value()) {
 | 
			
		||||
    this->publish_state(index.value());
 | 
			
		||||
    this->set_has_state(true);
 | 
			
		||||
    this->state = state;
 | 
			
		||||
    ESP_LOGD(TAG, "'%s': Sending state %s (index %zu)", name, state.c_str(), index.value());
 | 
			
		||||
    this->state_callback_.call(state, index.value());
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGE(TAG, "'%s': Invalid option %s", this->get_name().c_str(), state);
 | 
			
		||||
    ESP_LOGE(TAG, "'%s': invalid state for publish_state(): %s", name, state.c_str());
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Select::publish_state(size_t index) {
 | 
			
		||||
  if (!this->has_index(index)) {
 | 
			
		||||
    ESP_LOGE(TAG, "'%s': Invalid index %zu", this->get_name().c_str(), index);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  const char *option = this->option_at(index);
 | 
			
		||||
  this->set_has_state(true);
 | 
			
		||||
  this->active_index_ = index;
 | 
			
		||||
#pragma GCC diagnostic push
 | 
			
		||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
 | 
			
		||||
  this->state = option;  // Update deprecated member for backward compatibility
 | 
			
		||||
#pragma GCC diagnostic pop
 | 
			
		||||
  ESP_LOGD(TAG, "'%s': Sending state %s (index %zu)", this->get_name().c_str(), option, index);
 | 
			
		||||
  // Callback signature requires std::string, create temporary for compatibility
 | 
			
		||||
  this->state_callback_.call(std::string(option), index);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const char *Select::current_option() const { return this->has_state() ? this->option_at(this->active_index_) : ""; }
 | 
			
		||||
 | 
			
		||||
void Select::add_on_state_callback(std::function<void(std::string, size_t)> &&callback) {
 | 
			
		||||
  this->state_callback_.add(std::move(callback));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool Select::has_option(const std::string &option) const { return this->index_of(option.c_str()).has_value(); }
 | 
			
		||||
 | 
			
		||||
bool Select::has_option(const char *option) const { return this->index_of(option).has_value(); }
 | 
			
		||||
bool Select::has_option(const std::string &option) const { return this->index_of(option).has_value(); }
 | 
			
		||||
 | 
			
		||||
bool Select::has_index(size_t index) const { return index < this->size(); }
 | 
			
		||||
 | 
			
		||||
@@ -52,12 +33,10 @@ size_t Select::size() const {
 | 
			
		||||
  return options.size();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
optional<size_t> Select::index_of(const std::string &option) const { return this->index_of(option.c_str()); }
 | 
			
		||||
 | 
			
		||||
optional<size_t> Select::index_of(const char *option) const {
 | 
			
		||||
optional<size_t> Select::index_of(const std::string &option) const {
 | 
			
		||||
  const auto &options = traits.get_options();
 | 
			
		||||
  for (size_t i = 0; i < options.size(); i++) {
 | 
			
		||||
    if (strcmp(options[i], option) == 0) {
 | 
			
		||||
    if (strcmp(options[i], option.c_str()) == 0) {
 | 
			
		||||
      return i;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -66,17 +45,19 @@ optional<size_t> Select::index_of(const char *option) const {
 | 
			
		||||
 | 
			
		||||
optional<size_t> Select::active_index() const {
 | 
			
		||||
  if (this->has_state()) {
 | 
			
		||||
    return this->active_index_;
 | 
			
		||||
    return this->index_of(this->state);
 | 
			
		||||
  } else {
 | 
			
		||||
    return {};
 | 
			
		||||
  }
 | 
			
		||||
  return {};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
optional<std::string> Select::at(size_t index) const {
 | 
			
		||||
  if (this->has_index(index)) {
 | 
			
		||||
    const auto &options = traits.get_options();
 | 
			
		||||
    return std::string(options.at(index));
 | 
			
		||||
  } else {
 | 
			
		||||
    return {};
 | 
			
		||||
  }
 | 
			
		||||
  return {};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const char *Select::option_at(size_t index) const { return traits.get_options().at(index); }
 | 
			
		||||
 
 | 
			
		||||
@@ -30,31 +30,16 @@ namespace select {
 | 
			
		||||
 */
 | 
			
		||||
class Select : public EntityBase {
 | 
			
		||||
 public:
 | 
			
		||||
  std::string state;
 | 
			
		||||
  SelectTraits traits;
 | 
			
		||||
 | 
			
		||||
#pragma GCC diagnostic push
 | 
			
		||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
 | 
			
		||||
  /// @deprecated Use current_option() instead. This member will be removed in ESPHome 2026.5.0.
 | 
			
		||||
  __attribute__((deprecated("Use current_option() instead of .state. Will be removed in 2026.5.0")))
 | 
			
		||||
  std::string state{};
 | 
			
		||||
 | 
			
		||||
  Select() = default;
 | 
			
		||||
  ~Select() = default;
 | 
			
		||||
#pragma GCC diagnostic pop
 | 
			
		||||
 | 
			
		||||
  void publish_state(const std::string &state);
 | 
			
		||||
  void publish_state(const char *state);
 | 
			
		||||
  void publish_state(size_t index);
 | 
			
		||||
 | 
			
		||||
  /// Return the currently selected option (as const char* from flash).
 | 
			
		||||
  const char *current_option() const;
 | 
			
		||||
 | 
			
		||||
  /// Instantiate a SelectCall object to modify this select component's state.
 | 
			
		||||
  SelectCall make_call() { return SelectCall(this); }
 | 
			
		||||
 | 
			
		||||
  /// Return whether this select component contains the provided option.
 | 
			
		||||
  bool has_option(const std::string &option) const;
 | 
			
		||||
  bool has_option(const char *option) const;
 | 
			
		||||
 | 
			
		||||
  /// Return whether this select component contains the provided index offset.
 | 
			
		||||
  bool has_index(size_t index) const;
 | 
			
		||||
@@ -64,7 +49,6 @@ class Select : public EntityBase {
 | 
			
		||||
 | 
			
		||||
  /// Find the (optional) index offset of the provided option value.
 | 
			
		||||
  optional<size_t> index_of(const std::string &option) const;
 | 
			
		||||
  optional<size_t> index_of(const char *option) const;
 | 
			
		||||
 | 
			
		||||
  /// Return the (optional) index offset of the currently active option.
 | 
			
		||||
  optional<size_t> active_index() const;
 | 
			
		||||
@@ -80,36 +64,13 @@ class Select : public EntityBase {
 | 
			
		||||
 protected:
 | 
			
		||||
  friend class SelectCall;
 | 
			
		||||
 | 
			
		||||
  size_t active_index_{0};
 | 
			
		||||
 | 
			
		||||
  /** Set the value of the select by index, this is an optional virtual method.
 | 
			
		||||
  /** Set the value of the select, this is a virtual method that each select integration must implement.
 | 
			
		||||
   *
 | 
			
		||||
   * IMPORTANT: At least ONE of the two control() methods must be overridden by derived classes.
 | 
			
		||||
   * Overriding this index-based version is PREFERRED as it avoids string conversions.
 | 
			
		||||
   * This method is called by the SelectCall.
 | 
			
		||||
   *
 | 
			
		||||
   * This method is called by the SelectCall when the index is already known.
 | 
			
		||||
   * Default implementation converts to string and calls control(const std::string&).
 | 
			
		||||
   *
 | 
			
		||||
   * @param index The index as validated by the SelectCall.
 | 
			
		||||
   * @param value The value as validated by the SelectCall.
 | 
			
		||||
   */
 | 
			
		||||
  virtual void control(size_t index) { this->control(this->option_at(index)); }
 | 
			
		||||
 | 
			
		||||
  /** Set the value of the select, this is a virtual method that each select integration can implement.
 | 
			
		||||
   *
 | 
			
		||||
   * IMPORTANT: At least ONE of the two control() methods must be overridden by derived classes.
 | 
			
		||||
   * Overriding control(size_t) is PREFERRED as it avoids string conversions.
 | 
			
		||||
   *
 | 
			
		||||
   * This method is called by control(size_t) when not overridden, or directly by external code.
 | 
			
		||||
   * Default implementation converts to index and calls control(size_t).
 | 
			
		||||
   *
 | 
			
		||||
   * @param value The value as validated by the caller.
 | 
			
		||||
   */
 | 
			
		||||
  virtual void control(const std::string &value) {
 | 
			
		||||
    auto index = this->index_of(value);
 | 
			
		||||
    if (index.has_value()) {
 | 
			
		||||
      this->control(index.value());
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  virtual void control(const std::string &value) = 0;
 | 
			
		||||
 | 
			
		||||
  CallbackManager<void(std::string, size_t)> state_callback_;
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -7,21 +7,19 @@ namespace select {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "select";
 | 
			
		||||
 | 
			
		||||
SelectCall &SelectCall::set_option(const std::string &option) { return this->with_option(option); }
 | 
			
		||||
 | 
			
		||||
SelectCall &SelectCall::set_option(const char *option) { return this->with_option(option); }
 | 
			
		||||
 | 
			
		||||
SelectCall &SelectCall::set_index(size_t index) { return this->with_index(index); }
 | 
			
		||||
 | 
			
		||||
SelectCall &SelectCall::select_next(bool cycle) { return this->with_operation(SELECT_OP_NEXT).with_cycle(cycle); }
 | 
			
		||||
 | 
			
		||||
SelectCall &SelectCall::select_previous(bool cycle) {
 | 
			
		||||
  return this->with_operation(SELECT_OP_PREVIOUS).with_cycle(cycle);
 | 
			
		||||
SelectCall &SelectCall::set_option(const std::string &option) {
 | 
			
		||||
  return with_operation(SELECT_OP_SET).with_option(option);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
SelectCall &SelectCall::select_first() { return this->with_operation(SELECT_OP_FIRST); }
 | 
			
		||||
SelectCall &SelectCall::set_index(size_t index) { return with_operation(SELECT_OP_SET_INDEX).with_index(index); }
 | 
			
		||||
 | 
			
		||||
SelectCall &SelectCall::select_last() { return this->with_operation(SELECT_OP_LAST); }
 | 
			
		||||
SelectCall &SelectCall::select_next(bool cycle) { return with_operation(SELECT_OP_NEXT).with_cycle(cycle); }
 | 
			
		||||
 | 
			
		||||
SelectCall &SelectCall::select_previous(bool cycle) { return with_operation(SELECT_OP_PREVIOUS).with_cycle(cycle); }
 | 
			
		||||
 | 
			
		||||
SelectCall &SelectCall::select_first() { return with_operation(SELECT_OP_FIRST); }
 | 
			
		||||
 | 
			
		||||
SelectCall &SelectCall::select_last() { return with_operation(SELECT_OP_LAST); }
 | 
			
		||||
 | 
			
		||||
SelectCall &SelectCall::with_operation(SelectOperation operation) {
 | 
			
		||||
  this->operation_ = operation;
 | 
			
		||||
@@ -33,96 +31,89 @@ SelectCall &SelectCall::with_cycle(bool cycle) {
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
SelectCall &SelectCall::with_option(const std::string &option) { return this->with_option(option.c_str()); }
 | 
			
		||||
 | 
			
		||||
SelectCall &SelectCall::with_option(const char *option) {
 | 
			
		||||
  this->operation_ = SELECT_OP_SET;
 | 
			
		||||
  // Find the option index - this validates the option exists
 | 
			
		||||
  this->index_ = this->parent_->index_of(option);
 | 
			
		||||
SelectCall &SelectCall::with_option(const std::string &option) {
 | 
			
		||||
  this->option_ = option;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
SelectCall &SelectCall::with_index(size_t index) {
 | 
			
		||||
  this->operation_ = SELECT_OP_SET;
 | 
			
		||||
  if (index >= this->parent_->size()) {
 | 
			
		||||
    ESP_LOGW(TAG, "'%s' - Index value %zu out of bounds", this->parent_->get_name().c_str(), index);
 | 
			
		||||
    this->index_ = {};  // Store nullopt for invalid index
 | 
			
		||||
  } else {
 | 
			
		||||
    this->index_ = index;
 | 
			
		||||
  }
 | 
			
		||||
  this->index_ = index;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
optional<size_t> SelectCall::calculate_target_index_(const char *name) {
 | 
			
		||||
  const auto &options = this->parent_->traits.get_options();
 | 
			
		||||
  if (options.empty()) {
 | 
			
		||||
    ESP_LOGW(TAG, "'%s' - Select has no options", name);
 | 
			
		||||
    return {};
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->operation_ == SELECT_OP_FIRST) {
 | 
			
		||||
    return 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->operation_ == SELECT_OP_LAST) {
 | 
			
		||||
    return options.size() - 1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->operation_ == SELECT_OP_SET) {
 | 
			
		||||
    ESP_LOGD(TAG, "'%s' - Setting", name);
 | 
			
		||||
    if (!this->index_.has_value()) {
 | 
			
		||||
      ESP_LOGW(TAG, "'%s' - No option set", name);
 | 
			
		||||
      return {};
 | 
			
		||||
    }
 | 
			
		||||
    return this->index_.value();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // SELECT_OP_NEXT or SELECT_OP_PREVIOUS
 | 
			
		||||
  ESP_LOGD(TAG, "'%s' - Selecting %s, with%s cycling", name,
 | 
			
		||||
           this->operation_ == SELECT_OP_NEXT ? LOG_STR_LITERAL("next") : LOG_STR_LITERAL("previous"),
 | 
			
		||||
           this->cycle_ ? LOG_STR_LITERAL("") : LOG_STR_LITERAL("out"));
 | 
			
		||||
 | 
			
		||||
  const auto size = options.size();
 | 
			
		||||
  if (!this->parent_->has_state()) {
 | 
			
		||||
    return this->operation_ == SELECT_OP_NEXT ? 0 : size - 1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Use cached active_index_ instead of index_of() lookup
 | 
			
		||||
  const auto active_index = this->parent_->active_index_;
 | 
			
		||||
  if (this->cycle_) {
 | 
			
		||||
    return (size + active_index + (this->operation_ == SELECT_OP_NEXT ? +1 : -1)) % size;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->operation_ == SELECT_OP_PREVIOUS && active_index > 0) {
 | 
			
		||||
    return active_index - 1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->operation_ == SELECT_OP_NEXT && active_index < size - 1) {
 | 
			
		||||
    return active_index + 1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return {};  // Can't navigate further without cycling
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void SelectCall::perform() {
 | 
			
		||||
  auto *parent = this->parent_;
 | 
			
		||||
  const auto *name = parent->get_name().c_str();
 | 
			
		||||
  const auto &traits = parent->traits;
 | 
			
		||||
  const auto &options = traits.get_options();
 | 
			
		||||
 | 
			
		||||
  if (this->operation_ == SELECT_OP_NONE) {
 | 
			
		||||
    ESP_LOGW(TAG, "'%s' - SelectCall performed without selecting an operation", name);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Calculate target index (with_index() and with_option() already validate bounds/existence)
 | 
			
		||||
  auto target_index = this->calculate_target_index_(name);
 | 
			
		||||
  if (!target_index.has_value()) {
 | 
			
		||||
  if (options.empty()) {
 | 
			
		||||
    ESP_LOGW(TAG, "'%s' - Cannot perform SelectCall, select has no options", name);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  auto idx = target_index.value();
 | 
			
		||||
  // All operations use indices, call control() by index to avoid string conversion
 | 
			
		||||
  ESP_LOGD(TAG, "'%s' - Set selected option to: %s", name, parent->option_at(idx));
 | 
			
		||||
  parent->control(idx);
 | 
			
		||||
  std::string target_value;
 | 
			
		||||
 | 
			
		||||
  if (this->operation_ == SELECT_OP_SET) {
 | 
			
		||||
    ESP_LOGD(TAG, "'%s' - Setting", name);
 | 
			
		||||
    if (!this->option_.has_value()) {
 | 
			
		||||
      ESP_LOGW(TAG, "'%s' - No option value set for SelectCall", name);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    target_value = this->option_.value();
 | 
			
		||||
  } else if (this->operation_ == SELECT_OP_SET_INDEX) {
 | 
			
		||||
    if (!this->index_.has_value()) {
 | 
			
		||||
      ESP_LOGW(TAG, "'%s' - No index value set for SelectCall", name);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (this->index_.value() >= options.size()) {
 | 
			
		||||
      ESP_LOGW(TAG, "'%s' - Index value %zu out of bounds", name, this->index_.value());
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    target_value = options[this->index_.value()];
 | 
			
		||||
  } else if (this->operation_ == SELECT_OP_FIRST) {
 | 
			
		||||
    target_value = options.front();
 | 
			
		||||
  } else if (this->operation_ == SELECT_OP_LAST) {
 | 
			
		||||
    target_value = options.back();
 | 
			
		||||
  } else if (this->operation_ == SELECT_OP_NEXT || this->operation_ == SELECT_OP_PREVIOUS) {
 | 
			
		||||
    auto cycle = this->cycle_;
 | 
			
		||||
    ESP_LOGD(TAG, "'%s' - Selecting %s, with%s cycling", name, this->operation_ == SELECT_OP_NEXT ? "next" : "previous",
 | 
			
		||||
             cycle ? "" : "out");
 | 
			
		||||
    if (!parent->has_state()) {
 | 
			
		||||
      target_value = this->operation_ == SELECT_OP_NEXT ? options.front() : options.back();
 | 
			
		||||
    } else {
 | 
			
		||||
      auto index = parent->index_of(parent->state);
 | 
			
		||||
      if (index.has_value()) {
 | 
			
		||||
        auto size = options.size();
 | 
			
		||||
        if (cycle) {
 | 
			
		||||
          auto use_index = (size + index.value() + (this->operation_ == SELECT_OP_NEXT ? +1 : -1)) % size;
 | 
			
		||||
          target_value = options[use_index];
 | 
			
		||||
        } else {
 | 
			
		||||
          if (this->operation_ == SELECT_OP_PREVIOUS && index.value() > 0) {
 | 
			
		||||
            target_value = options[index.value() - 1];
 | 
			
		||||
          } else if (this->operation_ == SELECT_OP_NEXT && index.value() < options.size() - 1) {
 | 
			
		||||
            target_value = options[index.value() + 1];
 | 
			
		||||
          } else {
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        target_value = this->operation_ == SELECT_OP_NEXT ? options.front() : options.back();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!parent->has_option(target_value)) {
 | 
			
		||||
    ESP_LOGW(TAG, "'%s' - Option %s is not a valid option", name, target_value.c_str());
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGD(TAG, "'%s' - Set selected option to: %s", name, target_value.c_str());
 | 
			
		||||
  parent->control(target_value);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace select
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@ class Select;
 | 
			
		||||
enum SelectOperation {
 | 
			
		||||
  SELECT_OP_NONE,
 | 
			
		||||
  SELECT_OP_SET,
 | 
			
		||||
  SELECT_OP_SET_INDEX,
 | 
			
		||||
  SELECT_OP_NEXT,
 | 
			
		||||
  SELECT_OP_PREVIOUS,
 | 
			
		||||
  SELECT_OP_FIRST,
 | 
			
		||||
@@ -22,7 +23,6 @@ class SelectCall {
 | 
			
		||||
  void perform();
 | 
			
		||||
 | 
			
		||||
  SelectCall &set_option(const std::string &option);
 | 
			
		||||
  SelectCall &set_option(const char *option);
 | 
			
		||||
  SelectCall &set_index(size_t index);
 | 
			
		||||
 | 
			
		||||
  SelectCall &select_next(bool cycle);
 | 
			
		||||
@@ -33,13 +33,11 @@ class SelectCall {
 | 
			
		||||
  SelectCall &with_operation(SelectOperation operation);
 | 
			
		||||
  SelectCall &with_cycle(bool cycle);
 | 
			
		||||
  SelectCall &with_option(const std::string &option);
 | 
			
		||||
  SelectCall &with_option(const char *option);
 | 
			
		||||
  SelectCall &with_index(size_t index);
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  __attribute__((always_inline)) inline optional<size_t> calculate_target_index_(const char *name);
 | 
			
		||||
 | 
			
		||||
  Select *const parent_;
 | 
			
		||||
  optional<std::string> option_;
 | 
			
		||||
  optional<size_t> index_;
 | 
			
		||||
  SelectOperation operation_{SELECT_OP_NONE};
 | 
			
		||||
  bool cycle_;
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,9 @@ IMPLEMENTATION_BSD_SOCKETS = "bsd_sockets"
 | 
			
		||||
# Components register their socket needs and platforms read this to configure appropriately
 | 
			
		||||
KEY_SOCKET_CONSUMERS = "socket_consumers"
 | 
			
		||||
 | 
			
		||||
# Wake loop threadsafe support tracking
 | 
			
		||||
KEY_WAKE_LOOP_THREADSAFE_REQUIRED = "wake_loop_threadsafe_required"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def consume_sockets(
 | 
			
		||||
    value: int, consumer: str
 | 
			
		||||
@@ -37,6 +40,30 @@ def consume_sockets(
 | 
			
		||||
    return _consume_sockets
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def require_wake_loop_threadsafe() -> None:
 | 
			
		||||
    """Mark that wake_loop_threadsafe support is required by a component.
 | 
			
		||||
 | 
			
		||||
    Call this from components that need to wake the main event loop from background threads.
 | 
			
		||||
    This enables the shared UDP loopback socket mechanism (~208 bytes RAM).
 | 
			
		||||
    The socket is shared across all components that use this feature.
 | 
			
		||||
 | 
			
		||||
    IMPORTANT: This is for background thread context only, NOT ISR context.
 | 
			
		||||
    Socket operations are not safe to call from ISR handlers.
 | 
			
		||||
 | 
			
		||||
    Example:
 | 
			
		||||
        from esphome.components import socket
 | 
			
		||||
 | 
			
		||||
        async def to_code(config):
 | 
			
		||||
            socket.require_wake_loop_threadsafe()
 | 
			
		||||
    """
 | 
			
		||||
    # Only set up once (idempotent - multiple components can call this)
 | 
			
		||||
    if not CORE.data.get(KEY_WAKE_LOOP_THREADSAFE_REQUIRED, False):
 | 
			
		||||
        CORE.data[KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True
 | 
			
		||||
        cg.add_define("USE_WAKE_LOOP_THREADSAFE")
 | 
			
		||||
        # Consume 1 socket for the shared wake notification socket
 | 
			
		||||
        consume_sockets(1, "socket.wake_loop_threadsafe")({})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.SplitDefault(
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ void TemplateSelect::setup() {
 | 
			
		||||
    ESP_LOGD(TAG, "State from initial: %s", this->option_at(index));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->publish_state(index);
 | 
			
		||||
  this->publish_state(this->at(index).value());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void TemplateSelect::update() {
 | 
			
		||||
@@ -41,14 +41,16 @@ void TemplateSelect::update() {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void TemplateSelect::control(size_t index) {
 | 
			
		||||
  this->set_trigger_->trigger(std::string(this->option_at(index)));
 | 
			
		||||
void TemplateSelect::control(const std::string &value) {
 | 
			
		||||
  this->set_trigger_->trigger(value);
 | 
			
		||||
 | 
			
		||||
  if (this->optimistic_)
 | 
			
		||||
    this->publish_state(index);
 | 
			
		||||
    this->publish_state(value);
 | 
			
		||||
 | 
			
		||||
  if (this->restore_value_)
 | 
			
		||||
    this->pref_.save(&index);
 | 
			
		||||
  if (this->restore_value_) {
 | 
			
		||||
    auto index = this->index_of(value);
 | 
			
		||||
    this->pref_.save(&index.value());
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void TemplateSelect::dump_config() {
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ class TemplateSelect : public select::Select, public PollingComponent {
 | 
			
		||||
  void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void control(size_t index) override;
 | 
			
		||||
  void control(const std::string &value) override;
 | 
			
		||||
  bool optimistic_ = false;
 | 
			
		||||
  size_t initial_option_index_{0};
 | 
			
		||||
  bool restore_value_ = false;
 | 
			
		||||
 
 | 
			
		||||
@@ -17,21 +17,28 @@ void TuyaSelect::setup() {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    size_t mapping_idx = std::distance(mappings.cbegin(), it);
 | 
			
		||||
    this->publish_state(mapping_idx);
 | 
			
		||||
    auto value = this->at(mapping_idx);
 | 
			
		||||
    this->publish_state(value.value());
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void TuyaSelect::control(size_t index) {
 | 
			
		||||
void TuyaSelect::control(const std::string &value) {
 | 
			
		||||
  if (this->optimistic_)
 | 
			
		||||
    this->publish_state(index);
 | 
			
		||||
    this->publish_state(value);
 | 
			
		||||
 | 
			
		||||
  uint8_t mapping = this->mappings_.at(index);
 | 
			
		||||
  ESP_LOGV(TAG, "Setting %u datapoint value to %u:%s", this->select_id_, mapping, this->option_at(index));
 | 
			
		||||
  if (this->is_int_) {
 | 
			
		||||
    this->parent_->set_integer_datapoint_value(this->select_id_, mapping);
 | 
			
		||||
  } else {
 | 
			
		||||
    this->parent_->set_enum_datapoint_value(this->select_id_, mapping);
 | 
			
		||||
  auto idx = this->index_of(value);
 | 
			
		||||
  if (idx.has_value()) {
 | 
			
		||||
    uint8_t mapping = this->mappings_.at(idx.value());
 | 
			
		||||
    ESP_LOGV(TAG, "Setting %u datapoint value to %u:%s", this->select_id_, mapping, value.c_str());
 | 
			
		||||
    if (this->is_int_) {
 | 
			
		||||
      this->parent_->set_integer_datapoint_value(this->select_id_, mapping);
 | 
			
		||||
    } else {
 | 
			
		||||
      this->parent_->set_enum_datapoint_value(this->select_id_, mapping);
 | 
			
		||||
    }
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGW(TAG, "Invalid value %s", value.c_str());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void TuyaSelect::dump_config() {
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,7 @@ class TuyaSelect : public select::Select, public Component {
 | 
			
		||||
  void set_select_mappings(std::vector<uint8_t> mappings) { this->mappings_ = std::move(mappings); }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void control(size_t index) override;
 | 
			
		||||
  void control(const std::string &value) override;
 | 
			
		||||
 | 
			
		||||
  Tuya *parent_;
 | 
			
		||||
  bool optimistic_ = false;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import socket
 | 
			
		||||
from esphome.components.esp32 import (
 | 
			
		||||
    VARIANT_ESP32P4,
 | 
			
		||||
    VARIANT_ESP32S2,
 | 
			
		||||
@@ -11,7 +12,7 @@ from esphome.const import CONF_DEVICES, CONF_ID
 | 
			
		||||
from esphome.cpp_types import Component
 | 
			
		||||
from esphome.types import ConfigType
 | 
			
		||||
 | 
			
		||||
AUTO_LOAD = ["bytebuffer"]
 | 
			
		||||
AUTO_LOAD = ["bytebuffer", "socket"]
 | 
			
		||||
CODEOWNERS = ["@clydebarrow"]
 | 
			
		||||
DEPENDENCIES = ["esp32"]
 | 
			
		||||
usb_host_ns = cg.esphome_ns.namespace("usb_host")
 | 
			
		||||
@@ -71,6 +72,11 @@ async def to_code(config: ConfigType) -> None:
 | 
			
		||||
    max_requests = config[CONF_MAX_TRANSFER_REQUESTS]
 | 
			
		||||
    cg.add_define("USB_HOST_MAX_REQUESTS", max_requests)
 | 
			
		||||
 | 
			
		||||
    # USB uses the socket wake_loop_threadsafe() mechanism to wake the main loop from USB task
 | 
			
		||||
    # This enables low-latency (~12μs) USB event processing instead of waiting for
 | 
			
		||||
    # select() timeout (0-16ms). The wake socket is shared across all components.
 | 
			
		||||
    socket.require_wake_loop_threadsafe()
 | 
			
		||||
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    for device in config.get(CONF_DEVICES) or ():
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@
 | 
			
		||||
#include "usb_host.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
#include "esphome/components/bytebuffer/bytebuffer.h"
 | 
			
		||||
 | 
			
		||||
#include <cinttypes>
 | 
			
		||||
@@ -174,6 +175,11 @@ static void client_event_cb(const usb_host_client_event_msg_t *event_msg, void *
 | 
			
		||||
 | 
			
		||||
  // Push to lock-free queue (always succeeds since pool size == queue size)
 | 
			
		||||
  client->event_queue.push(event);
 | 
			
		||||
 | 
			
		||||
  // Wake main loop immediately to process USB event instead of waiting for select() timeout
 | 
			
		||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
 | 
			
		||||
  App.wake_loop_threadsafe();
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
void USBClient::setup() {
 | 
			
		||||
  usb_host_client_config_t config{.is_synchronous = false,
 | 
			
		||||
 
 | 
			
		||||
@@ -1188,7 +1188,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM
 | 
			
		||||
 | 
			
		||||
    if (request->method() == HTTP_GET && match.method_empty()) {
 | 
			
		||||
      auto detail = get_request_detail(request);
 | 
			
		||||
      std::string data = this->select_json(obj, obj->has_state() ? obj->current_option() : "", detail);
 | 
			
		||||
      std::string data = this->select_json(obj, obj->state, detail);
 | 
			
		||||
      request->send(200, "application/json", data.c_str());
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
@@ -1208,14 +1208,12 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM
 | 
			
		||||
  request->send(404);
 | 
			
		||||
}
 | 
			
		||||
std::string WebServer::select_state_json_generator(WebServer *web_server, void *source) {
 | 
			
		||||
  auto *obj = (select::Select *) (source);
 | 
			
		||||
  return web_server->select_json(obj, obj->has_state() ? obj->current_option() : "", DETAIL_STATE);
 | 
			
		||||
  return web_server->select_json((select::Select *) (source), ((select::Select *) (source))->state, DETAIL_STATE);
 | 
			
		||||
}
 | 
			
		||||
std::string WebServer::select_all_json_generator(WebServer *web_server, void *source) {
 | 
			
		||||
  auto *obj = (select::Select *) (source);
 | 
			
		||||
  return web_server->select_json(obj, obj->has_state() ? obj->current_option() : "", DETAIL_ALL);
 | 
			
		||||
  return web_server->select_json((select::Select *) (source), ((select::Select *) (source))->state, DETAIL_ALL);
 | 
			
		||||
}
 | 
			
		||||
std::string WebServer::select_json(select::Select *obj, const char *value, JsonDetail start_config) {
 | 
			
		||||
std::string WebServer::select_json(select::Select *obj, const std::string &value, JsonDetail start_config) {
 | 
			
		||||
  json::JsonBuilder builder;
 | 
			
		||||
  JsonObject root = builder.root();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -410,7 +410,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
 | 
			
		||||
  static std::string select_state_json_generator(WebServer *web_server, void *source);
 | 
			
		||||
  static std::string select_all_json_generator(WebServer *web_server, void *source);
 | 
			
		||||
  /// Dump the select state with its value as a JSON string.
 | 
			
		||||
  std::string select_json(select::Select *obj, const char *value, JsonDetail start_config);
 | 
			
		||||
  std::string select_json(select::Select *obj, const std::string &value, JsonDetail start_config);
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_CLIMATE
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
import textwrap
 | 
			
		||||
from typing import TypedDict
 | 
			
		||||
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
@@ -90,7 +91,7 @@ def zephyr_add_prj_conf(
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def zephyr_add_overlay(content):
 | 
			
		||||
    zephyr_data()[KEY_OVERLAY] += content
 | 
			
		||||
    zephyr_data()[KEY_OVERLAY] += textwrap.dedent(content)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def add_extra_build_file(filename: str, path: Path) -> bool:
 | 
			
		||||
 
 | 
			
		||||
@@ -122,6 +122,11 @@ void Application::setup() {
 | 
			
		||||
  // Clear setup priority overrides to free memory
 | 
			
		||||
  clear_setup_priority_overrides();
 | 
			
		||||
 | 
			
		||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
 | 
			
		||||
  // Set up wake socket for waking main loop from tasks
 | 
			
		||||
  this->setup_wake_loop_threadsafe_();
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  this->schedule_dump_config();
 | 
			
		||||
}
 | 
			
		||||
void Application::loop() {
 | 
			
		||||
@@ -472,6 +477,11 @@ void Application::enable_pending_loops_() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Application::before_loop_tasks_(uint32_t loop_start_time) {
 | 
			
		||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
 | 
			
		||||
  // Drain wake notifications first to clear socket for next wake
 | 
			
		||||
  this->drain_wake_notifications_();
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  // Process scheduled tasks
 | 
			
		||||
  this->scheduler.call(loop_start_time);
 | 
			
		||||
 | 
			
		||||
@@ -625,4 +635,73 @@ void Application::yield_with_select_(uint32_t delay_ms) {
 | 
			
		||||
 | 
			
		||||
Application App;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 | 
			
		||||
 | 
			
		||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
 | 
			
		||||
void Application::setup_wake_loop_threadsafe_() {
 | 
			
		||||
  // Create UDP socket for wake notifications
 | 
			
		||||
  this->wake_socket_fd_ = lwip_socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
 | 
			
		||||
  if (this->wake_socket_fd_ < 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "Wake socket create failed: %d", errno);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Bind to loopback with auto-assigned port
 | 
			
		||||
  struct sockaddr_in addr = {};
 | 
			
		||||
  addr.sin_family = AF_INET;
 | 
			
		||||
  addr.sin_addr.s_addr = lwip_htonl(INADDR_LOOPBACK);
 | 
			
		||||
  addr.sin_port = 0;  // Auto-assign port
 | 
			
		||||
 | 
			
		||||
  if (lwip_bind(this->wake_socket_fd_, (struct sockaddr *) &addr, sizeof(addr)) < 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "Wake socket bind failed: %d", errno);
 | 
			
		||||
    lwip_close(this->wake_socket_fd_);
 | 
			
		||||
    this->wake_socket_fd_ = -1;
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Get the assigned address and connect to it
 | 
			
		||||
  // Connecting a UDP socket allows using send() instead of sendto() for better performance
 | 
			
		||||
  struct sockaddr_in wake_addr;
 | 
			
		||||
  socklen_t len = sizeof(wake_addr);
 | 
			
		||||
  if (lwip_getsockname(this->wake_socket_fd_, (struct sockaddr *) &wake_addr, &len) < 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "Wake socket address failed: %d", errno);
 | 
			
		||||
    lwip_close(this->wake_socket_fd_);
 | 
			
		||||
    this->wake_socket_fd_ = -1;
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Connect to self (loopback) - allows using send() instead of sendto()
 | 
			
		||||
  // After connect(), no need to store wake_addr - the socket remembers it
 | 
			
		||||
  if (lwip_connect(this->wake_socket_fd_, (struct sockaddr *) &wake_addr, sizeof(wake_addr)) < 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "Wake socket connect failed: %d", errno);
 | 
			
		||||
    lwip_close(this->wake_socket_fd_);
 | 
			
		||||
    this->wake_socket_fd_ = -1;
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Set non-blocking mode
 | 
			
		||||
  int flags = lwip_fcntl(this->wake_socket_fd_, F_GETFL, 0);
 | 
			
		||||
  lwip_fcntl(this->wake_socket_fd_, F_SETFL, flags | O_NONBLOCK);
 | 
			
		||||
 | 
			
		||||
  // Register with application's select() loop
 | 
			
		||||
  if (!this->register_socket_fd(this->wake_socket_fd_)) {
 | 
			
		||||
    ESP_LOGW(TAG, "Wake socket register failed");
 | 
			
		||||
    lwip_close(this->wake_socket_fd_);
 | 
			
		||||
    this->wake_socket_fd_ = -1;
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Application::wake_loop_threadsafe() {
 | 
			
		||||
  // Called from FreeRTOS task context when events need immediate processing
 | 
			
		||||
  // Wakes up lwip_select() in main loop by writing to connected loopback socket
 | 
			
		||||
  if (this->wake_socket_fd_ >= 0) {
 | 
			
		||||
    const char dummy = 1;
 | 
			
		||||
    // Non-blocking send - if it fails (unlikely), select() will wake on timeout anyway
 | 
			
		||||
    // No error checking needed: we control both ends of this loopback socket.
 | 
			
		||||
    // This is safe to call from FreeRTOS tasks - send() is thread-safe in lwip
 | 
			
		||||
    // Socket is already connected to loopback address, so send() is faster than sendto()
 | 
			
		||||
    lwip_send(this->wake_socket_fd_, &dummy, 1, 0);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif  // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,10 @@
 | 
			
		||||
 | 
			
		||||
#ifdef USE_SOCKET_SELECT_SUPPORT
 | 
			
		||||
#include <sys/select.h>
 | 
			
		||||
#ifdef USE_WAKE_LOOP_THREADSAFE
 | 
			
		||||
#include <lwip/sockets.h>
 | 
			
		||||
#endif
 | 
			
		||||
#endif  // USE_SOCKET_SELECT_SUPPORT
 | 
			
		||||
 | 
			
		||||
#ifdef USE_BINARY_SENSOR
 | 
			
		||||
#include "esphome/components/binary_sensor/binary_sensor.h"
 | 
			
		||||
@@ -429,6 +432,13 @@ class Application {
 | 
			
		||||
  /// Check if there's data available on a socket without blocking
 | 
			
		||||
  /// This function is thread-safe for reading, but should be called after select() has run
 | 
			
		||||
  bool is_socket_ready(int fd) const;
 | 
			
		||||
 | 
			
		||||
#ifdef USE_WAKE_LOOP_THREADSAFE
 | 
			
		||||
  /// Wake the main event loop from a FreeRTOS task
 | 
			
		||||
  /// Thread-safe, can be called from task context to immediately wake select()
 | 
			
		||||
  /// IMPORTANT: NOT safe to call from ISR context (socket operations not ISR-safe)
 | 
			
		||||
  void wake_loop_threadsafe();
 | 
			
		||||
#endif
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
@@ -454,6 +464,11 @@ class Application {
 | 
			
		||||
  /// Perform a delay while also monitoring socket file descriptors for readiness
 | 
			
		||||
  void yield_with_select_(uint32_t delay_ms);
 | 
			
		||||
 | 
			
		||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
 | 
			
		||||
  void setup_wake_loop_threadsafe_();       // Create wake notification socket
 | 
			
		||||
  inline void drain_wake_notifications_();  // Read pending wake notifications in main loop (hot path - inlined)
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  // === Member variables ordered by size to minimize padding ===
 | 
			
		||||
 | 
			
		||||
  // Pointer-sized members first
 | 
			
		||||
@@ -481,6 +496,9 @@ class Application {
 | 
			
		||||
  FixedVector<Component *> looping_components_{};
 | 
			
		||||
#ifdef USE_SOCKET_SELECT_SUPPORT
 | 
			
		||||
  std::vector<int> socket_fds_;  // Vector of all monitored socket file descriptors
 | 
			
		||||
#ifdef USE_WAKE_LOOP_THREADSAFE
 | 
			
		||||
  int wake_socket_fd_{-1};  // Shared wake notification socket for waking main loop from tasks
 | 
			
		||||
#endif
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  // std::string members (typically 24-32 bytes each)
 | 
			
		||||
@@ -597,4 +615,28 @@ class Application {
 | 
			
		||||
/// Global storage of Application pointer - only one Application can exist.
 | 
			
		||||
extern Application App;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 | 
			
		||||
 | 
			
		||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
 | 
			
		||||
// Inline implementations for hot-path functions
 | 
			
		||||
// drain_wake_notifications_() is called on every loop iteration
 | 
			
		||||
 | 
			
		||||
// Small buffer for draining wake notification bytes (1 byte sent per wake)
 | 
			
		||||
// Size allows draining multiple notifications per recvfrom() without wasting stack
 | 
			
		||||
static constexpr size_t WAKE_NOTIFY_DRAIN_BUFFER_SIZE = 16;
 | 
			
		||||
 | 
			
		||||
inline void Application::drain_wake_notifications_() {
 | 
			
		||||
  // Called from main loop to drain any pending wake notifications
 | 
			
		||||
  // Must check is_socket_ready() to avoid blocking on empty socket
 | 
			
		||||
  if (this->wake_socket_fd_ >= 0 && this->is_socket_ready(this->wake_socket_fd_)) {
 | 
			
		||||
    char buffer[WAKE_NOTIFY_DRAIN_BUFFER_SIZE];
 | 
			
		||||
    // Drain all pending notifications with non-blocking reads
 | 
			
		||||
    // Multiple wake events may have triggered multiple writes, so drain until EWOULDBLOCK
 | 
			
		||||
    // We control both ends of this loopback socket (always write 1 byte per wake),
 | 
			
		||||
    // so no error checking needed - any errors indicate catastrophic system failure
 | 
			
		||||
    while (lwip_recvfrom(this->wake_socket_fd_, buffer, sizeof(buffer), 0, nullptr, nullptr) > 0) {
 | 
			
		||||
      // Just draining, no action needed - wake has already occurred
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif  // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -155,6 +155,7 @@
 | 
			
		||||
// IDF-specific feature flags
 | 
			
		||||
#ifdef USE_ESP_IDF
 | 
			
		||||
#define USE_MQTT_IDF_ENQUEUE
 | 
			
		||||
#define ESPHOME_LOOP_TASK_STACK_SIZE 8192
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
// ESP32-specific feature flags
 | 
			
		||||
@@ -195,6 +196,7 @@
 | 
			
		||||
#define USE_PSRAM
 | 
			
		||||
#define USE_SOCKET_IMPL_BSD_SOCKETS
 | 
			
		||||
#define USE_SOCKET_SELECT_SUPPORT
 | 
			
		||||
#define USE_WAKE_LOOP_THREADSAFE
 | 
			
		||||
#define USE_SPEAKER
 | 
			
		||||
#define USE_SPI
 | 
			
		||||
#define USE_VOICE_ASSISTANT
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								tests/components/ads1115/test.nrf52-adafruit.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								tests/components/ads1115/test.nrf52-adafruit.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
packages:
 | 
			
		||||
  i2c: !include ../../test_build_components/common/i2c/nrf52.yaml
 | 
			
		||||
 | 
			
		||||
<<: !include common.yaml
 | 
			
		||||
							
								
								
									
										4
									
								
								tests/components/ads1115/test.nrf52-mcumgr.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								tests/components/ads1115/test.nrf52-mcumgr.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
packages:
 | 
			
		||||
  i2c: !include ../../test_build_components/common/i2c/nrf52.yaml
 | 
			
		||||
 | 
			
		||||
<<: !include common.yaml
 | 
			
		||||
							
								
								
									
										4
									
								
								tests/components/aht10/test.nrf52-adafruit.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								tests/components/aht10/test.nrf52-adafruit.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
packages:
 | 
			
		||||
  i2c: !include ../../test_build_components/common/i2c/nrf52.yaml
 | 
			
		||||
 | 
			
		||||
<<: !include common.yaml
 | 
			
		||||
							
								
								
									
										6
									
								
								tests/components/esp32/test-stack_size.esp32-idf.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								tests/components/esp32/test-stack_size.esp32-idf.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
esp32:
 | 
			
		||||
  board: esp32dev
 | 
			
		||||
  framework:
 | 
			
		||||
    type: esp-idf
 | 
			
		||||
    advanced:
 | 
			
		||||
      loop_task_stack_size: 16384
 | 
			
		||||
							
								
								
									
										4
									
								
								tests/components/i2c/test.nrf52-adafruit.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								tests/components/i2c/test.nrf52-adafruit.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
packages:
 | 
			
		||||
  i2c: !include ../../test_build_components/common/i2c/nrf52.yaml
 | 
			
		||||
 | 
			
		||||
<<: !include common.yaml
 | 
			
		||||
							
								
								
									
										4
									
								
								tests/components/i2c/test.nrf52-mcumgr.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								tests/components/i2c/test.nrf52-mcumgr.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
packages:
 | 
			
		||||
  i2c: !include ../../test_build_components/common/i2c/nrf52.yaml
 | 
			
		||||
 | 
			
		||||
<<: !include common.yaml
 | 
			
		||||
							
								
								
									
										4
									
								
								tests/components/i2c/test.nrf52-xiao-ble.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								tests/components/i2c/test.nrf52-xiao-ble.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
packages:
 | 
			
		||||
  i2c: !include ../../test_build_components/common/i2c/nrf52.yaml
 | 
			
		||||
 | 
			
		||||
<<: !include common.yaml
 | 
			
		||||
							
								
								
									
										12
									
								
								tests/components/socket/conftest.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								tests/components/socket/conftest.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
"""Configuration file for socket component tests."""
 | 
			
		||||
 | 
			
		||||
import pytest
 | 
			
		||||
 | 
			
		||||
from esphome.core import CORE
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture(autouse=True)
 | 
			
		||||
def reset_core():
 | 
			
		||||
    """Reset CORE after each test."""
 | 
			
		||||
    yield
 | 
			
		||||
    CORE.reset()
 | 
			
		||||
							
								
								
									
										42
									
								
								tests/components/socket/test_wake_loop_threadsafe.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								tests/components/socket/test_wake_loop_threadsafe.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
			
		||||
from esphome.components import socket
 | 
			
		||||
from esphome.core import CORE
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_require_wake_loop_threadsafe__first_call() -> None:
 | 
			
		||||
    """Test that first call sets up define and consumes socket."""
 | 
			
		||||
    socket.require_wake_loop_threadsafe()
 | 
			
		||||
 | 
			
		||||
    # Verify CORE.data was updated
 | 
			
		||||
    assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True
 | 
			
		||||
 | 
			
		||||
    # Verify the define was added
 | 
			
		||||
    assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_require_wake_loop_threadsafe__idempotent() -> None:
 | 
			
		||||
    """Test that subsequent calls are idempotent."""
 | 
			
		||||
    # Set up initial state as if already called
 | 
			
		||||
    CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True
 | 
			
		||||
 | 
			
		||||
    # Call again - should not raise or fail
 | 
			
		||||
    socket.require_wake_loop_threadsafe()
 | 
			
		||||
 | 
			
		||||
    # Verify state is still True
 | 
			
		||||
    assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True
 | 
			
		||||
 | 
			
		||||
    # Define should not be added since flag was already True
 | 
			
		||||
    assert not any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_require_wake_loop_threadsafe__multiple_calls() -> None:
 | 
			
		||||
    """Test that multiple calls only set up once."""
 | 
			
		||||
    # Call three times
 | 
			
		||||
    socket.require_wake_loop_threadsafe()
 | 
			
		||||
    socket.require_wake_loop_threadsafe()
 | 
			
		||||
    socket.require_wake_loop_threadsafe()
 | 
			
		||||
 | 
			
		||||
    # Verify CORE.data was set
 | 
			
		||||
    assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True
 | 
			
		||||
 | 
			
		||||
    # Verify the define was added (only once, but we can just check it exists)
 | 
			
		||||
    assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines)
 | 
			
		||||
							
								
								
									
										11
									
								
								tests/test_build_components/common/i2c/nrf52.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								tests/test_build_components/common/i2c/nrf52.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
# Common I2C configuration for NRF52 tests
 | 
			
		||||
 | 
			
		||||
substitutions:
 | 
			
		||||
  scl_pin: P0.04
 | 
			
		||||
  sda_pin: P0.05
 | 
			
		||||
 | 
			
		||||
i2c:
 | 
			
		||||
  - id: i2c_bus
 | 
			
		||||
    scl: ${scl_pin}
 | 
			
		||||
    sda: ${sda_pin}
 | 
			
		||||
    scan: true
 | 
			
		||||
		Reference in New Issue
	
	Block a user