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.
- esphome/__main__.py +7 -0
- esphome/components/canbus/canbus.h +3 -3
- esphome/components/dashboard_import/dashboard_import.cpp +1 -1
- esphome/components/dashboard_import/dashboard_import.h +1 -1
- esphome/components/esp32/__init__.py +9 -8
- esphome/components/esp32_ble/__init__.py +9 -1
- esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp +0 -4
- esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +0 -4
- esphome/components/esp32_improv/esp32_improv_component.cpp +33 -21
- esphome/components/esp32_improv/esp32_improv_component.h +1 -0
- esphome/components/esphome/ota/ota_esphome.cpp +1 -1
- esphome/components/json/json_util.cpp +8 -2
- esphome/components/mcp23xxx_base/mcp23xxx_base.h +3 -3
- esphome/components/mdns/__init__.py +76 -15
- esphome/components/mdns/mdns_component.cpp +52 -57
- esphome/components/mdns/mdns_component.h +12 -1
- esphome/components/mdns/mdns_esp32.cpp +3 -9
- esphome/components/mdns/mdns_esp8266.cpp +1 -1
- esphome/components/mdns/mdns_libretiny.cpp +1 -2
- esphome/components/mdns/mdns_rp2040.cpp +1 -2
- esphome/components/opentherm/opentherm.cpp +5 -5
- esphome/components/opentherm/opentherm.h +3 -3
- esphome/components/openthread/openthread.cpp +5 -3
- esphome/components/uart/__init__.py +1 -1
- esphome/components/usb_host/__init__.py +10 -1
- esphome/components/usb_host/usb_host.h +24 -18
- esphome/components/usb_host/usb_host_client.cpp +18 -39
- esphome/components/wifi/wifi_component.cpp +3 -2
- esphome/const.py +1 -1
- esphome/core/__init__.py +2 -0
- esphome/core/defines.h +2 -0
- esphome/core/entity_helpers.py +9 -6
- esphome/espota2.py +1 -1
- esphome/pins.py +2 -2
- esphome/platformio_api.py +31 -0
- {esphome-2025.10.0b1.dist-info → esphome-2025.10.0b2.dist-info}/METADATA +3 -3
- {esphome-2025.10.0b1.dist-info → esphome-2025.10.0b2.dist-info}/RECORD +41 -41
- {esphome-2025.10.0b1.dist-info → esphome-2025.10.0b2.dist-info}/WHEEL +0 -0
- {esphome-2025.10.0b1.dist-info → esphome-2025.10.0b2.dist-info}/entry_points.txt +0 -0
- {esphome-2025.10.0b1.dist-info → esphome-2025.10.0b2.dist-info}/licenses/LICENSE +0 -0
- {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
|
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
|
-
|
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
|
-
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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 //
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
|
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(
|
186
|
-
txt_entries[i].mValueLength =
|
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
|
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 (
|
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
|
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 =
|
56
|
-
static_assert(MAX_REQUESTS <=
|
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
|
-
//
|
168
|
-
|
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
|
-
//
|
350
|
-
|
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
|
-
|
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 & (
|
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
|
-
|
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 %
|
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
|
-
//
|
456
|
-
|
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:
|
525
|
-
// -
|
526
|
-
// -
|
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
|
-
//
|
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
|
-
|
544
|
-
this->trq_in_use_.fetch_and(static_cast<
|
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(),
|
580
|
-
|
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
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)
|
esphome/core/entity_helpers.py
CHANGED
@@ -246,12 +246,15 @@ def entity_duplicate_validator(platform: str) -> Callable[[ConfigType], ConfigTy
|
|
246
246
|
"\n to distinguish them"
|
247
247
|
)
|
248
248
|
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
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(
|
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.
|
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==
|
39
|
-
Requires-Dist: aioesphomeapi==41.
|
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
|