esphome 2025.10.0b1__py3-none-any.whl → 2025.10.0b2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. esphome/__main__.py +7 -0
  2. esphome/components/canbus/canbus.h +3 -3
  3. esphome/components/dashboard_import/dashboard_import.cpp +1 -1
  4. esphome/components/dashboard_import/dashboard_import.h +1 -1
  5. esphome/components/esp32/__init__.py +9 -8
  6. esphome/components/esp32_ble/__init__.py +9 -1
  7. esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp +0 -4
  8. esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +0 -4
  9. esphome/components/esp32_improv/esp32_improv_component.cpp +33 -21
  10. esphome/components/esp32_improv/esp32_improv_component.h +1 -0
  11. esphome/components/esphome/ota/ota_esphome.cpp +1 -1
  12. esphome/components/json/json_util.cpp +8 -2
  13. esphome/components/mcp23xxx_base/mcp23xxx_base.h +3 -3
  14. esphome/components/mdns/__init__.py +76 -15
  15. esphome/components/mdns/mdns_component.cpp +52 -57
  16. esphome/components/mdns/mdns_component.h +12 -1
  17. esphome/components/mdns/mdns_esp32.cpp +3 -9
  18. esphome/components/mdns/mdns_esp8266.cpp +1 -1
  19. esphome/components/mdns/mdns_libretiny.cpp +1 -2
  20. esphome/components/mdns/mdns_rp2040.cpp +1 -2
  21. esphome/components/opentherm/opentherm.cpp +5 -5
  22. esphome/components/opentherm/opentherm.h +3 -3
  23. esphome/components/openthread/openthread.cpp +5 -3
  24. esphome/components/uart/__init__.py +1 -1
  25. esphome/components/usb_host/__init__.py +10 -1
  26. esphome/components/usb_host/usb_host.h +24 -18
  27. esphome/components/usb_host/usb_host_client.cpp +18 -39
  28. esphome/components/wifi/wifi_component.cpp +3 -2
  29. esphome/const.py +1 -1
  30. esphome/core/__init__.py +2 -0
  31. esphome/core/defines.h +2 -0
  32. esphome/core/entity_helpers.py +9 -6
  33. esphome/espota2.py +1 -1
  34. esphome/pins.py +2 -2
  35. esphome/platformio_api.py +31 -0
  36. {esphome-2025.10.0b1.dist-info → esphome-2025.10.0b2.dist-info}/METADATA +3 -3
  37. {esphome-2025.10.0b1.dist-info → esphome-2025.10.0b2.dist-info}/RECORD +41 -41
  38. {esphome-2025.10.0b1.dist-info → esphome-2025.10.0b2.dist-info}/WHEEL +0 -0
  39. {esphome-2025.10.0b1.dist-info → esphome-2025.10.0b2.dist-info}/entry_points.txt +0 -0
  40. {esphome-2025.10.0b1.dist-info → esphome-2025.10.0b2.dist-info}/licenses/LICENSE +0 -0
  41. {esphome-2025.10.0b1.dist-info → esphome-2025.10.0b2.dist-info}/top_level.txt +0 -0
@@ -2,7 +2,6 @@
2
2
  #if defined(USE_ESP32) && defined(USE_MDNS)
3
3
 
4
4
  #include <mdns.h>
5
- #include <cstring>
6
5
  #include "esphome/core/hal.h"
7
6
  #include "esphome/core/log.h"
8
7
  #include "mdns_component.h"
