Compare commits

...
Author SHA1 Message Date
Jesse Hills 704551a6c9 Merge pull request #21268 from esphome/bump-2026.10.0
2026.10.0
2026-10-22 00:42:40 +13:00
Jesse Hills f30e70acfa Bump version to 2026.10.0 2026-10-21 23:53:12 +13:00
Jeroen 594a691d9f [web_server_idf] Increase HTTP server task stack to 4512 bytes (#21038) 2026-10-21 23:53:09 +13:00
Jesse Hills 6c90b5c549 Merge pull request #21254 from esphome/bump-2026.10.0b6
2026.10.0b6
2026-10-21 16:39:54 +13:00
Jesse Hills c8df23a9cb Bump version to 2026.10.0b6 2026-10-21 16:17:51 +13:00
4fdce9d96b [web_server_idf] Reduce loopTask stack usage for state SSE (#21109)
Co-authored-by: Jeroen Jansen <jeroen@Jeroens-MacBook-Air.local>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-21 16:17:49 +13:00
J. Nick Koston 9d296a5748 [ota] Accept an unknown image size in the RP2040 backend so web_server OTA works (#21178) 2026-10-21 16:17:49 +13:00
esphome[bot] f2e013d71a Bump bundled esphome-device-builder to 1.25.0 (#21175) 2026-10-21 16:17:49 +13:00
Jesse Hills 2001dd2be8 Merge pull request #21097 from esphome/bump-2026.10.0b5
2026.10.0b5
2026-10-19 14:03:02 +13:00
Jesse Hills a6a4b49e75 Bump version to 2026.10.0b5 2026-10-19 11:29:05 +13:00
Bonne EgglestonandJ. Nick Koston 649f2c91f5 [api] Poll the outgoing dial every loop (#21085)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-19 11:29:02 +13:00
Bonne Eggleston ffa20abe96 [api] Poll the outgoing dial every loop on deep sleep builds (#21077) 2026-10-19 11:29:02 +13:00
J. Nick Koston f979ddcf0d [bl0906] Read stale receive bytes instead of spinning on flush() (#21035) 2026-10-19 11:29:02 +13:00
J. Nick Koston 26064f1415 [uart_tcp] Expose the connection entities as platforms (#21008) 2026-10-19 11:29:02 +13:00
J. Nick Koston c50ee215f2 [tcp_uart] Expose the connection entities as platforms (#21009) 2026-10-19 10:36:23 +13:00
Jonathan Swoboda 0fbba03522 [debug] Log uptime in dump_config (#20989) 2026-10-19 10:33:13 +13:00
Jonathan Swoboda 7ccaedfb9e [esp32] Bump recommended ESP-IDF to 5.5.6 (#20987) 2026-10-19 10:33:13 +13:00
esphome[bot] 0173127239 Bump bundled esphome-device-builder to 1.24.0 (#20851)
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
2026-10-19 10:33:13 +13:00
Clyde Stubbs 235c614e68 [epaper_spi] Ensure test card gets drawn (#20804) 2026-10-19 10:33:13 +13:00
Jonathan Swoboda 79823c89ab [mipi_rgb] Rotate pure RGB panels in software (#20757) 2026-10-19 10:33:13 +13:00
J. Nick Koston e09cb64756 [bluetooth_connection] Value-initialize the connection parameters for ESP-IDF 6.1 (#20774) 2026-10-19 10:33:13 +13:00
J. Nick Koston bd5b3fa641 [esp32_ble_client] Value-initialize the connection parameters for ESP-IDF 6.1 (#20776) 2026-10-19 10:33:13 +13:00
Kevin AhrendtandJ. Nick Koston f464994f85 [api][sendspin] Add the Sendspin client id to the capabilities message (#20775)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-19 10:33:13 +13:00
esphome[bot] 4552dbad7a Bump aioesphomeapi from 46.11.0 to 46.12.0 (#20781)
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
2026-10-19 10:33:13 +13:00
J. Nick Koston ad8cd0dd3e [noise] Initialize the protocol id's reserved field for ESP-IDF 6.1 (#20779) 2026-10-19 10:33:13 +13:00
puddlyandJ. Nick Koston dd2582bdfd [usb_uart] Expose interface_class and port_number (#20659)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-19 10:33:13 +13:00
J. Nick Koston ba575600dc [mqtt] Match subscription topics in a loop instead of recursing per character (#20767) 2026-10-19 10:33:13 +13:00
esphome[bot] cd6c0dd402 Bump aioesphomeapi from 46.10.0 to 46.11.0 (#20766)
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
2026-10-19 10:33:13 +13:00
4645d34e1a [web_server_idf] Keep active SSE sessions out of the LRU purge (#17383)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-19 10:33:13 +13:00
dff1422e36 [mdns] Advertise the connected interface in the network TXT record (#20673)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-19 10:33:13 +13:00
71 changed files with 987 additions and 222 deletions

No files matched your search

+1 -1
View File
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version # could be handy for archiving the generated documentation or if some version
# control system is used. # control system is used.
PROJECT_NUMBER = 2026.10.0b4 PROJECT_NUMBER = 2026.10.0
# Using the PROJECT_BRIEF tag one can provide an optional one line description # Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a # for a project that appears at the top of each page and should give viewer a
+1 -1
View File
@@ -22,7 +22,7 @@ RUN \
-r /requirements.txt -r /requirements.txt
# Install the ESPHome Device Builder dashboard. # Install the ESPHome Device Builder dashboard.
RUN uv pip install --no-cache-dir esphome-device-builder==1.23.1 RUN uv pip install --no-cache-dir esphome-device-builder==1.25.0
RUN \ RUN \
platformio settings set enable_telemetry No \ platformio settings set enable_telemetry No \
+8 -2
View File
@@ -403,6 +403,9 @@ message WizardCapabilities {
message SendspinCapabilities { message SendspinCapabilities {
// Bitmask of the Sendspin features this device supports // Bitmask of the Sendspin features this device supports
uint32 feature_flags = 1; uint32 feature_flags = 1;
// The Sendspin client_id: base64url of the client's static public key, 43 characters.
// Empty until the Sendspin client has started once since boot.
string client_id = 2 [(max_data_length) = 43];
} }
message DeviceCapabilitiesResponse { message DeviceCapabilitiesResponse {
@@ -3043,12 +3046,15 @@ enum SerialProxyIdentityFlag {
// IDs below are empty // IDs below are empty
} }
// The descriptor fields of a USB device as seen by the host stack // The descriptor fields of a USB device as seen by the host stack, and the port's index
// among those a vendor driver exposes on the interface
message UsbDeviceDescriptor { message UsbDeviceDescriptor {
uint32 vendor_id = 1; uint32 vendor_id = 1;
uint32 product_id = 2; uint32 product_id = 2;
uint32 bcd_device = 3; uint32 bcd_device = 3;
uint32 interface_number = 4; // bInterfaceNumber the host driver binds to uint32 interface_number = 4; // bInterfaceNumber the port is bound through
uint32 interface_class = 5; // bInterfaceClass of that interface
uint32 port_number = 6;
} }
// The identity of the device behind a port. Identifies one physical device among others of // The identity of the device behind a port. Identifies one physical device among others of
@@ -2128,6 +2128,7 @@ bool APIConnection::send_device_capabilities_response_() {
#endif #endif
#if defined(USE_SENDSPIN) && defined(USE_ESP_IDF) #if defined(USE_SENDSPIN) && defined(USE_ESP_IDF)
resp.sendspin.feature_flags = sendspin_::global_sendspin_hub->get_feature_flags(); resp.sendspin.feature_flags = sendspin_::global_sendspin_hub->get_feature_flags();
resp.sendspin.client_id = sendspin_::global_sendspin_hub->get_client_id();
#endif #endif
return this->send_message(resp); return this->send_message(resp);
} }
@@ -152,10 +152,7 @@ void OutgoingConnectionManager::loop(APIServer *server) {
void OutgoingConnectionManager::try_dial_(APIServer *server, uint32_t now) { void OutgoingConnectionManager::try_dial_(APIServer *server, uint32_t now) {
if (!network::is_connected()) { if (!network::is_connected()) {
// Flips within seconds of boot; recheck fast so a deep sleep wake return; // still WAITING with the wait elapsed, so the next loop rechecks
// window is not spent waiting
this->schedule_wait_(now, NETWORK_RETRY_MS);
return;
} }
struct sockaddr_storage addr; struct sockaddr_storage addr;
socklen_t addr_len = this->target_sockaddr_(&addr); socklen_t addr_len = this->target_sockaddr_(&addr);
@@ -197,7 +194,6 @@ void OutgoingConnectionManager::try_dial_(APIServer *server, uint32_t now) {
} }
this->state_ = DialState::DIAL_STATE_CONNECTING; this->state_ = DialState::DIAL_STATE_CONNECTING;
this->state_ts_ = now; this->state_ts_ = now;
this->last_poll_ = now;
} }
void OutgoingConnectionManager::poll_connect_(APIServer *server, uint32_t now) { void OutgoingConnectionManager::poll_connect_(APIServer *server, uint32_t now) {
@@ -206,10 +202,6 @@ void OutgoingConnectionManager::poll_connect_(APIServer *server, uint32_t now) {
this->schedule_retry_(now); this->schedule_retry_(now);
return; return;
} }
if (now - this->last_poll_ < CONNECT_POLL_INTERVAL_MS) {
return;
}
this->last_poll_ = now;
int err = 0; int err = 0;
switch (socket::poll_connect(*this->dial_socket_, err)) { switch (socket::poll_connect(*this->dial_socket_, err)) {
case socket::ConnectPollResult::CONNECT_POLL_RESULT_PENDING: case socket::ConnectPollResult::CONNECT_POLL_RESULT_PENDING:
@@ -59,8 +59,6 @@ class OutgoingConnectionManager {
static constexpr uint32_t BACKOFF_MIN_MS = 5000; static constexpr uint32_t BACKOFF_MIN_MS = 5000;
static constexpr uint32_t BACKOFF_MAX_MS = 300000; static constexpr uint32_t BACKOFF_MAX_MS = 300000;
static constexpr uint32_t CONNECT_TIMEOUT_MS = 10000; static constexpr uint32_t CONNECT_TIMEOUT_MS = 10000;
static constexpr uint32_t CONNECT_POLL_INTERVAL_MS = 250;
static constexpr uint32_t NETWORK_RETRY_MS = 500;
static constexpr uint32_t PRECONDITION_RETRY_MS = 5000; static constexpr uint32_t PRECONDITION_RETRY_MS = 5000;
// A deep sleep wake window is too short to spend on the delay, so those // A deep sleep wake window is too short to spend on the delay, so those
// builds dial out as soon as the target is gone // builds dial out as soon as the target is gone
@@ -104,7 +102,6 @@ class OutgoingConnectionManager {
uint32_t backoff_{BACKOFF_MIN_MS}; uint32_t backoff_{BACKOFF_MIN_MS};
uint32_t wait_{BOOT_WAIT_MS}; uint32_t wait_{BOOT_WAIT_MS};
uint32_t state_ts_{0}; uint32_t state_ts_{0};
uint32_t last_poll_{0};
// Byte-aligned types last // Byte-aligned types last
#ifndef API_OUTGOING_CONNECTION_HOST #ifndef API_OUTGOING_CONNECTION_HOST
+6
View File
@@ -310,12 +310,14 @@ uint32_t WizardCapabilities::calc_size_msg(const void *self) {
uint8_t *SendspinCapabilities::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) { uint8_t *SendspinCapabilities::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const SendspinCapabilities *>(self); const auto &msg = *static_cast<const SendspinCapabilities *>(self);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.feature_flags); pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.feature_flags);
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, msg.client_id);
return pos; return pos;
} }
uint32_t SendspinCapabilities::calc_size_msg(const void *self) { uint32_t SendspinCapabilities::calc_size_msg(const void *self) {
const auto &msg = *static_cast<const SendspinCapabilities *>(self); const auto &msg = *static_cast<const SendspinCapabilities *>(self);
uint32_t size = 0; uint32_t size = 0;
size += ProtoSize::calc_uint32(1, msg.feature_flags); size += ProtoSize::calc_uint32(1, msg.feature_flags);
size += !msg.client_id.empty() ? 2 + msg.client_id.size() : 0;
return size; return size;
} }
#endif #endif
@@ -4175,6 +4177,8 @@ uint8_t *UsbDeviceDescriptor::encode_msg(const void *self, uint8_t *__restrict__
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, msg.product_id); pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, msg.product_id);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, msg.bcd_device); pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, msg.bcd_device);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, msg.interface_number); pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, msg.interface_number);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 5, msg.interface_class);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 6, msg.port_number);
return pos; return pos;
} }
uint32_t UsbDeviceDescriptor::calc_size_msg(const void *self) { uint32_t UsbDeviceDescriptor::calc_size_msg(const void *self) {
@@ -4184,6 +4188,8 @@ uint32_t UsbDeviceDescriptor::calc_size_msg(const void *self) {
size += ProtoSize::calc_uint32(1, msg.product_id); size += ProtoSize::calc_uint32(1, msg.product_id);
size += ProtoSize::calc_uint32(1, msg.bcd_device); size += ProtoSize::calc_uint32(1, msg.bcd_device);
size += ProtoSize::calc_uint32(1, msg.interface_number); size += ProtoSize::calc_uint32(1, msg.interface_number);
size += ProtoSize::calc_uint32(1, msg.interface_class);
size += ProtoSize::calc_uint32(1, msg.port_number);
return size; return size;
} }
uint8_t *SerialProxyIdentity::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) { uint8_t *SerialProxyIdentity::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
+3
View File
@@ -750,6 +750,7 @@ class WizardCapabilities final : public ProtoMessage {
class SendspinCapabilities final : public ProtoMessage { class SendspinCapabilities final : public ProtoMessage {
public: public:
uint32_t feature_flags{0}; uint32_t feature_flags{0};
StringRef client_id{nullptr, 0}; // null until set, encode only
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM); static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG); return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
@@ -4083,6 +4084,8 @@ class UsbDeviceDescriptor final : public ProtoMessage {
uint32_t product_id{0}; uint32_t product_id{0};
uint32_t bcd_device{0}; uint32_t bcd_device{0};
uint32_t interface_number{0}; uint32_t interface_number{0};
uint32_t interface_class{0};
uint32_t port_number{0};
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM); static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG); return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
+3
View File
@@ -1119,6 +1119,7 @@ const char *WizardCapabilities::dump_to(DumpBuffer &out) const {
const char *SendspinCapabilities::dump_to(DumpBuffer &out) const { const char *SendspinCapabilities::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("SendspinCapabilities")); MessageDumpHelper helper(out, ESPHOME_PSTR("SendspinCapabilities"));
dump_field(out, ESPHOME_PSTR("feature_flags"), this->feature_flags); dump_field(out, ESPHOME_PSTR("feature_flags"), this->feature_flags);
dump_field(out, ESPHOME_PSTR("client_id"), this->client_id);
return out.c_str(); return out.c_str();
} }
#endif #endif
@@ -2942,6 +2943,8 @@ const char *UsbDeviceDescriptor::dump_to(DumpBuffer &out) const {
dump_field(out, ESPHOME_PSTR("product_id"), this->product_id); dump_field(out, ESPHOME_PSTR("product_id"), this->product_id);
dump_field(out, ESPHOME_PSTR("bcd_device"), this->bcd_device); dump_field(out, ESPHOME_PSTR("bcd_device"), this->bcd_device);
dump_field(out, ESPHOME_PSTR("interface_number"), this->interface_number); dump_field(out, ESPHOME_PSTR("interface_number"), this->interface_number);
dump_field(out, ESPHOME_PSTR("interface_class"), this->interface_class);
dump_field(out, ESPHOME_PSTR("port_number"), this->port_number);
return out.c_str(); return out.c_str();
} }
const char *SerialProxyIdentity::dump_to(DumpBuffer &out) const { const char *SerialProxyIdentity::dump_to(DumpBuffer &out) const {
+2 -2
View File
@@ -20,7 +20,7 @@ constexpr uint8_t bl0906_checksum(const uint8_t address, const DataPacket *data)
void BL0906::loop() { void BL0906::loop() {
while (this->available()) while (this->available())
this->flush(); this->read();
if (this->current_stage_ == STAGE_IDLE) { if (this->current_stage_ == STAGE_IDLE) {
// Woken up between cycles to drain the action queue. Go back to sleep. // Woken up between cycles to drain the action queue. Go back to sleep.
@@ -92,7 +92,7 @@ void BL0906::advance_stage_() {
void BL0906::setup() { void BL0906::setup() {
while (this->available()) while (this->available())
this->flush(); this->read();
this->write_array(USR_WRPROT_WITABLE, sizeof(USR_WRPROT_WITABLE)); this->write_array(USR_WRPROT_WITABLE, sizeof(USR_WRPROT_WITABLE));
// Calibration (1: register address; 2: value before calibration; 3: value after calibration) // Calibration (1: register address; 2: value before calibration; 3: value after calibration)
this->bias_correction_(BL0906_RMSOS_1, 0.01600, 0); // Calibration current_1 this->bias_correction_(BL0906_RMSOS_1, 0.01600, 0); // Calibration current_1
@@ -382,7 +382,7 @@ void BluedroidGattClient::set_disconnecting_() {
esp_err_t BluedroidGattClient::update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, esp_err_t BluedroidGattClient::update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency,
uint16_t timeout, const char *param_type) { uint16_t timeout, const char *param_type) {
esp_ble_conn_update_params_t conn_params = {{0}}; esp_ble_conn_update_params_t conn_params = {};
memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t)); memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t));
conn_params.min_int = min_interval; conn_params.min_int = min_interval;
conn_params.max_int = max_interval; conn_params.max_int = max_interval;
@@ -34,6 +34,15 @@ void DebugComponent::dump_config() {
this->free_heap_ = get_free_heap_(); this->free_heap_ = get_free_heap_();
ESP_LOGD(TAG, "Free Heap Size: %" PRIu32 " bytes", this->free_heap_); ESP_LOGD(TAG, "Free Heap Size: %" PRIu32 " bytes", this->free_heap_);
uint32_t uptime = static_cast<uint32_t>(millis_64() / 1000);
uint32_t days = uptime / 86400;
uptime -= days * 86400;
uint32_t hours = uptime / 3600;
uptime -= hours * 3600;
uint32_t minutes = uptime / 60;
uptime -= minutes * 60;
ESP_LOGD(TAG, "Uptime: %" PRIu32 "d %02" PRIu32 "h %02" PRIu32 "m %02" PRIu32 "s", days, hours, minutes, uptime);
pos = get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>(device_info_buffer), pos); pos = get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>(device_info_buffer), pos);
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
+1
View File
@@ -208,6 +208,7 @@ def _final_validate(config) -> None:
else: else:
# If no drawing methods are configured, and LVGL is not enabled, show a test card # If no drawing methods are configured, and LVGL is not enabled, show a test card
config[CONF_SHOW_TEST_CARD] = True config[CONF_SHOW_TEST_CARD] = True
config[CONF_UPDATE_INTERVAL] = update_interval("1min")
elif CONF_UPDATE_INTERVAL not in config: elif CONF_UPDATE_INTERVAL not in config:
config[CONF_UPDATE_INTERVAL] = update_interval("1min") config[CONF_UPDATE_INTERVAL] = update_interval("1min")
+5 -4
View File
@@ -1099,7 +1099,7 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = {
# See: https://github.com/pioarduino/esp-idf/releases # See: https://github.com/pioarduino/esp-idf/releases
ARDUINO_IDF_VERSION_LOOKUP = { ARDUINO_IDF_VERSION_LOOKUP = {
cv.Version(4, 0, 0, "alpha1"): cv.Version(6, 0, 1), cv.Version(4, 0, 0, "alpha1"): cv.Version(6, 0, 1),
cv.Version(3, 3, 12): cv.Version(5, 5, 5), cv.Version(3, 3, 12): cv.Version(5, 5, 6),
cv.Version(3, 3, 11): cv.Version(5, 5, 5), cv.Version(3, 3, 11): cv.Version(5, 5, 5),
cv.Version(3, 3, 10): cv.Version(5, 5, 5), cv.Version(3, 3, 10): cv.Version(5, 5, 5),
cv.Version(3, 3, 9): cv.Version(5, 5, 4), cv.Version(3, 3, 9): cv.Version(5, 5, 4),
@@ -1123,9 +1123,9 @@ ARDUINO_IDF_VERSION_LOOKUP = {
# The default/recommended esp-idf framework version # The default/recommended esp-idf framework version
# - https://github.com/espressif/esp-idf/releases # - https://github.com/espressif/esp-idf/releases
ESP_IDF_FRAMEWORK_VERSION_LOOKUP = { ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
"recommended": cv.Version(5, 5, 5), "recommended": cv.Version(5, 5, 6),
"latest": cv.Version(5, 5, 5), "latest": cv.Version(5, 5, 6),
"dev": cv.Version(5, 5, 5), "dev": cv.Version(5, 5, 6),
} }
ESP_IDF_PLATFORM_VERSION_LOOKUP = { ESP_IDF_PLATFORM_VERSION_LOOKUP = {
@@ -1135,6 +1135,7 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = {
cv.Version( cv.Version(
6, 0, 0 6, 0, 0
): "https://github.com/pioarduino/platform-espressif32.git#prep_IDF6", ): "https://github.com/pioarduino/platform-espressif32.git#prep_IDF6",
cv.Version(5, 5, 6): cv.Version(55, 3, 312),
cv.Version(5, 5, 5): cv.Version(55, 3, 312), cv.Version(5, 5, 5): cv.Version(55, 3, 312),
cv.Version(5, 5, 4): cv.Version(55, 3, 39), cv.Version(5, 5, 4): cv.Version(55, 3, 39),
cv.Version(5, 5, 3, "1"): cv.Version(55, 3, 37), cv.Version(5, 5, 3, "1"): cv.Version(55, 3, 37),
@@ -300,7 +300,7 @@ void BLEClientBase::log_warning_(const char *message) {
esp_err_t BLEClientBase::update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, esp_err_t BLEClientBase::update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency,
uint16_t timeout, const char *param_type) { uint16_t timeout, const char *param_type) {
esp_ble_conn_update_params_t conn_params = {{0}}; esp_ble_conn_update_params_t conn_params = {};
memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t)); memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t));
conn_params.min_int = min_interval; conn_params.min_int = min_interval;
conn_params.max_int = max_interval; conn_params.max_int = max_interval;
+52 -1
View File
@@ -20,6 +20,10 @@
#ifdef USE_DASHBOARD_IMPORT #ifdef USE_DASHBOARD_IMPORT
#include "esphome/components/dashboard_import/dashboard_import.h" #include "esphome/components/dashboard_import/dashboard_import.h"
#endif #endif
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
#include "esphome/components/ethernet/ethernet_component.h"
#include "esphome/components/wifi/wifi_component.h"
#endif
namespace esphome::mdns { namespace esphome::mdns {
@@ -39,6 +43,45 @@ MDNS_STATIC_CONST_CHAR(SERVICE_TCP, "_tcp");
// Wrap build-time defines into flash storage // Wrap build-time defines into flash storage
MDNS_STATIC_CONST_CHAR(VALUE_VERSION, ESPHOME_VERSION); MDNS_STATIC_CONST_CHAR(VALUE_VERSION, ESPHOME_VERSION);
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
MDNS_STATIC_CONST_CHAR(NETWORK_WIFI, "wifi");
MDNS_STATIC_CONST_CHAR(NETWORK_ETHERNET, "ethernet");
// Same order as the default route in NetworkComponent::loop()
const char *MDNSComponent::connected_network_() const {
const bool ethernet = ethernet::global_eth_component->is_connected();
const bool wifi = wifi::global_wifi_component->is_connected();
#ifdef USE_NETWORK_PRIMARY_INTERFACE_WIFI
if (wifi)
return NETWORK_WIFI;
#endif
if (ethernet)
return NETWORK_ETHERNET;
if (wifi)
return NETWORK_WIFI;
return nullptr;
}
void MDNSComponent::update_network_txt_() {
if (this->network_txt_ == nullptr)
return;
// With no interface connected, keep the last value instead of flapping to wifi
const char *network = this->connected_network_();
if (network == nullptr || network == this->network_txt_)
return;
if (!this->publish_network_txt_(network)) {
// The _esphomelib service is missing, so a retry would fail too
this->network_txt_ = nullptr;
return;
}
ESP_LOGD(TAG, "Network TXT: %s", network);
this->network_txt_ = network;
#ifdef USE_MDNS_STORE_SERVICES
this->network_record_->value = MDNS_STR(network);
#endif
}
#endif
void MDNSComponent::setup_buffers_and_register_(PlatformRegisterFn platform_register) { void MDNSComponent::setup_buffers_and_register_(PlatformRegisterFn platform_register) {
#ifdef USE_MDNS_STORE_SERVICES #ifdef USE_MDNS_STORE_SERVICES
auto &services = this->services_; auto &services = this->services_;
@@ -160,7 +203,15 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
txt_records.push_back({MDNS_STR(TXT_BOARD), MDNS_STR(VALUE_BOARD)}); txt_records.push_back({MDNS_STR(TXT_BOARD), MDNS_STR(VALUE_BOARD)});
#if defined(USE_WIFI) #ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
const char *network = this->connected_network_();
this->network_txt_ = network != nullptr ? network : NETWORK_WIFI;
txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(this->network_txt_)});
#ifdef USE_MDNS_STORE_SERVICES
// services is services_ here, which never reallocates
this->network_record_ = &txt_records.back();
#endif
#elif defined(USE_WIFI)
MDNS_STATIC_CONST_CHAR(NETWORK_WIFI, "wifi"); MDNS_STATIC_CONST_CHAR(NETWORK_WIFI, "wifi");
txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_WIFI)}); txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_WIFI)});
#elif defined(USE_ETHERNET) #elif defined(USE_ETHERNET)
+22
View File
@@ -32,6 +32,12 @@
#define USE_MDNS_DEVICE_INFO_TXT #define USE_MDNS_DEVICE_INFO_TXT
#endif #endif
// With both wifi and ethernet, the _esphomelib "network" TXT value follows the connected interface.
// OpenThread hands its services to the SRP client once, so it keeps the fixed value.
#if defined(USE_API) && defined(USE_WIFI) && defined(USE_ETHERNET) && !defined(USE_OPENTHREAD)
#define USE_MDNS_DYNAMIC_NETWORK_TXT
#endif
namespace esphome::mdns { namespace esphome::mdns {
// Helper struct that identifies strings that may be stored in flash storage (similar to LogString) // Helper struct that identifies strings that may be stored in flash storage (similar to LogString)
@@ -150,6 +156,22 @@ class MDNSComponent final : public Component
void setup_buffers_and_register_(PlatformRegisterFn platform_register); void setup_buffers_and_register_(PlatformRegisterFn platform_register);
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
static constexpr uint32_t NETWORK_TXT_CHECK_INTERVAL_MS = 1000;
/// The first connected interface in network: priority order ("ethernet" or "wifi"), else nullptr.
const char *connected_network_() const;
/// Check the connected interface and publish a changed "network" TXT value.
void update_network_txt_();
/// Platform-specific: replace the "network" TXT value of the registered _esphomelib service.
bool publish_network_txt_(const char *network);
/// Advertised value; nullptr until the _esphomelib service is registered, and after a failed publish.
const char *network_txt_{nullptr};
#ifdef USE_MDNS_STORE_SERVICES
/// The "network" record in services_, kept in step with network_txt_.
MDNSTXTRecord *network_record_{nullptr};
#endif
#endif
#ifdef USE_MDNS_DYNAMIC_TXT #ifdef USE_MDNS_DYNAMIC_TXT
/// Storage for runtime-generated TXT values from user lambdas /// Storage for runtime-generated TXT values from user lambdas
/// Pre-sized at compile time via MDNS_DYNAMIC_TXT_COUNT to avoid heap allocations. /// Pre-sized at compile time via MDNS_DYNAMIC_TXT_COUNT to avoid heap allocations.
+18 -1
View File
@@ -89,7 +89,24 @@ bool MDNSComponent::set_service_enabled(const char *service_type, const char *pr
} }
#endif // USE_MDNS_SUPPORTS_ENABLE_DISABLE && !USE_OPENTHREAD #endif // USE_MDNS_SUPPORTS_ENABLE_DISABLE && !USE_OPENTHREAD
void MDNSComponent::setup() { this->setup_buffers_and_register_(register_esp32); } #ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
bool MDNSComponent::publish_network_txt_(const char *network) {
// ESP-IDF copies the value and announces the changed record
esp_err_t err = mdns_service_txt_item_set("_esphomelib", "_tcp", "network", network);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Failed to update network TXT: %s", esp_err_to_name(err));
return false;
}
return true;
}
#endif
void MDNSComponent::setup() {
this->setup_buffers_and_register_(register_esp32);
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
this->set_interval(NETWORK_TXT_CHECK_INTERVAL_MS, [this]() { this->update_network_txt_(); });
#endif
}
void MDNSComponent::on_shutdown() { void MDNSComponent::on_shutdown() {
#ifndef USE_OPENTHREAD #ifndef USE_OPENTHREAD
+19
View File
@@ -46,7 +46,26 @@ void MDNSComponent::start_polling_window_() {
} }
#endif #endif
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
ESPHOME_LOG_TAG(TAG, "mdns");
bool MDNSComponent::publish_network_txt_(const char *network) {
// LEAmDNS replaces the value of an existing key; announce() sends the change
if (!MDNS.addServiceTxt("esphomelib", "tcp", "network", network)) {
ESP_LOGW(TAG, "Failed to update network TXT");
return false;
}
MDNS.announce();
this->start_polling_window_();
return true;
}
#endif
void MDNSComponent::setup() { void MDNSComponent::setup() {
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
// No-op until the first IP-up event registers the services
this->set_interval(NETWORK_TXT_CHECK_INTERVAL_MS, [this]() { this->update_network_txt_(); });
#endif
// arduino-pico stubs out LwipIntf::stateUpCB (the netif status callback LEAmDNS uses // arduino-pico stubs out LwipIntf::stateUpCB (the netif status callback LEAmDNS uses
// on ESP8266 for auto-restart), so we must drive begin()/notifyAPChange() from our // on ESP8266 for auto-restart), so we must drive begin()/notifyAPChange() from our
// own IP state listener. Both WiFi and Ethernet have the same listener signature — // own IP state listener. Both WiFi and Ethernet have the same listener signature —
@@ -9,6 +9,10 @@ class RgbDriverChip(DriverChip):
def transforms(self) -> set[str]: def transforms(self) -> set[str]:
""" """
Return the set of transformations supported by this driver chip. Return the set of transformations supported by this driver chip.
RGB displays do not support axis swapping, so this method removes CONF_SWAP_XY RGB displays do not support axis swapping, so this method removes CONF_SWAP_XY.
A model with an empty init sequence has no command interface at all, so it
cannot apply any transform in hardware.
""" """
if self.initsequence is not None and len(self.initsequence) == 0:
return set()
return super().transforms - {CONF_SWAP_XY} return super().transforms - {CONF_SWAP_XY}
+1 -57
View File
@@ -16,6 +16,7 @@
#include "lwip/dns.h" #include "lwip/dns.h"
#include "lwip/err.h" #include "lwip/err.h"
#include "mqtt_component.h" #include "mqtt_component.h"
#include "mqtt_topic_match.h"
#ifdef USE_API #ifdef USE_API
#include "esphome/components/api/api_server.h" #include "esphome/components/api/api_server.h"
@@ -581,63 +582,6 @@ void MQTTClientComponent::disable() {
this->on_shutdown(); this->on_shutdown();
} }
/** Check if the message topic matches the given subscription topic
*
* INFO: MQTT spec mandates that topics must not be empty and must be valid NULL-terminated UTF-8 strings.
*
* @param message The message topic that was received from the MQTT server. Note: this must not contain
* wildcard characters as mandated by the MQTT spec.
* @param subscription The subscription topic we are matching against.
* @param is_normal Is this a "normal" topic - Does the message topic not begin with a "$".
* @param past_separator Are we past the first '/' topic separator.
* @return true if the subscription topic matches the message topic, false otherwise.
*/
static bool topic_match(const char *message, const char *subscription, bool is_normal, bool past_separator) {
// Reached end of both strings at the same time, this means we have a successful match
if (*message == '\0' && *subscription == '\0')
return true;
// Either the message or the subscribe are at the end. This means they don't match.
if (*message == '\0' || *subscription == '\0')
return false;
bool do_wildcards = is_normal || past_separator;
if (*subscription == '+' && do_wildcards) {
// single level wildcard
// consume + from subscription
subscription++;
// consume everything from message until '/' found or end of string
while (*message != '\0' && *message != '/') {
message++;
}
// after this, both pointers will point to a '/' or to the end of the string
return topic_match(message, subscription, is_normal, true);
}
if (*subscription == '#' && do_wildcards) {
// multilevel wildcard - MQTT mandates that this must be at end of subscribe topic
return true;
}
// this handles '/' and normal characters at the same time.
if (*message != *subscription)
return false;
past_separator = past_separator || *subscription == '/';
// consume characters
subscription++;
message++;
return topic_match(message, subscription, is_normal, past_separator);
}
static bool topic_match(const char *message, const char *subscription) {
return topic_match(message, subscription, *message != '\0' && *message != '$', false);
}
void MQTTClientComponent::on_message(const std::string &topic, const std::string &payload) { void MQTTClientComponent::on_message(const std::string &topic, const std::string &payload) {
#ifdef USE_ESP8266 #ifdef USE_ESP8266
// IMPORTANT: This defer is REQUIRED to prevent stack overflow crashes on ESP8266. // IMPORTANT: This defer is REQUIRED to prevent stack overflow crashes on ESP8266.
@@ -0,0 +1,28 @@
#pragma once
namespace esphome::mqtt {
/// True if the message topic matches the subscription, with + and # wildcards.
/// A loop on purpose: recursing per character overflowed the ESP32 loop task's stack on long topics.
inline bool topic_match(const char *message, const char *subscription) {
// First-level wildcards do not match topics that begin with '$'
bool wildcards = *message != '$';
while (*message != '\0' && *subscription != '\0') {
if (wildcards && *subscription == '#')
return true;
if (wildcards && *subscription == '+') {
subscription++;
while (*message != '\0' && *message != '/')
message++;
continue;
}
if (*message != *subscription)
return false;
wildcards |= *subscription == '/';
message++;
subscription++;
}
return *message == *subscription; // a match only if both ended
}
} // namespace esphome::mqtt
@@ -37,6 +37,7 @@ int NoiseResponderHandshake::init(const NoiseContext &ctx, const uint8_t *prolog
.cipher_id = NOISE_CIPHER_CHACHAPOLY, .cipher_id = NOISE_CIPHER_CHACHAPOLY,
.hash_id = NOISE_HASH_SHA256, .hash_id = NOISE_HASH_SHA256,
.hybrid_id = NOISE_DH_NONE, .hybrid_id = NOISE_DH_NONE,
.reserved = {},
}; };
int err = noise_handshakestate_new_by_id(&this->handshake_, &nid, NOISE_ROLE_RESPONDER); int err = noise_handshakestate_new_by_id(&this->handshake_, &nid, NOISE_ROLE_RESPONDER);
+3 -2
View File
@@ -98,8 +98,9 @@ enum OTAType : uint8_t {
// The OTA backend method surface. Exactly one backend exists per build, // The OTA backend method surface. Exactly one backend exists per build,
// selected in ota_backend_factory.h where this concept is asserted on // selected in ota_backend_factory.h where this concept is asserted on
// make_ota_backend()'s return type. Semantics beyond the signatures: // make_ota_backend()'s return type. Semantics beyond the signatures:
// - begin: prepare for an image of the given size; ota_type defaults to an // - begin: prepare for an image of the given size; 0 means the size is unknown
// app update, so both call forms must be accepted. // (web_server upload) and the backend allows the largest image it can take.
// ota_type defaults to an app update, so both call forms must be accepted.
// - set_update_md5: expected digest of the incoming image, hex string. // - set_update_md5: expected digest of the incoming image, hex string.
// - write: consume the next chunk; end: finalize and mark bootable. // - write: consume the next chunk; end: finalize and mark bootable.
// - abort: safe to call in any state, including after end(). // - abort: safe to call in any state, including after end().
@@ -9,6 +9,12 @@
#include <Updater.h> #include <Updater.h>
// LittleFS region bounds, provided by the arduino-pico linker script.
// NOLINTNEXTLINE(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp)
extern "C" uint8_t _FS_start;
// NOLINTNEXTLINE(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp)
extern "C" uint8_t _FS_end;
namespace esphome::ota { namespace esphome::ota {
static const char *const TAG = "ota"; static const char *const TAG = "ota";
@@ -19,8 +25,11 @@ OTAResponseTypes ArduinoRP2OTABackend::begin(size_t image_size, OTAType ota_type
if (ota_type != OTA_TYPE_UPDATE_APP) { if (ota_type != OTA_TYPE_UPDATE_APP) {
return OTA_RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE; return OTA_RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE;
} }
// OTA size of 0 is not currently handled, but // 0 means the size is unknown (web_server upload). The updater only checks the size
// web_server is not supported for RP2040, so this is not an issue. // against the LittleFS region it stages the image in, so ask for all of it.
if (image_size == 0) {
image_size = reinterpret_cast<uintptr_t>(&_FS_end) - reinterpret_cast<uintptr_t>(&_FS_start);
}
bool ret = Update.begin(image_size, U_FLASH); bool ret = Update.begin(image_size, U_FLASH);
if (ret) { if (ret) {
rp2::preferences_prevent_write(true); rp2::preferences_prevent_write(true);
@@ -34,7 +43,7 @@ OTAResponseTypes ArduinoRP2OTABackend::begin(size_t image_size, OTAType ota_type
return OTA_RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG; return OTA_RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG;
if (error == UPDATE_ERROR_FLASH_CONFIG) if (error == UPDATE_ERROR_FLASH_CONFIG)
return OTA_RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG; return OTA_RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG;
if (error == UPDATE_ERROR_SPACE) if (error == UPDATE_ERROR_SPACE || error == UPDATE_ERROR_SIZE)
return OTA_RESPONSE_ERROR_RP2040_NOT_ENOUGH_SPACE; return OTA_RESPONSE_ERROR_RP2040_NOT_ENOUGH_SPACE;
ESP_LOGE(TAG, "Begin error: %d", error); ESP_LOGE(TAG, "Begin error: %d", error);
@@ -243,6 +243,14 @@ std::optional<std::string> SendspinHub::get_pairing_token() const {
return this->client_->pairing_token(); return this->client_->pairing_token();
} }
// THREAD CONTEXT: Main loop
StringRef SendspinHub::get_client_id() const {
if (this->client_ == nullptr) {
return StringRef();
}
return StringRef(this->client_->client_id());
}
// THREAD CONTEXT: Main loop (invoked from codegen before setup(), or from Sendspin components) // THREAD CONTEXT: Main loop (invoked from codegen before setup(), or from Sendspin components)
void SendspinHub::set_unpaired_access_enabled(bool enabled) { void SendspinHub::set_unpaired_access_enabled(bool enabled) {
this->unpaired_access_ = enabled; this->unpaired_access_ = enabled;
@@ -198,6 +198,12 @@ class SendspinHub final : public Component,
/// started once; it stays available after the client stops. Main loop only. /// started once; it stays available after the client stops. Main loop only.
std::optional<std::string> get_pairing_token() const; std::optional<std::string> get_pairing_token() const;
/// @brief Returns the Sendspin client_id: base64url of the client's static public key.
///
/// Empty until the client has started once; it stays set after the client stops. The returned StringRef points into
/// the client's string, which start() may rewrite, so do not keep it past the current call. Main loop only.
StringRef get_client_id() const;
/// @brief Sets the device information reported to the server in the `client/hello` message. /// @brief Sets the device information reported to the server in the `client/hello` message.
/// ///
/// Each takes a pointer to a string literal emitted by codegen, so it must stay valid for the /// Each takes a pointer to a string literal emitted by codegen, so it must stay valid for the
@@ -412,6 +412,8 @@ void SerialProxy::fill_identity_([[maybe_unused]] IdentityScratch &scratch, api:
msg.usb.product_id = scratch.product_id; msg.usb.product_id = scratch.product_id;
msg.usb.bcd_device = scratch.bcd_device; msg.usb.bcd_device = scratch.bcd_device;
msg.usb.interface_number = this->usb_channel_->get_interface_number(); msg.usb.interface_number = this->usb_channel_->get_interface_number();
msg.usb.interface_class = this->usb_channel_->get_interface_class();
msg.usb.port_number = this->usb_channel_->get_port_number();
msg.manufacturer = StringRef(scratch.manufacturer); msg.manufacturer = StringRef(scratch.manufacturer);
msg.product = StringRef(scratch.product); msg.product = StringRef(scratch.product);
msg.serial_number = StringRef(scratch.serial_number); msg.serial_number = StringRef(scratch.serial_number);
+5 -25
View File
@@ -1,8 +1,7 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import binary_sensor, sensor, socket, uart from esphome.components import socket, uart
from esphome.components.const import ( from esphome.components.const import (
CONF_ALLOWED_IPS, CONF_ALLOWED_IPS,
CONF_CONNECTED,
CONF_DATA_BITS, CONF_DATA_BITS,
CONF_HOST, CONF_HOST,
CONF_PARITY, CONF_PARITY,
@@ -11,22 +10,16 @@ from esphome.components.const import (
CONF_STOP_BITS, CONF_STOP_BITS,
) )
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import CONF_BAUD_RATE, CONF_ID, CONF_PORT
CONF_BAUD_RATE,
CONF_ID,
CONF_PORT,
DEVICE_CLASS_CONNECTIVITY,
ENTITY_CATEGORY_DIAGNOSTIC,
STATE_CLASS_TOTAL_INCREASING,
)
from esphome.types import ConfigType from esphome.types import ConfigType
CODEOWNERS = ["@Bascht74"] CODEOWNERS = ["@Bascht74"]
DEPENDENCIES = ["network"] DEPENDENCIES = ["network"]
AUTO_LOAD = ["uart", "binary_sensor", "sensor", "socket"] AUTO_LOAD = ["uart", "socket"]
DOMAIN = "tcp_uart"
MULTI_CONF = True MULTI_CONF = True
CONF_DISCONNECTS = "disconnects" CONF_TCP_UART_ID = "tcp_uart_id"
tcp_uart_ns = cg.esphome_ns.namespace("tcp_uart") tcp_uart_ns = cg.esphome_ns.namespace("tcp_uart")
TcpUart = tcp_uart_ns.class_("TcpUart", uart.UARTComponent, cg.Component) TcpUart = tcp_uart_ns.class_("TcpUart", uart.UARTComponent, cg.Component)
@@ -45,15 +38,6 @@ BASE_SCHEMA = cv.Schema(
cv.Optional( cv.Optional(
CONF_RECONNECT_INTERVAL, default="5s" CONF_RECONNECT_INTERVAL, default="5s"
): cv.positive_time_period_milliseconds, ): cv.positive_time_period_milliseconds,
cv.Optional(CONF_CONNECTED): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_CONNECTIVITY,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
cv.Optional(CONF_DISCONNECTS): sensor.sensor_schema(
accuracy_decimals=0,
state_class=STATE_CLASS_TOTAL_INCREASING,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
} }
).extend(cv.COMPONENT_SCHEMA) ).extend(cv.COMPONENT_SCHEMA)
@@ -99,7 +83,3 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_parity(config[CONF_PARITY])) cg.add(var.set_parity(config[CONF_PARITY]))
if (host := config.get(CONF_HOST)) is not None: if (host := config.get(CONF_HOST)) is not None:
cg.add(var.set_host(host)) cg.add(var.set_host(host))
binary_sensors = binary_sensor.sub_binary_sensors(config)
await binary_sensors(CONF_CONNECTED, var.set_connected_sensor)
sensors = sensor.sub_sensors(config)
await sensors(CONF_DISCONNECTS, var.set_disconnects_sensor)
@@ -0,0 +1,25 @@
import esphome.codegen as cg
from esphome.components import binary_sensor
from esphome.components.const import CONF_CONNECTED
import esphome.config_validation as cv
from esphome.const import CONF_ID, DEVICE_CLASS_CONNECTIVITY, ENTITY_CATEGORY_DIAGNOSTIC
from esphome.types import ConfigType
from . import CONF_TCP_UART_ID, TcpUart
DEPENDENCIES = ["tcp_uart"]
CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_TCP_UART_ID): cv.use_id(TcpUart),
cv.Optional(CONF_CONNECTED): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_CONNECTIVITY,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
}
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_TCP_UART_ID])
binary_sensors = binary_sensor.sub_binary_sensors(config)
await binary_sensors(CONF_CONNECTED, hub.set_connected_sensor)
+31
View File
@@ -0,0 +1,31 @@
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
ENTITY_CATEGORY_DIAGNOSTIC,
STATE_CLASS_TOTAL_INCREASING,
)
from esphome.types import ConfigType
from . import CONF_TCP_UART_ID, TcpUart
DEPENDENCIES = ["tcp_uart"]
CONF_DISCONNECTS = "disconnects"
CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_TCP_UART_ID): cv.use_id(TcpUart),
cv.Optional(CONF_DISCONNECTS): sensor.sensor_schema(
accuracy_decimals=0,
state_class=STATE_CLASS_TOTAL_INCREASING,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
}
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_TCP_UART_ID])
sensors = sensor.sub_sensors(config)
await sensors(CONF_DISCONNECTS, hub.set_disconnects_sensor)
+6
View File
@@ -17,9 +17,11 @@ void TcpUart::setup() {
#ifdef USE_SOCKET_TCP_LISTENER #ifdef USE_SOCKET_TCP_LISTENER
this->listener_.begin(TAG); this->listener_.begin(TAG);
#endif #endif
#ifdef USE_BINARY_SENSOR
if (this->connected_sensor_ != nullptr) { if (this->connected_sensor_ != nullptr) {
this->connected_sensor_->publish_state(false); this->connected_sensor_->publish_state(false);
} }
#endif
#ifdef USE_SENSOR #ifdef USE_SENSOR
if (this->disconnects_sensor_ != nullptr) { if (this->disconnects_sensor_ != nullptr) {
this->disconnects_sensor_->publish_state(0); this->disconnects_sensor_->publish_state(0);
@@ -38,7 +40,9 @@ void TcpUart::dump_config() {
#ifdef USE_SOCKET_TCP_LISTENER #ifdef USE_SOCKET_TCP_LISTENER
this->listener_.dump_config(); this->listener_.dump_config();
#endif #endif
#ifdef USE_BINARY_SENSOR
LOG_BINARY_SENSOR(" ", "Connected", this->connected_sensor_); LOG_BINARY_SENSOR(" ", "Connected", this->connected_sensor_);
#endif
#ifdef USE_SENSOR #ifdef USE_SENSOR
LOG_SENSOR(" ", "Disconnects", this->disconnects_sensor_); LOG_SENSOR(" ", "Disconnects", this->disconnects_sensor_);
#endif #endif
@@ -58,9 +62,11 @@ void TcpUart::sync_link_() {
// Unread bytes of the last session stay readable while down, never into the next one. // Unread bytes of the last session stay readable while down, never into the next one.
this->rx_start_ = this->rx_end_ = 0; this->rx_start_ = this->rx_end_ = 0;
} }
#ifdef USE_BINARY_SENSOR
if (this->connected_sensor_ != nullptr) { if (this->connected_sensor_ != nullptr) {
this->connected_sensor_->publish_state(up); this->connected_sensor_->publish_state(up);
} }
#endif
#ifdef USE_SENSOR #ifdef USE_SENSOR
// Only edges get here, so down is the falling edge. // Only edges get here, so down is the falling edge.
if (!up && this->disconnects_sensor_ != nullptr) { if (!up && this->disconnects_sensor_ != nullptr) {
+7
View File
@@ -1,6 +1,9 @@
#pragma once #pragma once
#include "esphome/core/defines.h"
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h" #include "esphome/components/binary_sensor/binary_sensor.h"
#endif
#ifdef USE_SENSOR #ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h" #include "esphome/components/sensor/sensor.h"
#endif #endif
@@ -23,7 +26,9 @@ class TcpUart : public uart::UARTComponent, public Component {
void set_host(const char *host) { this->link_.set_host(host); } void set_host(const char *host) { this->link_.set_host(host); }
void set_port(uint16_t port) { this->link_.set_port(port); } void set_port(uint16_t port) { this->link_.set_port(port); }
void set_reconnect_interval(uint32_t ms) { this->link_.set_reconnect_interval(ms); } void set_reconnect_interval(uint32_t ms) { this->link_.set_reconnect_interval(ms); }
#ifdef USE_BINARY_SENSOR
void set_connected_sensor(binary_sensor::BinarySensor *sensor) { this->connected_sensor_ = sensor; } void set_connected_sensor(binary_sensor::BinarySensor *sensor) { this->connected_sensor_ = sensor; }
#endif
#ifdef USE_SENSOR #ifdef USE_SENSOR
void set_disconnects_sensor(sensor::Sensor *sensor) { this->disconnects_sensor_ = sensor; } void set_disconnects_sensor(sensor::Sensor *sensor) { this->disconnects_sensor_ = sensor; }
#endif #endif
@@ -64,7 +69,9 @@ class TcpUart : public uart::UARTComponent, public Component {
#ifdef USE_SOCKET_TCP_LISTENER #ifdef USE_SOCKET_TCP_LISTENER
socket::TcpListener listener_; socket::TcpListener listener_;
#endif #endif
#ifdef USE_BINARY_SENSOR
binary_sensor::BinarySensor *connected_sensor_{nullptr}; binary_sensor::BinarySensor *connected_sensor_{nullptr};
#endif
#ifdef USE_SENSOR #ifdef USE_SENSOR
sensor::Sensor *disconnects_sensor_{nullptr}; sensor::Sensor *disconnects_sensor_{nullptr};
uint32_t disconnects_{0}; uint32_t disconnects_{0};
+3 -20
View File
@@ -1,8 +1,7 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import binary_sensor, sensor, socket, uart from esphome.components import socket, uart
from esphome.components.const import ( from esphome.components.const import (
CONF_ALLOWED_IPS, CONF_ALLOWED_IPS,
CONF_CONNECTED,
CONF_HOST, CONF_HOST,
CONF_RECONNECT_INTERVAL, CONF_RECONNECT_INTERVAL,
CONF_ROLE, CONF_ROLE,
@@ -14,9 +13,6 @@ from esphome.const import (
CONF_ID, CONF_ID,
CONF_PORT, CONF_PORT,
CONF_UART_ID, CONF_UART_ID,
DEVICE_CLASS_CONNECTIVITY,
ENTITY_CATEGORY_DIAGNOSTIC,
STATE_CLASS_TOTAL_INCREASING,
) )
from esphome.core import CORE from esphome.core import CORE
import esphome.final_validate as fv import esphome.final_validate as fv
@@ -25,10 +21,10 @@ from esphome.types import ConfigType
CODEOWNERS = ["@Bascht74"] CODEOWNERS = ["@Bascht74"]
DOMAIN = "uart_tcp" DOMAIN = "uart_tcp"
DEPENDENCIES = ["network", "uart"] DEPENDENCIES = ["network", "uart"]
AUTO_LOAD = ["binary_sensor", "sensor", "socket"] AUTO_LOAD = ["socket"]
MULTI_CONF = True MULTI_CONF = True
CONF_DISCONNECTS = "disconnects" CONF_UART_TCP_ID = "uart_tcp_id"
uart_tcp_ns = cg.esphome_ns.namespace("uart_tcp") uart_tcp_ns = cg.esphome_ns.namespace("uart_tcp")
UartTcp = uart_tcp_ns.class_("UartTcp", cg.Component, uart.UARTDevice) UartTcp = uart_tcp_ns.class_("UartTcp", cg.Component, uart.UARTDevice)
@@ -42,15 +38,6 @@ BASE_SCHEMA = cv.Schema(
cv.Optional( cv.Optional(
CONF_RECONNECT_INTERVAL, default="5s" CONF_RECONNECT_INTERVAL, default="5s"
): cv.positive_time_period_milliseconds, ): cv.positive_time_period_milliseconds,
cv.Optional(CONF_CONNECTED): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_CONNECTIVITY,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
cv.Optional(CONF_DISCONNECTS): sensor.sensor_schema(
accuracy_decimals=0,
state_class=STATE_CLASS_TOTAL_INCREASING,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
} }
).extend(cv.COMPONENT_SCHEMA) ).extend(cv.COMPONENT_SCHEMA)
@@ -155,7 +142,3 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_reconnect_interval(config[CONF_RECONNECT_INTERVAL])) cg.add(var.set_reconnect_interval(config[CONF_RECONNECT_INTERVAL]))
if (host := config.get(CONF_HOST)) is not None: if (host := config.get(CONF_HOST)) is not None:
cg.add(var.set_host(host)) cg.add(var.set_host(host))
binary_sensors = binary_sensor.sub_binary_sensors(config)
await binary_sensors(CONF_CONNECTED, var.set_connected_sensor)
sensors = sensor.sub_sensors(config)
await sensors(CONF_DISCONNECTS, var.set_disconnects_sensor)
@@ -0,0 +1,25 @@
import esphome.codegen as cg
from esphome.components import binary_sensor
from esphome.components.const import CONF_CONNECTED
import esphome.config_validation as cv
from esphome.const import CONF_ID, DEVICE_CLASS_CONNECTIVITY, ENTITY_CATEGORY_DIAGNOSTIC
from esphome.types import ConfigType
from . import CONF_UART_TCP_ID, UartTcp
DEPENDENCIES = ["uart_tcp"]
CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_UART_TCP_ID): cv.use_id(UartTcp),
cv.Optional(CONF_CONNECTED): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_CONNECTIVITY,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
}
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_UART_TCP_ID])
binary_sensors = binary_sensor.sub_binary_sensors(config)
await binary_sensors(CONF_CONNECTED, hub.set_connected_sensor)
+31
View File
@@ -0,0 +1,31 @@
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
ENTITY_CATEGORY_DIAGNOSTIC,
STATE_CLASS_TOTAL_INCREASING,
)
from esphome.types import ConfigType
from . import CONF_UART_TCP_ID, UartTcp
DEPENDENCIES = ["uart_tcp"]
CONF_DISCONNECTS = "disconnects"
CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_UART_TCP_ID): cv.use_id(UartTcp),
cv.Optional(CONF_DISCONNECTS): sensor.sensor_schema(
accuracy_decimals=0,
state_class=STATE_CLASS_TOTAL_INCREASING,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
}
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_UART_TCP_ID])
sensors = sensor.sub_sensors(config)
await sensors(CONF_DISCONNECTS, hub.set_disconnects_sensor)
+6
View File
@@ -19,9 +19,11 @@ void UartTcp::setup() {
#ifdef USE_SOCKET_TCP_LISTENER #ifdef USE_SOCKET_TCP_LISTENER
this->listener_.begin(TAG); this->listener_.begin(TAG);
#endif #endif
#ifdef USE_BINARY_SENSOR
if (this->connected_sensor_ != nullptr) { if (this->connected_sensor_ != nullptr) {
this->connected_sensor_->publish_state(false); this->connected_sensor_->publish_state(false);
} }
#endif
#ifdef USE_SENSOR #ifdef USE_SENSOR
if (this->disconnects_sensor_ != nullptr) { if (this->disconnects_sensor_ != nullptr) {
this->disconnects_sensor_->publish_state(0); this->disconnects_sensor_->publish_state(0);
@@ -40,7 +42,9 @@ void UartTcp::dump_config() {
#ifdef USE_SOCKET_TCP_LISTENER #ifdef USE_SOCKET_TCP_LISTENER
this->listener_.dump_config(); this->listener_.dump_config();
#endif #endif
#ifdef USE_BINARY_SENSOR
LOG_BINARY_SENSOR(" ", "Connected", this->connected_sensor_); LOG_BINARY_SENSOR(" ", "Connected", this->connected_sensor_);
#endif
#ifdef USE_SENSOR #ifdef USE_SENSOR
LOG_SENSOR(" ", "Disconnects", this->disconnects_sensor_); LOG_SENSOR(" ", "Disconnects", this->disconnects_sensor_);
#endif #endif
@@ -60,9 +64,11 @@ void UartTcp::sync_link_() {
// The driver kept whatever arrived while the link was down. // The driver kept whatever arrived while the link was down.
this->discard_uart_(); this->discard_uart_();
} }
#ifdef USE_BINARY_SENSOR
if (this->connected_sensor_ != nullptr) { if (this->connected_sensor_ != nullptr) {
this->connected_sensor_->publish_state(up); this->connected_sensor_->publish_state(up);
} }
#endif
#ifdef USE_SENSOR #ifdef USE_SENSOR
// Only edges get here, so down is the falling edge. // Only edges get here, so down is the falling edge.
if (!up && this->disconnects_sensor_ != nullptr) { if (!up && this->disconnects_sensor_ != nullptr) {
+7
View File
@@ -1,6 +1,9 @@
#pragma once #pragma once
#include "esphome/core/defines.h"
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h" #include "esphome/components/binary_sensor/binary_sensor.h"
#endif
#ifdef USE_SENSOR #ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h" #include "esphome/components/sensor/sensor.h"
#endif #endif
@@ -22,7 +25,9 @@ class UartTcp final : public Component, public uart::UARTDevice {
void set_host(const char *host) { this->link_.set_host(host); } void set_host(const char *host) { this->link_.set_host(host); }
void set_port(uint16_t port) { this->link_.set_port(port); } void set_port(uint16_t port) { this->link_.set_port(port); }
void set_reconnect_interval(uint32_t ms) { this->link_.set_reconnect_interval(ms); } void set_reconnect_interval(uint32_t ms) { this->link_.set_reconnect_interval(ms); }
#ifdef USE_BINARY_SENSOR
void set_connected_sensor(binary_sensor::BinarySensor *sensor) { this->connected_sensor_ = sensor; } void set_connected_sensor(binary_sensor::BinarySensor *sensor) { this->connected_sensor_ = sensor; }
#endif
#ifdef USE_SENSOR #ifdef USE_SENSOR
void set_disconnects_sensor(sensor::Sensor *sensor) { this->disconnects_sensor_ = sensor; } void set_disconnects_sensor(sensor::Sensor *sensor) { this->disconnects_sensor_ = sensor; }
#endif #endif
@@ -53,7 +58,9 @@ class UartTcp final : public Component, public uart::UARTDevice {
#ifdef USE_SOCKET_TCP_LISTENER #ifdef USE_SOCKET_TCP_LISTENER
socket::TcpListener listener_; socket::TcpListener listener_;
#endif #endif
#ifdef USE_BINARY_SENSOR
binary_sensor::BinarySensor *connected_sensor_{nullptr}; binary_sensor::BinarySensor *connected_sensor_{nullptr};
#endif
// Loop start time of the last socket-to-UART write; sizes the next paced write. // Loop start time of the last socket-to-UART write; sizes the next paced write.
uint32_t last_write_ms_{0}; uint32_t last_write_ms_{0};
#ifdef USE_SENSOR #ifdef USE_SENSOR
-8
View File
@@ -152,14 +152,6 @@ bool USBUartTypeCH34X::config_step(USBUartChannelBase *channel, uint8_t step, bo
} }
} }
std::vector<CdcEps> USBUartTypeCH34X::parse_descriptors(usb_device_handle_t dev_hdl) {
auto result = USBUartTypeCdcAcm::parse_descriptors(dev_hdl);
// ch34x doesn't use the interrupt endpoint, and we don't have endpoints to spare
for (auto &cdc_dev : result) {
cdc_dev.interrupt_interface_number = 0xFF;
}
return result;
}
} // namespace esphome::usb_uart } // namespace esphome::usb_uart
#endif // USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 || #endif // USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 ||
+5 -5
View File
@@ -88,11 +88,11 @@ std::vector<CdcEps> USBUartTypeCP210X::parse_descriptors(usb_device_handle_t dev
ESP_LOGE(TAG, "in_ep: usb_parse_endpoint_descriptor_by_index failed"); ESP_LOGE(TAG, "in_ep: usb_parse_endpoint_descriptor_by_index failed");
continue; continue;
} }
if (in_ep->bEndpointAddress & usb_host::USB_DIR_IN) { CdcEps eps = in_ep->bEndpointAddress & usb_host::USB_DIR_IN
cdc_devs.push_back({CdcEps{nullptr, in_ep, out_ep, data_desc->bInterfaceNumber}}); ? CdcEps{nullptr, in_ep, out_ep, data_desc->bInterfaceNumber}
} else { : CdcEps{nullptr, out_ep, in_ep, data_desc->bInterfaceNumber};
cdc_devs.push_back({CdcEps{nullptr, out_ep, in_ep, data_desc->bInterfaceNumber}}); eps.bulk_interface_class = data_desc->bInterfaceClass;
} cdc_devs.push_back(eps);
} }
return cdc_devs; return cdc_devs;
} }
+1
View File
@@ -217,6 +217,7 @@ static optional<CdcEps> get_uart(const usb_config_desc_t *config_desc, uint8_t i
} }
eps.bulk_interface_number = intf_desc->bInterfaceNumber; eps.bulk_interface_number = intf_desc->bInterfaceNumber;
eps.bulk_interface_class = intf_desc->bInterfaceClass;
return eps; return eps;
} }
+4 -1
View File
@@ -189,7 +189,10 @@ std::vector<CdcEps> USBUartTypePL2303::parse_descriptors(usb_device_handle_t dev
} }
if (in_ep && out_ep) { if (in_ep && out_ep) {
cdc_devs.push_back(CdcEps{notify_ep, in_ep, out_ep, intf->bInterfaceNumber, intf->bInterfaceNumber}); CdcEps eps{notify_ep, in_ep, out_ep, intf->bInterfaceNumber, intf->bInterfaceNumber};
eps.bulk_interface_class = intf->bInterfaceClass;
eps.interrupt_interface_class = intf->bInterfaceClass;
cdc_devs.push_back(eps);
break; // PL2303 is single-port break; // PL2303 is single-port
} }
} }
+4 -2
View File
@@ -41,14 +41,17 @@ static optional<CdcEps> get_cdc(const usb_config_desc_t *config_desc, uint8_t in
if (ep->bmAttributes == USB_BM_ATTRIBUTES_XFER_INT) { if (ep->bmAttributes == USB_BM_ATTRIBUTES_XFER_INT) {
eps.notify_ep = ep; eps.notify_ep = ep;
eps.interrupt_interface_number = intf_desc->bInterfaceNumber; eps.interrupt_interface_number = intf_desc->bInterfaceNumber;
eps.interrupt_interface_class = intf_desc->bInterfaceClass;
} else if (ep->bmAttributes == USB_BM_ATTRIBUTES_XFER_BULK && ep->bEndpointAddress & usb_host::USB_DIR_IN && } else if (ep->bmAttributes == USB_BM_ATTRIBUTES_XFER_BULK && ep->bEndpointAddress & usb_host::USB_DIR_IN &&
(eps.bulk_interface_number == 0xFF || eps.bulk_interface_number == intf_desc->bInterfaceNumber)) { (eps.bulk_interface_number == 0xFF || eps.bulk_interface_number == intf_desc->bInterfaceNumber)) {
eps.in_ep = ep; eps.in_ep = ep;
eps.bulk_interface_number = intf_desc->bInterfaceNumber; eps.bulk_interface_number = intf_desc->bInterfaceNumber;
eps.bulk_interface_class = intf_desc->bInterfaceClass;
} else if (ep->bmAttributes == USB_BM_ATTRIBUTES_XFER_BULK && !(ep->bEndpointAddress & usb_host::USB_DIR_IN) && } else if (ep->bmAttributes == USB_BM_ATTRIBUTES_XFER_BULK && !(ep->bEndpointAddress & usb_host::USB_DIR_IN) &&
(eps.bulk_interface_number == 0xFF || eps.bulk_interface_number == intf_desc->bInterfaceNumber)) { (eps.bulk_interface_number == 0xFF || eps.bulk_interface_number == intf_desc->bInterfaceNumber)) {
eps.out_ep = ep; eps.out_ep = ep;
eps.bulk_interface_number = intf_desc->bInterfaceNumber; eps.bulk_interface_number = intf_desc->bInterfaceNumber;
eps.bulk_interface_class = intf_desc->bInterfaceClass;
} else { } else {
ESP_LOGE(TAG, "Unexpected endpoint attributes: %02X", ep->bmAttributes); ESP_LOGE(TAG, "Unexpected endpoint attributes: %02X", ep->bmAttributes);
continue; continue;
@@ -431,8 +434,7 @@ void USBUartTypeCdcAcm::on_connected() {
if (channel->cdc_dev_.interrupt_interface_number != 0xFF && if (channel->cdc_dev_.interrupt_interface_number != 0xFF &&
channel->cdc_dev_.interrupt_interface_number != channel->cdc_dev_.bulk_interface_number) { channel->cdc_dev_.interrupt_interface_number != channel->cdc_dev_.bulk_interface_number) {
if (!this->claim_comm_interface_) { if (!this->claim_comm_interface_) {
ESP_LOGD(TAG, "Skipping comm interface %d (claim_comm_interface: false)", ESP_LOGD(TAG, "Comm interface %d left unclaimed", channel->cdc_dev_.interrupt_interface_number);
channel->cdc_dev_.interrupt_interface_number);
} else { } else {
auto err_comm = usb_host_interface_claim(this->handle_, this->device_handle_, auto err_comm = usb_host_interface_claim(this->handle_, this->device_handle_,
channel->cdc_dev_.interrupt_interface_number, 0); channel->cdc_dev_.interrupt_interface_number, 0);
+19 -2
View File
@@ -39,6 +39,9 @@ struct CdcEps {
// must remain valid even when the interface itself is not claimed. // must remain valid even when the interface itself is not claimed.
uint8_t interrupt_interface_number{0xFF}; uint8_t interrupt_interface_number{0xFF};
bool interrupt_interface_claimed{false}; bool interrupt_interface_claimed{false};
// bInterfaceClass of each interface above, which decides how a host binds the port
uint8_t bulk_interface_class{0};
uint8_t interrupt_interface_class{0};
}; };
enum CH34xChipType : uint8_t { enum CH34xChipType : uint8_t {
@@ -186,6 +189,16 @@ class USBUartChannelBase : public uart::UARTComponent, public Parented<USBUartCo
: this->cdc_dev_.bulk_interface_number; : this->cdc_dev_.bulk_interface_number;
} }
/// bInterfaceClass of the interface `get_interface_number()` names
uint8_t get_interface_class() const {
return this->cdc_dev_.interrupt_interface_number != 0xFF ? this->cdc_dev_.interrupt_interface_class
: this->cdc_dev_.bulk_interface_class;
}
/// Index of this port among the ports on its interface. Every driver here exposes one
/// port per interface; a multiplexed driver would index its channels.
uint8_t get_port_number() const { return 0; }
protected: protected:
// Not directly instantiable; construct a concrete channel type instead. // Not directly instantiable; construct a concrete channel type instead.
USBUartChannelBase(uint8_t index, uint16_t buffer_size) : input_buffer_(RingBuffer(buffer_size)), index_(index) {} USBUartChannelBase(uint8_t index, uint16_t buffer_size) : input_buffer_(RingBuffer(buffer_size)), index_(index) {}
@@ -317,13 +330,17 @@ class USBUartTypeCP210X : public USBUartTypeCdcAcm {
}; };
class USBUartTypeCH34X : public USBUartTypeCdcAcm { class USBUartTypeCH34X : public USBUartTypeCdcAcm {
public: public:
USBUartTypeCH34X(uint16_t vid, uint16_t pid) : USBUartTypeCdcAcm(vid, pid) {} USBUartTypeCH34X(uint16_t vid, uint16_t pid) : USBUartTypeCdcAcm(vid, pid) {
// ch34x doesn't use the interrupt endpoint, and we don't have endpoints to spare. The
// comm interface is left in place unclaimed: on the CDC-style chips it is still the
// interface a host binds, and so the one the port is identified by.
this->claim_comm_interface_ = false;
}
void dump_config() override; void dump_config() override;
protected: protected:
bool config_step(USBUartChannelBase *channel, uint8_t step, bool reload, bool ok, const uint8_t *response) override; bool config_step(USBUartChannelBase *channel, uint8_t step, bool reload, bool ok, const uint8_t *response) override;
bool config_device_step(uint8_t step, bool ok, const uint8_t *response) override; bool config_device_step(uint8_t step, bool ok, const uint8_t *response) override;
std::vector<CdcEps> parse_descriptors(usb_device_handle_t dev_hdl) override;
private: private:
CH34xChipType chiptype_{CHIP_UNKNOWN}; CH34xChipType chiptype_{CHIP_UNKNOWN};
@@ -57,7 +57,7 @@ namespace esphome::web_server_idf {
static const char *const TAG = "web_server_idf"; static const char *const TAG = "web_server_idf";
// Only send_json_() may hold the JSON arena: every other frame in this file is capped below one // Only prepare_json_() may hold the JSON arena: every other frame in this file is capped below one
// arena. Measured at -Os on GCC 14; newer toolchains stay checked on purpose, older ones skip it. // arena. Measured at -Os on GCC 14; newer toolchains stay checked on purpose, older ones skip it.
#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ >= 14 && defined(__OPTIMIZE_SIZE__) #if defined(__GNUC__) && !defined(__clang__) && __GNUC__ >= 14 && defined(__OPTIMIZE_SIZE__)
#pragma GCC diagnostic error "-Wstack-usage=2048" #pragma GCC diagnostic error "-Wstack-usage=2048"
@@ -148,10 +148,9 @@ void AsyncWebServer::begin() {
if (this->server_) { if (this->server_) {
this->end(); this->end();
} }
// Default httpd stack is defined by ESP-IDF. Increase to accommodate SerializationBuffer's // Allow room for SerializationBuffer's 640-byte stack buffer and HTTP request handling.
// 640-byte stack buffer used by web_server JSON request handlers.
httpd_config_t config = HTTPD_DEFAULT_CONFIG(); httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.stack_size = config.stack_size + 256; config.stack_size = 4512;
config.server_port = this->port_; config.server_port = this->port_;
config.uri_match_fn = [](const char * /*unused*/, const char * /*unused*/, size_t /*unused*/) { return true; }; config.uri_match_fn = [](const char * /*unused*/, const char * /*unused*/, size_t /*unused*/) { return true; };
// Always enable LRU purging to handle socket exhaustion gracefully. // Always enable LRU purging to handle socket exhaustion gracefully.
@@ -955,10 +954,39 @@ void AsyncEventSourceResponse::process_close_() {
} }
} }
void AsyncEventSourceResponse::refresh_lru_() {
// httpd only advances a session's LRU counter when a request arrives, and an event stream receives one
// request ever, so the purge that frees a socket for a new client would evict the live streams first.
// The HTTPD task owns the session table, so the refresh runs there.
// A stalled stream is the socket the purge should take, and a zeroed fd has no session left to refresh.
if (this->send_failure_started_ms_ != 0 || this->fd_.load() == 0)
return;
const uint32_t now = App.get_loop_component_start_time();
if (now - this->last_lru_refresh_ms_ < LRU_REFRESH_INTERVAL_MS ||
this->lru_work_queued_.load(std::memory_order_acquire)) {
return;
}
this->last_lru_refresh_ms_ = now; // a full queue also waits for the next interval
this->lru_work_queued_.store(true, std::memory_order_release);
if (httpd_queue_work(this->hd_, &AsyncEventSourceResponse::refresh_lru_work, this) != ESP_OK) {
this->lru_work_queued_.store(false, std::memory_order_release);
}
}
void AsyncEventSourceResponse::refresh_lru_work(void *arg) {
auto *response = static_cast<AsyncEventSourceResponse *>(arg);
const int fd = response->fd_.load();
if (response->owns_session_(fd)) {
httpd_sess_update_lru_counter(response->hd_, fd);
}
// Release self only after the HTTPD-task callback has finished every access.
response->lru_work_queued_.store(false, std::memory_order_release);
}
void AsyncEventSourceResponse::close_session_work(void *arg) { void AsyncEventSourceResponse::close_session_work(void *arg) {
auto *response = static_cast<AsyncEventSourceResponse *>(arg); auto *response = static_cast<AsyncEventSourceResponse *>(arg);
const int fd = response->fd_.load(); const int fd = response->fd_.load();
if (fd != 0 && httpd_sess_get_ctx(response->hd_, fd) == response) { if (response->owns_session_(fd)) {
// The HTTPD task remains the session owner. Shutting the socket down makes // The HTTPD task remains the session owner. Shutting the socket down makes
// its next select/recv path delete the session and invoke destroy(). // its next select/recv path delete the session and invoke destroy().
shutdown(fd, SHUT_RDWR); shutdown(fd, SHUT_RDWR);
@@ -972,7 +1000,7 @@ ssize_t AsyncEventSourceResponse::send_(struct iovec *iov, int iovcnt) {
// httpd frees a session before closing its socket, so the fd may already be a new client's. // httpd frees a session before closing its socket, so the fd may already be a new client's.
// Treated as would-block; the stall timer ends a session that never becomes ours again. // Treated as would-block; the stall timer ends a session that never becomes ours again.
const int fd = this->fd_.load(); const int fd = this->fd_.load();
if (httpd_sess_get_ctx(this->hd_, fd) != this) { if (!this->owns_session_(fd)) {
return 0; return 0;
} }
struct msghdr msg {}; struct msghdr msg {};
@@ -1081,6 +1109,7 @@ void AsyncEventSourceResponse::loop() {
this->process_close_(); this->process_close_();
return; return;
} }
this->refresh_lru_();
drain_tail_(); drain_tail_();
process_deferred_queue_(); process_deferred_queue_();
if (this->close_requested_) if (this->close_requested_)
@@ -1091,68 +1120,88 @@ void AsyncEventSourceResponse::loop() {
#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ >= 14 && defined(__OPTIMIZE_SIZE__) #if defined(__GNUC__) && !defined(__clang__) && __GNUC__ >= 14 && defined(__OPTIMIZE_SIZE__)
#pragma GCC diagnostic push #pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wstack-usage=" // the one frame that holds the arena and the JSON buffer #pragma GCC diagnostic ignored "-Wstack-usage=" // the one frame that holds the JSON arena
#endif #endif
bool AsyncEventSourceResponse::send_json_(void *source, message_generator_t *generator) { AsyncEventSourceResponse::JsonPrepareResult AsyncEventSourceResponse::prepare_json_(void *source,
// The arena lives only in this frame, so no call chain ever holds two of them message_generator_t *generator,
char *buffer, size_t *size_out) {
// Log lines emitted while the generator runs are dropped for this session: on the large path they would fill
// tail_ before it is overwritten, and on the small path they would go out ahead of the event being built.
SendGuard guard{*this};
json::JsonArena<json::JSON_ARENA_SIZE> arena; json::JsonArena<json::JSON_ARENA_SIZE> arena;
json::JsonBuilder builder(&arena); json::JsonBuilder builder(&arena);
generator(this->web_server_, source, builder); generator(this->web_server_, source, builder);
char buf[JSON_BUF_SIZE]; *size_out = builder.serialize_to(buffer, JSON_BUF_SIZE);
const size_t len = builder.serialize_to(buf, sizeof(buf)); if (*size_out < JSON_BUF_SIZE) {
if (len < sizeof(buf)) { return JsonPrepareResult::JSON_PREPARE_RESULT_BUFFER;
return this->try_send_nodefer(buf, len, "state");
} }
// Too large for the stack: the tail holds the whole chunk and loop() drains it. Serialized char prefix[PREFIX_BUF_SIZE];
// JSON has no raw line break, so the body is one data line. const size_t prefix_len = build_chunk_prefix(prefix, sizeof(prefix), "state", 0, 0, true);
if (!this->ready_to_send_()) {
return false;
}
{
SendGuard guard{*this};
char prefix[PREFIX_BUF_SIZE];
const size_t prefix_len = build_chunk_prefix(prefix, sizeof(prefix), "state", 0, 0, true);
// Grow the tail until the document fits. Nothing has reached the wire, so a document that // Nothing reaches the wire until the complete document and its framing are owned by the caller.
// cannot be held costs only this event, and the tail grown for it is released. size_t json_len = 0;
size_t json_len = 0; size_t cap = std::max<size_t>(JSON_BUF_SIZE * 2, this->tail_cap_);
size_t cap = std::max<size_t>(JSON_BUF_SIZE * 2, this->tail_cap_); for (;;) {
for (;;) { if (!this->reserve_tail_(cap)) {
if (!this->reserve_tail_(cap)) { *size_out = cap;
this->tail_alloc_failed_(cap); return JsonPrepareResult::JSON_PREPARE_RESULT_NO_MEMORY;
return false; // stays deferred, retried on a later pass
}
const size_t room = cap - prefix_len - SSE_SUFFIX_LEN;
json_len = builder.serialize_to(reinterpret_cast<char *>(this->tail_.get()) + prefix_len, room);
if (json_len < room) {
break;
}
if (cap >= TAIL_MAX_SIZE) {
ESP_LOGW(TAG, "State event does not fit %zu bytes, dropped", room);
this->tail_.reset();
this->tail_cap_ = 0;
this->send_failure_started_ms_ = 0;
return true; // would never fit, reported as sent
}
cap = std::min<size_t>(cap * 2, TAIL_MAX_SIZE);
} }
const size_t room = cap - prefix_len - SSE_SUFFIX_LEN;
const size_t total = prefix_len + json_len + SSE_SUFFIX_LEN; json_len = builder.serialize_to(reinterpret_cast<char *>(this->tail_.get()) + prefix_len, room);
write_chunk_header(prefix, total - CHUNK_HDR_LEN - CHUNK_END_LEN); if (json_len < room) {
uint8_t *dst = this->tail_.get(); break;
std::memcpy(dst, prefix, prefix_len); }
std::memcpy(dst + prefix_len + json_len, SSE_SUFFIX, SSE_SUFFIX_LEN); if (cap >= TAIL_MAX_SIZE) {
this->tail_len_ = total; *size_out = room;
this->tail_sent_ = 0; return JsonPrepareResult::JSON_PREPARE_RESULT_TOO_LARGE;
}
cap = std::min<size_t>(cap * 2, TAIL_MAX_SIZE);
} }
drain_tail_();
return true; const size_t total = prefix_len + json_len + SSE_SUFFIX_LEN;
write_chunk_header(prefix, total - CHUNK_HDR_LEN - CHUNK_END_LEN);
uint8_t *dst = this->tail_.get();
std::memcpy(dst, prefix, prefix_len);
std::memcpy(dst + prefix_len + json_len, SSE_SUFFIX, SSE_SUFFIX_LEN);
this->tail_len_ = total;
this->tail_sent_ = 0;
return JsonPrepareResult::JSON_PREPARE_RESULT_TAIL;
} }
#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ >= 14 && defined(__OPTIMIZE_SIZE__) #if defined(__GNUC__) && !defined(__clang__) && __GNUC__ >= 14 && defined(__OPTIMIZE_SIZE__)
#pragma GCC diagnostic pop #pragma GCC diagnostic pop
#endif #endif
bool AsyncEventSourceResponse::send_json_(void *source, message_generator_t *generator) {
if (!this->ready_to_send_()) {
return false;
}
char buffer[JSON_BUF_SIZE];
size_t size;
const auto result = this->prepare_json_(source, generator, buffer, &size);
switch (result) {
case JsonPrepareResult::JSON_PREPARE_RESULT_BUFFER:
return this->try_send_nodefer(buffer, size, "state");
case JsonPrepareResult::JSON_PREPARE_RESULT_TAIL:
this->drain_tail_();
return true;
case JsonPrepareResult::JSON_PREPARE_RESULT_NO_MEMORY: {
SendGuard guard{*this};
this->tail_alloc_failed_(size);
return false;
}
case JsonPrepareResult::JSON_PREPARE_RESULT_TOO_LARGE: {
SendGuard guard{*this};
ESP_LOGW(TAG, "State event does not fit %zu bytes, dropped", size);
this->tail_.reset();
this->tail_cap_ = 0;
this->send_failure_started_ms_ = 0;
return true;
}
}
return false;
}
void AsyncEventSourceResponse::tail_alloc_failed_(size_t cap) { void AsyncEventSourceResponse::tail_alloc_failed_(size_t cap) {
// Same stall clock as a socket that stops draining, so a session cannot retry forever // Same stall clock as a socket that stops draining, so a session cannot retry forever
const uint32_t now = App.get_loop_component_start_time(); const uint32_t now = App.get_loop_component_start_time();
@@ -317,6 +317,17 @@ class AsyncEventSourceResponse {
size_t sent); size_t sent);
// Send a state event; JSON too large for the stack buffer is serialized into tail_ instead // Send a state event; JSON too large for the stack buffer is serialized into tail_ instead
bool send_json_(void *source, message_generator_t *generator); bool send_json_(void *source, message_generator_t *generator);
enum class JsonPrepareResult : uint8_t {
JSON_PREPARE_RESULT_BUFFER,
JSON_PREPARE_RESULT_TAIL,
JSON_PREPARE_RESULT_NO_MEMORY,
JSON_PREPARE_RESULT_TOO_LARGE,
};
// Keep the arena out of the caller's frame so it is gone before the state send.
// size_out: BUFFER = JSON length, NO_MEMORY = requested tail capacity, TOO_LARGE = available JSON room;
// unused for TAIL.
JsonPrepareResult __attribute__((noinline))
prepare_json_(void *source, message_generator_t *generator, char *buffer, size_t *size_out);
// Warn once, and close the session once the stall timeout passes with no memory for the tail // Warn once, and close the session once the stall timeout passes with no memory for the tail
void tail_alloc_failed_(size_t cap); void tail_alloc_failed_(size_t cap);
void request_close_(); void request_close_();
@@ -329,10 +340,15 @@ class AsyncEventSourceResponse {
}; };
void process_close_(); void process_close_();
static void close_session_work(void *arg); static void close_session_work(void *arg);
void refresh_lru_();
static void refresh_lru_work(void *arg);
// httpd frees a session before closing its socket, so the fd may already belong to a new client
bool owns_session_(int fd) const { return fd != 0 && httpd_sess_get_ctx(this->hd_, fd) == this; }
// Deletable only after destroy() zeroed fd_ and no queued HTTPD close work still references this object. // Deletable only after destroy() zeroed fd_ and no queued HTTPD work still references this object.
bool safe_to_delete_() const { bool safe_to_delete_() const {
return this->fd_.load() == 0 && !this->close_work_queued_.load(std::memory_order_acquire); return this->fd_.load() == 0 && !this->close_work_queued_.load(std::memory_order_acquire) &&
!this->lru_work_queued_.load(std::memory_order_acquire);
} }
static void destroy(void *p); static void destroy(void *p);
@@ -347,11 +363,13 @@ class AsyncEventSourceResponse {
RAMUniquePtr<uint8_t[]> tail_; RAMUniquePtr<uint8_t[]> tail_;
uint32_t send_failure_started_ms_{0}; // Zero means no send stall in progress. uint32_t send_failure_started_ms_{0}; // Zero means no send stall in progress.
uint32_t next_close_attempt_ms_{0}; uint32_t next_close_attempt_ms_{0};
uint32_t last_lru_refresh_ms_{0};
uint16_t tail_cap_{0}; uint16_t tail_cap_{0};
uint16_t tail_len_{0}; // Zero means nothing pending uint16_t tail_len_{0}; // Zero means nothing pending
uint16_t tail_sent_{0}; uint16_t tail_sent_{0};
// Set on the main loop before queueing close work, cleared by the HTTPD-task callback when done. // Set on the main loop before queueing close work, cleared by the HTTPD-task callback when done.
std::atomic<bool> close_work_queued_{false}; std::atomic<bool> close_work_queued_{false};
std::atomic<bool> lru_work_queued_{false};
// Main-loop only; the HTTPD task never reads or writes these flags. // Main-loop only; the HTTPD task never reads or writes these flags.
bool close_requested_{false}; bool close_requested_{false};
bool close_retry_warning_logged_{false}; bool close_retry_warning_logged_{false};
@@ -373,6 +391,7 @@ class AsyncEventSourceResponse {
static constexpr uint32_t SEND_STALL_TIMEOUT_MS = 20000; static constexpr uint32_t SEND_STALL_TIMEOUT_MS = 20000;
static constexpr uint32_t CLOSE_RETRY_INTERVAL_MS = 250; static constexpr uint32_t CLOSE_RETRY_INTERVAL_MS = 250;
static constexpr uint32_t CLOSE_CONFIRM_INTERVAL_MS = 1000; static constexpr uint32_t CLOSE_CONFIRM_INTERVAL_MS = 1000;
static constexpr uint32_t LRU_REFRESH_INTERVAL_MS = 1000;
}; };
using AsyncEventSourceClient = AsyncEventSourceResponse; using AsyncEventSourceClient = AsyncEventSourceResponse;
+1 -1
View File
@@ -4,7 +4,7 @@ from enum import Enum
from esphome.enum import StrEnum from esphome.enum import StrEnum
__version__ = "2026.10.0b4" __version__ = "2026.10.0"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = ( VALID_SUBSTITUTIONS_CHARACTERS = (
+2 -2
View File
@@ -147,7 +147,7 @@ extends = common:arduino
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.312/platform-espressif32.zip platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.312/platform-espressif32.zip
platform_packages = platform_packages =
pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.12/esp32-core-3.3.12.tar.xz pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.12/esp32-core-3.3.12.tar.xz
pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.5/esp-idf-v5.5.5.tar.xz pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.6/esp-idf-v5.5.6.tar.xz
framework = arduino, espidf ; Arduino as an ESP-IDF component framework = arduino, espidf ; Arduino as an ESP-IDF component
lib_deps = lib_deps =
@@ -183,7 +183,7 @@ extra_scripts =
extends = common:idf extends = common:idf
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.312/platform-espressif32.zip platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.312/platform-espressif32.zip
platform_packages = platform_packages =
pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.5/esp-idf-v5.5.5.tar.xz pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.6/esp-idf-v5.5.6.tar.xz
framework = espidf framework = espidf
lib_deps = lib_deps =
+1 -1
View File
@@ -12,7 +12,7 @@ pyserial==3.5
platformio==6.2.0 platformio==6.2.0
esptool==5.4.0 esptool==5.4.0
click==8.5.0 click==8.5.0
aioesphomeapi==46.10.0 aioesphomeapi==46.12.0
aiohappyeyeballs==2.7.1 # Happy Eyeballs for requests downloads; already pulled in by aioesphomeapi aiohappyeyeballs==2.7.1 # Happy Eyeballs for requests downloads; already pulled in by aioesphomeapi
zeroconf==0.151.5 zeroconf==0.151.5
puremagic==2.2.0 puremagic==2.2.0
@@ -0,0 +1,22 @@
esphome:
name: test
esp32:
board: esp32dev
spi:
clk_pin: GPIO18
mosi_pin: GPIO19
display:
- platform: epaper_spi
id: epaper_display
model: ssd1677
dc_pin: GPIO21
busy_pin: GPIO22
reset_pin: GPIO23
cs_pin: GPIO5
update_interval: never
dimensions:
width: 200
height: 200
@@ -580,3 +580,15 @@ def test_model_with_no_default_init_sequence_generates(
# init_sequence_length, ...); a length of 0 confirms the empty init # init_sequence_length, ...); a length of 0 confirms the empty init
# sequence array was generated instead of raising during code generation. # sequence array was generated instead of raising during code generation.
assert re.search(r"epaper_spi::EPaperT133A01\([^;]*,\s*\w+,\s*0\);", main_cpp) assert re.search(r"epaper_spi::EPaperT133A01\([^;]*,\s*\w+,\s*0\);", main_cpp)
def test_test_card_overrides_update_interval(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""With nothing else to draw, update_interval is not left at never for the test card."""
main_cpp = generate_main(component_config_path("test_card_update_interval.yaml"))
match = re.search(r"epaper_display->set_update_interval\((\d+)\);", main_cpp)
assert match is not None
assert int(match.group(1)) != 4294967295
@@ -93,12 +93,12 @@ def test_transform_matches_model_support(
from esphome.components.mipi_rgb.display import CONFIG_SCHEMA, MODELS from esphome.components.mipi_rgb.display import CONFIG_SCHEMA, MODELS
# ESP32-8048S070 supports both mirror axes but not swap_xy (RGB displays # The full-height ST7701S supports both mirror axes but not swap_xy (RGB displays
# never support axis swapping). # never support axis swapping).
model = MODELS["ESP32-8048S070"] model = MODELS["ST7701S"]
assert model.transforms == {CONF_MIRROR_X, CONF_MIRROR_Y} assert model.transforms == {CONF_MIRROR_X, CONF_MIRROR_Y}
base = {"model": "ESP32-8048S070", "data_pins": DATA_PINS, "pclk_pin": 21} base = {"model": "ST7701S", "data_pins": DATA_PINS, "pclk_pin": 21}
CONFIG_SCHEMA({**base, "transform": {"mirror_x": True, "mirror_y": False}}) CONFIG_SCHEMA({**base, "transform": {"mirror_x": True, "mirror_y": False}})
# An unsupported axis may be explicitly disabled (a harmless no-op)... # An unsupported axis may be explicitly disabled (a harmless no-op)...
@@ -139,6 +139,35 @@ def test_st7701s_only_supports_mirror_x(
CONFIG_SCHEMA({**base, "transform": {"mirror_x": True, "mirror_y": True}}) CONFIG_SCHEMA({**base, "transform": {"mirror_x": True, "mirror_y": True}})
def test_pure_rgb_model_has_no_hardware_transform(
set_core_config: SetCoreConfigCallable,
) -> None:
"""A model with no init sequence has no command interface, so no transform
can be applied in hardware and every rotation must be done in software.
Previously such models claimed mirror_x/mirror_y support, so a rotation of
180 was handed to the (nonexistent) hardware and the display was not rotated.
"""
_set_s3(set_core_config)
from esphome.components.mipi_rgb.display import CONFIG_SCHEMA, MODELS
model = MODELS["ESP32-8048S070"]
assert model.initsequence == ()
assert model.transforms == set()
base = {"model": "ESP32-8048S070", "data_pins": DATA_PINS, "pclk_pin": 21}
for rotation in (90, 180, 270):
config = CONFIG_SCHEMA({**base, "id": f"rot_{rotation}", "rotation": rotation})
assert not model.rotation_as_transform(config)
metadata = get_display_metadata(config["id"])
assert metadata.rotation == rotation
assert not metadata.has_hardware_rotation
with pytest.raises(cv.Invalid, match="'mirror_x' is not supported by this model"):
CONFIG_SCHEMA({**base, "transform": {"mirror_x": True}})
def test_metadata_records_rotation( def test_metadata_records_rotation(
set_core_config: SetCoreConfigCallable, set_core_config: SetCoreConfigCallable,
) -> None: ) -> None:
@@ -0,0 +1,9 @@
# wifi and ethernet together: the _esphomelib "network" TXT value follows
# the connected interface, so the update path is compiled.
wifi:
ssid: MySSID
password: password1
api:
mdns:
@@ -0,0 +1,17 @@
packages:
mdns: !include common-dual-interface.yaml
ethernet:
type: W5500
clk_pin: GPIO19
mosi_pin: GPIO21
miso_pin: GPIO23
cs_pin: GPIO18
interrupt_pin: GPIO36
reset_pin: GPIO22
clock_speed: 10Mhz
network:
priority:
- ethernet
- wifi
@@ -0,0 +1,17 @@
packages:
mdns: !include common-dual-interface.yaml
ethernet:
type: W5500
clk_pin: 18
mosi_pin: 19
miso_pin: 16
cs_pin: 17
interrupt_pin: 21
reset_pin: 20
# wifi first, so the USE_NETWORK_PRIMARY_INTERFACE_WIFI order is compiled
network:
priority:
- wifi
- ethernet
+103
View File
@@ -0,0 +1,103 @@
#include <gtest/gtest.h>
#include <random>
#include <string>
#include "esphome/components/mqtt/mqtt_topic_match.h"
namespace esphome::mqtt::testing {
// The previous recursive implementation, kept to check the loop matches it
static bool reference_match(const char *message, const char *subscription, bool is_normal, bool past_separator) {
if (*message == '\0' && *subscription == '\0')
return true;
if (*message == '\0' || *subscription == '\0')
return false;
bool do_wildcards = is_normal || past_separator;
if (*subscription == '+' && do_wildcards) {
subscription++;
while (*message != '\0' && *message != '/')
message++;
return reference_match(message, subscription, is_normal, true);
}
if (*subscription == '#' && do_wildcards)
return true;
if (*message != *subscription)
return false;
past_separator = past_separator || *subscription == '/';
return reference_match(message + 1, subscription + 1, is_normal, past_separator);
}
static bool reference_match(const char *message, const char *subscription) {
return reference_match(message, subscription, *message != '\0' && *message != '$', false);
}
TEST(MQTTTopicMatch, Exact) {
EXPECT_TRUE(topic_match("a/b/c", "a/b/c"));
EXPECT_FALSE(topic_match("a/b/c", "a/b"));
EXPECT_FALSE(topic_match("a/b", "a/b/c"));
EXPECT_FALSE(topic_match("a/b/c", "a/b/d"));
}
TEST(MQTTTopicMatch, SingleLevelWildcard) {
EXPECT_TRUE(topic_match("a/b/c", "a/+/c"));
EXPECT_TRUE(topic_match("a/b/c", "+/+/+"));
EXPECT_TRUE(topic_match("a//c", "a/+/c"));
EXPECT_FALSE(topic_match("a/b/c", "a/+"));
EXPECT_FALSE(topic_match("a/b/c/d", "a/+/c"));
}
TEST(MQTTTopicMatch, MultiLevelWildcard) {
EXPECT_TRUE(topic_match("a/b/c", "a/#"));
EXPECT_TRUE(topic_match("a/b/c", "#"));
EXPECT_TRUE(topic_match("a/b/c", "a/+/#"));
EXPECT_FALSE(topic_match("b/c", "a/#"));
}
TEST(MQTTTopicMatch, DollarTopicsSkipFirstLevelWildcards) {
EXPECT_FALSE(topic_match("$SYS/broker", "#"));
EXPECT_FALSE(topic_match("$SYS/broker", "+/broker"));
EXPECT_TRUE(topic_match("$SYS/broker", "$SYS/#"));
EXPECT_TRUE(topic_match("$SYS/broker", "$SYS/+"));
}
TEST(MQTTTopicMatch, EdgeCases) {
EXPECT_TRUE(topic_match("", ""));
EXPECT_FALSE(topic_match("a", ""));
// Kept from the previous code; the MQTT spec has "#" match the parent level and "+" an empty last level
EXPECT_FALSE(topic_match("a/b", "a/b/#"));
EXPECT_FALSE(topic_match("a/", "a/+"));
EXPECT_TRUE(topic_match("a/$x", "a/+"));
}
// Recursion overflowed the ESP32 loop stack at about 250 characters
TEST(MQTTTopicMatch, LongTopic) {
std::string topic(4096, 'x');
topic[100] = '/';
EXPECT_TRUE(topic_match(topic.c_str(), topic.c_str()));
std::string other = topic;
other.back() = 'y';
EXPECT_FALSE(topic_match(topic.c_str(), other.c_str()));
}
TEST(MQTTTopicMatch, MatchesPreviousImplementation) {
// Small alphabet so wildcards, separators and '$' meet often
static constexpr char MESSAGE_CHARS[] = {'a', 'b', '/', '$'};
static constexpr char SUBSCRIPTION_CHARS[] = {'a', 'b', '/', '$', '+', '#'};
std::mt19937 rng(1234); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed) reproducible
auto random_string = [&rng](const char *chars, size_t count) {
std::string s(rng() % 8, ' ');
for (char &c : s)
c = chars[rng() % count];
return s;
};
for (int i = 0; i < 20000; i++) {
std::string message = random_string(MESSAGE_CHARS, sizeof(MESSAGE_CHARS));
std::string subscription = random_string(SUBSCRIPTION_CHARS, sizeof(SUBSCRIPTION_CHARS));
ASSERT_EQ(topic_match(message.c_str(), subscription.c_str()),
reference_match(message.c_str(), subscription.c_str()))
<< "message='" << message << "' subscription='" << subscription << "'";
}
}
} // namespace esphome::mqtt::testing
+16
View File
@@ -0,0 +1,16 @@
import esphome.codegen as cg
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
# The harness gives a multi-instance component an empty list and never calls its to_code,
# so take a single empty config instead and emit what the proxy messages and handlers
# need. No proxy is configured, so the count is 0, which keeps the capability messages as
# the api tests expect them; the USB identity stays out, since the USB channel exists on the ESP32 only.
manifest.multi_conf = False
async def to_code_testing(config):
cg.add_define("USE_SERIAL_PROXY")
cg.add_define("SERIAL_PROXY_COUNT", 0)
manifest.to_code = to_code_testing
@@ -0,0 +1,51 @@
#include <gtest/gtest.h>
#include <cstdint>
#include <vector>
#include "esphome/components/api/api_buffer.h"
#include "esphome/components/api/api_pb2.h"
#include "esphome/components/api/proto.h"
namespace esphome::api::testing {
// Encodes into a buffer of exactly the calculated size, so any overrun trips the debug bounds check.
static std::vector<uint8_t> encode(const UsbDeviceDescriptor &msg) {
const uint32_t size = msg.calculate_size();
APIBuffer buf;
EXPECT_TRUE(buf.resize(size == 0 ? 1 : size));
uint8_t *pos = buf.data();
#ifdef ESPHOME_DEBUG_API
uint8_t *proto_debug_end_ = buf.data() + buf.size();
#endif
uint8_t *end = UsbDeviceDescriptor::encode_msg(&msg, pos PROTO_ENCODE_DEBUG_ARG);
EXPECT_EQ(static_cast<uint32_t>(end - buf.data()), size);
return {buf.data(), end};
}
TEST(UsbDeviceDescriptor, EncodesTheInterfaceClassAndPort) {
UsbDeviceDescriptor msg;
msg.vendor_id = 0x10C4;
msg.product_id = 0xEA60;
msg.bcd_device = 0x0100;
msg.interface_number = 1;
msg.interface_class = 0xFF;
msg.port_number = 2;
const std::vector<uint8_t> expected = {0x08, 0xC4, 0x21, 0x10, 0xE0, 0xD4, 0x03, 0x18, 0x80,
0x02, 0x20, 0x01, 0x28, 0xFF, 0x01, 0x30, 0x02};
EXPECT_EQ(encode(msg), expected);
}
TEST(UsbDeviceDescriptor, ZeroClassAndPortLeaveTheWireAsBefore) {
// The new fields are additive with zero defaults, so an old client sees the message it always did
UsbDeviceDescriptor msg;
msg.vendor_id = 0x303A;
msg.product_id = 0x1001;
msg.interface_number = 1;
const std::vector<uint8_t> expected = {0x08, 0xBA, 0x60, 0x10, 0x81, 0x20, 0x20, 0x01};
EXPECT_EQ(encode(msg), expected);
}
} // namespace esphome::api::testing
+3 -2
View File
@@ -1,3 +1,4 @@
# The disconnects counter is compiled only with USE_SENSOR; declaring the # The connection entities are compiled only with USE_BINARY_SENSOR and
# sensor domain makes the C++ unit test build define it. # USE_SENSOR; declaring the domains makes the C++ unit test build define them.
binary_sensor:
sensor: sensor:
+8
View File
@@ -10,8 +10,16 @@ tcp_uart:
allowed_ips: allowed_ips:
- 192.0.2.20 - 192.0.2.20
- 192.0.2.0/24 - 192.0.2.0/24
binary_sensor:
- platform: tcp_uart
tcp_uart_id: tcp_uart_server
connected: connected:
name: TCP UART Server Connected name: TCP UART Server Connected
sensor:
- platform: tcp_uart
tcp_uart_id: tcp_uart_server
disconnects: disconnects:
name: TCP UART Server Disconnects name: TCP UART Server Disconnects
@@ -7,7 +7,15 @@ tcp_uart:
host: 192.0.2.10 host: 192.0.2.10
port: 502 port: 502
reconnect_interval: 10s reconnect_interval: 10s
binary_sensor:
- platform: tcp_uart
tcp_uart_id: tcp_uart_1
connected: connected:
name: TCP UART Connected name: TCP UART Connected
sensor:
- platform: tcp_uart
tcp_uart_id: tcp_uart_1
disconnects: disconnects:
name: TCP UART Disconnects name: TCP UART Disconnects
+3 -2
View File
@@ -1,3 +1,4 @@
# The disconnects counter is compiled only with USE_SENSOR; declaring the # The connection entities are compiled only with USE_BINARY_SENSOR and
# sensor domain makes the C++ unit test build define it. # USE_SENSOR; declaring the domains makes the C++ unit test build define them.
binary_sensor:
sensor: sensor:
+8
View File
@@ -10,7 +10,15 @@ uart_tcp:
allowed_ips: allowed_ips:
- 192.168.1.10 - 192.168.1.10
- 192.168.1.0/24 - 192.168.1.0/24
binary_sensor:
- platform: uart_tcp
uart_tcp_id: bridge
connected: connected:
name: UART TCP Connected name: UART TCP Connected
sensor:
- platform: uart_tcp
uart_tcp_id: bridge
disconnects: disconnects:
name: UART TCP Disconnects name: UART TCP Disconnects
@@ -12,7 +12,15 @@ uart_tcp:
host: 192.0.2.20 host: 192.0.2.20
port: 502 port: 502
reconnect_interval: 10s reconnect_interval: 10s
binary_sensor:
- platform: uart_tcp
uart_tcp_id: bridge
connected: connected:
name: UART TCP Connected name: UART TCP Connected
sensor:
- platform: uart_tcp
uart_tcp_id: bridge
disconnects: disconnects:
name: UART TCP Disconnects name: UART TCP Disconnects
+8
View File
@@ -8,7 +8,15 @@ uart_tcp:
uart_id: uart_bus uart_id: uart_bus
host: 127.0.0.1 host: 127.0.0.1
port: 44502 port: 44502
binary_sensor:
- platform: uart_tcp
uart_tcp_id: bridge
connected: connected:
name: UART TCP Connected name: UART TCP Connected
sensor:
- platform: uart_tcp
uart_tcp_id: bridge
disconnects: disconnects:
name: UART TCP Disconnects name: UART TCP Disconnects
@@ -6,3 +6,6 @@ web_server:
username: admin username: admin
password: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA password: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
type: basic type: basic
ota:
- platform: web_server
@@ -20,6 +20,10 @@ tcp_uart:
host: 127.0.0.1 host: 127.0.0.1
port: 18125 port: 18125
reconnect_interval: 3s reconnect_interval: 3s
binary_sensor:
- platform: tcp_uart
tcp_uart_id: tcp_uart_1
connected: connected:
name: Bridge Connected name: Bridge Connected
@@ -0,0 +1,64 @@
esphome:
name: uart-mock-bl0906-stray-rx
host:
api:
batch_delay: 0ms # Disable batching to receive all state updates
logger:
level: VERBOSE
external_components:
- source:
type: local
path: EXTERNAL_COMPONENT_PATH
# Dummy uart entry to satisfy DEPENDENCIES = ["uart"]
uart:
baud_rate: 9600
port: /dev/null
# Answers register reads; the first temperature answer has one stray trailing byte
uart_mock:
id: mock_uart
baud_rate: 19200
on_tx:
- then:
- lambda: |-
// Command and register address arrive as separate one-byte writes
static bool read_pending = false;
static bool stray_sent = false;
if (data.size() != 1)
return;
if (!read_pending) {
read_pending = data[0] == 0x35;
return;
}
read_pending = false;
const uint8_t address = data[0];
uint32_t raw;
switch (address) {
case 0x5E: // temperature: (300 - 64) * 12.5 / 59 - 40 = 10 C
raw = 300;
break;
case 0x16: // voltage
raw = 0x2A1B3C;
break;
default:
return;
}
const uint8_t l = raw & 0xFF, m = (raw >> 8) & 0xFF, h = (raw >> 16) & 0xFF;
std::vector<uint8_t> reply{l, m, h, static_cast<uint8_t>((address + l + m + h) ^ 0xFF)};
if (address == 0x5E && !stray_sent) {
stray_sent = true;
reply.push_back(0xAA);
}
id(mock_uart).inject_to_rx_buffer(reply);
sensor:
- platform: bl0906
uart_id: mock_uart
update_interval: 1s
temperature:
name: Temperature
voltage:
name: Voltage
@@ -24,11 +24,15 @@ uart_tcp:
port: 18126 port: 18126
allowed_ips: allowed_ips:
- 127.0.0.1 - 127.0.0.1
connected:
name: Bridge Connected
- id: denied_bridge - id: denied_bridge
uart_id: denied_bus uart_id: denied_bus
role: server role: server
port: 18127 port: 18127
allowed_ips: allowed_ips:
- 192.0.2.1 - 192.0.2.1
binary_sensor:
- platform: uart_tcp
uart_tcp_id: bridge
connected:
name: Bridge Connected
@@ -0,0 +1,57 @@
"""A BL0906 that leaves a stray byte in the receive buffer keeps reading."""
from __future__ import annotations
import asyncio
from pathlib import Path
from aioesphomeapi import EntityState, SensorInfo
import pytest
from .state_utils import require_entity
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_uart_mock_bl0906_stray_rx(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""A stray received byte does not stop the read cycle."""
external_components_path = str(
Path(__file__).parent / "fixtures" / "external_components"
)
yaml_config = yaml_config.replace(
"EXTERNAL_COMPONENT_PATH", external_components_path
)
lines: list[str] = []
states: dict[int, EntityState] = {}
async with (
run_compiled(yaml_config, line_callback=lines.append),
api_client_connected() as client,
):
entities, _ = await client.list_entities_services()
temperature = require_entity(entities, "temperature", SensorInfo)
voltage = require_entity(entities, "voltage", SensorInfo)
client.subscribe_states(lambda state: states.__setitem__(state.key, state))
def state_of(entity: SensorInfo) -> float | None:
state = states.get(entity.key)
return None if state is None or state.missing_state else state.state
try:
async with asyncio.timeout(10.0):
while state_of(voltage) is None:
await asyncio.sleep(0.05)
except TimeoutError:
pytest.fail(f"No voltage reading; log: {lines[-20:]}")
assert any("RX inject 5 bytes" in x for x in lines), "stray byte not sent"
assert state_of(temperature) == pytest.approx(10.0, abs=0.01)
uref = 1.097 * 100000 / (13162 * 100 * 1000)
assert state_of(voltage) == pytest.approx(0x2A1B3C * uref, rel=1e-5)
assert not any("Junk on wire" in x for x in lines)
@@ -71,7 +71,7 @@ NEW_CAPABILITY_FIELDS: dict[str, dict[str, int]] = {
"sendspin": 6, "sendspin": 6,
}, },
"WizardCapabilities": {"configured": 1}, "WizardCapabilities": {"configured": 1},
"SendspinCapabilities": {"feature_flags": 1}, "SendspinCapabilities": {"feature_flags": 1, "client_id": 2},
"BluetoothProxyCapabilities": { "BluetoothProxyCapabilities": {
"feature_flags": 1, "feature_flags": 1,
"mac_address": 2, "mac_address": 2,