mirror of
https://github.com/esphome/esphome.git
synced 2025-08-05 09:57:47 +00:00
Merge branch 'integration' into memory_api
This commit is contained in:
commit
1c00e0b9c1
@ -378,6 +378,7 @@ esphome/components/rp2040_pwm/* @jesserockz
|
|||||||
esphome/components/rpi_dpi_rgb/* @clydebarrow
|
esphome/components/rpi_dpi_rgb/* @clydebarrow
|
||||||
esphome/components/rtl87xx/* @kuba2k2
|
esphome/components/rtl87xx/* @kuba2k2
|
||||||
esphome/components/rtttl/* @glmnet
|
esphome/components/rtttl/* @glmnet
|
||||||
|
esphome/components/runtime_stats/* @bdraco
|
||||||
esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti
|
esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti
|
||||||
esphome/components/scd4x/* @martgras @sjtrny
|
esphome/components/scd4x/* @martgras @sjtrny
|
||||||
esphome/components/script/* @esphome/core
|
esphome/components/script/* @esphome/core
|
||||||
|
@ -86,8 +86,8 @@ void APIConnection::start() {
|
|||||||
APIError err = this->helper_->init();
|
APIError err = this->helper_->init();
|
||||||
if (err != APIError::OK) {
|
if (err != APIError::OK) {
|
||||||
on_fatal_error();
|
on_fatal_error();
|
||||||
ESP_LOGW(TAG, "%s: Helper init failed: %s errno=%d", this->get_client_combined_info().c_str(),
|
ESP_LOGW(TAG, "%s: Helper init failed %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err),
|
||||||
api_error_to_str(err), errno);
|
errno);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this->client_info_ = helper_->getpeername();
|
this->client_info_ = helper_->getpeername();
|
||||||
@ -119,7 +119,7 @@ void APIConnection::loop() {
|
|||||||
APIError err = this->helper_->loop();
|
APIError err = this->helper_->loop();
|
||||||
if (err != APIError::OK) {
|
if (err != APIError::OK) {
|
||||||
on_fatal_error();
|
on_fatal_error();
|
||||||
ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->get_client_combined_info().c_str(),
|
ESP_LOGW(TAG, "%s: Socket operation failed %s errno=%d", this->get_client_combined_info().c_str(),
|
||||||
api_error_to_str(err), errno);
|
api_error_to_str(err), errno);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -136,14 +136,8 @@ void APIConnection::loop() {
|
|||||||
break;
|
break;
|
||||||
} else if (err != APIError::OK) {
|
} else if (err != APIError::OK) {
|
||||||
on_fatal_error();
|
on_fatal_error();
|
||||||
if (err == APIError::SOCKET_READ_FAILED && errno == ECONNRESET) {
|
ESP_LOGW(TAG, "%s: Reading failed %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err),
|
||||||
ESP_LOGW(TAG, "%s: Connection reset", this->get_client_combined_info().c_str());
|
errno);
|
||||||
} else if (err == APIError::CONNECTION_CLOSED) {
|
|
||||||
ESP_LOGW(TAG, "%s: Connection closed", this->get_client_combined_info().c_str());
|
|
||||||
} else {
|
|
||||||
ESP_LOGW(TAG, "%s: Reading failed: %s errno=%d", this->get_client_combined_info().c_str(),
|
|
||||||
api_error_to_str(err), errno);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
this->last_traffic_ = now;
|
this->last_traffic_ = now;
|
||||||
@ -1435,6 +1429,24 @@ bool APIConnection::try_send_log_message(int level, const char *tag, const char
|
|||||||
return this->send_buffer(buffer, SubscribeLogsResponse::MESSAGE_TYPE);
|
return this->send_buffer(buffer, SubscribeLogsResponse::MESSAGE_TYPE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void APIConnection::complete_authentication_() {
|
||||||
|
// Early return if already authenticated
|
||||||
|
if (this->flags_.connection_state == static_cast<uint8_t>(ConnectionState::AUTHENTICATED)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED);
|
||||||
|
ESP_LOGD(TAG, "%s connected", this->get_client_combined_info().c_str());
|
||||||
|
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
|
||||||
|
this->parent_->get_client_connected_trigger()->trigger(this->client_info_, this->client_peername_);
|
||||||
|
#endif
|
||||||
|
#ifdef USE_HOMEASSISTANT_TIME
|
||||||
|
if (homeassistant::global_homeassistant_time != nullptr) {
|
||||||
|
this->send_time_request();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
HelloResponse APIConnection::hello(const HelloRequest &msg) {
|
HelloResponse APIConnection::hello(const HelloRequest &msg) {
|
||||||
this->client_info_ = msg.client_info;
|
this->client_info_ = msg.client_info;
|
||||||
this->client_peername_ = this->helper_->getpeername();
|
this->client_peername_ = this->helper_->getpeername();
|
||||||
@ -1450,7 +1462,14 @@ HelloResponse APIConnection::hello(const HelloRequest &msg) {
|
|||||||
resp.server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")";
|
resp.server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")";
|
||||||
resp.name = App.get_name();
|
resp.name = App.get_name();
|
||||||
|
|
||||||
|
#ifdef USE_API_PASSWORD
|
||||||
|
// Password required - wait for authentication
|
||||||
this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::CONNECTED);
|
this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::CONNECTED);
|
||||||
|
#else
|
||||||
|
// No password configured - auto-authenticate
|
||||||
|
this->complete_authentication_();
|
||||||
|
#endif
|
||||||
|
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
ConnectResponse APIConnection::connect(const ConnectRequest &msg) {
|
ConnectResponse APIConnection::connect(const ConnectRequest &msg) {
|
||||||
@ -1463,23 +1482,14 @@ ConnectResponse APIConnection::connect(const ConnectRequest &msg) {
|
|||||||
// bool invalid_password = 1;
|
// bool invalid_password = 1;
|
||||||
resp.invalid_password = !correct;
|
resp.invalid_password = !correct;
|
||||||
if (correct) {
|
if (correct) {
|
||||||
ESP_LOGD(TAG, "%s connected", this->get_client_combined_info().c_str());
|
this->complete_authentication_();
|
||||||
this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED);
|
|
||||||
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
|
|
||||||
this->parent_->get_client_connected_trigger()->trigger(this->client_info_, this->client_peername_);
|
|
||||||
#endif
|
|
||||||
#ifdef USE_HOMEASSISTANT_TIME
|
|
||||||
if (homeassistant::global_homeassistant_time != nullptr) {
|
|
||||||
this->send_time_request();
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) {
|
DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) {
|
||||||
DeviceInfoResponse resp{};
|
DeviceInfoResponse resp{};
|
||||||
#ifdef USE_API_PASSWORD
|
#ifdef USE_API_PASSWORD
|
||||||
resp.uses_password = this->parent_->uses_password();
|
resp.uses_password = true;
|
||||||
#else
|
#else
|
||||||
resp.uses_password = false;
|
resp.uses_password = false;
|
||||||
#endif
|
#endif
|
||||||
@ -1596,7 +1606,7 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) {
|
|||||||
APIError err = this->helper_->loop();
|
APIError err = this->helper_->loop();
|
||||||
if (err != APIError::OK) {
|
if (err != APIError::OK) {
|
||||||
on_fatal_error();
|
on_fatal_error();
|
||||||
ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->get_client_combined_info().c_str(),
|
ESP_LOGW(TAG, "%s: Socket operation failed %s errno=%d", this->get_client_combined_info().c_str(),
|
||||||
api_error_to_str(err), errno);
|
api_error_to_str(err), errno);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -1617,12 +1627,8 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
|
|||||||
return false;
|
return false;
|
||||||
if (err != APIError::OK) {
|
if (err != APIError::OK) {
|
||||||
on_fatal_error();
|
on_fatal_error();
|
||||||
if (err == APIError::SOCKET_WRITE_FAILED && errno == ECONNRESET) {
|
ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", this->get_client_combined_info().c_str(),
|
||||||
ESP_LOGW(TAG, "%s: Connection reset", this->get_client_combined_info().c_str());
|
api_error_to_str(err), errno);
|
||||||
} else {
|
|
||||||
ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", this->get_client_combined_info().c_str(),
|
|
||||||
api_error_to_str(err), errno);
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Do not set last_traffic_ on send
|
// Do not set last_traffic_ on send
|
||||||
@ -1630,11 +1636,11 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
|
|||||||
}
|
}
|
||||||
void APIConnection::on_unauthenticated_access() {
|
void APIConnection::on_unauthenticated_access() {
|
||||||
this->on_fatal_error();
|
this->on_fatal_error();
|
||||||
ESP_LOGD(TAG, "%s requested access without authentication", this->get_client_combined_info().c_str());
|
ESP_LOGD(TAG, "%s access without authentication", this->get_client_combined_info().c_str());
|
||||||
}
|
}
|
||||||
void APIConnection::on_no_setup_connection() {
|
void APIConnection::on_no_setup_connection() {
|
||||||
this->on_fatal_error();
|
this->on_fatal_error();
|
||||||
ESP_LOGD(TAG, "%s requested access without full connection", this->get_client_combined_info().c_str());
|
ESP_LOGD(TAG, "%s access without full connection", this->get_client_combined_info().c_str());
|
||||||
}
|
}
|
||||||
void APIConnection::on_fatal_error() {
|
void APIConnection::on_fatal_error() {
|
||||||
this->helper_->close();
|
this->helper_->close();
|
||||||
@ -1799,12 +1805,8 @@ void APIConnection::process_batch_() {
|
|||||||
this->helper_->write_protobuf_packets(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, packet_info);
|
this->helper_->write_protobuf_packets(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, packet_info);
|
||||||
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
|
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
|
||||||
on_fatal_error();
|
on_fatal_error();
|
||||||
if (err == APIError::SOCKET_WRITE_FAILED && errno == ECONNRESET) {
|
ESP_LOGW(TAG, "%s: Batch write failed %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err),
|
||||||
ESP_LOGW(TAG, "%s: Connection reset during batch write", this->get_client_combined_info().c_str());
|
errno);
|
||||||
} else {
|
|
||||||
ESP_LOGW(TAG, "%s: Batch write failed %s errno=%d", this->get_client_combined_info().c_str(),
|
|
||||||
api_error_to_str(err), errno);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
|
@ -273,6 +273,9 @@ class APIConnection : public APIServerConnection {
|
|||||||
ProtoWriteBuffer allocate_batch_message_buffer(uint16_t size);
|
ProtoWriteBuffer allocate_batch_message_buffer(uint16_t size);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
// Helper function to handle authentication completion
|
||||||
|
void complete_authentication_();
|
||||||
|
|
||||||
// Helper function to fill common entity info fields
|
// Helper function to fill common entity info fields
|
||||||
static void fill_entity_info_base(esphome::EntityBase *entity, InfoResponseProtoMessage &response) {
|
static void fill_entity_info_base(esphome::EntityBase *entity, InfoResponseProtoMessage &response) {
|
||||||
// Set common fields that are shared by all entity types
|
// Set common fields that are shared by all entity types
|
||||||
|
@ -219,8 +219,6 @@ void APIServer::dump_config() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#ifdef USE_API_PASSWORD
|
#ifdef USE_API_PASSWORD
|
||||||
bool APIServer::uses_password() const { return !this->password_.empty(); }
|
|
||||||
|
|
||||||
bool APIServer::check_password(const std::string &password) const {
|
bool APIServer::check_password(const std::string &password) const {
|
||||||
// depend only on input password length
|
// depend only on input password length
|
||||||
const char *a = this->password_.c_str();
|
const char *a = this->password_.c_str();
|
||||||
@ -428,7 +426,7 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
|
|||||||
ESP_LOGD(TAG, "Noise PSK saved");
|
ESP_LOGD(TAG, "Noise PSK saved");
|
||||||
if (make_active) {
|
if (make_active) {
|
||||||
this->set_timeout(100, [this, psk]() {
|
this->set_timeout(100, [this, psk]() {
|
||||||
ESP_LOGW(TAG, "Disconnecting all clients to reset connections");
|
ESP_LOGW(TAG, "Disconnecting all clients to reset PSK");
|
||||||
this->set_noise_psk(psk);
|
this->set_noise_psk(psk);
|
||||||
for (auto &c : this->clients_) {
|
for (auto &c : this->clients_) {
|
||||||
c->send_message(DisconnectRequest());
|
c->send_message(DisconnectRequest());
|
||||||
|
@ -39,7 +39,6 @@ class APIServer : public Component, public Controller {
|
|||||||
bool teardown() override;
|
bool teardown() override;
|
||||||
#ifdef USE_API_PASSWORD
|
#ifdef USE_API_PASSWORD
|
||||||
bool check_password(const std::string &password) const;
|
bool check_password(const std::string &password) const;
|
||||||
bool uses_password() const;
|
|
||||||
void set_password(const std::string &password);
|
void set_password(const std::string &password);
|
||||||
#endif
|
#endif
|
||||||
void set_port(uint16_t port);
|
void set_port(uint16_t port);
|
||||||
|
@ -175,26 +175,7 @@ class Proto32Bit {
|
|||||||
const uint32_t value_;
|
const uint32_t value_;
|
||||||
};
|
};
|
||||||
|
|
||||||
class Proto64Bit {
|
// NOTE: Proto64Bit class removed - wire type 1 (64-bit fixed) not supported
|
||||||
public:
|
|
||||||
explicit Proto64Bit(uint64_t value) : value_(value) {}
|
|
||||||
uint64_t as_fixed64() const { return this->value_; }
|
|
||||||
int64_t as_sfixed64() const { return static_cast<int64_t>(this->value_); }
|
|
||||||
double as_double() const {
|
|
||||||
union {
|
|
||||||
uint64_t raw;
|
|
||||||
double value;
|
|
||||||
} s{};
|
|
||||||
s.raw = this->value_;
|
|
||||||
return s.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected:
|
|
||||||
const uint64_t value_;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Forward declaration needed for method declaration
|
|
||||||
class ProtoMessage;
|
|
||||||
|
|
||||||
class ProtoWriteBuffer {
|
class ProtoWriteBuffer {
|
||||||
public:
|
public:
|
||||||
@ -261,20 +242,10 @@ class ProtoWriteBuffer {
|
|||||||
this->write((value >> 16) & 0xFF);
|
this->write((value >> 16) & 0xFF);
|
||||||
this->write((value >> 24) & 0xFF);
|
this->write((value >> 24) & 0xFF);
|
||||||
}
|
}
|
||||||
void encode_fixed64(uint32_t field_id, uint64_t value, bool force = false) {
|
// NOTE: Wire type 1 (64-bit fixed: double, fixed64, sfixed64) is intentionally
|
||||||
if (value == 0 && !force)
|
// not supported to reduce overhead on embedded systems. All ESPHome devices are
|
||||||
return;
|
// 32-bit microcontrollers where 64-bit operations are expensive. If 64-bit support
|
||||||
|
// is needed in the future, the necessary encoding/decoding functions must be added.
|
||||||
this->encode_field_raw(field_id, 1); // type 1: 64-bit fixed64
|
|
||||||
this->write((value >> 0) & 0xFF);
|
|
||||||
this->write((value >> 8) & 0xFF);
|
|
||||||
this->write((value >> 16) & 0xFF);
|
|
||||||
this->write((value >> 24) & 0xFF);
|
|
||||||
this->write((value >> 32) & 0xFF);
|
|
||||||
this->write((value >> 40) & 0xFF);
|
|
||||||
this->write((value >> 48) & 0xFF);
|
|
||||||
this->write((value >> 56) & 0xFF);
|
|
||||||
}
|
|
||||||
void encode_float(uint32_t field_id, float value, bool force = false) {
|
void encode_float(uint32_t field_id, float value, bool force = false) {
|
||||||
if (value == 0.0f && !force)
|
if (value == 0.0f && !force)
|
||||||
return;
|
return;
|
||||||
@ -340,7 +311,7 @@ class ProtoMessage {
|
|||||||
virtual bool decode_varint(uint32_t field_id, ProtoVarInt value) { return false; }
|
virtual bool decode_varint(uint32_t field_id, ProtoVarInt value) { return false; }
|
||||||
virtual bool decode_length(uint32_t field_id, ProtoLengthDelimited value) { return false; }
|
virtual bool decode_length(uint32_t field_id, ProtoLengthDelimited value) { return false; }
|
||||||
virtual bool decode_32bit(uint32_t field_id, Proto32Bit value) { return false; }
|
virtual bool decode_32bit(uint32_t field_id, Proto32Bit value) { return false; }
|
||||||
virtual bool decode_64bit(uint32_t field_id, Proto64Bit value) { return false; }
|
// NOTE: decode_64bit removed - wire type 1 not supported
|
||||||
};
|
};
|
||||||
|
|
||||||
class ProtoSize {
|
class ProtoSize {
|
||||||
@ -665,33 +636,8 @@ class ProtoSize {
|
|||||||
total_size += field_id_size + varint(value);
|
total_size += field_id_size + varint(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// NOTE: sint64 support functions (add_sint64_field, add_sint64_field_repeated) removed
|
||||||
* @brief Calculates and adds the size of a sint64 field to the total message size
|
// sint64 type is not supported by ESPHome API to reduce overhead on embedded systems
|
||||||
*
|
|
||||||
* Sint64 fields use ZigZag encoding, which is more efficient for negative values.
|
|
||||||
*/
|
|
||||||
static inline void add_sint64_field(uint32_t &total_size, uint32_t field_id_size, int64_t value) {
|
|
||||||
// Skip calculation if value is zero
|
|
||||||
if (value == 0) {
|
|
||||||
return; // No need to update total_size
|
|
||||||
}
|
|
||||||
|
|
||||||
// ZigZag encoding for sint64: (n << 1) ^ (n >> 63)
|
|
||||||
uint64_t zigzag = (static_cast<uint64_t>(value) << 1) ^ (static_cast<uint64_t>(value >> 63));
|
|
||||||
total_size += field_id_size + varint(zigzag);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Calculates and adds the size of a sint64 field to the total message size (repeated field version)
|
|
||||||
*
|
|
||||||
* Sint64 fields use ZigZag encoding, which is more efficient for negative values.
|
|
||||||
*/
|
|
||||||
static inline void add_sint64_field_repeated(uint32_t &total_size, uint32_t field_id_size, int64_t value) {
|
|
||||||
// Always calculate size for repeated fields
|
|
||||||
// ZigZag encoding for sint64: (n << 1) ^ (n >> 63)
|
|
||||||
uint64_t zigzag = (static_cast<uint64_t>(value) << 1) ^ (static_cast<uint64_t>(value >> 63));
|
|
||||||
total_size += field_id_size + varint(zigzag);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Calculates and adds the size of a string/bytes field to the total message size
|
* @brief Calculates and adds the size of a string/bytes field to the total message size
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
from esphome import pins
|
from esphome import pins
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components import binary_sensor
|
from esphome.components import binary_sensor
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_PIN
|
from esphome.const import CONF_ID, CONF_NAME, CONF_NUMBER, CONF_PIN
|
||||||
|
from esphome.core import CORE
|
||||||
|
|
||||||
from .. import gpio_ns
|
from .. import gpio_ns
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
GPIOBinarySensor = gpio_ns.class_(
|
GPIOBinarySensor = gpio_ns.class_(
|
||||||
"GPIOBinarySensor", binary_sensor.BinarySensor, cg.Component
|
"GPIOBinarySensor", binary_sensor.BinarySensor, cg.Component
|
||||||
)
|
)
|
||||||
@ -41,6 +46,22 @@ async def to_code(config):
|
|||||||
pin = await cg.gpio_pin_expression(config[CONF_PIN])
|
pin = await cg.gpio_pin_expression(config[CONF_PIN])
|
||||||
cg.add(var.set_pin(pin))
|
cg.add(var.set_pin(pin))
|
||||||
|
|
||||||
cg.add(var.set_use_interrupt(config[CONF_USE_INTERRUPT]))
|
# Check for ESP8266 GPIO16 interrupt limitation
|
||||||
if config[CONF_USE_INTERRUPT]:
|
# GPIO16 on ESP8266 is a special pin that doesn't support interrupts through
|
||||||
|
# the Arduino attachInterrupt() function. This is the only known GPIO pin
|
||||||
|
# across all supported platforms that has this limitation, so we handle it
|
||||||
|
# here instead of in the platform-specific code.
|
||||||
|
use_interrupt = config[CONF_USE_INTERRUPT]
|
||||||
|
if use_interrupt and CORE.is_esp8266 and config[CONF_PIN][CONF_NUMBER] == 16:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"GPIO binary_sensor '%s': GPIO16 on ESP8266 doesn't support interrupts. "
|
||||||
|
"Falling back to polling mode (same as in ESPHome <2025.7). "
|
||||||
|
"The sensor will work exactly as before, but other pins have better "
|
||||||
|
"performance with interrupts.",
|
||||||
|
config.get(CONF_NAME, config[CONF_ID]),
|
||||||
|
)
|
||||||
|
use_interrupt = False
|
||||||
|
|
||||||
|
cg.add(var.set_use_interrupt(use_interrupt))
|
||||||
|
if use_interrupt:
|
||||||
cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE]))
|
cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE]))
|
||||||
|
@ -29,9 +29,9 @@ from ..defines import (
|
|||||||
)
|
)
|
||||||
from ..helpers import add_lv_use, lvgl_components_required
|
from ..helpers import add_lv_use, lvgl_components_required
|
||||||
from ..lv_validation import (
|
from ..lv_validation import (
|
||||||
angle,
|
|
||||||
get_end_value,
|
get_end_value,
|
||||||
get_start_value,
|
get_start_value,
|
||||||
|
lv_angle,
|
||||||
lv_bool,
|
lv_bool,
|
||||||
lv_color,
|
lv_color,
|
||||||
lv_float,
|
lv_float,
|
||||||
@ -162,7 +162,7 @@ SCALE_SCHEMA = cv.Schema(
|
|||||||
cv.Optional(CONF_RANGE_FROM, default=0.0): cv.float_,
|
cv.Optional(CONF_RANGE_FROM, default=0.0): cv.float_,
|
||||||
cv.Optional(CONF_RANGE_TO, default=100.0): cv.float_,
|
cv.Optional(CONF_RANGE_TO, default=100.0): cv.float_,
|
||||||
cv.Optional(CONF_ANGLE_RANGE, default=270): cv.int_range(0, 360),
|
cv.Optional(CONF_ANGLE_RANGE, default=270): cv.int_range(0, 360),
|
||||||
cv.Optional(CONF_ROTATION): angle,
|
cv.Optional(CONF_ROTATION): lv_angle,
|
||||||
cv.Optional(CONF_INDICATORS): cv.ensure_list(INDICATOR_SCHEMA),
|
cv.Optional(CONF_INDICATORS): cv.ensure_list(INDICATOR_SCHEMA),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -187,7 +187,7 @@ class MeterType(WidgetType):
|
|||||||
for scale_conf in config.get(CONF_SCALES, ()):
|
for scale_conf in config.get(CONF_SCALES, ()):
|
||||||
rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2
|
rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2
|
||||||
if CONF_ROTATION in scale_conf:
|
if CONF_ROTATION in scale_conf:
|
||||||
rotation = scale_conf[CONF_ROTATION] // 10
|
rotation = await lv_angle.process(scale_conf[CONF_ROTATION])
|
||||||
with LocalVariable(
|
with LocalVariable(
|
||||||
"meter_var", "lv_meter_scale_t", lv_expr.meter_add_scale(var)
|
"meter_var", "lv_meter_scale_t", lv_expr.meter_add_scale(var)
|
||||||
) as meter_var:
|
) as meter_var:
|
||||||
@ -205,21 +205,20 @@ class MeterType(WidgetType):
|
|||||||
var,
|
var,
|
||||||
meter_var,
|
meter_var,
|
||||||
ticks[CONF_COUNT],
|
ticks[CONF_COUNT],
|
||||||
ticks[CONF_WIDTH],
|
await size.process(ticks[CONF_WIDTH]),
|
||||||
ticks[CONF_LENGTH],
|
await size.process(ticks[CONF_LENGTH]),
|
||||||
color,
|
color,
|
||||||
)
|
)
|
||||||
if CONF_MAJOR in ticks:
|
if CONF_MAJOR in ticks:
|
||||||
major = ticks[CONF_MAJOR]
|
major = ticks[CONF_MAJOR]
|
||||||
color = await lv_color.process(major[CONF_COLOR])
|
|
||||||
lv.meter_set_scale_major_ticks(
|
lv.meter_set_scale_major_ticks(
|
||||||
var,
|
var,
|
||||||
meter_var,
|
meter_var,
|
||||||
major[CONF_STRIDE],
|
major[CONF_STRIDE],
|
||||||
major[CONF_WIDTH],
|
await size.process(major[CONF_WIDTH]),
|
||||||
major[CONF_LENGTH],
|
await size.process(major[CONF_LENGTH]),
|
||||||
color,
|
await lv_color.process(major[CONF_COLOR]),
|
||||||
major[CONF_LABEL_GAP],
|
await size.process(major[CONF_LABEL_GAP]),
|
||||||
)
|
)
|
||||||
for indicator in scale_conf.get(CONF_INDICATORS, ()):
|
for indicator in scale_conf.get(CONF_INDICATORS, ()):
|
||||||
(t, v) = next(iter(indicator.items()))
|
(t, v) = next(iter(indicator.items()))
|
||||||
@ -233,7 +232,11 @@ class MeterType(WidgetType):
|
|||||||
lv_assign(
|
lv_assign(
|
||||||
ivar,
|
ivar,
|
||||||
lv_expr.meter_add_needle_line(
|
lv_expr.meter_add_needle_line(
|
||||||
var, meter_var, v[CONF_WIDTH], color, v[CONF_R_MOD]
|
var,
|
||||||
|
meter_var,
|
||||||
|
await size.process(v[CONF_WIDTH]),
|
||||||
|
color,
|
||||||
|
await size.process(v[CONF_R_MOD]),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if t == CONF_ARC:
|
if t == CONF_ARC:
|
||||||
@ -241,7 +244,11 @@ class MeterType(WidgetType):
|
|||||||
lv_assign(
|
lv_assign(
|
||||||
ivar,
|
ivar,
|
||||||
lv_expr.meter_add_arc(
|
lv_expr.meter_add_arc(
|
||||||
var, meter_var, v[CONF_WIDTH], color, v[CONF_R_MOD]
|
var,
|
||||||
|
meter_var,
|
||||||
|
await size.process(v[CONF_WIDTH]),
|
||||||
|
color,
|
||||||
|
await size.process(v[CONF_R_MOD]),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if t == CONF_TICK_STYLE:
|
if t == CONF_TICK_STYLE:
|
||||||
@ -257,7 +264,7 @@ class MeterType(WidgetType):
|
|||||||
color_start,
|
color_start,
|
||||||
color_end,
|
color_end,
|
||||||
v[CONF_LOCAL],
|
v[CONF_LOCAL],
|
||||||
v[CONF_WIDTH],
|
size.process(v[CONF_WIDTH]),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if t == CONF_IMAGE:
|
if t == CONF_IMAGE:
|
||||||
|
34
esphome/components/runtime_stats/__init__.py
Normal file
34
esphome/components/runtime_stats/__init__.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
"""
|
||||||
|
Runtime statistics component for ESPHome.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import esphome.codegen as cg
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import CONF_ID
|
||||||
|
|
||||||
|
CODEOWNERS = ["@bdraco"]
|
||||||
|
|
||||||
|
CONF_LOG_INTERVAL = "log_interval"
|
||||||
|
|
||||||
|
runtime_stats_ns = cg.esphome_ns.namespace("runtime_stats")
|
||||||
|
RuntimeStatsCollector = runtime_stats_ns.class_("RuntimeStatsCollector")
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(RuntimeStatsCollector),
|
||||||
|
cv.Optional(
|
||||||
|
CONF_LOG_INTERVAL, default="60s"
|
||||||
|
): cv.positive_time_period_milliseconds,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
"""Generate code for the runtime statistics component."""
|
||||||
|
# Define USE_RUNTIME_STATS when this component is used
|
||||||
|
cg.add_define("USE_RUNTIME_STATS")
|
||||||
|
|
||||||
|
# Create the runtime stats instance (constructor sets global_runtime_stats)
|
||||||
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
|
|
||||||
|
cg.add(var.set_log_interval(config[CONF_LOG_INTERVAL]))
|
102
esphome/components/runtime_stats/runtime_stats.cpp
Normal file
102
esphome/components/runtime_stats/runtime_stats.cpp
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
#include "runtime_stats.h"
|
||||||
|
|
||||||
|
#ifdef USE_RUNTIME_STATS
|
||||||
|
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
|
||||||
|
namespace runtime_stats {
|
||||||
|
|
||||||
|
RuntimeStatsCollector::RuntimeStatsCollector() : log_interval_(60000), next_log_time_(0) {
|
||||||
|
global_runtime_stats = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeStatsCollector::record_component_time(Component *component, uint32_t duration_ms, uint32_t current_time) {
|
||||||
|
if (component == nullptr)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Check if we have cached the name for this component
|
||||||
|
auto name_it = this->component_names_cache_.find(component);
|
||||||
|
if (name_it == this->component_names_cache_.end()) {
|
||||||
|
// First time seeing this component, cache its name
|
||||||
|
const char *source = component->get_component_source();
|
||||||
|
this->component_names_cache_[component] = source;
|
||||||
|
this->component_stats_[source].record_time(duration_ms);
|
||||||
|
} else {
|
||||||
|
this->component_stats_[name_it->second].record_time(duration_ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this->next_log_time_ == 0) {
|
||||||
|
this->next_log_time_ = current_time + this->log_interval_;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeStatsCollector::log_stats_() {
|
||||||
|
ESP_LOGI(TAG, "Component Runtime Statistics");
|
||||||
|
ESP_LOGI(TAG, "Period stats (last %" PRIu32 "ms):", this->log_interval_);
|
||||||
|
|
||||||
|
// First collect stats we want to display
|
||||||
|
std::vector<ComponentStatPair> stats_to_display;
|
||||||
|
|
||||||
|
for (const auto &it : this->component_stats_) {
|
||||||
|
const ComponentRuntimeStats &stats = it.second;
|
||||||
|
if (stats.get_period_count() > 0) {
|
||||||
|
ComponentStatPair pair = {it.first, &stats};
|
||||||
|
stats_to_display.push_back(pair);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by period runtime (descending)
|
||||||
|
std::sort(stats_to_display.begin(), stats_to_display.end(), std::greater<ComponentStatPair>());
|
||||||
|
|
||||||
|
// Log top components by period runtime
|
||||||
|
for (const auto &it : stats_to_display) {
|
||||||
|
const char *source = it.name;
|
||||||
|
const ComponentRuntimeStats *stats = it.stats;
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", source,
|
||||||
|
stats->get_period_count(), stats->get_period_avg_time_ms(), stats->get_period_max_time_ms(),
|
||||||
|
stats->get_period_time_ms());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log total stats since boot
|
||||||
|
ESP_LOGI(TAG, "Total stats (since boot):");
|
||||||
|
|
||||||
|
// Re-sort by total runtime for all-time stats
|
||||||
|
std::sort(stats_to_display.begin(), stats_to_display.end(),
|
||||||
|
[](const ComponentStatPair &a, const ComponentStatPair &b) {
|
||||||
|
return a.stats->get_total_time_ms() > b.stats->get_total_time_ms();
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const auto &it : stats_to_display) {
|
||||||
|
const char *source = it.name;
|
||||||
|
const ComponentRuntimeStats *stats = it.stats;
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", source,
|
||||||
|
stats->get_total_count(), stats->get_total_avg_time_ms(), stats->get_total_max_time_ms(),
|
||||||
|
stats->get_total_time_ms());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeStatsCollector::process_pending_stats(uint32_t current_time) {
|
||||||
|
if (this->next_log_time_ == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (current_time >= this->next_log_time_) {
|
||||||
|
this->log_stats_();
|
||||||
|
this->reset_stats_();
|
||||||
|
this->next_log_time_ = current_time + this->log_interval_;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace runtime_stats
|
||||||
|
|
||||||
|
runtime_stats::RuntimeStatsCollector *global_runtime_stats =
|
||||||
|
nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||||
|
|
||||||
|
} // namespace esphome
|
||||||
|
|
||||||
|
#endif // USE_RUNTIME_STATS
|
132
esphome/components/runtime_stats/runtime_stats.h
Normal file
132
esphome/components/runtime_stats/runtime_stats.h
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/core/defines.h"
|
||||||
|
|
||||||
|
#ifdef USE_RUNTIME_STATS
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <vector>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
|
||||||
|
class Component; // Forward declaration
|
||||||
|
|
||||||
|
namespace runtime_stats {
|
||||||
|
|
||||||
|
static const char *const TAG = "runtime_stats";
|
||||||
|
|
||||||
|
class ComponentRuntimeStats {
|
||||||
|
public:
|
||||||
|
ComponentRuntimeStats()
|
||||||
|
: period_count_(0),
|
||||||
|
period_time_ms_(0),
|
||||||
|
period_max_time_ms_(0),
|
||||||
|
total_count_(0),
|
||||||
|
total_time_ms_(0),
|
||||||
|
total_max_time_ms_(0) {}
|
||||||
|
|
||||||
|
void record_time(uint32_t duration_ms) {
|
||||||
|
// Update period counters
|
||||||
|
this->period_count_++;
|
||||||
|
this->period_time_ms_ += duration_ms;
|
||||||
|
if (duration_ms > this->period_max_time_ms_)
|
||||||
|
this->period_max_time_ms_ = duration_ms;
|
||||||
|
|
||||||
|
// Update total counters
|
||||||
|
this->total_count_++;
|
||||||
|
this->total_time_ms_ += duration_ms;
|
||||||
|
if (duration_ms > this->total_max_time_ms_)
|
||||||
|
this->total_max_time_ms_ = duration_ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset_period_stats() {
|
||||||
|
this->period_count_ = 0;
|
||||||
|
this->period_time_ms_ = 0;
|
||||||
|
this->period_max_time_ms_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Period stats (reset each logging interval)
|
||||||
|
uint32_t get_period_count() const { return this->period_count_; }
|
||||||
|
uint32_t get_period_time_ms() const { return this->period_time_ms_; }
|
||||||
|
uint32_t get_period_max_time_ms() const { return this->period_max_time_ms_; }
|
||||||
|
float get_period_avg_time_ms() const {
|
||||||
|
return this->period_count_ > 0 ? this->period_time_ms_ / static_cast<float>(this->period_count_) : 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Total stats (persistent until reboot)
|
||||||
|
uint32_t get_total_count() const { return this->total_count_; }
|
||||||
|
uint32_t get_total_time_ms() const { return this->total_time_ms_; }
|
||||||
|
uint32_t get_total_max_time_ms() const { return this->total_max_time_ms_; }
|
||||||
|
float get_total_avg_time_ms() const {
|
||||||
|
return this->total_count_ > 0 ? this->total_time_ms_ / static_cast<float>(this->total_count_) : 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// Period stats (reset each logging interval)
|
||||||
|
uint32_t period_count_;
|
||||||
|
uint32_t period_time_ms_;
|
||||||
|
uint32_t period_max_time_ms_;
|
||||||
|
|
||||||
|
// Total stats (persistent until reboot)
|
||||||
|
uint32_t total_count_;
|
||||||
|
uint32_t total_time_ms_;
|
||||||
|
uint32_t total_max_time_ms_;
|
||||||
|
};
|
||||||
|
|
||||||
|
// For sorting components by run time
|
||||||
|
struct ComponentStatPair {
|
||||||
|
const char *name;
|
||||||
|
const ComponentRuntimeStats *stats;
|
||||||
|
|
||||||
|
bool operator>(const ComponentStatPair &other) const {
|
||||||
|
// Sort by period time as that's what we're displaying in the logs
|
||||||
|
return stats->get_period_time_ms() > other.stats->get_period_time_ms();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class RuntimeStatsCollector {
|
||||||
|
public:
|
||||||
|
RuntimeStatsCollector();
|
||||||
|
|
||||||
|
void set_log_interval(uint32_t log_interval) { this->log_interval_ = log_interval; }
|
||||||
|
uint32_t get_log_interval() const { return this->log_interval_; }
|
||||||
|
|
||||||
|
void record_component_time(Component *component, uint32_t duration_ms, uint32_t current_time);
|
||||||
|
|
||||||
|
// Process any pending stats printing (should be called after component loop)
|
||||||
|
void process_pending_stats(uint32_t current_time);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void log_stats_();
|
||||||
|
|
||||||
|
void reset_stats_() {
|
||||||
|
for (auto &it : this->component_stats_) {
|
||||||
|
it.second.reset_period_stats();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use const char* keys for efficiency
|
||||||
|
// Custom comparator for const char* keys in map
|
||||||
|
// Without this, std::map would compare pointer addresses instead of string contents,
|
||||||
|
// causing identical component names at different addresses to be treated as different keys
|
||||||
|
struct CStrCompare {
|
||||||
|
bool operator()(const char *a, const char *b) const { return std::strcmp(a, b) < 0; }
|
||||||
|
};
|
||||||
|
std::map<const char *, ComponentRuntimeStats, CStrCompare> component_stats_;
|
||||||
|
std::map<Component *, const char *> component_names_cache_;
|
||||||
|
uint32_t log_interval_;
|
||||||
|
uint32_t next_log_time_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace runtime_stats
|
||||||
|
|
||||||
|
extern runtime_stats::RuntimeStatsCollector
|
||||||
|
*global_runtime_stats; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||||
|
|
||||||
|
} // namespace esphome
|
||||||
|
|
||||||
|
#endif // USE_RUNTIME_STATS
|
@ -1711,162 +1711,161 @@ std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_c
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
bool WebServer::canHandle(AsyncWebServerRequest *request) const {
|
bool WebServer::canHandle(AsyncWebServerRequest *request) const {
|
||||||
if (request->url() == "/")
|
const auto &url = request->url();
|
||||||
|
const auto method = request->method();
|
||||||
|
|
||||||
|
// Simple URL checks
|
||||||
|
if (url == "/")
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
#ifdef USE_ARDUINO
|
#ifdef USE_ARDUINO
|
||||||
if (request->url() == "/events") {
|
if (url == "/events")
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_WEBSERVER_CSS_INCLUDE
|
#ifdef USE_WEBSERVER_CSS_INCLUDE
|
||||||
if (request->url() == "/0.css")
|
if (url == "/0.css")
|
||||||
return true;
|
return true;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_WEBSERVER_JS_INCLUDE
|
#ifdef USE_WEBSERVER_JS_INCLUDE
|
||||||
if (request->url() == "/0.js")
|
if (url == "/0.js")
|
||||||
return true;
|
return true;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS
|
#ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS
|
||||||
if (request->method() == HTTP_OPTIONS && request->hasHeader(HEADER_CORS_REQ_PNA)) {
|
if (method == HTTP_OPTIONS && request->hasHeader(HEADER_CORS_REQ_PNA))
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Store the URL to prevent temporary string destruction
|
// Parse URL for component checks
|
||||||
// request->url() returns a reference to a String (on Arduino) or std::string (on ESP-IDF)
|
|
||||||
// UrlMatch stores pointers to the string's data, so we must ensure the string outlives match_url()
|
|
||||||
const auto &url = request->url();
|
|
||||||
UrlMatch match = match_url(url.c_str(), url.length(), true);
|
UrlMatch match = match_url(url.c_str(), url.length(), true);
|
||||||
if (!match.valid)
|
if (!match.valid)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
// Common pattern check
|
||||||
|
bool is_get = method == HTTP_GET;
|
||||||
|
bool is_post = method == HTTP_POST;
|
||||||
|
bool is_get_or_post = is_get || is_post;
|
||||||
|
|
||||||
|
if (!is_get_or_post)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// GET-only components
|
||||||
|
if (is_get) {
|
||||||
#ifdef USE_SENSOR
|
#ifdef USE_SENSOR
|
||||||
if (request->method() == HTTP_GET && match.domain_equals("sensor"))
|
if (match.domain_equals("sensor"))
|
||||||
return true;
|
return true;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_SWITCH
|
|
||||||
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("switch"))
|
|
||||||
return true;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef USE_BUTTON
|
|
||||||
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("button"))
|
|
||||||
return true;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef USE_BINARY_SENSOR
|
#ifdef USE_BINARY_SENSOR
|
||||||
if (request->method() == HTTP_GET && match.domain_equals("binary_sensor"))
|
if (match.domain_equals("binary_sensor"))
|
||||||
return true;
|
return true;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_FAN
|
|
||||||
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("fan"))
|
|
||||||
return true;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef USE_LIGHT
|
|
||||||
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("light"))
|
|
||||||
return true;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef USE_TEXT_SENSOR
|
#ifdef USE_TEXT_SENSOR
|
||||||
if (request->method() == HTTP_GET && match.domain_equals("text_sensor"))
|
if (match.domain_equals("text_sensor"))
|
||||||
return true;
|
return true;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_COVER
|
|
||||||
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("cover"))
|
|
||||||
return true;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef USE_NUMBER
|
|
||||||
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("number"))
|
|
||||||
return true;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef USE_DATETIME_DATE
|
|
||||||
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("date"))
|
|
||||||
return true;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef USE_DATETIME_TIME
|
|
||||||
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("time"))
|
|
||||||
return true;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef USE_DATETIME_DATETIME
|
|
||||||
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("datetime"))
|
|
||||||
return true;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef USE_TEXT
|
|
||||||
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("text"))
|
|
||||||
return true;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef USE_SELECT
|
|
||||||
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("select"))
|
|
||||||
return true;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef USE_CLIMATE
|
|
||||||
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("climate"))
|
|
||||||
return true;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef USE_LOCK
|
|
||||||
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("lock"))
|
|
||||||
return true;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef USE_VALVE
|
|
||||||
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("valve"))
|
|
||||||
return true;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef USE_ALARM_CONTROL_PANEL
|
|
||||||
if ((request->method() == HTTP_GET || request->method() == HTTP_POST) && match.domain_equals("alarm_control_panel"))
|
|
||||||
return true;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef USE_EVENT
|
#ifdef USE_EVENT
|
||||||
if (request->method() == HTTP_GET && match.domain_equals("event"))
|
if (match.domain_equals("event"))
|
||||||
return true;
|
return true;
|
||||||
#endif
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef USE_UPDATE
|
// GET+POST components
|
||||||
if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain_equals("update"))
|
if (is_get_or_post) {
|
||||||
return true;
|
#ifdef USE_SWITCH
|
||||||
|
if (match.domain_equals("switch"))
|
||||||
|
return true;
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef USE_BUTTON
|
||||||
|
if (match.domain_equals("button"))
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
#ifdef USE_FAN
|
||||||
|
if (match.domain_equals("fan"))
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
#ifdef USE_LIGHT
|
||||||
|
if (match.domain_equals("light"))
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
#ifdef USE_COVER
|
||||||
|
if (match.domain_equals("cover"))
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
#ifdef USE_NUMBER
|
||||||
|
if (match.domain_equals("number"))
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
#ifdef USE_DATETIME_DATE
|
||||||
|
if (match.domain_equals("date"))
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
#ifdef USE_DATETIME_TIME
|
||||||
|
if (match.domain_equals("time"))
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
#ifdef USE_DATETIME_DATETIME
|
||||||
|
if (match.domain_equals("datetime"))
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
#ifdef USE_TEXT
|
||||||
|
if (match.domain_equals("text"))
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
#ifdef USE_SELECT
|
||||||
|
if (match.domain_equals("select"))
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
#ifdef USE_CLIMATE
|
||||||
|
if (match.domain_equals("climate"))
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
#ifdef USE_LOCK
|
||||||
|
if (match.domain_equals("lock"))
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
#ifdef USE_VALVE
|
||||||
|
if (match.domain_equals("valve"))
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
#ifdef USE_ALARM_CONTROL_PANEL
|
||||||
|
if (match.domain_equals("alarm_control_panel"))
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
#ifdef USE_UPDATE
|
||||||
|
if (match.domain_equals("update"))
|
||||||
|
return true;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
void WebServer::handleRequest(AsyncWebServerRequest *request) {
|
void WebServer::handleRequest(AsyncWebServerRequest *request) {
|
||||||
if (request->url() == "/") {
|
const auto &url = request->url();
|
||||||
|
|
||||||
|
// Handle static routes first
|
||||||
|
if (url == "/") {
|
||||||
this->handle_index_request(request);
|
this->handle_index_request(request);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef USE_ARDUINO
|
#ifdef USE_ARDUINO
|
||||||
if (request->url() == "/events") {
|
if (url == "/events") {
|
||||||
this->events_.add_new_client(this, request);
|
this->events_.add_new_client(this, request);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_WEBSERVER_CSS_INCLUDE
|
#ifdef USE_WEBSERVER_CSS_INCLUDE
|
||||||
if (request->url() == "/0.css") {
|
if (url == "/0.css") {
|
||||||
this->handle_css_request(request);
|
this->handle_css_request(request);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_WEBSERVER_JS_INCLUDE
|
#ifdef USE_WEBSERVER_JS_INCLUDE
|
||||||
if (request->url() == "/0.js") {
|
if (url == "/0.js") {
|
||||||
this->handle_js_request(request);
|
this->handle_js_request(request);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1879,147 +1878,85 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// See comment in canHandle() for why we store the URL reference
|
// Parse URL for component routing
|
||||||
const auto &url = request->url();
|
|
||||||
UrlMatch match = match_url(url.c_str(), url.length(), false);
|
UrlMatch match = match_url(url.c_str(), url.length(), false);
|
||||||
|
|
||||||
|
// Component routing using minimal code repetition
|
||||||
|
struct ComponentRoute {
|
||||||
|
const char *domain;
|
||||||
|
void (WebServer::*handler)(AsyncWebServerRequest *, const UrlMatch &);
|
||||||
|
};
|
||||||
|
|
||||||
|
static const ComponentRoute routes[] = {
|
||||||
#ifdef USE_SENSOR
|
#ifdef USE_SENSOR
|
||||||
if (match.domain_equals("sensor")) {
|
{"sensor", &WebServer::handle_sensor_request},
|
||||||
this->handle_sensor_request(request, match);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_SWITCH
|
#ifdef USE_SWITCH
|
||||||
if (match.domain_equals("switch")) {
|
{"switch", &WebServer::handle_switch_request},
|
||||||
this->handle_switch_request(request, match);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_BUTTON
|
#ifdef USE_BUTTON
|
||||||
if (match.domain_equals("button")) {
|
{"button", &WebServer::handle_button_request},
|
||||||
this->handle_button_request(request, match);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_BINARY_SENSOR
|
#ifdef USE_BINARY_SENSOR
|
||||||
if (match.domain_equals("binary_sensor")) {
|
{"binary_sensor", &WebServer::handle_binary_sensor_request},
|
||||||
this->handle_binary_sensor_request(request, match);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_FAN
|
#ifdef USE_FAN
|
||||||
if (match.domain_equals("fan")) {
|
{"fan", &WebServer::handle_fan_request},
|
||||||
this->handle_fan_request(request, match);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_LIGHT
|
#ifdef USE_LIGHT
|
||||||
if (match.domain_equals("light")) {
|
{"light", &WebServer::handle_light_request},
|
||||||
this->handle_light_request(request, match);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_TEXT_SENSOR
|
#ifdef USE_TEXT_SENSOR
|
||||||
if (match.domain_equals("text_sensor")) {
|
{"text_sensor", &WebServer::handle_text_sensor_request},
|
||||||
this->handle_text_sensor_request(request, match);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_COVER
|
#ifdef USE_COVER
|
||||||
if (match.domain_equals("cover")) {
|
{"cover", &WebServer::handle_cover_request},
|
||||||
this->handle_cover_request(request, match);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_NUMBER
|
#ifdef USE_NUMBER
|
||||||
if (match.domain_equals("number")) {
|
{"number", &WebServer::handle_number_request},
|
||||||
this->handle_number_request(request, match);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_DATETIME_DATE
|
#ifdef USE_DATETIME_DATE
|
||||||
if (match.domain_equals("date")) {
|
{"date", &WebServer::handle_date_request},
|
||||||
this->handle_date_request(request, match);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_DATETIME_TIME
|
#ifdef USE_DATETIME_TIME
|
||||||
if (match.domain_equals("time")) {
|
{"time", &WebServer::handle_time_request},
|
||||||
this->handle_time_request(request, match);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_DATETIME_DATETIME
|
#ifdef USE_DATETIME_DATETIME
|
||||||
if (match.domain_equals("datetime")) {
|
{"datetime", &WebServer::handle_datetime_request},
|
||||||
this->handle_datetime_request(request, match);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_TEXT
|
#ifdef USE_TEXT
|
||||||
if (match.domain_equals("text")) {
|
{"text", &WebServer::handle_text_request},
|
||||||
this->handle_text_request(request, match);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_SELECT
|
#ifdef USE_SELECT
|
||||||
if (match.domain_equals("select")) {
|
{"select", &WebServer::handle_select_request},
|
||||||
this->handle_select_request(request, match);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_CLIMATE
|
#ifdef USE_CLIMATE
|
||||||
if (match.domain_equals("climate")) {
|
{"climate", &WebServer::handle_climate_request},
|
||||||
this->handle_climate_request(request, match);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_LOCK
|
#ifdef USE_LOCK
|
||||||
if (match.domain_equals("lock")) {
|
{"lock", &WebServer::handle_lock_request},
|
||||||
this->handle_lock_request(request, match);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_VALVE
|
#ifdef USE_VALVE
|
||||||
if (match.domain_equals("valve")) {
|
{"valve", &WebServer::handle_valve_request},
|
||||||
this->handle_valve_request(request, match);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_ALARM_CONTROL_PANEL
|
#ifdef USE_ALARM_CONTROL_PANEL
|
||||||
if (match.domain_equals("alarm_control_panel")) {
|
{"alarm_control_panel", &WebServer::handle_alarm_control_panel_request},
|
||||||
this->handle_alarm_control_panel_request(request, match);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_UPDATE
|
#ifdef USE_UPDATE
|
||||||
if (match.domain_equals("update")) {
|
{"update", &WebServer::handle_update_request},
|
||||||
this->handle_update_request(request, match);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check each route
|
||||||
|
for (const auto &route : routes) {
|
||||||
|
if (match.domain_equals(route.domain)) {
|
||||||
|
(this->*route.handler)(request, match);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// No matching handler found - send 404
|
// No matching handler found - send 404
|
||||||
ESP_LOGV(TAG, "Request for unknown URL: %s", request->url().c_str());
|
ESP_LOGV(TAG, "Request for unknown URL: %s", url.c_str());
|
||||||
request->send(404, "text/plain", "Not Found");
|
request->send(404, "text/plain", "Not Found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,4 +40,7 @@ async def to_code(config):
|
|||||||
if CORE.is_esp8266:
|
if CORE.is_esp8266:
|
||||||
cg.add_library("ESP8266WiFi", None)
|
cg.add_library("ESP8266WiFi", None)
|
||||||
# https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json
|
# https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json
|
||||||
cg.add_library("ESP32Async/ESPAsyncWebServer", "3.7.8")
|
# Use fork with libretiny compatibility fix
|
||||||
|
cg.add_library(
|
||||||
|
"https://github.com/bdraco/ESPAsyncWebServer.git#libretiny_Fix", None
|
||||||
|
)
|
||||||
|
@ -4,6 +4,9 @@
|
|||||||
#include "esphome/core/hal.h"
|
#include "esphome/core/hal.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <ranges>
|
#include <ranges>
|
||||||
|
#ifdef USE_RUNTIME_STATS
|
||||||
|
#include "esphome/components/runtime_stats/runtime_stats.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef USE_STATUS_LED
|
#ifdef USE_STATUS_LED
|
||||||
#include "esphome/components/status_led/status_led.h"
|
#include "esphome/components/status_led/status_led.h"
|
||||||
@ -141,6 +144,14 @@ void Application::loop() {
|
|||||||
this->in_loop_ = false;
|
this->in_loop_ = false;
|
||||||
this->app_state_ = new_app_state;
|
this->app_state_ = new_app_state;
|
||||||
|
|
||||||
|
#ifdef USE_RUNTIME_STATS
|
||||||
|
// Process any pending runtime stats printing after all components have run
|
||||||
|
// This ensures stats printing doesn't affect component timing measurements
|
||||||
|
if (global_runtime_stats != nullptr) {
|
||||||
|
global_runtime_stats->process_pending_stats(last_op_end_time);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// Use the last component's end time instead of calling millis() again
|
// Use the last component's end time instead of calling millis() again
|
||||||
auto elapsed = last_op_end_time - this->last_loop_;
|
auto elapsed = last_op_end_time - this->last_loop_;
|
||||||
if (elapsed >= this->loop_interval_ || HighFrequencyLoopRequester::is_high_frequency()) {
|
if (elapsed >= this->loop_interval_ || HighFrequencyLoopRequester::is_high_frequency()) {
|
||||||
|
@ -9,6 +9,9 @@
|
|||||||
#include "esphome/core/hal.h"
|
#include "esphome/core/hal.h"
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
#ifdef USE_RUNTIME_STATS
|
||||||
|
#include "esphome/components/runtime_stats/runtime_stats.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
|
|
||||||
@ -395,6 +398,13 @@ uint32_t WarnIfComponentBlockingGuard::finish() {
|
|||||||
uint32_t curr_time = millis();
|
uint32_t curr_time = millis();
|
||||||
|
|
||||||
uint32_t blocking_time = curr_time - this->started_;
|
uint32_t blocking_time = curr_time - this->started_;
|
||||||
|
|
||||||
|
#ifdef USE_RUNTIME_STATS
|
||||||
|
// Record component runtime stats
|
||||||
|
if (global_runtime_stats != nullptr) {
|
||||||
|
global_runtime_stats->record_component_time(this->component_, blocking_time, curr_time);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
bool should_warn;
|
bool should_warn;
|
||||||
if (this->component_ != nullptr) {
|
if (this->component_ != nullptr) {
|
||||||
should_warn = this->component_->should_warn_of_blocking(blocking_time);
|
should_warn = this->component_->should_warn_of_blocking(blocking_time);
|
||||||
|
@ -288,6 +288,37 @@ class TypeInfo(ABC):
|
|||||||
|
|
||||||
TYPE_INFO: dict[int, TypeInfo] = {}
|
TYPE_INFO: dict[int, TypeInfo] = {}
|
||||||
|
|
||||||
|
# Unsupported 64-bit types that would add overhead for embedded systems
|
||||||
|
# TYPE_DOUBLE = 1, TYPE_FIXED64 = 6, TYPE_SFIXED64 = 16, TYPE_SINT64 = 18
|
||||||
|
UNSUPPORTED_TYPES = {1: "double", 6: "fixed64", 16: "sfixed64", 18: "sint64"}
|
||||||
|
|
||||||
|
|
||||||
|
def validate_field_type(field_type: int, field_name: str = "") -> None:
|
||||||
|
"""Validate that the field type is supported by ESPHome API.
|
||||||
|
|
||||||
|
Raises ValueError for unsupported 64-bit types.
|
||||||
|
"""
|
||||||
|
if field_type in UNSUPPORTED_TYPES:
|
||||||
|
type_name = UNSUPPORTED_TYPES[field_type]
|
||||||
|
field_info = f" (field: {field_name})" if field_name else ""
|
||||||
|
raise ValueError(
|
||||||
|
f"64-bit type '{type_name}'{field_info} is not supported by ESPHome API. "
|
||||||
|
"These types add significant overhead for embedded systems. "
|
||||||
|
"If you need 64-bit support, please add the necessary encoding/decoding "
|
||||||
|
"functions to proto.h/proto.cpp first."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_type_info_for_field(field: descriptor.FieldDescriptorProto) -> TypeInfo:
|
||||||
|
"""Get the appropriate TypeInfo for a field, handling repeated fields.
|
||||||
|
|
||||||
|
Also validates that the field type is supported.
|
||||||
|
"""
|
||||||
|
if field.label == 3: # repeated
|
||||||
|
return RepeatedTypeInfo(field)
|
||||||
|
validate_field_type(field.type, field.name)
|
||||||
|
return TYPE_INFO[field.type](field)
|
||||||
|
|
||||||
|
|
||||||
def register_type(name: int):
|
def register_type(name: int):
|
||||||
"""Decorator to register a type with a name and number."""
|
"""Decorator to register a type with a name and number."""
|
||||||
@ -707,6 +738,7 @@ class SInt64Type(TypeInfo):
|
|||||||
class RepeatedTypeInfo(TypeInfo):
|
class RepeatedTypeInfo(TypeInfo):
|
||||||
def __init__(self, field: descriptor.FieldDescriptorProto) -> None:
|
def __init__(self, field: descriptor.FieldDescriptorProto) -> None:
|
||||||
super().__init__(field)
|
super().__init__(field)
|
||||||
|
validate_field_type(field.type, field.name)
|
||||||
self._ti: TypeInfo = TYPE_INFO[field.type](field)
|
self._ti: TypeInfo = TYPE_INFO[field.type](field)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -971,10 +1003,7 @@ def calculate_message_estimated_size(desc: descriptor.DescriptorProto) -> int:
|
|||||||
total_size = 0
|
total_size = 0
|
||||||
|
|
||||||
for field in desc.field:
|
for field in desc.field:
|
||||||
if field.label == 3: # repeated
|
ti = get_type_info_for_field(field)
|
||||||
ti = RepeatedTypeInfo(field)
|
|
||||||
else:
|
|
||||||
ti = TYPE_INFO[field.type](field)
|
|
||||||
|
|
||||||
# Add estimated size for this field
|
# Add estimated size for this field
|
||||||
total_size += ti.get_estimated_size()
|
total_size += ti.get_estimated_size()
|
||||||
@ -1284,10 +1313,7 @@ def build_base_class(
|
|||||||
# For base classes, we only declare the fields but don't handle encode/decode
|
# For base classes, we only declare the fields but don't handle encode/decode
|
||||||
# The derived classes will handle encoding/decoding with their specific field numbers
|
# The derived classes will handle encoding/decoding with their specific field numbers
|
||||||
for field in common_fields:
|
for field in common_fields:
|
||||||
if field.label == 3: # repeated
|
ti = get_type_info_for_field(field)
|
||||||
ti = RepeatedTypeInfo(field)
|
|
||||||
else:
|
|
||||||
ti = TYPE_INFO[field.type](field)
|
|
||||||
|
|
||||||
# Only add field declarations, not encode/decode logic
|
# Only add field declarations, not encode/decode logic
|
||||||
protected_content.extend(ti.protected_content)
|
protected_content.extend(ti.protected_content)
|
||||||
|
69
tests/component_tests/gpio/test_gpio_binary_sensor.py
Normal file
69
tests/component_tests/gpio/test_gpio_binary_sensor.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
"""Tests for the GPIO binary sensor component."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
def test_gpio_binary_sensor_basic_setup(
|
||||||
|
generate_main: Callable[[str | Path], str],
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
When the GPIO binary sensor is set in the yaml file, it should be registered in main
|
||||||
|
"""
|
||||||
|
main_cpp = generate_main("tests/component_tests/gpio/test_gpio_binary_sensor.yaml")
|
||||||
|
|
||||||
|
assert "new gpio::GPIOBinarySensor();" in main_cpp
|
||||||
|
assert "App.register_binary_sensor" in main_cpp
|
||||||
|
assert "bs_gpio->set_use_interrupt(true);" in main_cpp
|
||||||
|
assert "bs_gpio->set_interrupt_type(gpio::INTERRUPT_ANY_EDGE);" in main_cpp
|
||||||
|
|
||||||
|
|
||||||
|
def test_gpio_binary_sensor_esp8266_gpio16_disables_interrupt(
|
||||||
|
generate_main: Callable[[str | Path], str],
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Test that ESP8266 GPIO16 automatically disables interrupt mode with a warning
|
||||||
|
"""
|
||||||
|
main_cpp = generate_main(
|
||||||
|
"tests/component_tests/gpio/test_gpio_binary_sensor_esp8266.yaml"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check that interrupt is disabled for GPIO16
|
||||||
|
assert "bs_gpio16->set_use_interrupt(false);" in main_cpp
|
||||||
|
|
||||||
|
# Check that the warning was logged
|
||||||
|
assert "GPIO16 on ESP8266 doesn't support interrupts" in caplog.text
|
||||||
|
assert "Falling back to polling mode" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_gpio_binary_sensor_esp8266_other_pins_use_interrupt(
|
||||||
|
generate_main: Callable[[str | Path], str],
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Test that ESP8266 pins other than GPIO16 still use interrupt mode
|
||||||
|
"""
|
||||||
|
main_cpp = generate_main(
|
||||||
|
"tests/component_tests/gpio/test_gpio_binary_sensor_esp8266.yaml"
|
||||||
|
)
|
||||||
|
|
||||||
|
# GPIO5 should still use interrupts
|
||||||
|
assert "bs_gpio5->set_use_interrupt(true);" in main_cpp
|
||||||
|
assert "bs_gpio5->set_interrupt_type(gpio::INTERRUPT_ANY_EDGE);" in main_cpp
|
||||||
|
|
||||||
|
|
||||||
|
def test_gpio_binary_sensor_explicit_polling_mode(
|
||||||
|
generate_main: Callable[[str | Path], str],
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Test that explicitly setting use_interrupt: false works
|
||||||
|
"""
|
||||||
|
main_cpp = generate_main(
|
||||||
|
"tests/component_tests/gpio/test_gpio_binary_sensor_polling.yaml"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "bs_polling->set_use_interrupt(false);" in main_cpp
|
11
tests/component_tests/gpio/test_gpio_binary_sensor.yaml
Normal file
11
tests/component_tests/gpio/test_gpio_binary_sensor.yaml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
esphome:
|
||||||
|
name: test
|
||||||
|
|
||||||
|
esp32:
|
||||||
|
board: esp32dev
|
||||||
|
|
||||||
|
binary_sensor:
|
||||||
|
- platform: gpio
|
||||||
|
pin: 5
|
||||||
|
name: "Test GPIO Binary Sensor"
|
||||||
|
id: bs_gpio
|
@ -0,0 +1,20 @@
|
|||||||
|
esphome:
|
||||||
|
name: test
|
||||||
|
|
||||||
|
esp8266:
|
||||||
|
board: d1_mini
|
||||||
|
|
||||||
|
binary_sensor:
|
||||||
|
- platform: gpio
|
||||||
|
pin:
|
||||||
|
number: 16
|
||||||
|
mode: INPUT_PULLDOWN_16
|
||||||
|
name: "GPIO16 Touch Sensor"
|
||||||
|
id: bs_gpio16
|
||||||
|
|
||||||
|
- platform: gpio
|
||||||
|
pin:
|
||||||
|
number: 5
|
||||||
|
mode: INPUT_PULLUP
|
||||||
|
name: "GPIO5 Button"
|
||||||
|
id: bs_gpio5
|
@ -0,0 +1,12 @@
|
|||||||
|
esphome:
|
||||||
|
name: test
|
||||||
|
|
||||||
|
esp32:
|
||||||
|
board: esp32dev
|
||||||
|
|
||||||
|
binary_sensor:
|
||||||
|
- platform: gpio
|
||||||
|
pin: 5
|
||||||
|
name: "Polling Mode Sensor"
|
||||||
|
id: bs_polling
|
||||||
|
use_interrupt: false
|
@ -919,21 +919,21 @@ lvgl:
|
|||||||
text_color: 0xFFFFFF
|
text_color: 0xFFFFFF
|
||||||
scales:
|
scales:
|
||||||
- ticks:
|
- ticks:
|
||||||
width: 1
|
width: !lambda return 1;
|
||||||
count: 61
|
count: 61
|
||||||
length: 20
|
length: 20%
|
||||||
color: 0xFFFFFF
|
color: 0xFFFFFF
|
||||||
range_from: 0
|
range_from: 0
|
||||||
range_to: 60
|
range_to: 60
|
||||||
angle_range: 360
|
angle_range: 360
|
||||||
rotation: 270
|
rotation: !lambda return 2700;
|
||||||
indicators:
|
indicators:
|
||||||
- line:
|
- line:
|
||||||
opa: 50%
|
opa: 50%
|
||||||
id: minute_hand
|
id: minute_hand
|
||||||
color: 0xFF0000
|
color: 0xFF0000
|
||||||
r_mod: -1
|
r_mod: !lambda return -1;
|
||||||
width: 3
|
width: !lambda return 3;
|
||||||
-
|
-
|
||||||
angle_range: 330
|
angle_range: 330
|
||||||
rotation: 300
|
rotation: 300
|
||||||
|
2
tests/components/runtime_stats/common.yaml
Normal file
2
tests/components/runtime_stats/common.yaml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# Test runtime_stats component with default configuration
|
||||||
|
runtime_stats:
|
1
tests/components/runtime_stats/test.esp32-ard.yaml
Normal file
1
tests/components/runtime_stats/test.esp32-ard.yaml
Normal file
@ -0,0 +1 @@
|
|||||||
|
<<: !include common.yaml
|
14
tests/integration/fixtures/host_mode_api_password.yaml
Normal file
14
tests/integration/fixtures/host_mode_api_password.yaml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
esphome:
|
||||||
|
name: host-mode-api-password
|
||||||
|
host:
|
||||||
|
api:
|
||||||
|
password: "test_password_123"
|
||||||
|
logger:
|
||||||
|
level: DEBUG
|
||||||
|
# Test sensor to verify connection works
|
||||||
|
sensor:
|
||||||
|
- platform: template
|
||||||
|
name: Test Sensor
|
||||||
|
id: test_sensor
|
||||||
|
lambda: return 42.0;
|
||||||
|
update_interval: 0.1s
|
39
tests/integration/fixtures/runtime_stats.yaml
Normal file
39
tests/integration/fixtures/runtime_stats.yaml
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
esphome:
|
||||||
|
name: runtime-stats-test
|
||||||
|
|
||||||
|
host:
|
||||||
|
|
||||||
|
api:
|
||||||
|
|
||||||
|
logger:
|
||||||
|
level: DEBUG
|
||||||
|
logs:
|
||||||
|
runtime_stats: INFO
|
||||||
|
|
||||||
|
runtime_stats:
|
||||||
|
log_interval: 1s
|
||||||
|
|
||||||
|
# Add some components that will execute periodically to generate stats
|
||||||
|
sensor:
|
||||||
|
- platform: template
|
||||||
|
name: "Test Sensor 1"
|
||||||
|
id: test_sensor_1
|
||||||
|
lambda: return 42.0;
|
||||||
|
update_interval: 0.1s
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "Test Sensor 2"
|
||||||
|
id: test_sensor_2
|
||||||
|
lambda: return 24.0;
|
||||||
|
update_interval: 0.2s
|
||||||
|
|
||||||
|
switch:
|
||||||
|
- platform: template
|
||||||
|
name: "Test Switch"
|
||||||
|
id: test_switch
|
||||||
|
optimistic: true
|
||||||
|
|
||||||
|
interval:
|
||||||
|
- interval: 0.5s
|
||||||
|
then:
|
||||||
|
- switch.toggle: test_switch
|
53
tests/integration/test_host_mode_api_password.py
Normal file
53
tests/integration/test_host_mode_api_password.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
"""Integration test for API password authentication."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from aioesphomeapi import APIConnectionError
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_host_mode_api_password(
|
||||||
|
yaml_config: str,
|
||||||
|
run_compiled: RunCompiledFunction,
|
||||||
|
api_client_connected: APIClientConnectedFactory,
|
||||||
|
) -> None:
|
||||||
|
"""Test API authentication with password."""
|
||||||
|
async with run_compiled(yaml_config):
|
||||||
|
# Connect with correct password
|
||||||
|
async with api_client_connected(password="test_password_123") as client:
|
||||||
|
# Verify we can get device info
|
||||||
|
device_info = await client.device_info()
|
||||||
|
assert device_info is not None
|
||||||
|
assert device_info.uses_password is True
|
||||||
|
assert device_info.name == "host-mode-api-password"
|
||||||
|
|
||||||
|
# Subscribe to states to ensure authenticated connection works
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
state_future: asyncio.Future[bool] = loop.create_future()
|
||||||
|
states = {}
|
||||||
|
|
||||||
|
def on_state(state):
|
||||||
|
states[state.key] = state
|
||||||
|
if not state_future.done():
|
||||||
|
state_future.set_result(True)
|
||||||
|
|
||||||
|
client.subscribe_states(on_state)
|
||||||
|
|
||||||
|
# Wait for at least one state with timeout
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(state_future, timeout=5.0)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
pytest.fail("No states received within timeout")
|
||||||
|
|
||||||
|
# Should have received at least one state (the test sensor)
|
||||||
|
assert len(states) > 0
|
||||||
|
|
||||||
|
# Test with wrong password - should fail
|
||||||
|
with pytest.raises(APIConnectionError, match="Invalid password"):
|
||||||
|
async with api_client_connected(password="wrong_password"):
|
||||||
|
pass # Should not reach here
|
88
tests/integration/test_runtime_stats.py
Normal file
88
tests/integration/test_runtime_stats.py
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
"""Test runtime statistics component."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import re
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_runtime_stats(
|
||||||
|
yaml_config: str,
|
||||||
|
run_compiled: RunCompiledFunction,
|
||||||
|
api_client_connected: APIClientConnectedFactory,
|
||||||
|
) -> None:
|
||||||
|
"""Test runtime stats logs statistics at configured interval and tracks components."""
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
|
||||||
|
# Track how many times we see the total stats
|
||||||
|
stats_count = 0
|
||||||
|
first_stats_future = loop.create_future()
|
||||||
|
second_stats_future = loop.create_future()
|
||||||
|
|
||||||
|
# Track component stats
|
||||||
|
component_stats_found = set()
|
||||||
|
|
||||||
|
# Patterns to match - need to handle ANSI color codes and timestamps
|
||||||
|
# The log format is: [HH:MM:SS][color codes][I][tag]: message
|
||||||
|
total_stats_pattern = re.compile(r"Total stats \(since boot\):")
|
||||||
|
# Match component names that may include dots (e.g., template.sensor)
|
||||||
|
component_pattern = re.compile(
|
||||||
|
r"^\[[^\]]+\].*?\s+([\w.]+):\s+count=(\d+),\s+avg=([\d.]+)ms"
|
||||||
|
)
|
||||||
|
|
||||||
|
def check_output(line: str) -> None:
|
||||||
|
"""Check log output for runtime stats messages."""
|
||||||
|
nonlocal stats_count
|
||||||
|
|
||||||
|
# Check for total stats line
|
||||||
|
if total_stats_pattern.search(line):
|
||||||
|
stats_count += 1
|
||||||
|
|
||||||
|
if stats_count == 1 and not first_stats_future.done():
|
||||||
|
first_stats_future.set_result(True)
|
||||||
|
elif stats_count == 2 and not second_stats_future.done():
|
||||||
|
second_stats_future.set_result(True)
|
||||||
|
|
||||||
|
# Check for component stats
|
||||||
|
match = component_pattern.match(line)
|
||||||
|
if match:
|
||||||
|
component_name = match.group(1)
|
||||||
|
component_stats_found.add(component_name)
|
||||||
|
|
||||||
|
async with (
|
||||||
|
run_compiled(yaml_config, line_callback=check_output),
|
||||||
|
api_client_connected() as client,
|
||||||
|
):
|
||||||
|
# Verify device is connected
|
||||||
|
device_info = await client.device_info()
|
||||||
|
assert device_info is not None
|
||||||
|
|
||||||
|
# Wait for first "Total stats" log (should happen at 1s)
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(first_stats_future, timeout=5.0)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
pytest.fail("First 'Total stats' log not seen within 5 seconds")
|
||||||
|
|
||||||
|
# Wait for second "Total stats" log (should happen at 2s)
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(second_stats_future, timeout=5.0)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
pytest.fail(f"Second 'Total stats' log not seen. Total seen: {stats_count}")
|
||||||
|
|
||||||
|
# Verify we got at least 2 stats logs
|
||||||
|
assert stats_count >= 2, (
|
||||||
|
f"Expected at least 2 'Total stats' logs, got {stats_count}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify we found stats for our components
|
||||||
|
assert "template.sensor" in component_stats_found, (
|
||||||
|
f"Expected template.sensor stats, found: {component_stats_found}"
|
||||||
|
)
|
||||||
|
assert "template.switch" in component_stats_found, (
|
||||||
|
f"Expected template.switch stats, found: {component_stats_found}"
|
||||||
|
)
|
Loading…
x
Reference in New Issue
Block a user