@@ -29,21 +28,16 @@ void MDNSComponent::setup() {
29
28
  std::vector<mdns_txt_item_t> txt_records;
30
29
  for (const auto &record : service.txt_records) {
31
30
  mdns_txt_item_t it{};
32
- // key is a compile-time string literal in flash, no need to strdup
31
+ // key and value are either compile-time string literals in flash or pointers to dynamic_txt_values_
32
+ // Both remain valid for the lifetime of this function, and ESP-IDF makes internal copies
33
33
  it.key = MDNS_STR_ARG(record.key);
34
- // value is a temporary from TemplatableValue, must strdup to keep it alive
35
- it.value = strdup(const_cast<TemplatableValue<std::string> &>(record.value).value().c_str());
34
+ it.value = MDNS_STR_ARG(record.value);
36
35
  txt_records.push_back(it);
37
36
  }
38
37
  uint16_t port = const_cast<TemplatableValue<uint16_t> &>(service.port).value();
39
38
  err = mdns_service_add(nullptr, MDNS_STR_ARG(service.service_type), MDNS_STR_ARG(service.proto), port,
40
39
  txt_records.data(), txt_records.size());
41
40
 
42
- // free records
43
- for (const auto &it : txt_records) {
44
- free((void *) it.value); // NOLINT(cppcoreguidelines-no-malloc)
45
- }
46
-
47
41
  if (err != ESP_OK) {
48
42
  ESP_LOGW(TAG, "Failed to register service %s: %s", MDNS_STR_ARG(service.service_type), esp_err_to_name(err));
49
43
  }
@@ -33,7 +33,7 @@ void MDNSComponent::setup() {
33
33
  MDNS.addService(FPSTR(service_type), FPSTR(proto), port);
34
34
  for (const auto &record : service.txt_records) {
35
35
  MDNS.addServiceTxt(FPSTR(service_type), FPSTR(proto), FPSTR(MDNS_STR_ARG(record.key)),
36
- const_cast<TemplatableValue<std::string> &>(record.value).value().c_str());
36
+ FPSTR(MDNS_STR_ARG(record.value)));
37
37
  }
38
38
  }
39
39
  }
@@ -32,8 +32,7 @@ void MDNSComponent::setup() {
32
32
  uint16_t port_ = const_cast<TemplatableValue<uint16_t> &>(service.port).value();
33
33
  MDNS.addService(service_type, proto, port_);
34
34
  for (const auto &record : service.txt_records) {
35
- MDNS.addServiceTxt(service_type, proto, MDNS_STR_ARG(record.key),
36
- const_cast<TemplatableValue<std::string> &>(record.value).value().c_str());
35
+ MDNS.addServiceTxt(service_type, proto, MDNS_STR_ARG(record.key), MDNS_STR_ARG(record.value));
37
36
  }
38
37
  }
39
38
  }
@@ -32,8 +32,7 @@ void MDNSComponent::setup() {
32
32
  uint16_t port = const_cast<TemplatableValue<uint16_t> &>(service.port).value();
33
33
  MDNS.addService(service_type, proto, port);
34
34
  for (const auto &record : service.txt_records) {
35
- MDNS.addServiceTxt(service_type, proto, MDNS_STR_ARG(record.key),
36
- const_cast<TemplatableValue<std::string> &>(record.value).value().c_str());
35
+ MDNS.addServiceTxt(service_type, proto, MDNS_STR_ARG(record.key), MDNS_STR_ARG(record.value));
37
36
  }
38
37
  }
39
38
  }
@@ -7,7 +7,7 @@
7
7
 
8
8
  #include "opentherm.h"
9
9
  #include "esphome/core/helpers.h"
10
- #if defined(ESP32) || defined(USE_ESP_IDF)
10
+ #ifdef USE_ESP32
11
11
  #include "driver/timer.h"
12
12
  #include "esp_err.h"
13
13
  #endif
@@ -31,7 +31,7 @@ OpenTherm *OpenTherm::instance = nullptr;
31
31
  OpenTherm::OpenTherm(InternalGPIOPin *in_pin, InternalGPIOPin *out_pin, int32_t device_timeout)
32
32
  : in_pin_(in_pin),
33
33
  out_pin_(out_pin),
34
- #if defined(ESP32) || defined(USE_ESP_IDF)
34
+ #ifdef USE_ESP32
35
35
  timer_group_(TIMER_GROUP_0),
36
36
  timer_idx_(TIMER_0),
37
37
  #endif
@@ -57,7 +57,7 @@ bool OpenTherm::initialize() {
57
57
  this->out_pin_->setup();
58
58
  this->out_pin_->digital_write(true);
59
59
 
60
- #if defined(ESP32) || defined(USE_ESP_IDF)
60
+ #ifdef USE_ESP32
61
61
  return this->init_esp32_timer_();
62
62
  #else
63
63
  return true;
@@ -238,7 +238,7 @@ void IRAM_ATTR OpenTherm::write_bit_(uint8_t high, uint8_t clock) {
238
238
  }
239
239
  }
240
240
 
241
- #if defined(ESP32) || defined(USE_ESP_IDF)
241
+ #ifdef USE_ESP32
242
242
 
243
243
  bool OpenTherm::init_esp32_timer_() {
244
244
  // Search for a free timer. Maybe unstable, we'll see.
@@ -365,7 +365,7 @@ void IRAM_ATTR OpenTherm::stop_timer_() {
365
365
  }
366
366
  }
