mirror of
https://github.com/esphome/esphome.git
synced 2026-10-19 12:42:50 +00:00
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
||||
# could be handy for archiving the generated documentation or if some version
|
||||
# control system is used.
|
||||
|
||||
PROJECT_NUMBER = 2026.10.0b4
|
||||
PROJECT_NUMBER = 2026.10.0b5
|
||||
|
||||
# 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
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ RUN \
|
||||
-r /requirements.txt
|
||||
|
||||
# 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.24.0
|
||||
|
||||
RUN \
|
||||
platformio settings set enable_telemetry No \
|
||||
|
||||
@@ -403,6 +403,9 @@ message WizardCapabilities {
|
||||
message SendspinCapabilities {
|
||||
// Bitmask of the Sendspin features this device supports
|
||||
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 {
|
||||
@@ -3043,12 +3046,15 @@ enum SerialProxyIdentityFlag {
|
||||
// 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 {
|
||||
uint32 vendor_id = 1;
|
||||
uint32 product_id = 2;
|
||||
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
|
||||
|
||||
@@ -2128,6 +2128,7 @@ bool APIConnection::send_device_capabilities_response_() {
|
||||
#endif
|
||||
#if defined(USE_SENDSPIN) && defined(USE_ESP_IDF)
|
||||
resp.sendspin.feature_flags = sendspin_::global_sendspin_hub->get_feature_flags();
|
||||
resp.sendspin.client_id = sendspin_::global_sendspin_hub->get_client_id();
|
||||
#endif
|
||||
return this->send_message(resp);
|
||||
}
|
||||
|
||||
@@ -152,10 +152,7 @@ void OutgoingConnectionManager::loop(APIServer *server) {
|
||||
|
||||
void OutgoingConnectionManager::try_dial_(APIServer *server, uint32_t now) {
|
||||
if (!network::is_connected()) {
|
||||
// Flips within seconds of boot; recheck fast so a deep sleep wake
|
||||
// window is not spent waiting
|
||||
this->schedule_wait_(now, NETWORK_RETRY_MS);
|
||||
return;
|
||||
return; // still WAITING with the wait elapsed, so the next loop rechecks
|
||||
}
|
||||
struct sockaddr_storage 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_ts_ = now;
|
||||
this->last_poll_ = 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);
|
||||
return;
|
||||
}
|
||||
if (now - this->last_poll_ < CONNECT_POLL_INTERVAL_MS) {
|
||||
return;
|
||||
}
|
||||
this->last_poll_ = now;
|
||||
int err = 0;
|
||||
switch (socket::poll_connect(*this->dial_socket_, err)) {
|
||||
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_MAX_MS = 300000;
|
||||
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;
|
||||
// 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
|
||||
@@ -104,7 +102,6 @@ class OutgoingConnectionManager {
|
||||
uint32_t backoff_{BACKOFF_MIN_MS};
|
||||
uint32_t wait_{BOOT_WAIT_MS};
|
||||
uint32_t state_ts_{0};
|
||||
uint32_t last_poll_{0};
|
||||
|
||||
// Byte-aligned types last
|
||||
#ifndef API_OUTGOING_CONNECTION_HOST
|
||||
|
||||
@@ -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) {
|
||||
const auto &msg = *static_cast<const SendspinCapabilities *>(self);
|
||||
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;
|
||||
}
|
||||
uint32_t SendspinCapabilities::calc_size_msg(const void *self) {
|
||||
const auto &msg = *static_cast<const SendspinCapabilities *>(self);
|
||||
uint32_t size = 0;
|
||||
size += ProtoSize::calc_uint32(1, msg.feature_flags);
|
||||
size += !msg.client_id.empty() ? 2 + msg.client_id.size() : 0;
|
||||
return size;
|
||||
}
|
||||
#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, 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, 5, msg.interface_class);
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 6, msg.port_number);
|
||||
return pos;
|
||||
}
|
||||
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.bcd_device);
|
||||
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;
|
||||
}
|
||||
uint8_t *SerialProxyIdentity::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
|
||||
@@ -750,6 +750,7 @@ class WizardCapabilities final : public ProtoMessage {
|
||||
class SendspinCapabilities final : public ProtoMessage {
|
||||
public:
|
||||
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);
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
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 bcd_device{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);
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
|
||||
|
||||
@@ -1119,6 +1119,7 @@ const char *WizardCapabilities::dump_to(DumpBuffer &out) const {
|
||||
const char *SendspinCapabilities::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("SendspinCapabilities"));
|
||||
dump_field(out, ESPHOME_PSTR("feature_flags"), this->feature_flags);
|
||||
dump_field(out, ESPHOME_PSTR("client_id"), this->client_id);
|
||||
return out.c_str();
|
||||
}
|
||||
#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("bcd_device"), this->bcd_device);
|
||||
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();
|
||||
}
|
||||
const char *SerialProxyIdentity::dump_to(DumpBuffer &out) const {
|
||||
|
||||
@@ -20,7 +20,7 @@ constexpr uint8_t bl0906_checksum(const uint8_t address, const DataPacket *data)
|
||||
|
||||
void BL0906::loop() {
|
||||
while (this->available())
|
||||
this->flush();
|
||||
this->read();
|
||||
|
||||
if (this->current_stage_ == STAGE_IDLE) {
|
||||
// Woken up between cycles to drain the action queue. Go back to sleep.
|
||||
@@ -92,7 +92,7 @@ void BL0906::advance_stage_() {
|
||||
|
||||
void BL0906::setup() {
|
||||
while (this->available())
|
||||
this->flush();
|
||||
this->read();
|
||||
this->write_array(USR_WRPROT_WITABLE, sizeof(USR_WRPROT_WITABLE));
|
||||
// Calibration (1: register address; 2: value before calibration; 3: value after calibration)
|
||||
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,
|
||||
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));
|
||||
conn_params.min_int = min_interval;
|
||||
conn_params.max_int = max_interval;
|
||||
|
||||
@@ -34,6 +34,15 @@ void DebugComponent::dump_config() {
|
||||
this->free_heap_ = get_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);
|
||||
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
|
||||
@@ -208,6 +208,7 @@ def _final_validate(config) -> None:
|
||||
else:
|
||||
# If no drawing methods are configured, and LVGL is not enabled, show a test card
|
||||
config[CONF_SHOW_TEST_CARD] = True
|
||||
config[CONF_UPDATE_INTERVAL] = update_interval("1min")
|
||||
elif CONF_UPDATE_INTERVAL not in config:
|
||||
config[CONF_UPDATE_INTERVAL] = update_interval("1min")
|
||||
|
||||
|
||||
@@ -1099,7 +1099,7 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = {
|
||||
# See: https://github.com/pioarduino/esp-idf/releases
|
||||
ARDUINO_IDF_VERSION_LOOKUP = {
|
||||
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, 10): cv.Version(5, 5, 5),
|
||||
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
|
||||
# - https://github.com/espressif/esp-idf/releases
|
||||
ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
|
||||
"recommended": cv.Version(5, 5, 5),
|
||||
"latest": cv.Version(5, 5, 5),
|
||||
"dev": cv.Version(5, 5, 5),
|
||||
"recommended": cv.Version(5, 5, 6),
|
||||
"latest": cv.Version(5, 5, 6),
|
||||
"dev": cv.Version(5, 5, 6),
|
||||
}
|
||||
|
||||
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
||||
@@ -1135,6 +1135,7 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
||||
cv.Version(
|
||||
6, 0, 0
|
||||
): "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, 4): cv.Version(55, 3, 39),
|
||||
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,
|
||||
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));
|
||||
conn_params.min_int = min_interval;
|
||||
conn_params.max_int = max_interval;
|
||||
|
||||
@@ -20,6 +20,10 @@
|
||||
#ifdef USE_DASHBOARD_IMPORT
|
||||
#include "esphome/components/dashboard_import/dashboard_import.h"
|
||||
#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 {
|
||||
|
||||
@@ -39,6 +43,45 @@ MDNS_STATIC_CONST_CHAR(SERVICE_TCP, "_tcp");
|
||||
// Wrap build-time defines into flash storage
|
||||
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) {
|
||||
#ifdef USE_MDNS_STORE_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)});
|
||||
|
||||
#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");
|
||||
txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_WIFI)});
|
||||
#elif defined(USE_ETHERNET)
|
||||
|
||||
@@ -32,6 +32,12 @@
|
||||
#define USE_MDNS_DEVICE_INFO_TXT
|
||||
#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 {
|
||||
|
||||
// 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);
|
||||
|
||||
#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
|
||||
/// Storage for runtime-generated TXT values from user lambdas
|
||||
/// Pre-sized at compile time via MDNS_DYNAMIC_TXT_COUNT to avoid heap allocations.
|
||||
|
||||
@@ -89,7 +89,24 @@ bool MDNSComponent::set_service_enabled(const char *service_type, const char *pr
|
||||
}
|
||||
#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() {
|
||||
#ifndef USE_OPENTHREAD
|
||||
|
||||
@@ -46,7 +46,26 @@ void MDNSComponent::start_polling_window_() {
|
||||
}
|
||||
#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() {
|
||||
#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
|
||||
// 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 —
|
||||
|
||||
@@ -9,6 +9,10 @@ class RgbDriverChip(DriverChip):
|
||||
def transforms(self) -> set[str]:
|
||||
"""
|
||||
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}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "lwip/dns.h"
|
||||
#include "lwip/err.h"
|
||||
#include "mqtt_component.h"
|
||||
#include "mqtt_topic_match.h"
|
||||
|
||||
#ifdef USE_API
|
||||
#include "esphome/components/api/api_server.h"
|
||||
@@ -581,63 +582,6 @@ void MQTTClientComponent::disable() {
|
||||
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) {
|
||||
#ifdef USE_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,
|
||||
.hash_id = NOISE_HASH_SHA256,
|
||||
.hybrid_id = NOISE_DH_NONE,
|
||||
.reserved = {},
|
||||
};
|
||||
|
||||
int err = noise_handshakestate_new_by_id(&this->handshake_, &nid, NOISE_ROLE_RESPONDER);
|
||||
|
||||
@@ -243,6 +243,14 @@ std::optional<std::string> SendspinHub::get_pairing_token() const {
|
||||
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)
|
||||
void SendspinHub::set_unpaired_access_enabled(bool 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.
|
||||
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.
|
||||
///
|
||||
/// 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.bcd_device = scratch.bcd_device;
|
||||
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.product = StringRef(scratch.product);
|
||||
msg.serial_number = StringRef(scratch.serial_number);
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
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 (
|
||||
CONF_ALLOWED_IPS,
|
||||
CONF_CONNECTED,
|
||||
CONF_DATA_BITS,
|
||||
CONF_HOST,
|
||||
CONF_PARITY,
|
||||
@@ -11,22 +10,16 @@ from esphome.components.const import (
|
||||
CONF_STOP_BITS,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BAUD_RATE,
|
||||
CONF_ID,
|
||||
CONF_PORT,
|
||||
DEVICE_CLASS_CONNECTIVITY,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
STATE_CLASS_TOTAL_INCREASING,
|
||||
)
|
||||
from esphome.const import CONF_BAUD_RATE, CONF_ID, CONF_PORT
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@Bascht74"]
|
||||
DEPENDENCIES = ["network"]
|
||||
AUTO_LOAD = ["uart", "binary_sensor", "sensor", "socket"]
|
||||
AUTO_LOAD = ["uart", "socket"]
|
||||
DOMAIN = "tcp_uart"
|
||||
MULTI_CONF = True
|
||||
|
||||
CONF_DISCONNECTS = "disconnects"
|
||||
CONF_TCP_UART_ID = "tcp_uart_id"
|
||||
|
||||
tcp_uart_ns = cg.esphome_ns.namespace("tcp_uart")
|
||||
TcpUart = tcp_uart_ns.class_("TcpUart", uart.UARTComponent, cg.Component)
|
||||
@@ -45,15 +38,6 @@ BASE_SCHEMA = cv.Schema(
|
||||
cv.Optional(
|
||||
CONF_RECONNECT_INTERVAL, default="5s"
|
||||
): 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)
|
||||
|
||||
@@ -99,7 +83,3 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(var.set_parity(config[CONF_PARITY]))
|
||||
if (host := config.get(CONF_HOST)) is not None:
|
||||
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)
|
||||
@@ -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)
|
||||
@@ -17,9 +17,11 @@ void TcpUart::setup() {
|
||||
#ifdef USE_SOCKET_TCP_LISTENER
|
||||
this->listener_.begin(TAG);
|
||||
#endif
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
if (this->connected_sensor_ != nullptr) {
|
||||
this->connected_sensor_->publish_state(false);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
if (this->disconnects_sensor_ != nullptr) {
|
||||
this->disconnects_sensor_->publish_state(0);
|
||||
@@ -38,7 +40,9 @@ void TcpUart::dump_config() {
|
||||
#ifdef USE_SOCKET_TCP_LISTENER
|
||||
this->listener_.dump_config();
|
||||
#endif
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
LOG_BINARY_SENSOR(" ", "Connected", this->connected_sensor_);
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
LOG_SENSOR(" ", "Disconnects", this->disconnects_sensor_);
|
||||
#endif
|
||||
@@ -58,9 +62,11 @@ void TcpUart::sync_link_() {
|
||||
// Unread bytes of the last session stay readable while down, never into the next one.
|
||||
this->rx_start_ = this->rx_end_ = 0;
|
||||
}
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
if (this->connected_sensor_ != nullptr) {
|
||||
this->connected_sensor_->publish_state(up);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
// Only edges get here, so down is the falling edge.
|
||||
if (!up && this->disconnects_sensor_ != nullptr) {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#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_port(uint16_t port) { this->link_.set_port(port); }
|
||||
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; }
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
void set_disconnects_sensor(sensor::Sensor *sensor) { this->disconnects_sensor_ = sensor; }
|
||||
#endif
|
||||
@@ -64,7 +69,9 @@ class TcpUart : public uart::UARTComponent, public Component {
|
||||
#ifdef USE_SOCKET_TCP_LISTENER
|
||||
socket::TcpListener listener_;
|
||||
#endif
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
binary_sensor::BinarySensor *connected_sensor_{nullptr};
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
sensor::Sensor *disconnects_sensor_{nullptr};
|
||||
uint32_t disconnects_{0};
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
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 (
|
||||
CONF_ALLOWED_IPS,
|
||||
CONF_CONNECTED,
|
||||
CONF_HOST,
|
||||
CONF_RECONNECT_INTERVAL,
|
||||
CONF_ROLE,
|
||||
@@ -14,9 +13,6 @@ from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_PORT,
|
||||
CONF_UART_ID,
|
||||
DEVICE_CLASS_CONNECTIVITY,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
STATE_CLASS_TOTAL_INCREASING,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
import esphome.final_validate as fv
|
||||
@@ -25,10 +21,10 @@ from esphome.types import ConfigType
|
||||
CODEOWNERS = ["@Bascht74"]
|
||||
DOMAIN = "uart_tcp"
|
||||
DEPENDENCIES = ["network", "uart"]
|
||||
AUTO_LOAD = ["binary_sensor", "sensor", "socket"]
|
||||
AUTO_LOAD = ["socket"]
|
||||
MULTI_CONF = True
|
||||
|
||||
CONF_DISCONNECTS = "disconnects"
|
||||
CONF_UART_TCP_ID = "uart_tcp_id"
|
||||
|
||||
uart_tcp_ns = cg.esphome_ns.namespace("uart_tcp")
|
||||
UartTcp = uart_tcp_ns.class_("UartTcp", cg.Component, uart.UARTDevice)
|
||||
@@ -42,15 +38,6 @@ BASE_SCHEMA = cv.Schema(
|
||||
cv.Optional(
|
||||
CONF_RECONNECT_INTERVAL, default="5s"
|
||||
): 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)
|
||||
|
||||
@@ -155,7 +142,3 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(var.set_reconnect_interval(config[CONF_RECONNECT_INTERVAL]))
|
||||
if (host := config.get(CONF_HOST)) is not None:
|
||||
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)
|
||||
@@ -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)
|
||||
@@ -19,9 +19,11 @@ void UartTcp::setup() {
|
||||
#ifdef USE_SOCKET_TCP_LISTENER
|
||||
this->listener_.begin(TAG);
|
||||
#endif
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
if (this->connected_sensor_ != nullptr) {
|
||||
this->connected_sensor_->publish_state(false);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
if (this->disconnects_sensor_ != nullptr) {
|
||||
this->disconnects_sensor_->publish_state(0);
|
||||
@@ -40,7 +42,9 @@ void UartTcp::dump_config() {
|
||||
#ifdef USE_SOCKET_TCP_LISTENER
|
||||
this->listener_.dump_config();
|
||||
#endif
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
LOG_BINARY_SENSOR(" ", "Connected", this->connected_sensor_);
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
LOG_SENSOR(" ", "Disconnects", this->disconnects_sensor_);
|
||||
#endif
|
||||
@@ -60,9 +64,11 @@ void UartTcp::sync_link_() {
|
||||
// The driver kept whatever arrived while the link was down.
|
||||
this->discard_uart_();
|
||||
}
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
if (this->connected_sensor_ != nullptr) {
|
||||
this->connected_sensor_->publish_state(up);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
// Only edges get here, so down is the falling edge.
|
||||
if (!up && this->disconnects_sensor_ != nullptr) {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#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_port(uint16_t port) { this->link_.set_port(port); }
|
||||
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; }
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
void set_disconnects_sensor(sensor::Sensor *sensor) { this->disconnects_sensor_ = sensor; }
|
||||
#endif
|
||||
@@ -53,7 +58,9 @@ class UartTcp final : public Component, public uart::UARTDevice {
|
||||
#ifdef USE_SOCKET_TCP_LISTENER
|
||||
socket::TcpListener listener_;
|
||||
#endif
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
binary_sensor::BinarySensor *connected_sensor_{nullptr};
|
||||
#endif
|
||||
// Loop start time of the last socket-to-UART write; sizes the next paced write.
|
||||
uint32_t last_write_ms_{0};
|
||||
#ifdef USE_SENSOR
|
||||
|
||||
@@ -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
|
||||
|
||||
#endif // USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 ||
|
||||
|
||||
@@ -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");
|
||||
continue;
|
||||
}
|
||||
if (in_ep->bEndpointAddress & usb_host::USB_DIR_IN) {
|
||||
cdc_devs.push_back({CdcEps{nullptr, in_ep, out_ep, data_desc->bInterfaceNumber}});
|
||||
} else {
|
||||
cdc_devs.push_back({CdcEps{nullptr, out_ep, in_ep, data_desc->bInterfaceNumber}});
|
||||
}
|
||||
CdcEps eps = in_ep->bEndpointAddress & usb_host::USB_DIR_IN
|
||||
? CdcEps{nullptr, in_ep, out_ep, data_desc->bInterfaceNumber}
|
||||
: CdcEps{nullptr, out_ep, in_ep, data_desc->bInterfaceNumber};
|
||||
eps.bulk_interface_class = data_desc->bInterfaceClass;
|
||||
cdc_devs.push_back(eps);
|
||||
}
|
||||
return cdc_devs;
|
||||
}
|
||||
|
||||
@@ -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_class = intf_desc->bInterfaceClass;
|
||||
return eps;
|
||||
}
|
||||
|
||||
|
||||
@@ -189,7 +189,10 @@ std::vector<CdcEps> USBUartTypePL2303::parse_descriptors(usb_device_handle_t dev
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
eps.notify_ep = ep;
|
||||
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 &&
|
||||
(eps.bulk_interface_number == 0xFF || eps.bulk_interface_number == intf_desc->bInterfaceNumber)) {
|
||||
eps.in_ep = ep;
|
||||
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) &&
|
||||
(eps.bulk_interface_number == 0xFF || eps.bulk_interface_number == intf_desc->bInterfaceNumber)) {
|
||||
eps.out_ep = ep;
|
||||
eps.bulk_interface_number = intf_desc->bInterfaceNumber;
|
||||
eps.bulk_interface_class = intf_desc->bInterfaceClass;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Unexpected endpoint attributes: %02X", ep->bmAttributes);
|
||||
continue;
|
||||
@@ -431,8 +434,7 @@ void USBUartTypeCdcAcm::on_connected() {
|
||||
if (channel->cdc_dev_.interrupt_interface_number != 0xFF &&
|
||||
channel->cdc_dev_.interrupt_interface_number != channel->cdc_dev_.bulk_interface_number) {
|
||||
if (!this->claim_comm_interface_) {
|
||||
ESP_LOGD(TAG, "Skipping comm interface %d (claim_comm_interface: false)",
|
||||
channel->cdc_dev_.interrupt_interface_number);
|
||||
ESP_LOGD(TAG, "Comm interface %d left unclaimed", channel->cdc_dev_.interrupt_interface_number);
|
||||
} else {
|
||||
auto err_comm = usb_host_interface_claim(this->handle_, this->device_handle_,
|
||||
channel->cdc_dev_.interrupt_interface_number, 0);
|
||||
|
||||
@@ -39,6 +39,9 @@ struct CdcEps {
|
||||
// must remain valid even when the interface itself is not claimed.
|
||||
uint8_t interrupt_interface_number{0xFF};
|
||||
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 {
|
||||
@@ -186,6 +189,16 @@ class USBUartChannelBase : public uart::UARTComponent, public Parented<USBUartCo
|
||||
: 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:
|
||||
// Not directly instantiable; construct a concrete channel type instead.
|
||||
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 {
|
||||
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;
|
||||
|
||||
protected:
|
||||
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;
|
||||
std::vector<CdcEps> parse_descriptors(usb_device_handle_t dev_hdl) override;
|
||||
|
||||
private:
|
||||
CH34xChipType chiptype_{CHIP_UNKNOWN};
|
||||
|
||||
@@ -955,10 +955,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) {
|
||||
auto *response = static_cast<AsyncEventSourceResponse *>(arg);
|
||||
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
|
||||
// its next select/recv path delete the session and invoke destroy().
|
||||
shutdown(fd, SHUT_RDWR);
|
||||
@@ -972,7 +1001,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.
|
||||
// Treated as would-block; the stall timer ends a session that never becomes ours again.
|
||||
const int fd = this->fd_.load();
|
||||
if (httpd_sess_get_ctx(this->hd_, fd) != this) {
|
||||
if (!this->owns_session_(fd)) {
|
||||
return 0;
|
||||
}
|
||||
struct msghdr msg {};
|
||||
@@ -1081,6 +1110,7 @@ void AsyncEventSourceResponse::loop() {
|
||||
this->process_close_();
|
||||
return;
|
||||
}
|
||||
this->refresh_lru_();
|
||||
drain_tail_();
|
||||
process_deferred_queue_();
|
||||
if (this->close_requested_)
|
||||
|
||||
@@ -329,10 +329,15 @@ class AsyncEventSourceResponse {
|
||||
};
|
||||
void process_close_();
|
||||
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 {
|
||||
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);
|
||||
@@ -347,11 +352,13 @@ class AsyncEventSourceResponse {
|
||||
RAMUniquePtr<uint8_t[]> tail_;
|
||||
uint32_t send_failure_started_ms_{0}; // Zero means no send stall in progress.
|
||||
uint32_t next_close_attempt_ms_{0};
|
||||
uint32_t last_lru_refresh_ms_{0};
|
||||
uint16_t tail_cap_{0};
|
||||
uint16_t tail_len_{0}; // Zero means nothing pending
|
||||
uint16_t tail_sent_{0};
|
||||
// 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> lru_work_queued_{false};
|
||||
// Main-loop only; the HTTPD task never reads or writes these flags.
|
||||
bool close_requested_{false};
|
||||
bool close_retry_warning_logged_{false};
|
||||
@@ -373,6 +380,7 @@ class AsyncEventSourceResponse {
|
||||
static constexpr uint32_t SEND_STALL_TIMEOUT_MS = 20000;
|
||||
static constexpr uint32_t CLOSE_RETRY_INTERVAL_MS = 250;
|
||||
static constexpr uint32_t CLOSE_CONFIRM_INTERVAL_MS = 1000;
|
||||
static constexpr uint32_t LRU_REFRESH_INTERVAL_MS = 1000;
|
||||
};
|
||||
|
||||
using AsyncEventSourceClient = AsyncEventSourceResponse;
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ from enum import Enum
|
||||
|
||||
from esphome.enum import StrEnum
|
||||
|
||||
__version__ = "2026.10.0b4"
|
||||
__version__ = "2026.10.0b5"
|
||||
|
||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||
|
||||
+2
-2
@@ -147,7 +147,7 @@ extends = common:arduino
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.312/platform-espressif32.zip
|
||||
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-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
|
||||
lib_deps =
|
||||
@@ -183,7 +183,7 @@ extra_scripts =
|
||||
extends = common:idf
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.312/platform-espressif32.zip
|
||||
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
|
||||
lib_deps =
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ pyserial==3.5
|
||||
platformio==6.2.0
|
||||
esptool==5.4.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
|
||||
zeroconf==0.151.5
|
||||
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
|
||||
# sequence array was generated instead of raising during code generation.
|
||||
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
|
||||
|
||||
# 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).
|
||||
model = MODELS["ESP32-8048S070"]
|
||||
model = MODELS["ST7701S"]
|
||||
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}})
|
||||
|
||||
# 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}})
|
||||
|
||||
|
||||
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(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,3 +1,4 @@
|
||||
# The disconnects counter is compiled only with USE_SENSOR; declaring the
|
||||
# sensor domain makes the C++ unit test build define it.
|
||||
# The connection entities are compiled only with USE_BINARY_SENSOR and
|
||||
# USE_SENSOR; declaring the domains makes the C++ unit test build define them.
|
||||
binary_sensor:
|
||||
sensor:
|
||||
|
||||
@@ -10,8 +10,16 @@ tcp_uart:
|
||||
allowed_ips:
|
||||
- 192.0.2.20
|
||||
- 192.0.2.0/24
|
||||
|
||||
binary_sensor:
|
||||
- platform: tcp_uart
|
||||
tcp_uart_id: tcp_uart_server
|
||||
connected:
|
||||
name: TCP UART Server Connected
|
||||
|
||||
sensor:
|
||||
- platform: tcp_uart
|
||||
tcp_uart_id: tcp_uart_server
|
||||
disconnects:
|
||||
name: TCP UART Server Disconnects
|
||||
|
||||
|
||||
@@ -7,7 +7,15 @@ tcp_uart:
|
||||
host: 192.0.2.10
|
||||
port: 502
|
||||
reconnect_interval: 10s
|
||||
|
||||
binary_sensor:
|
||||
- platform: tcp_uart
|
||||
tcp_uart_id: tcp_uart_1
|
||||
connected:
|
||||
name: TCP UART Connected
|
||||
|
||||
sensor:
|
||||
- platform: tcp_uart
|
||||
tcp_uart_id: tcp_uart_1
|
||||
disconnects:
|
||||
name: TCP UART Disconnects
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# The disconnects counter is compiled only with USE_SENSOR; declaring the
|
||||
# sensor domain makes the C++ unit test build define it.
|
||||
# The connection entities are compiled only with USE_BINARY_SENSOR and
|
||||
# USE_SENSOR; declaring the domains makes the C++ unit test build define them.
|
||||
binary_sensor:
|
||||
sensor:
|
||||
|
||||
@@ -10,7 +10,15 @@ uart_tcp:
|
||||
allowed_ips:
|
||||
- 192.168.1.10
|
||||
- 192.168.1.0/24
|
||||
|
||||
binary_sensor:
|
||||
- platform: uart_tcp
|
||||
uart_tcp_id: bridge
|
||||
connected:
|
||||
name: UART TCP Connected
|
||||
|
||||
sensor:
|
||||
- platform: uart_tcp
|
||||
uart_tcp_id: bridge
|
||||
disconnects:
|
||||
name: UART TCP Disconnects
|
||||
|
||||
@@ -12,7 +12,15 @@ uart_tcp:
|
||||
host: 192.0.2.20
|
||||
port: 502
|
||||
reconnect_interval: 10s
|
||||
|
||||
binary_sensor:
|
||||
- platform: uart_tcp
|
||||
uart_tcp_id: bridge
|
||||
connected:
|
||||
name: UART TCP Connected
|
||||
|
||||
sensor:
|
||||
- platform: uart_tcp
|
||||
uart_tcp_id: bridge
|
||||
disconnects:
|
||||
name: UART TCP Disconnects
|
||||
|
||||
@@ -8,7 +8,15 @@ uart_tcp:
|
||||
uart_id: uart_bus
|
||||
host: 127.0.0.1
|
||||
port: 44502
|
||||
|
||||
binary_sensor:
|
||||
- platform: uart_tcp
|
||||
uart_tcp_id: bridge
|
||||
connected:
|
||||
name: UART TCP Connected
|
||||
|
||||
sensor:
|
||||
- platform: uart_tcp
|
||||
uart_tcp_id: bridge
|
||||
disconnects:
|
||||
name: UART TCP Disconnects
|
||||
|
||||
@@ -20,6 +20,10 @@ tcp_uart:
|
||||
host: 127.0.0.1
|
||||
port: 18125
|
||||
reconnect_interval: 3s
|
||||
|
||||
binary_sensor:
|
||||
- platform: tcp_uart
|
||||
tcp_uart_id: tcp_uart_1
|
||||
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
|
||||
allowed_ips:
|
||||
- 127.0.0.1
|
||||
connected:
|
||||
name: Bridge Connected
|
||||
- id: denied_bridge
|
||||
uart_id: denied_bus
|
||||
role: server
|
||||
port: 18127
|
||||
allowed_ips:
|
||||
- 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,
|
||||
},
|
||||
"WizardCapabilities": {"configured": 1},
|
||||
"SendspinCapabilities": {"feature_flags": 1},
|
||||
"SendspinCapabilities": {"feature_flags": 1, "client_id": 2},
|
||||
"BluetoothProxyCapabilities": {
|
||||
"feature_flags": 1,
|
||||
"mac_address": 2,
|
||||
|
||||
Reference in New Issue
Block a user