367
367
 
368
- #endif // END ESP32
368
+ #endif // USE_ESP32
369
369
 
370
370
  #ifdef ESP8266
371
371
  // 5 kHz timer_
@@ -12,7 +12,7 @@
12
12
  #include "esphome/core/helpers.h"
13
13
  #include "esphome/core/log.h"
14
14
 
15
- #if defined(ESP32) || defined(USE_ESP_IDF)
15
+ #ifdef USE_ESP32
16
16
  #include "driver/timer.h"
17
17
  #endif
18
18
 
@@ -356,7 +356,7 @@ class OpenTherm {
356
356
  ISRInternalGPIOPin isr_in_pin_;
357
357
  ISRInternalGPIOPin isr_out_pin_;
358
358
 
359
- #if defined(ESP32) || defined(USE_ESP_IDF)
359
+ #ifdef USE_ESP32
360
360
  timer_group_t timer_group_;
361
361
  timer_idx_t timer_idx_;
362
362
  #endif
@@ -370,7 +370,7 @@ class OpenTherm {
370
370
  int32_t timeout_counter_; // <0 no timeout
371
371
  int32_t device_timeout_;
372
372
 
373
- #if defined(ESP32) || defined(USE_ESP_IDF)
373
+ #ifdef USE_ESP32
374
374
  esp_err_t timer_error_ = ESP_OK;
375
375
  TimerErrorType timer_error_type_ = TimerErrorType::NO_TIMER_ERROR;
376
376
 
@@ -180,10 +180,12 @@ void OpenThreadSrpComponent::setup() {
180
180
  entry->mService.mNumTxtEntries = service.txt_records.size();
181
181
  for (size_t i = 0; i < service.txt_records.size(); i++) {
182
182
  const auto &txt = service.txt_records[i];
183
- auto value = const_cast<TemplatableValue<std::string> &>(txt.value).value();
183
+ // Value is either a compile-time string literal in flash or a pointer to dynamic_txt_values_
184
+ // OpenThread SRP client expects the data to persist, so we strdup it
185
+ const char *value_str = MDNS_STR_ARG(txt.value);
184
186
  txt_entries[i].mKey = MDNS_STR_ARG(txt.key);
185
- txt_entries[i].mValue = reinterpret_cast<const uint8_t *>(strdup(value.c_str()));
186
- txt_entries[i].mValueLength = value.size();
187
+ txt_entries[i].mValue = reinterpret_cast<const uint8_t *>(strdup(value_str));
188
+ txt_entries[i].mValueLength = strlen(value_str);
187
189
  }
188
190
  entry->mService.mTxtEntries = txt_entries;
189
191
  entry->mService.mNumTxtEntries = service.txt_records.size();
@@ -347,7 +347,7 @@ def final_validate_device_schema(
347
347
 
348
348
  def validate_pin(opt, device):
349
349
  def validator(value):
350
- if opt in device:
350
+ if opt in device and not CORE.testing_mode:
351
351
  raise cv.Invalid(
352
352
  f"The uart {opt} is used both by {name} and {device[opt]}, "
353
353
  f"but can only be used by one. Please create a new uart bus for {name}."
@@ -9,6 +9,7 @@ from esphome.components.esp32 import (
9
9
  import esphome.config_validation as cv
10
10
  from esphome.const import CONF_DEVICES, CONF_ID
11
11
  from esphome.cpp_types import Component
12
+ from esphome.types import ConfigType
12
13
 
13
14
  AUTO_LOAD = ["bytebuffer"]
14
15
  CODEOWNERS = ["@clydebarrow"]
@@ -20,6 +21,7 @@ USBClient = usb_host_ns.class_("USBClient", Component)
20
21
  CONF_VID = "vid"
21
22
  CONF_PID = "pid"
22
23
  CONF_ENABLE_HUBS = "enable_hubs"
24
+ CONF_MAX_TRANSFER_REQUESTS = "max_transfer_requests"
23
25
 
24
26
 
25
27
  def usb_device_schema(cls=USBClient, vid: int = None, pid: [int] = None) -> cv.Schema:
@@ -44,6 +46,9 @@ CONFIG_SCHEMA = cv.All(
44
46
  {
45
47
  cv.GenerateID(): cv.declare_id(USBHost),
46
48
  cv.Optional(CONF_ENABLE_HUBS, default=False): cv.boolean,
49
+ cv.Optional(CONF_MAX_TRANSFER_REQUESTS, default=16): cv.int_range(
50
+ min=1, max=32
51
+ ),
47
52
  cv.Optional(CONF_DEVICES): cv.ensure_list(usb_device_schema()),
48
53
  }
49
54
  ),
@@ -58,10 +63,14 @@ async def register_usb_client(config):
58
63
  return var
59
64
 
60
65
 
61
- async def to_code(config):
66
+ async def to_code(config: ConfigType) -> None:
62
67
  add_idf_sdkconfig_option("CONFIG_USB_HOST_CONTROL_TRANSFER_MAX_SIZE", 1024)
63
68
  if config.get(CONF_ENABLE_HUBS):
64
69
  add_idf_sdkconfig_option("CONFIG_USB_HOST_HUBS_SUPPORTED", True)
70
+
71
+ max_requests = config[CONF_MAX_TRANSFER_REQUESTS]
72
+ cg.add_define("USB_HOST_MAX_REQUESTS", max_requests)
73
+
65
74
  var = cg.new_Pvariable(config[CONF_ID])
66
75
  await cg.register_component(var, config)
67
76
  for device in config.get(CONF_DEVICES) or ():
@@ -2,6 +2,7 @@
2
2
 
3
3
  // Should not be needed, but it's required to pass CI clang-tidy checks
4
4
  #if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4)
5
+ #include "esphome/core/defines.h"
5
6
  #include "esphome/core/component.h"
6
7
  #include <vector>
7
8
  #include "usb/usb_host.h"
@@ -16,23 +17,25 @@ namespace usb_host {
16
17
 
17
18
  // THREADING MODEL:
18
19
  // This component uses a dedicated USB task for event processing to prevent data loss.
19
- // - USB Task (high priority): Handles USB events, executes transfer callbacks
20
- // - Main Loop Task: Initiates transfers, processes completion events
20
+ // - USB Task (high priority): Handles USB events, executes transfer callbacks, releases transfer slots
21
+ // - Main Loop Task: Initiates transfers, processes device connect/disconnect events
21
22
  //
22
23
  // Thread-safe communication:
23
24
  // - Lock-free queues for USB task -> main loop events (SPSC pattern)
24
- // - Lock-free TransferRequest pool using atomic bitmask (MCSP pattern)
25
+ // - Lock-free TransferRequest pool using atomic bitmask (MCMP pattern - multi-consumer, multi-producer)
25
26
  //
26
27
  // TransferRequest pool access pattern:
27
28
  // - get_trq_() [allocate]: Called from BOTH USB task and main loop threads
28
29
  // * USB task: via USB UART input callbacks that restart transfers immediately
29
30
  // * Main loop: for output transfers and flow-controlled input restarts
30
- // - release_trq() [deallocate]: Called from main loop thread only
31
+ // - release_trq() [deallocate]: Called from BOTH USB task and main loop threads
32
+ // * USB task: immediately after transfer callback completes (critical for preventing slot exhaustion)
33
+ // * Main loop: when transfer submission fails
31
34
  //
32
- // The multi-threaded allocation is intentional for performance:
33
- // - USB task can immediately restart input transfers without context switching
35
+ // The multi-threaded allocation/deallocation is intentional for performance:
36
+ // - USB task can immediately restart input transfers and release slots without context switching
34
37
  // - Main loop controls backpressure by deciding when to restart after consuming data
35
- // The atomic bitmask ensures thread-safe allocation without mutex blocking.
38
+ // The atomic bitmask ensures thread-safe allocation/deallocation without mutex blocking.
36
39
 
37
40
  static const char *const TAG = "usb_host";
38
41
 
@@ -52,8 +55,17 @@ static const uint8_t USB_DIR_IN = 1 << 7;
52
55
  static const uint8_t USB_DIR_OUT = 0;
53
56
  static const size_t SETUP_PACKET_SIZE = 8;
54
57
 
55
- static const size_t MAX_REQUESTS = 16; // maximum number of outstanding requests possible.
56
- static_assert(MAX_REQUESTS <= 16, "MAX_REQUESTS must be <= 16 to fit in uint16_t bitmask");
58
+ static const size_t MAX_REQUESTS = USB_HOST_MAX_REQUESTS; // maximum number of outstanding requests possible.
59
+ static_assert(MAX_REQUESTS >= 1 && MAX_REQUESTS <= 32, "MAX_REQUESTS must be between 1 and 32");
60
+
61
+ // Select appropriate bitmask type for tracking allocation of TransferRequest slots.
62
+ // The bitmask must have at least as many bits as MAX_REQUESTS, so:
63
+ // - Use uint16_t for up to 16 requests (MAX_REQUESTS <= 16)
64
+ // - Use uint32_t for 17-32 requests (MAX_REQUESTS > 16)
65
+ // This is tied to the static_assert above, which enforces MAX_REQUESTS is between 1 and 32.
66
+ // If MAX_REQUESTS is increased above 32, this logic and the static_assert must be updated.
67
+ using trq_bitmask_t = std::conditional<(MAX_REQUESTS <= 16), uint16_t, uint32_t>::type;
68
+
57
69
  static constexpr size_t USB_EVENT_QUEUE_SIZE = 32; // Size of event queue between USB task and main loop
58
70
  static constexpr size_t USB_TASK_STACK_SIZE = 4096; // Stack size for USB task (same as ESP-IDF USB examples)
59
71
  static constexpr UBaseType_t USB_TASK_PRIORITY = 5; // Higher priority than main loop (tskIDLE_PRIORITY + 5)
@@ -83,8 +95,6 @@ struct TransferRequest {
83
95
  enum EventType : uint8_t {
84
96
  EVENT_DEVICE_NEW,
85
97
  EVENT_DEVICE_GONE,
86
- EVENT_TRANSFER_COMPLETE,
87
- EVENT_CONTROL_COMPLETE,
88
98
  };
89
99
 
90
100
  struct UsbEvent {
@@ -96,9 +106,6 @@ struct UsbEvent {
96
106
  struct {
97
107
  usb_device_handle_t handle;
98
108
  } device_gone;
99
- struct {
100
- TransferRequest *trq;
101
- } transfer;
102
109
  } data;
103
110
 
104
111
  // Required for EventPool - no cleanup needed for POD types
@@ -163,10 +170,9 @@ class USBClient : public Component {
163
170
  uint16_t pid_{};
164
171
  // Lock-free pool management using atomic bitmask (no dynamic allocation)
165
172
  // Bit i = 1: requests_[i] is in use, Bit i = 0: requests_[i] is available
166
- // Supports multiple concurrent consumers (both threads can allocate)
167
- // Single producer for deallocation (main loop only)
168
- // Limited to 16 slots by uint16_t size (enforced by static_assert)
169
- std::atomic<uint16_t> trq_in_use_;
173
+ // Supports multiple concurrent consumers and producers (both threads can allocate/deallocate)
174
+ // Bitmask type automatically selected: uint16_t for <= 16 slots, uint32_t for 17-32 slots
175
+ std::atomic<trq_bitmask_t> trq_in_use_;
170
176
  TransferRequest requests_[MAX_REQUESTS]{};
171
177
  };
172
178
  class USBHost : public Component {
@@ -228,12 +228,6 @@ void USBClient::loop() {
228
228
  case EVENT_DEVICE_GONE:
229
229
  this->on_removed(event->data.device_gone.handle);
230
230
  break;
231
- case EVENT_TRANSFER_COMPLETE:
232
- case EVENT_CONTROL_COMPLETE: {
233
- auto *trq = event->data.transfer.trq;
234
- this->release_trq(trq);
235
- break;
236
- }
237
231
  }
238
232
  // Return event to pool for reuse
239
233
  this->event_pool.release(event);
@@ -313,25 +307,6 @@ void USBClient::on_removed(usb_device_handle_t handle) {
313
307
  }
314
308
  }
315
309
 
316
- // Helper to queue transfer cleanup to main loop
317
- static void queue_transfer_cleanup(TransferRequest *trq, EventType type) {
318
- auto *client = trq->client;
319
-
320
- // Allocate event from pool
321
- UsbEvent *event = client->event_pool.allocate();
322
- if (event == nullptr) {
323
- // No events available - increment counter for periodic logging
324
- client->event_queue.increment_dropped_count();
325
- return;
326
- }
327
-
328
- event->type = type;
329
- event->data.transfer.trq = trq;
330
-
331
- // Push to lock-free queue (always succeeds since pool size == queue size)
332
- client->event_queue.push(event);
333
- }
334
-
335
310
  // CALLBACK CONTEXT: USB task (called from usb_host_client_handle_events in USB task)
336
311
  static void control_callback(const usb_transfer_t *xfer) {
337
312
  auto *trq = static_cast<TransferRequest *>(xfer->context);
@@ -346,8 +321,9 @@ static void control_callback(const usb_transfer_t *xfer) {
346
321
  trq->callback(trq->status);
347
322
  }
348
323
 
349
- // Queue cleanup to main loop
350
- queue_transfer_cleanup(trq, EVENT_CONTROL_COMPLETE);
324
+ // Release transfer slot immediately in USB task
325
+ // The release_trq() uses thread-safe atomic operations
326
+ trq->client->release_trq(trq);
351
327
  }
352
328
 
353
329
  // THREAD CONTEXT: Called from both USB task and main loop threads (multi-consumer)
@@ -358,20 +334,20 @@ static void control_callback(const usb_transfer_t *xfer) {
358
334
  // This multi-threaded access is intentional for performance - USB task can
359
335
  // immediately restart transfers without waiting for main loop scheduling.
360
336
  TransferRequest *USBClient::get_trq_() {
361
- uint16_t mask = this->trq_in_use_.load(std::memory_order_relaxed);
337
+ trq_bitmask_t mask = this->trq_in_use_.load(std::memory_order_relaxed);
362
338
 
363
339
  // Find first available slot (bit = 0) and try to claim it atomically
364
340
  // We use a while loop to allow retrying the same slot after CAS failure
365
341
  size_t i = 0;
366
342
  while (i != MAX_REQUESTS) {
367
- if (mask & (1U << i)) {
343
+ if (mask & (static_cast<trq_bitmask_t>(1) << i)) {
368
344
  // Slot is in use, move to next slot
369
345
  i++;
370
346
  continue;
371
347
  }
372
348
 
373
349
  // Slot i appears available, try to claim it atomically
374
- uint16_t desired = mask | (1U << i); // Set bit i to mark as in-use
350
+ trq_bitmask_t desired = mask | (static_cast<trq_bitmask_t>(1) << i); // Set bit i to mark as in-use
375
351
 
376
352
  if (this->trq_in_use_.compare_exchange_weak(mask, desired, std::memory_order_acquire, std::memory_order_relaxed)) {
377
353
  // Successfully claimed slot i - prepare the TransferRequest
@@ -386,7 +362,7 @@ TransferRequest *USBClient::get_trq_() {
386
362
  i = 0;
387
363
  }
388
364
 
389
- ESP_LOGE(TAG, "All %d transfer slots in use", MAX_REQUESTS);
365
+ ESP_LOGE(TAG, "All %zu transfer slots in use", MAX_REQUESTS);
390
366
  return nullptr;
391
367
  }
392
368
  void USBClient::disconnect() {
@@ -452,8 +428,11 @@ static void transfer_callback(usb_transfer_t *xfer) {
452
428
  trq->callback(trq->status);
453
429
  }
454
430
 
455
- // Queue cleanup to main loop
456
- queue_transfer_cleanup(trq, EVENT_TRANSFER_COMPLETE);
431
+ // Release transfer slot AFTER callback completes to prevent slot exhaustion
432
+ // This is critical for high-throughput transfers (e.g., USB UART at 115200 baud)
433
+ // The callback has finished accessing xfer->data_buffer, so it's safe to release
434
+ // The release_trq() uses thread-safe atomic operations
435
+ trq->client->release_trq(trq);
457
436
  }
458
437
  /**
459
438
  * Performs a transfer input operation.
@@ -521,12 +500,12 @@ void USBClient::dump_config() {
521
500
  " Product id %04X",
522
501
  this->vid_, this->pid_);
523
502
  }
524
- // THREAD CONTEXT: Only called from main loop thread (single producer for deallocation)
525
- // - Via event processing when handling EVENT_TRANSFER_COMPLETE/EVENT_CONTROL_COMPLETE
526
- // - Directly when transfer submission fails
503
+ // THREAD CONTEXT: Called from both USB task and main loop threads
504
+ // - USB task: Immediately after transfer callback completes
505
+ // - Main loop: When transfer submission fails
527
506
  //
528
507
  // THREAD SAFETY: Lock-free using atomic AND to clear bit
529
- // Single-producer pattern makes this simpler than allocation
508
+ // Thread-safe atomic operation allows multi-threaded deallocation
530
509
  void USBClient::release_trq(TransferRequest *trq) {
531
510
  if (trq == nullptr)
532
511
  return;
@@ -540,8 +519,8 @@ void USBClient::release_trq(TransferRequest *trq) {
540
519
 
541
520
  // Atomically clear bit i to mark slot as available
542
521
  // fetch_and with inverted bitmask clears the bit atomically
543
- uint16_t bit = 1U << index;
544
- this->trq_in_use_.fetch_and(static_cast<uint16_t>(~bit), std::memory_order_release);
522
+ trq_bitmask_t bit = static_cast<trq_bitmask_t>(1) << index;
523
+ this->trq_in_use_.fetch_and(static_cast<trq_bitmask_t>(~bit), std::memory_order_release);
545
524
  }
546
525
 
547
526
  } // namespace usb_host
@@ -576,8 +576,9 @@ __attribute__((noinline)) static void log_scan_result(const WiFiScanResult &res)
576
576
  format_mac_addr_upper(bssid.data(), bssid_s);
577
577
 
578
578
  if (res.get_matches()) {
579
- ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), res.get_is_hidden() ? "(HIDDEN) " : "",
580
- bssid_s, LOG_STR_ARG(get_signal_bars(res.get_rssi())));
579
+ ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(),
580
+ res.get_is_hidden() ? LOG_STR_LITERAL("(HIDDEN) ") : LOG_STR_LITERAL(""), bssid_s,
581
+ LOG_STR_ARG(get_signal_bars(res.get_rssi())));
581
582
  ESP_LOGD(TAG,
582
583
  " Channel: %u\n"
583
584
  " RSSI: %d dB",
esphome/const.py CHANGED
@@ -4,7 +4,7 @@ from enum import Enum
4
4
 
5
5
  from esphome.enum import StrEnum
6
6
 
7
- __version__ = "2025.10.0b1"
7
+ __version__ = "2025.10.0b2"
8
8
 
9
9
  ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
10
10
  VALID_SUBSTITUTIONS_CHARACTERS = (
esphome/core/__init__.py CHANGED
@@ -529,6 +529,8 @@ class EsphomeCore:
529
529
  self.dashboard = False
530
530
  # True if command is run from vscode api
531
531
  self.vscode = False
532
+ # True if running in testing mode (disables validation checks for grouped testing)
533
+ self.testing_mode = False
532
534
  # The name of the node
533
535
  self.name: str | None = None
534
536
  # The friendly name of the node
esphome/core/defines.h CHANGED
@@ -84,6 +84,7 @@
84
84
  #define USE_LVGL_TOUCHSCREEN
85
85
  #define USE_MDNS
86
86
  #define MDNS_SERVICE_COUNT 3
87
+ #define MDNS_DYNAMIC_TXT_COUNT 3
87
88
  #define USE_MEDIA_PLAYER
88
89
  #define USE_NEXTION_TFT_UPLOAD
89
90
  #define USE_NUMBER
@@ -190,6 +191,7 @@
190
191
  #define USE_WEBSERVER_PORT 80 // NOLINT
191
192
  #define USE_WEBSERVER_SORTING
192
193
  #define USE_WIFI_11KV_SUPPORT
194
+ #define USB_HOST_MAX_REQUESTS 16
193
195
 
194
196
  #ifdef USE_ARDUINO
195
197
  #define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 2, 1)
@@ -246,12 +246,15 @@ def entity_duplicate_validator(platform: str) -> Callable[[ConfigType], ConfigTy
246
246
  "\n to distinguish them"
247
247
  )
248
248
 
249
- raise cv.Invalid(
250
- f"Duplicate {platform} entity with name '{entity_name}' found{device_prefix}. "
251
- f"{conflict_msg}. "
252
- "Each entity on a device must have a unique name within its platform."
253
- f"{sanitized_msg}"
254
- )
249
+ # Skip duplicate entity name validation when testing_mode is enabled
250
+ # This flag is used for grouped component testing
251
+ if not CORE.testing_mode:
252
+ raise cv.Invalid(
253
+ f"Duplicate {platform} entity with name '{entity_name}' found{device_prefix}. "
254
+ f"{conflict_msg}. "
255
+ "Each entity on a device must have a unique name within its platform."
256
+ f"{sanitized_msg}"
257
+ )
255
258
 
256
259
  # Store metadata about this entity
257
260
  entity_metadata: EntityMetadata = {
esphome/espota2.py CHANGED
@@ -410,7 +410,7 @@ def run_ota_impl_(
410
410
  af, socktype, _, _, sa = r
411
411
  _LOGGER.info("Connecting to %s port %s...", sa[0], sa[1])
412
412
  sock = socket.socket(af, socktype)
413
- sock.settimeout(10.0)
413
+ sock.settimeout(20.0)
414
414
  try:
415
415
  sock.connect(sa)
416
416
  except OSError as err:
esphome/pins.py CHANGED
@@ -118,11 +118,11 @@ class PinRegistry(dict):
118
118
  parent_config = fconf.get_config_for_path(parent_path)
119
119
  final_val_fun(pin_config, parent_config)
120
120
  allow_others = pin_config.get(CONF_ALLOW_OTHER_USES, False)
121
- if count != 1 and not allow_others:
121
+ if count != 1 and not allow_others and not CORE.testing_mode:
122
122
  raise cv.Invalid(
123
123
  f"Pin {pin_config[CONF_NUMBER]} is used in multiple places"
124
124
  )
125
- if count == 1 and allow_others:
125
+ if count == 1 and allow_others and not CORE.testing_mode:
126
126
  raise cv.Invalid(
127
127
  f"Pin {pin_config[CONF_NUMBER]} incorrectly sets {CONF_ALLOW_OTHER_USES}: true"
128
128
  )
esphome/platformio_api.py CHANGED
@@ -5,6 +5,7 @@ import os
5
5
  from pathlib import Path
6
6
  import re
7
7
  import subprocess
8
+ from typing import Any
8
9
 
9
10
  from esphome.const import CONF_COMPILE_PROCESS_LIMIT, CONF_ESPHOME, KEY_CORE
10
11
  from esphome.core import CORE, EsphomeError
@@ -42,6 +43,35 @@ def patch_structhash():
42
43
  cli.clean_build_dir = patched_clean_build_dir
43
44
 
44
45
 
46
+ def patch_file_downloader():
47
+ """Patch PlatformIO's FileDownloader to retry on PackageException errors."""
48
+ from platformio.package.download import FileDownloader
49
+ from platformio.package.exception import PackageException
50
+
51
+ original_init = FileDownloader.__init__
52
+
53
+ def patched_init(self, *args: Any, **kwargs: Any) -> None:
54
+ max_retries = 3
55
+
56
+ for attempt in range(max_retries):
57
+ try:
58
+ return original_init(self, *args, **kwargs)
59
+ except PackageException as e:
60
+ if attempt < max_retries - 1:
61
+ _LOGGER.warning(
62
+ "Package download failed: %s. Retrying... (attempt %d/%d)",
63
+ str(e),
64
+ attempt + 1,
65
+ max_retries,
66
+ )
67
+ else:
68
+ # Final attempt - re-raise
69
+ raise
70
+ return None
71
+
72
+ FileDownloader.__init__ = patched_init
73
+
74
+
45
75
  IGNORE_LIB_WARNINGS = f"(?:{'|'.join(['Hash', 'Update'])})"
46
76
  FILTER_PLATFORMIO_LINES = [
47
77
  r"Verbose mode can be enabled via `-v, --verbose` option.*",
@@ -99,6 +129,7 @@ def run_platformio_cli(*args, **kwargs) -> str | int:
99
129
  import platformio.__main__
100
130
 
101
131
  patch_structhash()
132
+ patch_file_downloader()
102
133
  return run_external_command(platformio.__main__.main, *cmd, **kwargs)
103
134
 
104
135
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: esphome
3
- Version: 2025.10.0b1
3
+ Version: 2025.10.0b2
4
4
  Summary: ESPHome is a system to configure your microcontrollers by simple yet powerful configuration files and control them remotely through Home Automation systems.
5
5
  Author-email: The ESPHome Authors <esphome@openhomefoundation.org>
6
6
  License-Expression: MIT
@@ -35,8 +35,8 @@ Requires-Dist: pyserial==3.5
35
35
  Requires-Dist: platformio==6.1.18
36
36
  Requires-Dist: esptool==5.1.0
37
37
  Requires-Dist: click==8.1.7
38
- Requires-Dist: esphome-dashboard==20250904.0
39
- Requires-Dist: aioesphomeapi==41.13.0
38
+ Requires-Dist: esphome-dashboard==20251009.0
39
+ Requires-Dist: aioesphomeapi==41.14.0
40
40
  Requires-Dist: zeroconf==0.148.0
41
41
  Requires-Dist: puremagic==1.30
42
42
  Requires-Dist: ruamel.yaml==0.18.15