1
0
mirror of https://github.com/esphome/esphome.git synced 2025-09-26 15:12:21 +01:00

Merge branch 'dev' into auth_connection_checks_dry

This commit is contained in:
J. Nick Koston
2025-09-23 17:58:45 -05:00
committed by GitHub
31 changed files with 641 additions and 1003 deletions

View File

@@ -731,6 +731,16 @@ def command_clean_mqtt(args: ArgsProtocol, config: ConfigType) -> int | None:
return clean_mqtt(config, args) return clean_mqtt(config, args)
def command_clean_platform(args: ArgsProtocol, config: ConfigType) -> int | None:
try:
writer.clean_platform()
except OSError as err:
_LOGGER.error("Error deleting platform files: %s", err)
return 1
_LOGGER.info("Done!")
return 0
def command_mqtt_fingerprint(args: ArgsProtocol, config: ConfigType) -> int | None: def command_mqtt_fingerprint(args: ArgsProtocol, config: ConfigType) -> int | None:
from esphome import mqtt from esphome import mqtt
@@ -929,9 +939,10 @@ POST_CONFIG_ACTIONS = {
"upload": command_upload, "upload": command_upload,
"logs": command_logs, "logs": command_logs,
"run": command_run, "run": command_run,
"clean-mqtt": command_clean_mqtt,
"mqtt-fingerprint": command_mqtt_fingerprint,
"clean": command_clean, "clean": command_clean,
"clean-mqtt": command_clean_mqtt,
"clean-platform": command_clean_platform,
"mqtt-fingerprint": command_mqtt_fingerprint,
"idedata": command_idedata, "idedata": command_idedata,
"rename": command_rename, "rename": command_rename,
"discover": command_discover, "discover": command_discover,
@@ -940,6 +951,7 @@ POST_CONFIG_ACTIONS = {
SIMPLE_CONFIG_ACTIONS = [ SIMPLE_CONFIG_ACTIONS = [
"clean", "clean",
"clean-mqtt", "clean-mqtt",
"clean-platform",
"config", "config",
] ]
@@ -1144,6 +1156,13 @@ def parse_args(argv):
"configuration", help="Your YAML configuration file(s).", nargs="+" "configuration", help="Your YAML configuration file(s).", nargs="+"
) )
parser_clean = subparsers.add_parser(
"clean-platform", help="Delete all platform files."
)
parser_clean.add_argument(
"configuration", help="Your YAML configuration file(s).", nargs="+"
)
parser_dashboard = subparsers.add_parser( parser_dashboard = subparsers.add_parser(
"dashboard", help="Create a simple web server for a dashboard." "dashboard", help="Create a simple web server for a dashboard."
) )

View File

@@ -102,7 +102,7 @@ message HelloRequest {
// For example "Home Assistant" // For example "Home Assistant"
// Not strictly necessary to send but nice for debugging // Not strictly necessary to send but nice for debugging
// purposes. // purposes.
string client_info = 1; string client_info = 1 [(pointer_to_buffer) = true];
uint32 api_version_major = 2; uint32 api_version_major = 2;
uint32 api_version_minor = 3; uint32 api_version_minor = 3;
} }
@@ -139,7 +139,7 @@ message AuthenticationRequest {
option (ifdef) = "USE_API_PASSWORD"; option (ifdef) = "USE_API_PASSWORD";
// The password to log in with // The password to log in with
string password = 1; string password = 1 [(pointer_to_buffer) = true];
} }
// Confirmation of successful connection. After this the connection is available for all traffic. // Confirmation of successful connection. After this the connection is available for all traffic.
@@ -769,7 +769,7 @@ message HomeassistantServiceMap {
string value = 2 [(no_zero_copy) = true]; string value = 2 [(no_zero_copy) = true];
} }
message HomeassistantServiceResponse { message HomeassistantActionRequest {
option (id) = 35; option (id) = 35;
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (no_delay) = true; option (no_delay) = true;
@@ -824,7 +824,7 @@ message GetTimeResponse {
option (no_delay) = true; option (no_delay) = true;
fixed32 epoch_seconds = 1; fixed32 epoch_seconds = 1;
string timezone = 2; string timezone = 2 [(pointer_to_buffer) = true];
} }
// ==================== USER-DEFINES SERVICES ==================== // ==================== USER-DEFINES SERVICES ====================
@@ -1465,7 +1465,7 @@ message BluetoothDeviceRequest {
uint64 address = 1; uint64 address = 1;
BluetoothDeviceRequestType request_type = 2; BluetoothDeviceRequestType request_type = 2;
bool has_address_type = 3; bool has_address_type = 3; // Deprecated, should be removed in 2027.8 - https://github.com/esphome/esphome/pull/10318
uint32 address_type = 4; uint32 address_type = 4;
} }
@@ -1571,7 +1571,7 @@ message BluetoothGATTWriteRequest {
uint32 handle = 2; uint32 handle = 2;
bool response = 3; bool response = 3;
bytes data = 4; bytes data = 4 [(pointer_to_buffer) = true];
} }
message BluetoothGATTReadDescriptorRequest { message BluetoothGATTReadDescriptorRequest {
@@ -1591,7 +1591,7 @@ message BluetoothGATTWriteDescriptorRequest {
uint64 address = 1; uint64 address = 1;
uint32 handle = 2; uint32 handle = 2;
bytes data = 3; bytes data = 3 [(pointer_to_buffer) = true];
} }
message BluetoothGATTNotifyRequest { message BluetoothGATTNotifyRequest {
@@ -2292,7 +2292,7 @@ message ZWaveProxyFrame {
option (ifdef) = "USE_ZWAVE_PROXY"; option (ifdef) = "USE_ZWAVE_PROXY";
option (no_delay) = true; option (no_delay) = true;
bytes data = 1 [(fixed_array_size) = 257]; bytes data = 1 [(pointer_to_buffer) = true];
} }
enum ZWaveProxyRequestType { enum ZWaveProxyRequestType {

View File

@@ -1078,8 +1078,14 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) {
if (homeassistant::global_homeassistant_time != nullptr) { if (homeassistant::global_homeassistant_time != nullptr) {
homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds); homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds);
#ifdef USE_TIME_TIMEZONE #ifdef USE_TIME_TIMEZONE
if (!value.timezone.empty() && value.timezone != homeassistant::global_homeassistant_time->get_timezone()) { if (value.timezone_len > 0) {
homeassistant::global_homeassistant_time->set_timezone(value.timezone); const std::string &current_tz = homeassistant::global_homeassistant_time->get_timezone();
// Compare without allocating a string
if (current_tz.length() != value.timezone_len ||
memcmp(current_tz.c_str(), value.timezone, value.timezone_len) != 0) {
homeassistant::global_homeassistant_time->set_timezone(
std::string(reinterpret_cast<const char *>(value.timezone), value.timezone_len));
}
} }
#endif #endif
} }
@@ -1374,7 +1380,7 @@ void APIConnection::complete_authentication_() {
} }
bool APIConnection::send_hello_response(const HelloRequest &msg) { bool APIConnection::send_hello_response(const HelloRequest &msg) {
this->client_info_.name = msg.client_info; this->client_info_.name.assign(reinterpret_cast<const char *>(msg.client_info), msg.client_info_len);
this->client_info_.peername = this->helper_->getpeername(); this->client_info_.peername = this->helper_->getpeername();
this->client_api_version_major_ = msg.api_version_major; this->client_api_version_major_ = msg.api_version_major;
this->client_api_version_minor_ = msg.api_version_minor; this->client_api_version_minor_ = msg.api_version_minor;
@@ -1402,7 +1408,7 @@ bool APIConnection::send_hello_response(const HelloRequest &msg) {
bool APIConnection::send_authenticate_response(const AuthenticationRequest &msg) { bool APIConnection::send_authenticate_response(const AuthenticationRequest &msg) {
AuthenticationResponse resp; AuthenticationResponse resp;
// bool invalid_password = 1; // bool invalid_password = 1;
resp.invalid_password = !this->parent_->check_password(msg.password); resp.invalid_password = !this->parent_->check_password(msg.password, msg.password_len);
if (!resp.invalid_password) { if (!resp.invalid_password) {
this->complete_authentication_(); this->complete_authentication_();
} }

View File

@@ -10,8 +10,8 @@
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/entity_base.h" #include "esphome/core/entity_base.h"
#include <vector>
#include <functional> #include <functional>
#include <vector>
namespace esphome::api { namespace esphome::api {
@@ -132,10 +132,10 @@ class APIConnection final : public APIServerConnection {
#endif #endif
bool try_send_log_message(int level, const char *tag, const char *line, size_t message_len); bool try_send_log_message(int level, const char *tag, const char *line, size_t message_len);
#ifdef USE_API_HOMEASSISTANT_SERVICES #ifdef USE_API_HOMEASSISTANT_SERVICES
void send_homeassistant_service_call(const HomeassistantServiceResponse &call) { void send_homeassistant_action(const HomeassistantActionRequest &call) {
if (!this->flags_.service_call_subscription) if (!this->flags_.service_call_subscription)
return; return;
this->send_message(call, HomeassistantServiceResponse::MESSAGE_TYPE); this->send_message(call, HomeassistantActionRequest::MESSAGE_TYPE);
} }
#endif #endif
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY

View File

@@ -32,6 +32,13 @@ extend google.protobuf.FieldOptions {
optional string fixed_array_size_define = 50010; optional string fixed_array_size_define = 50010;
optional string fixed_array_with_length_define = 50011; optional string fixed_array_with_length_define = 50011;
// pointer_to_buffer: Use pointer instead of array for fixed-size byte fields
// When set, the field will be declared as a pointer (const uint8_t *data)
// instead of an array (uint8_t data[N]). This allows zero-copy on decode
// by pointing directly to the protobuf buffer. The buffer must remain valid
// until the message is processed (which is guaranteed for stack-allocated messages).
optional bool pointer_to_buffer = 50012 [default=false];
// container_pointer: Zero-copy optimization for repeated fields. // container_pointer: Zero-copy optimization for repeated fields.
// //
// When container_pointer is set on a repeated field, the generated message will // When container_pointer is set on a repeated field, the generated message will

View File

@@ -22,9 +22,12 @@ bool HelloRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
} }
bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) { switch (field_id) {
case 1: case 1: {
this->client_info = value.as_string(); // Use raw data directly to avoid allocation
this->client_info = value.data();
this->client_info_len = value.size();
break; break;
}
default: default:
return false; return false;
} }
@@ -45,9 +48,12 @@ void HelloResponse::calculate_size(ProtoSize &size) const {
#ifdef USE_API_PASSWORD #ifdef USE_API_PASSWORD
bool AuthenticationRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { bool AuthenticationRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) { switch (field_id) {
case 1: case 1: {
this->password = value.as_string(); // Use raw data directly to avoid allocation
this->password = value.data();
this->password_len = value.size();
break; break;
}
default: default:
return false; return false;
} }
@@ -866,7 +872,7 @@ void HomeassistantServiceMap::calculate_size(ProtoSize &size) const {
size.add_length(1, this->key_ref_.size()); size.add_length(1, this->key_ref_.size());
size.add_length(1, this->value.size()); size.add_length(1, this->value.size());
} }
void HomeassistantServiceResponse::encode(ProtoWriteBuffer buffer) const { void HomeassistantActionRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->service_ref_); buffer.encode_string(1, this->service_ref_);
for (auto &it : this->data) { for (auto &it : this->data) {
buffer.encode_message(2, it, true); buffer.encode_message(2, it, true);
@@ -879,7 +885,7 @@ void HomeassistantServiceResponse::encode(ProtoWriteBuffer buffer) const {
} }
buffer.encode_bool(5, this->is_event); buffer.encode_bool(5, this->is_event);
} }
void HomeassistantServiceResponse::calculate_size(ProtoSize &size) const { void HomeassistantActionRequest::calculate_size(ProtoSize &size) const {
size.add_length(1, this->service_ref_.size()); size.add_length(1, this->service_ref_.size());
size.add_repeated_message(1, this->data); size.add_repeated_message(1, this->data);
size.add_repeated_message(1, this->data_template); size.add_repeated_message(1, this->data_template);
@@ -917,9 +923,12 @@ bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDel
#endif #endif
bool GetTimeResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { bool GetTimeResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) { switch (field_id) {
case 2: case 2: {
this->timezone = value.as_string(); // Use raw data directly to avoid allocation
this->timezone = value.data();
this->timezone_len = value.size();
break; break;
}
default: default:
return false; return false;
} }
@@ -2028,9 +2037,12 @@ bool BluetoothGATTWriteRequest::decode_varint(uint32_t field_id, ProtoVarInt val
} }
bool BluetoothGATTWriteRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { bool BluetoothGATTWriteRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) { switch (field_id) {
case 4: case 4: {
this->data = value.as_string(); // Use raw data directly to avoid allocation
this->data = value.data();
this->data_len = value.size();
break; break;
}
default: default:
return false; return false;
} }
@@ -2064,9 +2076,12 @@ bool BluetoothGATTWriteDescriptorRequest::decode_varint(uint32_t field_id, Proto
} }
bool BluetoothGATTWriteDescriptorRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { bool BluetoothGATTWriteDescriptorRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) { switch (field_id) {
case 3: case 3: {
this->data = value.as_string(); // Use raw data directly to avoid allocation
this->data = value.data();
this->data_len = value.size();
break; break;
}
default: default:
return false; return false;
} }
@@ -3029,12 +3044,9 @@ bool UpdateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
bool ZWaveProxyFrame::decode_length(uint32_t field_id, ProtoLengthDelimited value) { bool ZWaveProxyFrame::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) { switch (field_id) {
case 1: { case 1: {
const std::string &data_str = value.as_string(); // Use raw data directly to avoid allocation
this->data_len = data_str.size(); this->data = value.data();
if (this->data_len > 257) { this->data_len = value.size();
this->data_len = 257;
}
memcpy(this->data, data_str.data(), this->data_len);
break; break;
} }
default: default:

View File

@@ -330,11 +330,12 @@ class CommandProtoMessage : public ProtoDecodableMessage {
class HelloRequest final : public ProtoDecodableMessage { class HelloRequest final : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 1; static constexpr uint8_t MESSAGE_TYPE = 1;
static constexpr uint8_t ESTIMATED_SIZE = 17; static constexpr uint8_t ESTIMATED_SIZE = 27;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "hello_request"; } const char *message_name() const override { return "hello_request"; }
#endif #endif
std::string client_info{}; const uint8_t *client_info{nullptr};
uint16_t client_info_len{0};
uint32_t api_version_major{0}; uint32_t api_version_major{0};
uint32_t api_version_minor{0}; uint32_t api_version_minor{0};
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@@ -370,11 +371,12 @@ class HelloResponse final : public ProtoMessage {
class AuthenticationRequest final : public ProtoDecodableMessage { class AuthenticationRequest final : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 3; static constexpr uint8_t MESSAGE_TYPE = 3;
static constexpr uint8_t ESTIMATED_SIZE = 9; static constexpr uint8_t ESTIMATED_SIZE = 19;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "authentication_request"; } const char *message_name() const override { return "authentication_request"; }
#endif #endif
std::string password{}; const uint8_t *password{nullptr};
uint16_t password_len{0};
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -1098,12 +1100,12 @@ class HomeassistantServiceMap final : public ProtoMessage {
protected: protected:
}; };
class HomeassistantServiceResponse final : public ProtoMessage { class HomeassistantActionRequest final : public ProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 35; static constexpr uint8_t MESSAGE_TYPE = 35;
static constexpr uint8_t ESTIMATED_SIZE = 113; static constexpr uint8_t ESTIMATED_SIZE = 113;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "homeassistant_service_response"; } const char *message_name() const override { return "homeassistant_action_request"; }
#endif #endif
StringRef service_ref_{}; StringRef service_ref_{};
void set_service(const StringRef &ref) { this->service_ref_ = ref; } void set_service(const StringRef &ref) { this->service_ref_ = ref; }
@@ -1188,12 +1190,13 @@ class GetTimeRequest final : public ProtoMessage {
class GetTimeResponse final : public ProtoDecodableMessage { class GetTimeResponse final : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 37; static constexpr uint8_t MESSAGE_TYPE = 37;
static constexpr uint8_t ESTIMATED_SIZE = 14; static constexpr uint8_t ESTIMATED_SIZE = 24;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "get_time_response"; } const char *message_name() const override { return "get_time_response"; }
#endif #endif
uint32_t epoch_seconds{0}; uint32_t epoch_seconds{0};
std::string timezone{}; const uint8_t *timezone{nullptr};
uint16_t timezone_len{0};
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -1985,14 +1988,15 @@ class BluetoothGATTReadResponse final : public ProtoMessage {
class BluetoothGATTWriteRequest final : public ProtoDecodableMessage { class BluetoothGATTWriteRequest final : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 75; static constexpr uint8_t MESSAGE_TYPE = 75;
static constexpr uint8_t ESTIMATED_SIZE = 19; static constexpr uint8_t ESTIMATED_SIZE = 29;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "bluetooth_gatt_write_request"; } const char *message_name() const override { return "bluetooth_gatt_write_request"; }
#endif #endif
uint64_t address{0}; uint64_t address{0};
uint32_t handle{0}; uint32_t handle{0};
bool response{false}; bool response{false};
std::string data{}; const uint8_t *data{nullptr};
uint16_t data_len{0};
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -2020,13 +2024,14 @@ class BluetoothGATTReadDescriptorRequest final : public ProtoDecodableMessage {
class BluetoothGATTWriteDescriptorRequest final : public ProtoDecodableMessage { class BluetoothGATTWriteDescriptorRequest final : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 77; static constexpr uint8_t MESSAGE_TYPE = 77;
static constexpr uint8_t ESTIMATED_SIZE = 17; static constexpr uint8_t ESTIMATED_SIZE = 27;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "bluetooth_gatt_write_descriptor_request"; } const char *message_name() const override { return "bluetooth_gatt_write_descriptor_request"; }
#endif #endif
uint64_t address{0}; uint64_t address{0};
uint32_t handle{0}; uint32_t handle{0};
std::string data{}; const uint8_t *data{nullptr};
uint16_t data_len{0};
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -2929,11 +2934,11 @@ class UpdateCommandRequest final : public CommandProtoMessage {
class ZWaveProxyFrame final : public ProtoDecodableMessage { class ZWaveProxyFrame final : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 128; static constexpr uint8_t MESSAGE_TYPE = 128;
static constexpr uint8_t ESTIMATED_SIZE = 33; static constexpr uint8_t ESTIMATED_SIZE = 19;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "z_wave_proxy_frame"; } const char *message_name() const override { return "z_wave_proxy_frame"; }
#endif #endif
uint8_t data[257]{}; const uint8_t *data{nullptr};
uint16_t data_len{0}; uint16_t data_len{0};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override; void calculate_size(ProtoSize &size) const override;

View File

@@ -670,7 +670,9 @@ template<> const char *proto_enum_to_string<enums::ZWaveProxyRequestType>(enums:
void HelloRequest::dump_to(std::string &out) const { void HelloRequest::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "HelloRequest"); MessageDumpHelper helper(out, "HelloRequest");
dump_field(out, "client_info", this->client_info); out.append(" client_info: ");
out.append(format_hex_pretty(this->client_info, this->client_info_len));
out.append("\n");
dump_field(out, "api_version_major", this->api_version_major); dump_field(out, "api_version_major", this->api_version_major);
dump_field(out, "api_version_minor", this->api_version_minor); dump_field(out, "api_version_minor", this->api_version_minor);
} }
@@ -682,7 +684,12 @@ void HelloResponse::dump_to(std::string &out) const {
dump_field(out, "name", this->name_ref_); dump_field(out, "name", this->name_ref_);
} }
#ifdef USE_API_PASSWORD #ifdef USE_API_PASSWORD
void AuthenticationRequest::dump_to(std::string &out) const { dump_field(out, "password", this->password); } void AuthenticationRequest::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "AuthenticationRequest");
out.append(" password: ");
out.append(format_hex_pretty(this->password, this->password_len));
out.append("\n");
}
void AuthenticationResponse::dump_to(std::string &out) const { void AuthenticationResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "AuthenticationResponse"); MessageDumpHelper helper(out, "AuthenticationResponse");
dump_field(out, "invalid_password", this->invalid_password); dump_field(out, "invalid_password", this->invalid_password);
@@ -1094,8 +1101,8 @@ void HomeassistantServiceMap::dump_to(std::string &out) const {
dump_field(out, "key", this->key_ref_); dump_field(out, "key", this->key_ref_);
dump_field(out, "value", this->value); dump_field(out, "value", this->value);
} }
void HomeassistantServiceResponse::dump_to(std::string &out) const { void HomeassistantActionRequest::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "HomeassistantServiceResponse"); MessageDumpHelper helper(out, "HomeassistantActionRequest");
dump_field(out, "service", this->service_ref_); dump_field(out, "service", this->service_ref_);
for (const auto &it : this->data) { for (const auto &it : this->data) {
out.append(" data: "); out.append(" data: ");
@@ -1136,7 +1143,9 @@ void GetTimeRequest::dump_to(std::string &out) const { out.append("GetTimeReques
void GetTimeResponse::dump_to(std::string &out) const { void GetTimeResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "GetTimeResponse"); MessageDumpHelper helper(out, "GetTimeResponse");
dump_field(out, "epoch_seconds", this->epoch_seconds); dump_field(out, "epoch_seconds", this->epoch_seconds);
dump_field(out, "timezone", this->timezone); out.append(" timezone: ");
out.append(format_hex_pretty(this->timezone, this->timezone_len));
out.append("\n");
} }
#ifdef USE_API_SERVICES #ifdef USE_API_SERVICES
void ListEntitiesServicesArgument::dump_to(std::string &out) const { void ListEntitiesServicesArgument::dump_to(std::string &out) const {
@@ -1649,7 +1658,7 @@ void BluetoothGATTWriteRequest::dump_to(std::string &out) const {
dump_field(out, "handle", this->handle); dump_field(out, "handle", this->handle);
dump_field(out, "response", this->response); dump_field(out, "response", this->response);
out.append(" data: "); out.append(" data: ");
out.append(format_hex_pretty(reinterpret_cast<const uint8_t *>(this->data.data()), this->data.size())); out.append(format_hex_pretty(this->data, this->data_len));
out.append("\n"); out.append("\n");
} }
void BluetoothGATTReadDescriptorRequest::dump_to(std::string &out) const { void BluetoothGATTReadDescriptorRequest::dump_to(std::string &out) const {
@@ -1662,7 +1671,7 @@ void BluetoothGATTWriteDescriptorRequest::dump_to(std::string &out) const {
dump_field(out, "address", this->address); dump_field(out, "address", this->address);
dump_field(out, "handle", this->handle); dump_field(out, "handle", this->handle);
out.append(" data: "); out.append(" data: ");
out.append(format_hex_pretty(reinterpret_cast<const uint8_t *>(this->data.data()), this->data.size())); out.append(format_hex_pretty(this->data, this->data_len));
out.append("\n"); out.append("\n");
} }
void BluetoothGATTNotifyRequest::dump_to(std::string &out) const { void BluetoothGATTNotifyRequest::dump_to(std::string &out) const {

View File

@@ -217,12 +217,12 @@ void APIServer::dump_config() {
} }
#ifdef USE_API_PASSWORD #ifdef USE_API_PASSWORD
bool APIServer::check_password(const std::string &password) const { bool APIServer::check_password(const uint8_t *password_data, size_t password_len) 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();
uint32_t len_a = this->password_.length(); uint32_t len_a = this->password_.length();
const char *b = password.c_str(); const char *b = reinterpret_cast<const char *>(password_data);
uint32_t len_b = password.length(); uint32_t len_b = password_len;
// disable optimization with volatile // disable optimization with volatile
volatile uint32_t length = len_b; volatile uint32_t length = len_b;
@@ -245,6 +245,7 @@ bool APIServer::check_password(const std::string &password) const {
return result == 0; return result == 0;
} }
#endif #endif
void APIServer::handle_disconnect(APIConnection *conn) {} void APIServer::handle_disconnect(APIConnection *conn) {}
@@ -370,9 +371,9 @@ void APIServer::set_password(const std::string &password) { this->password_ = pa
void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; } void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; }
#ifdef USE_API_HOMEASSISTANT_SERVICES #ifdef USE_API_HOMEASSISTANT_SERVICES
void APIServer::send_homeassistant_service_call(const HomeassistantServiceResponse &call) { void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call) {
for (auto &client : this->clients_) { for (auto &client : this->clients_) {
client->send_homeassistant_service_call(call); client->send_homeassistant_action(call);
} }
} }
#endif #endif

View File

@@ -37,7 +37,7 @@ class APIServer : public Component, public Controller {
void on_shutdown() override; void on_shutdown() override;
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 uint8_t *password_data, size_t password_len) 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);
@@ -107,7 +107,8 @@ class APIServer : public Component, public Controller {
void on_media_player_update(media_player::MediaPlayer *obj) override; void on_media_player_update(media_player::MediaPlayer *obj) override;
#endif #endif
#ifdef USE_API_HOMEASSISTANT_SERVICES #ifdef USE_API_HOMEASSISTANT_SERVICES
void send_homeassistant_service_call(const HomeassistantServiceResponse &call); void send_homeassistant_action(const HomeassistantActionRequest &call);
#endif #endif
#ifdef USE_API_SERVICES #ifdef USE_API_SERVICES
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }

View File

@@ -179,9 +179,9 @@ class CustomAPIDevice {
* @param service_name The service to call. * @param service_name The service to call.
*/ */
void call_homeassistant_service(const std::string &service_name) { void call_homeassistant_service(const std::string &service_name) {
HomeassistantServiceResponse resp; HomeassistantActionRequest resp;
resp.set_service(StringRef(service_name)); resp.set_service(StringRef(service_name));
global_api_server->send_homeassistant_service_call(resp); global_api_server->send_homeassistant_action(resp);
} }
/** Call a Home Assistant service from ESPHome. /** Call a Home Assistant service from ESPHome.
@@ -199,7 +199,7 @@ class CustomAPIDevice {
* @param data The data for the service call, mapping from string to string. * @param data The data for the service call, mapping from string to string.
*/ */
void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) { void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) {
HomeassistantServiceResponse resp; HomeassistantActionRequest resp;
resp.set_service(StringRef(service_name)); resp.set_service(StringRef(service_name));
for (auto &it : data) { for (auto &it : data) {
resp.data.emplace_back(); resp.data.emplace_back();
@@ -207,7 +207,7 @@ class CustomAPIDevice {
kv.set_key(StringRef(it.first)); kv.set_key(StringRef(it.first));
kv.value = it.second; kv.value = it.second;
} }
global_api_server->send_homeassistant_service_call(resp); global_api_server->send_homeassistant_action(resp);
} }
/** Fire an ESPHome event in Home Assistant. /** Fire an ESPHome event in Home Assistant.
@@ -221,10 +221,10 @@ class CustomAPIDevice {
* @param event_name The event to fire. * @param event_name The event to fire.
*/ */
void fire_homeassistant_event(const std::string &event_name) { void fire_homeassistant_event(const std::string &event_name) {
HomeassistantServiceResponse resp; HomeassistantActionRequest resp;
resp.set_service(StringRef(event_name)); resp.set_service(StringRef(event_name));
resp.is_event = true; resp.is_event = true;
global_api_server->send_homeassistant_service_call(resp); global_api_server->send_homeassistant_action(resp);
} }
/** Fire an ESPHome event in Home Assistant. /** Fire an ESPHome event in Home Assistant.
@@ -241,7 +241,7 @@ class CustomAPIDevice {
* @param data The data for the event, mapping from string to string. * @param data The data for the event, mapping from string to string.
*/ */
void fire_homeassistant_event(const std::string &service_name, const std::map<std::string, std::string> &data) { void fire_homeassistant_event(const std::string &service_name, const std::map<std::string, std::string> &data) {
HomeassistantServiceResponse resp; HomeassistantActionRequest resp;
resp.set_service(StringRef(service_name)); resp.set_service(StringRef(service_name));
resp.is_event = true; resp.is_event = true;
for (auto &it : data) { for (auto &it : data) {
@@ -250,7 +250,7 @@ class CustomAPIDevice {
kv.set_key(StringRef(it.first)); kv.set_key(StringRef(it.first));
kv.value = it.second; kv.value = it.second;
} }
global_api_server->send_homeassistant_service_call(resp); global_api_server->send_homeassistant_action(resp);
} }
#else #else
template<typename T = void> void call_homeassistant_service(const std::string &service_name) { template<typename T = void> void call_homeassistant_service(const std::string &service_name) {

View File

@@ -3,10 +3,10 @@
#include "api_server.h" #include "api_server.h"
#ifdef USE_API #ifdef USE_API
#ifdef USE_API_HOMEASSISTANT_SERVICES #ifdef USE_API_HOMEASSISTANT_SERVICES
#include <vector>
#include "api_pb2.h" #include "api_pb2.h"
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include <vector>
namespace esphome::api { namespace esphome::api {
@@ -62,7 +62,7 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
} }
void play(Ts... x) override { void play(Ts... x) override {
HomeassistantServiceResponse resp; HomeassistantActionRequest resp;
std::string service_value = this->service_.value(x...); std::string service_value = this->service_.value(x...);
resp.set_service(StringRef(service_value)); resp.set_service(StringRef(service_value));
resp.is_event = this->is_event_; resp.is_event = this->is_event_;
@@ -84,7 +84,7 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
kv.set_key(StringRef(it.key)); kv.set_key(StringRef(it.key));
kv.value = it.value.value(x...); kv.value = it.value.value(x...);
} }
this->parent_->send_homeassistant_service_call(resp); this->parent_->send_homeassistant_action(resp);
} }
protected: protected:

View File

@@ -182,6 +182,10 @@ class ProtoLengthDelimited {
explicit ProtoLengthDelimited(const uint8_t *value, size_t length) : value_(value), length_(length) {} explicit ProtoLengthDelimited(const uint8_t *value, size_t length) : value_(value), length_(length) {}
std::string as_string() const { return std::string(reinterpret_cast<const char *>(this->value_), this->length_); } std::string as_string() const { return std::string(reinterpret_cast<const char *>(this->value_), this->length_); }
// Direct access to raw data without string allocation
const uint8_t *data() const { return this->value_; }
size_t size() const { return this->length_; }
/** /**
* Decode the length-delimited data into an existing ProtoDecodableMessage instance. * Decode the length-delimited data into an existing ProtoDecodableMessage instance.
* *

View File

@@ -514,7 +514,8 @@ esp_err_t BluetoothConnection::read_characteristic(uint16_t handle) {
return this->check_and_log_error_("esp_ble_gattc_read_char", err); return this->check_and_log_error_("esp_ble_gattc_read_char", err);
} }
esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const std::string &data, bool response) { esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const uint8_t *data, size_t length,
bool response) {
if (!this->connected()) { if (!this->connected()) {
this->log_gatt_not_connected_("write", "characteristic"); this->log_gatt_not_connected_("write", "characteristic");
return ESP_GATT_NOT_CONNECTED; return ESP_GATT_NOT_CONNECTED;
@@ -522,8 +523,11 @@ esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const std::
ESP_LOGV(TAG, "[%d] [%s] Writing GATT characteristic handle %d", this->connection_index_, this->address_str_.c_str(), ESP_LOGV(TAG, "[%d] [%s] Writing GATT characteristic handle %d", this->connection_index_, this->address_str_.c_str(),
handle); handle);
// ESP-IDF's API requires a non-const uint8_t* but it doesn't modify the data
// The BTC layer immediately copies the data to its own buffer (see btc_gattc.c)
// const_cast is safe here and was previously hidden by a C-style cast
esp_err_t err = esp_err_t err =
esp_ble_gattc_write_char(this->gattc_if_, this->conn_id_, handle, data.size(), (uint8_t *) data.data(), esp_ble_gattc_write_char(this->gattc_if_, this->conn_id_, handle, length, const_cast<uint8_t *>(data),
response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
return this->check_and_log_error_("esp_ble_gattc_write_char", err); return this->check_and_log_error_("esp_ble_gattc_write_char", err);
} }
@@ -540,7 +544,7 @@ esp_err_t BluetoothConnection::read_descriptor(uint16_t handle) {
return this->check_and_log_error_("esp_ble_gattc_read_char_descr", err); return this->check_and_log_error_("esp_ble_gattc_read_char_descr", err);
} }
esp_err_t BluetoothConnection::write_descriptor(uint16_t handle, const std::string &data, bool response) { esp_err_t BluetoothConnection::write_descriptor(uint16_t handle, const uint8_t *data, size_t length, bool response) {
if (!this->connected()) { if (!this->connected()) {
this->log_gatt_not_connected_("write", "descriptor"); this->log_gatt_not_connected_("write", "descriptor");
return ESP_GATT_NOT_CONNECTED; return ESP_GATT_NOT_CONNECTED;
@@ -548,8 +552,11 @@ esp_err_t BluetoothConnection::write_descriptor(uint16_t handle, const std::stri
ESP_LOGV(TAG, "[%d] [%s] Writing GATT descriptor handle %d", this->connection_index_, this->address_str_.c_str(), ESP_LOGV(TAG, "[%d] [%s] Writing GATT descriptor handle %d", this->connection_index_, this->address_str_.c_str(),
handle); handle);
// ESP-IDF's API requires a non-const uint8_t* but it doesn't modify the data
// The BTC layer immediately copies the data to its own buffer (see btc_gattc.c)
// const_cast is safe here and was previously hidden by a C-style cast
esp_err_t err = esp_ble_gattc_write_char_descr( esp_err_t err = esp_ble_gattc_write_char_descr(
this->gattc_if_, this->conn_id_, handle, data.size(), (uint8_t *) data.data(), this->gattc_if_, this->conn_id_, handle, length, const_cast<uint8_t *>(data),
response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
return this->check_and_log_error_("esp_ble_gattc_write_char_descr", err); return this->check_and_log_error_("esp_ble_gattc_write_char_descr", err);
} }

View File

@@ -18,9 +18,9 @@ class BluetoothConnection final : public esp32_ble_client::BLEClientBase {
esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override; esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override;
esp_err_t read_characteristic(uint16_t handle); esp_err_t read_characteristic(uint16_t handle);
esp_err_t write_characteristic(uint16_t handle, const std::string &data, bool response); esp_err_t write_characteristic(uint16_t handle, const uint8_t *data, size_t length, bool response);
esp_err_t read_descriptor(uint16_t handle); esp_err_t read_descriptor(uint16_t handle);
esp_err_t write_descriptor(uint16_t handle, const std::string &data, bool response); esp_err_t write_descriptor(uint16_t handle, const uint8_t *data, size_t length, bool response);
esp_err_t notify_characteristic(uint16_t handle, bool enable); esp_err_t notify_characteristic(uint16_t handle, bool enable);

View File

@@ -305,7 +305,7 @@ void BluetoothProxy::bluetooth_gatt_write(const api::BluetoothGATTWriteRequest &
return; return;
} }
auto err = connection->write_characteristic(msg.handle, msg.data, msg.response); auto err = connection->write_characteristic(msg.handle, msg.data, msg.data_len, msg.response);
if (err != ESP_OK) { if (err != ESP_OK) {
this->send_gatt_error(msg.address, msg.handle, err); this->send_gatt_error(msg.address, msg.handle, err);
} }
@@ -331,7 +331,7 @@ void BluetoothProxy::bluetooth_gatt_write_descriptor(const api::BluetoothGATTWri
return; return;
} }
auto err = connection->write_descriptor(msg.handle, msg.data, true); auto err = connection->write_descriptor(msg.handle, msg.data, msg.data_len, true);
if (err != ESP_OK) { if (err != ESP_OK) {
this->send_gatt_error(msg.address, msg.handle, err); this->send_gatt_error(msg.address, msg.handle, err);
} }

View File

@@ -87,7 +87,7 @@ void HomeassistantNumber::control(float value) {
static constexpr auto ENTITY_ID_KEY = StringRef::from_lit("entity_id"); static constexpr auto ENTITY_ID_KEY = StringRef::from_lit("entity_id");
static constexpr auto VALUE_KEY = StringRef::from_lit("value"); static constexpr auto VALUE_KEY = StringRef::from_lit("value");
api::HomeassistantServiceResponse resp; api::HomeassistantActionRequest resp;
resp.set_service(SERVICE_NAME); resp.set_service(SERVICE_NAME);
resp.data.emplace_back(); resp.data.emplace_back();
@@ -100,7 +100,7 @@ void HomeassistantNumber::control(float value) {
entity_value.set_key(VALUE_KEY); entity_value.set_key(VALUE_KEY);
entity_value.value = to_string(value); entity_value.value = to_string(value);
api::global_api_server->send_homeassistant_service_call(resp); api::global_api_server->send_homeassistant_action(resp);
} }
} // namespace homeassistant } // namespace homeassistant

View File

@@ -44,7 +44,7 @@ void HomeassistantSwitch::write_state(bool state) {
static constexpr auto SERVICE_OFF = StringRef::from_lit("homeassistant.turn_off"); static constexpr auto SERVICE_OFF = StringRef::from_lit("homeassistant.turn_off");
static constexpr auto ENTITY_ID_KEY = StringRef::from_lit("entity_id"); static constexpr auto ENTITY_ID_KEY = StringRef::from_lit("entity_id");
api::HomeassistantServiceResponse resp; api::HomeassistantActionRequest resp;
if (state) { if (state) {
resp.set_service(SERVICE_ON); resp.set_service(SERVICE_ON);
} else { } else {
@@ -56,7 +56,7 @@ void HomeassistantSwitch::write_state(bool state) {
entity_id_kv.set_key(ENTITY_ID_KEY); entity_id_kv.set_key(ENTITY_ID_KEY);
entity_id_kv.value = this->entity_id_; entity_id_kv.value = this->entity_id_;
api::global_api_server->send_homeassistant_service_call(resp); api::global_api_server->send_homeassistant_action(resp);
} }
} // namespace homeassistant } // namespace homeassistant

View File

@@ -125,8 +125,8 @@ EAP_AUTH_SCHEMA = cv.All(
cv.Optional(CONF_USERNAME): cv.string_strict, cv.Optional(CONF_USERNAME): cv.string_strict,
cv.Optional(CONF_PASSWORD): cv.string_strict, cv.Optional(CONF_PASSWORD): cv.string_strict,
cv.Optional(CONF_CERTIFICATE_AUTHORITY): wpa2_eap.validate_certificate, cv.Optional(CONF_CERTIFICATE_AUTHORITY): wpa2_eap.validate_certificate,
cv.SplitDefault(CONF_TTLS_PHASE_2, esp32_idf="mschapv2"): cv.All( cv.SplitDefault(CONF_TTLS_PHASE_2, esp32="mschapv2"): cv.All(
cv.enum(TTLS_PHASE_2), cv.only_with_esp_idf cv.enum(TTLS_PHASE_2), cv.only_on_esp32
), ),
cv.Inclusive( cv.Inclusive(
CONF_CERTIFICATE, "certificate_and_key" CONF_CERTIFICATE, "certificate_and_key"
@@ -280,11 +280,11 @@ CONFIG_SCHEMA = cv.All(
cv.SplitDefault(CONF_OUTPUT_POWER, esp8266=20.0): cv.All( cv.SplitDefault(CONF_OUTPUT_POWER, esp8266=20.0): cv.All(
cv.decibel, cv.float_range(min=8.5, max=20.5) cv.decibel, cv.float_range(min=8.5, max=20.5)
), ),
cv.SplitDefault(CONF_ENABLE_BTM, esp32_idf=False): cv.All( cv.SplitDefault(CONF_ENABLE_BTM, esp32=False): cv.All(
cv.boolean, cv.only_with_esp_idf cv.boolean, cv.only_on_esp32
), ),
cv.SplitDefault(CONF_ENABLE_RRM, esp32_idf=False): cv.All( cv.SplitDefault(CONF_ENABLE_RRM, esp32=False): cv.All(
cv.boolean, cv.only_with_esp_idf cv.boolean, cv.only_on_esp32
), ),
cv.Optional(CONF_PASSIVE_SCAN, default=False): cv.boolean, cv.Optional(CONF_PASSIVE_SCAN, default=False): cv.boolean,
cv.Optional("enable_mdns"): cv.invalid( cv.Optional("enable_mdns"): cv.invalid(
@@ -416,10 +416,10 @@ async def to_code(config):
if CORE.is_esp8266: if CORE.is_esp8266:
cg.add_library("ESP8266WiFi", None) cg.add_library("ESP8266WiFi", None)
elif (CORE.is_esp32 and CORE.using_arduino) or CORE.is_rp2040: elif CORE.is_rp2040:
cg.add_library("WiFi", None) cg.add_library("WiFi", None)
if CORE.is_esp32 and CORE.using_esp_idf: if CORE.is_esp32:
if config[CONF_ENABLE_BTM] or config[CONF_ENABLE_RRM]: if config[CONF_ENABLE_BTM] or config[CONF_ENABLE_RRM]:
add_idf_sdkconfig_option("CONFIG_WPA_11KV_SUPPORT", True) add_idf_sdkconfig_option("CONFIG_WPA_11KV_SUPPORT", True)
cg.add_define("USE_WIFI_11KV_SUPPORT") cg.add_define("USE_WIFI_11KV_SUPPORT")
@@ -506,8 +506,10 @@ async def wifi_set_sta_to_code(config, action_id, template_arg, args):
FILTER_SOURCE_FILES = filter_source_files_from_platform( FILTER_SOURCE_FILES = filter_source_files_from_platform(
{ {
"wifi_component_esp32_arduino.cpp": {PlatformFramework.ESP32_ARDUINO}, "wifi_component_esp_idf.cpp": {
"wifi_component_esp_idf.cpp": {PlatformFramework.ESP32_IDF}, PlatformFramework.ESP32_IDF,
PlatformFramework.ESP32_ARDUINO,
},
"wifi_component_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO}, "wifi_component_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
"wifi_component_libretiny.cpp": { "wifi_component_libretiny.cpp": {
PlatformFramework.BK72XX_ARDUINO, PlatformFramework.BK72XX_ARDUINO,

View File

@@ -3,7 +3,7 @@
#include <cinttypes> #include <cinttypes>
#include <map> #include <map>
#ifdef USE_ESP_IDF #ifdef USE_ESP32
#if (ESP_IDF_VERSION_MAJOR >= 5 && ESP_IDF_VERSION_MINOR >= 1) #if (ESP_IDF_VERSION_MAJOR >= 5 && ESP_IDF_VERSION_MINOR >= 1)
#include <esp_eap_client.h> #include <esp_eap_client.h>
#else #else
@@ -11,7 +11,7 @@
#endif #endif
#endif #endif
#if defined(USE_ESP32) || defined(USE_ESP_IDF) #if defined(USE_ESP32)
#include <esp_wifi.h> #include <esp_wifi.h>
#endif #endif
#ifdef USE_ESP8266 #ifdef USE_ESP8266
@@ -344,7 +344,7 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) {
ESP_LOGV(TAG, " Identity: " LOG_SECRET("'%s'"), eap_config.identity.c_str()); ESP_LOGV(TAG, " Identity: " LOG_SECRET("'%s'"), eap_config.identity.c_str());
ESP_LOGV(TAG, " Username: " LOG_SECRET("'%s'"), eap_config.username.c_str()); ESP_LOGV(TAG, " Username: " LOG_SECRET("'%s'"), eap_config.username.c_str());
ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), eap_config.password.c_str()); ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), eap_config.password.c_str());
#ifdef USE_ESP_IDF #ifdef USE_ESP32
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
std::map<esp_eap_ttls_phase2_types, std::string> phase2types = {{ESP_EAP_TTLS_PHASE2_PAP, "pap"}, std::map<esp_eap_ttls_phase2_types, std::string> phase2types = {{ESP_EAP_TTLS_PHASE2_PAP, "pap"},
{ESP_EAP_TTLS_PHASE2_CHAP, "chap"}, {ESP_EAP_TTLS_PHASE2_CHAP, "chap"},

View File

@@ -20,7 +20,7 @@
#include <WiFi.h> #include <WiFi.h>
#endif #endif
#if defined(USE_ESP_IDF) && defined(USE_WIFI_WPA2_EAP) #if defined(USE_ESP32) && defined(USE_WIFI_WPA2_EAP)
#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1) #if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1)
#include <esp_eap_client.h> #include <esp_eap_client.h>
#else #else
@@ -113,7 +113,7 @@ struct EAPAuth {
const char *client_cert; const char *client_cert;
const char *client_key; const char *client_key;
// used for EAP-TTLS // used for EAP-TTLS
#ifdef USE_ESP_IDF #ifdef USE_ESP32
esp_eap_ttls_phase2_types ttls_phase_2; esp_eap_ttls_phase2_types ttls_phase_2;
#endif #endif
}; };
@@ -199,7 +199,7 @@ enum WiFiPowerSaveMode : uint8_t {
WIFI_POWER_SAVE_HIGH, WIFI_POWER_SAVE_HIGH,
}; };
#ifdef USE_ESP_IDF #ifdef USE_ESP32
struct IDFWiFiEvent; struct IDFWiFiEvent;
#endif #endif
@@ -368,7 +368,7 @@ class WiFiComponent : public Component {
void wifi_event_callback_(arduino_event_id_t event, arduino_event_info_t info); void wifi_event_callback_(arduino_event_id_t event, arduino_event_info_t info);
void wifi_scan_done_callback_(); void wifi_scan_done_callback_();
#endif #endif
#ifdef USE_ESP_IDF #ifdef USE_ESP32
void wifi_process_event_(IDFWiFiEvent *data); void wifi_process_event_(IDFWiFiEvent *data);
#endif #endif

View File

@@ -1,860 +0,0 @@
#include "wifi_component.h"
#ifdef USE_WIFI
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
#include <esp_netif.h>
#include <esp_wifi.h>
#include <algorithm>
#include <utility>
#ifdef USE_WIFI_WPA2_EAP
#include <esp_eap_client.h>
#endif
#ifdef USE_WIFI_AP
#include "dhcpserver/dhcpserver.h"
#endif // USE_WIFI_AP
#include "lwip/apps/sntp.h"
#include "lwip/dns.h"
#include "lwip/err.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/util.h"
namespace esphome {
namespace wifi {
static const char *const TAG = "wifi_esp32";
static esp_netif_t *s_sta_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
#ifdef USE_WIFI_AP
static esp_netif_t *s_ap_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
#endif // USE_WIFI_AP
static bool s_sta_connecting = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void WiFiComponent::wifi_pre_setup_() {
uint8_t mac[6];
if (has_custom_mac_address()) {
get_mac_address_raw(mac);
set_mac_address(mac);
}
auto f = std::bind(&WiFiComponent::wifi_event_callback_, this, std::placeholders::_1, std::placeholders::_2);
WiFi.onEvent(f);
WiFi.persistent(false);
// Make sure WiFi is in clean state before anything starts
this->wifi_mode_(false, false);
}
bool WiFiComponent::wifi_mode_(optional<bool> sta, optional<bool> ap) {
wifi_mode_t current_mode = WiFiClass::getMode();
bool current_sta = current_mode == WIFI_MODE_STA || current_mode == WIFI_MODE_APSTA;
bool current_ap = current_mode == WIFI_MODE_AP || current_mode == WIFI_MODE_APSTA;
bool set_sta = sta.value_or(current_sta);
bool set_ap = ap.value_or(current_ap);
wifi_mode_t set_mode;
if (set_sta && set_ap) {
set_mode = WIFI_MODE_APSTA;
} else if (set_sta && !set_ap) {
set_mode = WIFI_MODE_STA;
} else if (!set_sta && set_ap) {
set_mode = WIFI_MODE_AP;
} else {
set_mode = WIFI_MODE_NULL;
}
if (current_mode == set_mode)
return true;
if (set_sta && !current_sta) {
ESP_LOGV(TAG, "Enabling STA");
} else if (!set_sta && current_sta) {
ESP_LOGV(TAG, "Disabling STA");
}
if (set_ap && !current_ap) {
ESP_LOGV(TAG, "Enabling AP");
} else if (!set_ap && current_ap) {
ESP_LOGV(TAG, "Disabling AP");
}
bool ret = WiFiClass::mode(set_mode);
if (!ret) {
ESP_LOGW(TAG, "Setting mode failed");
return false;
}
// WiFiClass::mode above calls esp_netif_create_default_wifi_sta() and
// esp_netif_create_default_wifi_ap(), which creates the interfaces.
// s_sta_netif handle is set during ESPHOME_EVENT_ID_WIFI_STA_START event
#ifdef USE_WIFI_AP
if (set_ap)
s_ap_netif = esp_netif_get_handle_from_ifkey("WIFI_AP_DEF");
#endif
return ret;
}
bool WiFiComponent::wifi_sta_pre_setup_() {
if (!this->wifi_mode_(true, {}))
return false;
WiFi.setAutoReconnect(false);
delay(10);
return true;
}
bool WiFiComponent::wifi_apply_output_power_(float output_power) {
int8_t val = static_cast<int8_t>(output_power * 4);
return esp_wifi_set_max_tx_power(val) == ESP_OK;
}
bool WiFiComponent::wifi_apply_power_save_() {
wifi_ps_type_t power_save;
switch (this->power_save_) {
case WIFI_POWER_SAVE_LIGHT:
power_save = WIFI_PS_MIN_MODEM;
break;
case WIFI_POWER_SAVE_HIGH:
power_save = WIFI_PS_MAX_MODEM;
break;
case WIFI_POWER_SAVE_NONE:
default:
power_save = WIFI_PS_NONE;
break;
}
return esp_wifi_set_ps(power_save) == ESP_OK;
}
bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
// enable STA
if (!this->wifi_mode_(true, {}))
return false;
// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv417wifi_sta_config_t
wifi_config_t conf;
memset(&conf, 0, sizeof(conf));
if (ap.get_ssid().size() > sizeof(conf.sta.ssid)) {
ESP_LOGE(TAG, "SSID too long");
return false;
}
if (ap.get_password().size() > sizeof(conf.sta.password)) {
ESP_LOGE(TAG, "Password too long");
return false;
}
memcpy(reinterpret_cast<char *>(conf.sta.ssid), ap.get_ssid().c_str(), ap.get_ssid().size());
memcpy(reinterpret_cast<char *>(conf.sta.password), ap.get_password().c_str(), ap.get_password().size());
// The weakest authmode to accept in the fast scan mode
if (ap.get_password().empty()) {
conf.sta.threshold.authmode = WIFI_AUTH_OPEN;
} else {
conf.sta.threshold.authmode = WIFI_AUTH_WPA_WPA2_PSK;
}
#ifdef USE_WIFI_WPA2_EAP
if (ap.get_eap().has_value()) {
conf.sta.threshold.authmode = WIFI_AUTH_WPA2_ENTERPRISE;
}
#endif
if (ap.get_bssid().has_value()) {
conf.sta.bssid_set = true;
memcpy(conf.sta.bssid, ap.get_bssid()->data(), 6);
} else {
conf.sta.bssid_set = false;
}
if (ap.get_channel().has_value()) {
conf.sta.channel = *ap.get_channel();
conf.sta.scan_method = WIFI_FAST_SCAN;
} else {
conf.sta.scan_method = WIFI_ALL_CHANNEL_SCAN;
}
// Listen interval for ESP32 station to receive beacon when WIFI_PS_MAX_MODEM is set.
// Units: AP beacon intervals. Defaults to 3 if set to 0.
conf.sta.listen_interval = 0;
// Protected Management Frame
// Device will prefer to connect in PMF mode if other device also advertises PMF capability.
conf.sta.pmf_cfg.capable = true;
conf.sta.pmf_cfg.required = false;
// note, we do our own filtering
// The minimum rssi to accept in the fast scan mode
conf.sta.threshold.rssi = -127;
conf.sta.threshold.authmode = WIFI_AUTH_OPEN;
wifi_config_t current_conf;
esp_err_t err;
err = esp_wifi_get_config(WIFI_IF_STA, &current_conf);
if (err != ERR_OK) {
ESP_LOGW(TAG, "esp_wifi_get_config failed: %s", esp_err_to_name(err));
// can continue
}
if (memcmp(&current_conf, &conf, sizeof(wifi_config_t)) != 0) { // NOLINT
err = esp_wifi_disconnect();
if (err != ESP_OK) {
ESP_LOGV(TAG, "esp_wifi_disconnect failed: %s", esp_err_to_name(err));
return false;
}
}
err = esp_wifi_set_config(WIFI_IF_STA, &conf);
if (err != ESP_OK) {
ESP_LOGV(TAG, "esp_wifi_set_config failed: %s", esp_err_to_name(err));
return false;
}
if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) {
return false;
}
// setup enterprise authentication if required
#ifdef USE_WIFI_WPA2_EAP
if (ap.get_eap().has_value()) {
// note: all certificates and keys have to be null terminated. Lengths are appended by +1 to include \0.
EAPAuth eap = ap.get_eap().value();
err = esp_eap_client_set_identity((uint8_t *) eap.identity.c_str(), eap.identity.length());
if (err != ESP_OK) {
ESP_LOGV(TAG, "esp_eap_client_set_identity failed: %d", err);
}
int ca_cert_len = strlen(eap.ca_cert);
int client_cert_len = strlen(eap.client_cert);
int client_key_len = strlen(eap.client_key);
if (ca_cert_len) {
err = esp_eap_client_set_ca_cert((uint8_t *) eap.ca_cert, ca_cert_len + 1);
if (err != ESP_OK) {
ESP_LOGV(TAG, "esp_eap_client_set_ca_cert failed: %d", err);
}
}
// workout what type of EAP this is
// validation is not required as the config tool has already validated it
if (client_cert_len && client_key_len) {
// if we have certs, this must be EAP-TLS
err = esp_eap_client_set_certificate_and_key((uint8_t *) eap.client_cert, client_cert_len + 1,
(uint8_t *) eap.client_key, client_key_len + 1,
(uint8_t *) eap.password.c_str(), strlen(eap.password.c_str()));
if (err != ESP_OK) {
ESP_LOGV(TAG, "esp_eap_client_set_certificate_and_key failed: %d", err);
}
} else {
// in the absence of certs, assume this is username/password based
err = esp_eap_client_set_username((uint8_t *) eap.username.c_str(), eap.username.length());
if (err != ESP_OK) {
ESP_LOGV(TAG, "esp_eap_client_set_username failed: %d", err);
}
err = esp_eap_client_set_password((uint8_t *) eap.password.c_str(), eap.password.length());
if (err != ESP_OK) {
ESP_LOGV(TAG, "esp_eap_client_set_password failed: %d", err);
}
}
err = esp_wifi_sta_enterprise_enable();
if (err != ESP_OK) {
ESP_LOGV(TAG, "esp_wifi_sta_enterprise_enable failed: %d", err);
}
}
#endif // USE_WIFI_WPA2_EAP
this->wifi_apply_hostname_();
s_sta_connecting = true;
err = esp_wifi_connect();
if (err != ESP_OK) {
ESP_LOGW(TAG, "esp_wifi_connect failed: %s", esp_err_to_name(err));
return false;
}
return true;
}
bool WiFiComponent::wifi_sta_ip_config_(optional<ManualIP> manual_ip) {
// enable STA
if (!this->wifi_mode_(true, {}))
return false;
// Check if the STA interface is initialized before using it
if (s_sta_netif == nullptr) {
ESP_LOGW(TAG, "STA interface not initialized");
return false;
}
esp_netif_dhcp_status_t dhcp_status;
esp_err_t err = esp_netif_dhcpc_get_status(s_sta_netif, &dhcp_status);
if (err != ESP_OK) {
ESP_LOGV(TAG, "esp_netif_dhcpc_get_status failed: %s", esp_err_to_name(err));
return false;
}
if (!manual_ip.has_value()) {
// sntp_servermode_dhcp lwip/sntp.c (Required to lock TCPIP core functionality!)
// https://github.com/esphome/issues/issues/6591
// https://github.com/espressif/arduino-esp32/issues/10526
{
LwIPLock lock;
// lwIP starts the SNTP client if it gets an SNTP server from DHCP. We don't need the time, and more importantly,
// the built-in SNTP client has a memory leak in certain situations. Disable this feature.
// https://github.com/esphome/issues/issues/2299
sntp_servermode_dhcp(false);
}
// No manual IP is set; use DHCP client
if (dhcp_status != ESP_NETIF_DHCP_STARTED) {
err = esp_netif_dhcpc_start(s_sta_netif);
if (err != ESP_OK) {
ESP_LOGV(TAG, "Starting DHCP client failed: %d", err);
}
return err == ESP_OK;
}
return true;
}
esp_netif_ip_info_t info; // struct of ip4_addr_t with ip, netmask, gw
info.ip = manual_ip->static_ip;
info.gw = manual_ip->gateway;
info.netmask = manual_ip->subnet;
err = esp_netif_dhcpc_stop(s_sta_netif);
if (err != ESP_OK && err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED) {
ESP_LOGV(TAG, "Stopping DHCP client failed: %s", esp_err_to_name(err));
}
err = esp_netif_set_ip_info(s_sta_netif, &info);
if (err != ESP_OK) {
ESP_LOGV(TAG, "Setting manual IP info failed: %s", esp_err_to_name(err));
}
esp_netif_dns_info_t dns;
if (manual_ip->dns1.is_set()) {
dns.ip = manual_ip->dns1;
esp_netif_set_dns_info(s_sta_netif, ESP_NETIF_DNS_MAIN, &dns);
}
if (manual_ip->dns2.is_set()) {
dns.ip = manual_ip->dns2;
esp_netif_set_dns_info(s_sta_netif, ESP_NETIF_DNS_BACKUP, &dns);
}
return true;
}
network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() {
if (!this->has_sta())
return {};
network::IPAddresses addresses;
esp_netif_ip_info_t ip;
esp_err_t err = esp_netif_get_ip_info(s_sta_netif, &ip);
if (err != ESP_OK) {
ESP_LOGV(TAG, "esp_netif_get_ip_info failed: %s", esp_err_to_name(err));
// TODO: do something smarter
// return false;
} else {
addresses[0] = network::IPAddress(&ip.ip);
}
#if USE_NETWORK_IPV6
struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES];
uint8_t count = 0;
count = esp_netif_get_all_ip6(s_sta_netif, if_ip6s);
assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES);
for (int i = 0; i < count; i++) {
addresses[i + 1] = network::IPAddress(&if_ip6s[i]);
}
#endif /* USE_NETWORK_IPV6 */
return addresses;
}
bool WiFiComponent::wifi_apply_hostname_() {
// setting is done in SYSTEM_EVENT_STA_START callback
return true;
}
const char *get_auth_mode_str(uint8_t mode) {
switch (mode) {
case WIFI_AUTH_OPEN:
return "OPEN";
case WIFI_AUTH_WEP:
return "WEP";
case WIFI_AUTH_WPA_PSK:
return "WPA PSK";
case WIFI_AUTH_WPA2_PSK:
return "WPA2 PSK";
case WIFI_AUTH_WPA_WPA2_PSK:
return "WPA/WPA2 PSK";
case WIFI_AUTH_WPA2_ENTERPRISE:
return "WPA2 Enterprise";
case WIFI_AUTH_WPA3_PSK:
return "WPA3 PSK";
case WIFI_AUTH_WPA2_WPA3_PSK:
return "WPA2/WPA3 PSK";
case WIFI_AUTH_WAPI_PSK:
return "WAPI PSK";
default:
return "UNKNOWN";
}
}
using esphome_ip4_addr_t = esp_ip4_addr_t;
std::string format_ip4_addr(const esphome_ip4_addr_t &ip) {
char buf[20];
sprintf(buf, "%u.%u.%u.%u", uint8_t(ip.addr >> 0), uint8_t(ip.addr >> 8), uint8_t(ip.addr >> 16),
uint8_t(ip.addr >> 24));
return buf;
}
const char *get_op_mode_str(uint8_t mode) {
switch (mode) {
case WIFI_OFF:
return "OFF";
case WIFI_STA:
return "STA";
case WIFI_AP:
return "AP";
case WIFI_AP_STA:
return "AP+STA";
default:
return "UNKNOWN";
}
}
const char *get_disconnect_reason_str(uint8_t reason) {
switch (reason) {
case WIFI_REASON_AUTH_EXPIRE:
return "Auth Expired";
case WIFI_REASON_AUTH_LEAVE:
return "Auth Leave";
case WIFI_REASON_ASSOC_EXPIRE:
return "Association Expired";
case WIFI_REASON_ASSOC_TOOMANY:
return "Too Many Associations";
case WIFI_REASON_NOT_AUTHED:
return "Not Authenticated";
case WIFI_REASON_NOT_ASSOCED:
return "Not Associated";
case WIFI_REASON_ASSOC_LEAVE:
return "Association Leave";
case WIFI_REASON_ASSOC_NOT_AUTHED:
return "Association not Authenticated";
case WIFI_REASON_DISASSOC_PWRCAP_BAD:
return "Disassociate Power Cap Bad";
case WIFI_REASON_DISASSOC_SUPCHAN_BAD:
return "Disassociate Supported Channel Bad";
case WIFI_REASON_IE_INVALID:
return "IE Invalid";
case WIFI_REASON_MIC_FAILURE:
return "Mic Failure";
case WIFI_REASON_4WAY_HANDSHAKE_TIMEOUT:
return "4-Way Handshake Timeout";
case WIFI_REASON_GROUP_KEY_UPDATE_TIMEOUT:
return "Group Key Update Timeout";
case WIFI_REASON_IE_IN_4WAY_DIFFERS:
return "IE In 4-Way Handshake Differs";
case WIFI_REASON_GROUP_CIPHER_INVALID:
return "Group Cipher Invalid";
case WIFI_REASON_PAIRWISE_CIPHER_INVALID:
return "Pairwise Cipher Invalid";
case WIFI_REASON_AKMP_INVALID:
return "AKMP Invalid";
case WIFI_REASON_UNSUPP_RSN_IE_VERSION:
return "Unsupported RSN IE version";
case WIFI_REASON_INVALID_RSN_IE_CAP:
return "Invalid RSN IE Cap";
case WIFI_REASON_802_1X_AUTH_FAILED:
return "802.1x Authentication Failed";
case WIFI_REASON_CIPHER_SUITE_REJECTED:
return "Cipher Suite Rejected";
case WIFI_REASON_BEACON_TIMEOUT:
return "Beacon Timeout";
case WIFI_REASON_NO_AP_FOUND:
return "AP Not Found";
case WIFI_REASON_AUTH_FAIL:
return "Authentication Failed";
case WIFI_REASON_ASSOC_FAIL:
return "Association Failed";
case WIFI_REASON_HANDSHAKE_TIMEOUT:
return "Handshake Failed";
case WIFI_REASON_CONNECTION_FAIL:
return "Connection Failed";
case WIFI_REASON_AP_TSF_RESET:
return "AP TSF reset";
case WIFI_REASON_ROAMING:
return "Station Roaming";
case WIFI_REASON_ASSOC_COMEBACK_TIME_TOO_LONG:
return "Association comeback time too long";
case WIFI_REASON_SA_QUERY_TIMEOUT:
return "SA query timeout";
case WIFI_REASON_NO_AP_FOUND_W_COMPATIBLE_SECURITY:
return "No AP found with compatible security";
case WIFI_REASON_NO_AP_FOUND_IN_AUTHMODE_THRESHOLD:
return "No AP found in auth mode threshold";
case WIFI_REASON_NO_AP_FOUND_IN_RSSI_THRESHOLD:
return "No AP found in RSSI threshold";
case WIFI_REASON_UNSPECIFIED:
default:
return "Unspecified";
}
}
void WiFiComponent::wifi_loop_() {}
#define ESPHOME_EVENT_ID_WIFI_READY ARDUINO_EVENT_WIFI_READY
#define ESPHOME_EVENT_ID_WIFI_SCAN_DONE ARDUINO_EVENT_WIFI_SCAN_DONE
#define ESPHOME_EVENT_ID_WIFI_STA_START ARDUINO_EVENT_WIFI_STA_START
#define ESPHOME_EVENT_ID_WIFI_STA_STOP ARDUINO_EVENT_WIFI_STA_STOP
#define ESPHOME_EVENT_ID_WIFI_STA_CONNECTED ARDUINO_EVENT_WIFI_STA_CONNECTED
#define ESPHOME_EVENT_ID_WIFI_STA_DISCONNECTED ARDUINO_EVENT_WIFI_STA_DISCONNECTED
#define ESPHOME_EVENT_ID_WIFI_STA_AUTHMODE_CHANGE ARDUINO_EVENT_WIFI_STA_AUTHMODE_CHANGE
#define ESPHOME_EVENT_ID_WIFI_STA_GOT_IP ARDUINO_EVENT_WIFI_STA_GOT_IP
#define ESPHOME_EVENT_ID_WIFI_STA_GOT_IP6 ARDUINO_EVENT_WIFI_STA_GOT_IP6
#define ESPHOME_EVENT_ID_WIFI_STA_LOST_IP ARDUINO_EVENT_WIFI_STA_LOST_IP
#define ESPHOME_EVENT_ID_WIFI_AP_START ARDUINO_EVENT_WIFI_AP_START
#define ESPHOME_EVENT_ID_WIFI_AP_STOP ARDUINO_EVENT_WIFI_AP_STOP
#define ESPHOME_EVENT_ID_WIFI_AP_STACONNECTED ARDUINO_EVENT_WIFI_AP_STACONNECTED
#define ESPHOME_EVENT_ID_WIFI_AP_STADISCONNECTED ARDUINO_EVENT_WIFI_AP_STADISCONNECTED
#define ESPHOME_EVENT_ID_WIFI_AP_STAIPASSIGNED ARDUINO_EVENT_WIFI_AP_STAIPASSIGNED
#define ESPHOME_EVENT_ID_WIFI_AP_PROBEREQRECVED ARDUINO_EVENT_WIFI_AP_PROBEREQRECVED
#define ESPHOME_EVENT_ID_WIFI_AP_GOT_IP6 ARDUINO_EVENT_WIFI_AP_GOT_IP6
using esphome_wifi_event_id_t = arduino_event_id_t;
using esphome_wifi_event_info_t = arduino_event_info_t;
void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_wifi_event_info_t info) {
switch (event) {
case ESPHOME_EVENT_ID_WIFI_READY: {
ESP_LOGV(TAG, "Ready");
break;
}
case ESPHOME_EVENT_ID_WIFI_SCAN_DONE: {
auto it = info.wifi_scan_done;
ESP_LOGV(TAG, "Scan done: status=%u number=%u scan_id=%u", it.status, it.number, it.scan_id);
this->wifi_scan_done_callback_();
break;
}
case ESPHOME_EVENT_ID_WIFI_STA_START: {
ESP_LOGV(TAG, "STA start");
// apply hostname
s_sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
esp_err_t err = esp_netif_set_hostname(s_sta_netif, App.get_name().c_str());
if (err != ERR_OK) {
ESP_LOGW(TAG, "esp_netif_set_hostname failed: %s", esp_err_to_name(err));
}
break;
}
case ESPHOME_EVENT_ID_WIFI_STA_STOP: {
ESP_LOGV(TAG, "STA stop");
break;
}
case ESPHOME_EVENT_ID_WIFI_STA_CONNECTED: {
auto it = info.wifi_sta_connected;
char buf[33];
memcpy(buf, it.ssid, it.ssid_len);
buf[it.ssid_len] = '\0';
ESP_LOGV(TAG, "Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf,
format_mac_address_pretty(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode));
#if USE_NETWORK_IPV6
this->set_timeout(100, [] { WiFi.enableIPv6(); });
#endif /* USE_NETWORK_IPV6 */
break;
}
case ESPHOME_EVENT_ID_WIFI_STA_DISCONNECTED: {
auto it = info.wifi_sta_disconnected;
char buf[33];
memcpy(buf, it.ssid, it.ssid_len);
buf[it.ssid_len] = '\0';
if (it.reason == WIFI_REASON_NO_AP_FOUND) {
ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf);
} else {
ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf,
format_mac_address_pretty(it.bssid).c_str(), get_disconnect_reason_str(it.reason));
}
uint8_t reason = it.reason;
if (reason == WIFI_REASON_AUTH_EXPIRE || reason == WIFI_REASON_BEACON_TIMEOUT ||
reason == WIFI_REASON_NO_AP_FOUND || reason == WIFI_REASON_ASSOC_FAIL ||
reason == WIFI_REASON_HANDSHAKE_TIMEOUT) {
err_t err = esp_wifi_disconnect();
if (err != ESP_OK) {
ESP_LOGV(TAG, "Disconnect failed: %s", esp_err_to_name(err));
}
this->error_from_callback_ = true;
}
s_sta_connecting = false;
break;
}
case ESPHOME_EVENT_ID_WIFI_STA_AUTHMODE_CHANGE: {
auto it = info.wifi_sta_authmode_change;
ESP_LOGV(TAG, "Authmode Change old=%s new=%s", get_auth_mode_str(it.old_mode), get_auth_mode_str(it.new_mode));
// Mitigate CVE-2020-12638
// https://lbsfilm.at/blog/wpa2-authenticationmode-downgrade-in-espressif-microprocessors
if (it.old_mode != WIFI_AUTH_OPEN && it.new_mode == WIFI_AUTH_OPEN) {
ESP_LOGW(TAG, "Potential Authmode downgrade detected, disconnecting");
// we can't call retry_connect() from this context, so disconnect immediately
// and notify main thread with error_from_callback_
err_t err = esp_wifi_disconnect();
if (err != ESP_OK) {
ESP_LOGW(TAG, "Disconnect failed: %s", esp_err_to_name(err));
}
this->error_from_callback_ = true;
}
break;
}
case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP: {
auto it = info.got_ip.ip_info;
ESP_LOGV(TAG, "static_ip=%s gateway=%s", format_ip4_addr(it.ip).c_str(), format_ip4_addr(it.gw).c_str());
this->got_ipv4_address_ = true;
#if USE_NETWORK_IPV6
s_sta_connecting = this->num_ipv6_addresses_ < USE_NETWORK_MIN_IPV6_ADDR_COUNT;
#else
s_sta_connecting = false;
#endif /* USE_NETWORK_IPV6 */
break;
}
#if USE_NETWORK_IPV6
case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP6: {
auto it = info.got_ip6.ip6_info;
ESP_LOGV(TAG, "IPv6 address=" IPV6STR, IPV62STR(it.ip));
this->num_ipv6_addresses_++;
s_sta_connecting = !(this->got_ipv4_address_ & (this->num_ipv6_addresses_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT));
break;
}
#endif /* USE_NETWORK_IPV6 */
case ESPHOME_EVENT_ID_WIFI_STA_LOST_IP: {
ESP_LOGV(TAG, "Lost IP");
this->got_ipv4_address_ = false;
break;
}
case ESPHOME_EVENT_ID_WIFI_AP_START: {
ESP_LOGV(TAG, "AP start");
break;
}
case ESPHOME_EVENT_ID_WIFI_AP_STOP: {
ESP_LOGV(TAG, "AP stop");
break;
}
case ESPHOME_EVENT_ID_WIFI_AP_STACONNECTED: {
auto it = info.wifi_sta_connected;
auto &mac = it.bssid;
ESP_LOGV(TAG, "AP client connected MAC=%s", format_mac_address_pretty(mac).c_str());
break;
}
case ESPHOME_EVENT_ID_WIFI_AP_STADISCONNECTED: {
auto it = info.wifi_sta_disconnected;
auto &mac = it.bssid;
ESP_LOGV(TAG, "AP client disconnected MAC=%s", format_mac_address_pretty(mac).c_str());
break;
}
case ESPHOME_EVENT_ID_WIFI_AP_STAIPASSIGNED: {
ESP_LOGV(TAG, "AP client assigned IP");
break;
}
case ESPHOME_EVENT_ID_WIFI_AP_PROBEREQRECVED: {
auto it = info.wifi_ap_probereqrecved;
ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_address_pretty(it.mac).c_str(), it.rssi);
break;
}
default:
break;
}
}
WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() {
const auto status = WiFi.status();
if (status == WL_CONNECT_FAILED || status == WL_CONNECTION_LOST) {
return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED;
}
if (status == WL_NO_SSID_AVAIL) {
return WiFiSTAConnectStatus::ERROR_NETWORK_NOT_FOUND;
}
if (s_sta_connecting) {
return WiFiSTAConnectStatus::CONNECTING;
}
if (status == WL_CONNECTED) {
return WiFiSTAConnectStatus::CONNECTED;
}
return WiFiSTAConnectStatus::IDLE;
}
bool WiFiComponent::wifi_scan_start_(bool passive) {
// enable STA
if (!this->wifi_mode_(true, {}))
return false;
// need to use WiFi because of WiFiScanClass allocations :(
int16_t err = WiFi.scanNetworks(true, true, passive, 200);
if (err != WIFI_SCAN_RUNNING) {
ESP_LOGV(TAG, "WiFi.scanNetworks failed: %d", err);
return false;
}
return true;
}
void WiFiComponent::wifi_scan_done_callback_() {
this->scan_result_.clear();
int16_t num = WiFi.scanComplete();
if (num < 0)
return;
this->scan_result_.reserve(static_cast<unsigned int>(num));
for (int i = 0; i < num; i++) {
String ssid = WiFi.SSID(i);
wifi_auth_mode_t authmode = WiFi.encryptionType(i);
int32_t rssi = WiFi.RSSI(i);
uint8_t *bssid = WiFi.BSSID(i);
int32_t channel = WiFi.channel(i);
WiFiScanResult scan({bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]}, std::string(ssid.c_str()),
channel, rssi, authmode != WIFI_AUTH_OPEN, ssid.length() == 0);
this->scan_result_.push_back(scan);
}
WiFi.scanDelete();
this->scan_done_ = true;
}
#ifdef USE_WIFI_AP
bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
esp_err_t err;
// enable AP
if (!this->wifi_mode_({}, true))
return false;
// Check if the AP interface is initialized before using it
if (s_ap_netif == nullptr) {
ESP_LOGW(TAG, "AP interface not initialized");
return false;
}
esp_netif_ip_info_t info;
if (manual_ip.has_value()) {
info.ip = manual_ip->static_ip;
info.gw = manual_ip->gateway;
info.netmask = manual_ip->subnet;
} else {
info.ip = network::IPAddress(192, 168, 4, 1);
info.gw = network::IPAddress(192, 168, 4, 1);
info.netmask = network::IPAddress(255, 255, 255, 0);
}
err = esp_netif_dhcps_stop(s_ap_netif);
if (err != ESP_OK && err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED) {
ESP_LOGE(TAG, "esp_netif_dhcps_stop failed: %s", esp_err_to_name(err));
return false;
}
err = esp_netif_set_ip_info(s_ap_netif, &info);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_netif_set_ip_info failed: %d", err);
return false;
}
dhcps_lease_t lease;
lease.enable = true;
network::IPAddress start_address = network::IPAddress(&info.ip);
start_address += 99;
lease.start_ip = start_address;
ESP_LOGV(TAG, "DHCP server IP lease start: %s", start_address.str().c_str());
start_address += 10;
lease.end_ip = start_address;
ESP_LOGV(TAG, "DHCP server IP lease end: %s", start_address.str().c_str());
err = esp_netif_dhcps_option(s_ap_netif, ESP_NETIF_OP_SET, ESP_NETIF_REQUESTED_IP_ADDRESS, &lease, sizeof(lease));
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_netif_dhcps_option failed: %d", err);
return false;
}
err = esp_netif_dhcps_start(s_ap_netif);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_netif_dhcps_start failed: %d", err);
return false;
}
return true;
}
bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
// enable AP
if (!this->wifi_mode_({}, true))
return false;
wifi_config_t conf;
memset(&conf, 0, sizeof(conf));
if (ap.get_ssid().size() > sizeof(conf.ap.ssid)) {
ESP_LOGE(TAG, "AP SSID too long");
return false;
}
memcpy(reinterpret_cast<char *>(conf.ap.ssid), ap.get_ssid().c_str(), ap.get_ssid().size());
conf.ap.channel = ap.get_channel().value_or(1);
conf.ap.ssid_hidden = ap.get_ssid().size();
conf.ap.max_connection = 5;
conf.ap.beacon_interval = 100;
if (ap.get_password().empty()) {
conf.ap.authmode = WIFI_AUTH_OPEN;
*conf.ap.password = 0;
} else {
conf.ap.authmode = WIFI_AUTH_WPA2_PSK;
if (ap.get_password().size() > sizeof(conf.ap.password)) {
ESP_LOGE(TAG, "AP password too long");
return false;
}
memcpy(reinterpret_cast<char *>(conf.ap.password), ap.get_password().c_str(), ap.get_password().size());
}
// pairwise cipher of SoftAP, group cipher will be derived using this.
conf.ap.pairwise_cipher = WIFI_CIPHER_TYPE_CCMP;
esp_err_t err = esp_wifi_set_config(WIFI_IF_AP, &conf);
if (err != ESP_OK) {
ESP_LOGV(TAG, "esp_wifi_set_config failed: %d", err);
return false;
}
yield();
if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) {
ESP_LOGV(TAG, "wifi_ap_ip_config_ failed");
return false;
}
return true;
}
network::IPAddress WiFiComponent::wifi_soft_ap_ip() {
esp_netif_ip_info_t ip;
esp_netif_get_ip_info(s_ap_netif, &ip);
return network::IPAddress(&ip.ip);
}
#endif // USE_WIFI_AP
bool WiFiComponent::wifi_disconnect_() { return esp_wifi_disconnect(); }
bssid_t WiFiComponent::wifi_bssid() {
bssid_t bssid{};
uint8_t *raw_bssid = WiFi.BSSID();
if (raw_bssid != nullptr) {
for (size_t i = 0; i < bssid.size(); i++)
bssid[i] = raw_bssid[i];
}
return bssid;
}
std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); }
int8_t WiFiComponent::wifi_rssi() { return WiFi.RSSI(); }
int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); }
network::IPAddress WiFiComponent::wifi_subnet_mask_() { return network::IPAddress(WiFi.subnetMask()); }
network::IPAddress WiFiComponent::wifi_gateway_ip_() { return network::IPAddress(WiFi.gatewayIP()); }
network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return network::IPAddress(WiFi.dnsIP(num)); }
} // namespace wifi
} // namespace esphome
#endif // USE_ESP32_FRAMEWORK_ARDUINO
#endif

View File

@@ -1,7 +1,7 @@
#include "wifi_component.h" #include "wifi_component.h"
#ifdef USE_WIFI #ifdef USE_WIFI
#ifdef USE_ESP_IDF #ifdef USE_ESP32
#include <esp_event.h> #include <esp_event.h>
#include <esp_netif.h> #include <esp_netif.h>
@@ -1050,5 +1050,5 @@ network::IPAddress WiFiComponent::wifi_dns_ip_(int num) {
} // namespace wifi } // namespace wifi
} // namespace esphome } // namespace esphome
#endif // USE_ESP_IDF #endif // USE_ESP32
#endif #endif

View File

@@ -1,4 +1,5 @@
#include "zwave_proxy.h" #include "zwave_proxy.h"
#include "esphome/core/application.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/util.h" #include "esphome/core/util.h"
@@ -12,6 +13,7 @@ static constexpr uint8_t ZWAVE_COMMAND_GET_NETWORK_IDS = 0x20;
// GET_NETWORK_IDS response: [SOF][LENGTH][TYPE][CMD][HOME_ID(4)][NODE_ID][...] // GET_NETWORK_IDS response: [SOF][LENGTH][TYPE][CMD][HOME_ID(4)][NODE_ID][...]
static constexpr uint8_t ZWAVE_COMMAND_TYPE_RESPONSE = 0x01; // Response type field value static constexpr uint8_t ZWAVE_COMMAND_TYPE_RESPONSE = 0x01; // Response type field value
static constexpr uint8_t ZWAVE_MIN_GET_NETWORK_IDS_LENGTH = 9; // TYPE + CMD + HOME_ID(4) + NODE_ID + checksum static constexpr uint8_t ZWAVE_MIN_GET_NETWORK_IDS_LENGTH = 9; // TYPE + CMD + HOME_ID(4) + NODE_ID + checksum
static constexpr uint32_t HOME_ID_TIMEOUT_MS = 100; // Timeout for waiting for home ID during setup
static uint8_t calculate_frame_checksum(const uint8_t *data, uint8_t length) { static uint8_t calculate_frame_checksum(const uint8_t *data, uint8_t length) {
// Calculate Z-Wave frame checksum // Calculate Z-Wave frame checksum
@@ -26,7 +28,44 @@ static uint8_t calculate_frame_checksum(const uint8_t *data, uint8_t length) {
ZWaveProxy::ZWaveProxy() { global_zwave_proxy = this; } ZWaveProxy::ZWaveProxy() { global_zwave_proxy = this; }
void ZWaveProxy::setup() { this->send_simple_command_(ZWAVE_COMMAND_GET_NETWORK_IDS); } void ZWaveProxy::setup() {
this->setup_time_ = App.get_loop_component_start_time();
this->send_simple_command_(ZWAVE_COMMAND_GET_NETWORK_IDS);
}
float ZWaveProxy::get_setup_priority() const {
// Set up before API so home ID is ready when API starts
return setup_priority::BEFORE_CONNECTION;
}
bool ZWaveProxy::can_proceed() {
// If we already have the home ID, we can proceed
if (this->home_id_ready_) {
return true;
}
// Handle any pending responses
if (this->response_handler_()) {
ESP_LOGV(TAG, "Handled response during setup");
}
// Process UART data to check for home ID
this->process_uart_();
// Check if we got the home ID after processing
if (this->home_id_ready_) {
return true;
}
// Wait up to HOME_ID_TIMEOUT_MS for home ID response
const uint32_t now = App.get_loop_component_start_time();
if (now - this->setup_time_ > HOME_ID_TIMEOUT_MS) {
ESP_LOGW(TAG, "Timeout reading Home ID during setup");
return true; // Proceed anyway after timeout
}
return false; // Keep waiting
}
void ZWaveProxy::loop() { void ZWaveProxy::loop() {
if (this->response_handler_()) { if (this->response_handler_()) {
@@ -37,6 +76,11 @@ void ZWaveProxy::loop() {
this->api_connection_ = nullptr; // Unsubscribe if disconnected this->api_connection_ = nullptr; // Unsubscribe if disconnected
} }
this->process_uart_();
this->status_clear_warning();
}
void ZWaveProxy::process_uart_() {
while (this->available()) { while (this->available()) {
uint8_t byte; uint8_t byte;
if (!this->read_byte(&byte)) { if (!this->read_byte(&byte)) {
@@ -56,24 +100,24 @@ void ZWaveProxy::loop() {
// Extract the 4-byte Home ID starting at offset 4 // Extract the 4-byte Home ID starting at offset 4
// The frame parser has already validated the checksum and ensured all bytes are present // The frame parser has already validated the checksum and ensured all bytes are present
std::memcpy(this->home_id_.data(), this->buffer_.data() + 4, this->home_id_.size()); std::memcpy(this->home_id_.data(), this->buffer_.data() + 4, this->home_id_.size());
this->home_id_ready_ = true;
ESP_LOGI(TAG, "Home ID: %s", ESP_LOGI(TAG, "Home ID: %s",
format_hex_pretty(this->home_id_.data(), this->home_id_.size(), ':', false).c_str()); format_hex_pretty(this->home_id_.data(), this->home_id_.size(), ':', false).c_str());
} }
ESP_LOGV(TAG, "Sending to client: %s", YESNO(this->api_connection_ != nullptr)); ESP_LOGV(TAG, "Sending to client: %s", YESNO(this->api_connection_ != nullptr));
if (this->api_connection_ != nullptr) { if (this->api_connection_ != nullptr) {
// minimize copying to reduce CPU overhead // Zero-copy: point directly to our buffer
this->outgoing_proto_msg_.data = this->buffer_.data();
if (this->in_bootloader_) { if (this->in_bootloader_) {
this->outgoing_proto_msg_.data_len = this->buffer_index_; this->outgoing_proto_msg_.data_len = this->buffer_index_;
} else { } else {
// If this is a data frame, use frame length indicator + 2 (for SoF + checksum), else assume 1 for ACK/NAK/CAN // If this is a data frame, use frame length indicator + 2 (for SoF + checksum), else assume 1 for ACK/NAK/CAN
this->outgoing_proto_msg_.data_len = this->buffer_[0] == ZWAVE_FRAME_TYPE_START ? this->buffer_[1] + 2 : 1; this->outgoing_proto_msg_.data_len = this->buffer_[0] == ZWAVE_FRAME_TYPE_START ? this->buffer_[1] + 2 : 1;
} }
std::memcpy(this->outgoing_proto_msg_.data, this->buffer_.data(), this->outgoing_proto_msg_.data_len);
this->api_connection_->send_message(this->outgoing_proto_msg_, api::ZWaveProxyFrame::MESSAGE_TYPE); this->api_connection_->send_message(this->outgoing_proto_msg_, api::ZWaveProxyFrame::MESSAGE_TYPE);
} }
} }
} }
this->status_clear_warning();
} }
void ZWaveProxy::dump_config() { ESP_LOGCONFIG(TAG, "Z-Wave Proxy"); } void ZWaveProxy::dump_config() { ESP_LOGCONFIG(TAG, "Z-Wave Proxy"); }
@@ -228,7 +272,9 @@ void ZWaveProxy::parse_start_(uint8_t byte) {
} }
// Forward response (ACK/NAK/CAN) back to client for processing // Forward response (ACK/NAK/CAN) back to client for processing
if (this->api_connection_ != nullptr) { if (this->api_connection_ != nullptr) {
this->outgoing_proto_msg_.data[0] = byte; // Store single byte in buffer and point to it
this->buffer_[0] = byte;
this->outgoing_proto_msg_.data = this->buffer_.data();
this->outgoing_proto_msg_.data_len = 1; this->outgoing_proto_msg_.data_len = 1;
this->api_connection_->send_message(this->outgoing_proto_msg_, api::ZWaveProxyFrame::MESSAGE_TYPE); this->api_connection_->send_message(this->outgoing_proto_msg_, api::ZWaveProxyFrame::MESSAGE_TYPE);
} }

View File

@@ -11,6 +11,8 @@
namespace esphome { namespace esphome {
namespace zwave_proxy { namespace zwave_proxy {
static constexpr size_t MAX_ZWAVE_FRAME_SIZE = 257; // Maximum Z-Wave frame size
enum ZWaveResponseTypes : uint8_t { enum ZWaveResponseTypes : uint8_t {
ZWAVE_FRAME_TYPE_ACK = 0x06, ZWAVE_FRAME_TYPE_ACK = 0x06,
ZWAVE_FRAME_TYPE_CAN = 0x18, ZWAVE_FRAME_TYPE_CAN = 0x18,
@@ -44,6 +46,8 @@ class ZWaveProxy : public uart::UARTDevice, public Component {
void setup() override; void setup() override;
void loop() override; void loop() override;
void dump_config() override; void dump_config() override;
float get_setup_priority() const override;
bool can_proceed() override;
void zwave_proxy_request(api::APIConnection *api_connection, api::enums::ZWaveProxyRequestType type); void zwave_proxy_request(api::APIConnection *api_connection, api::enums::ZWaveProxyRequestType type);
api::APIConnection *get_api_connection() { return this->api_connection_; } api::APIConnection *get_api_connection() { return this->api_connection_; }
@@ -60,19 +64,24 @@ class ZWaveProxy : public uart::UARTDevice, public Component {
bool parse_byte_(uint8_t byte); // Returns true if frame parsing was completed (a frame is ready in the buffer) bool parse_byte_(uint8_t byte); // Returns true if frame parsing was completed (a frame is ready in the buffer)
void parse_start_(uint8_t byte); void parse_start_(uint8_t byte);
bool response_handler_(); bool response_handler_();
void process_uart_(); // Process all available UART data
api::APIConnection *api_connection_{nullptr}; // Current subscribed client // Pre-allocated message - always ready to send
api::ZWaveProxyFrame outgoing_proto_msg_;
std::array<uint8_t, MAX_ZWAVE_FRAME_SIZE> buffer_; // Fixed buffer for incoming data
std::array<uint8_t, 4> home_id_{0, 0, 0, 0}; // Fixed buffer for home ID std::array<uint8_t, 4> home_id_{0, 0, 0, 0}; // Fixed buffer for home ID
std::array<uint8_t, sizeof(api::ZWaveProxyFrame::data)> buffer_; // Fixed buffer for incoming data
// Pointers and 32-bit values (aligned together)
api::APIConnection *api_connection_{nullptr}; // Current subscribed client
uint32_t setup_time_{0}; // Time when setup() was called
// 8-bit values (grouped together to minimize padding)
uint8_t buffer_index_{0}; // Index for populating the data buffer uint8_t buffer_index_{0}; // Index for populating the data buffer
uint8_t end_frame_after_{0}; // Payload reception ends after this index uint8_t end_frame_after_{0}; // Payload reception ends after this index
uint8_t last_response_{0}; // Last response type sent uint8_t last_response_{0}; // Last response type sent
ZWaveParsingState parsing_state_{ZWAVE_PARSING_STATE_WAIT_START}; ZWaveParsingState parsing_state_{ZWAVE_PARSING_STATE_WAIT_START};
bool in_bootloader_{false}; // True if the device is detected to be in bootloader mode bool in_bootloader_{false}; // True if the device is detected to be in bootloader mode
bool home_id_ready_{false}; // True when home ID has been received from Z-Wave module
// Pre-allocated message - always ready to send
api::ZWaveProxyFrame outgoing_proto_msg_;
}; };
extern ZWaveProxy *global_zwave_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) extern ZWaveProxy *global_zwave_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)

View File

@@ -479,6 +479,12 @@ class EsphomeCleanMqttHandler(EsphomeCommandWebSocket):
return [*DASHBOARD_COMMAND, "clean-mqtt", config_file] return [*DASHBOARD_COMMAND, "clean-mqtt", config_file]
class EsphomeCleanPlatformHandler(EsphomeCommandWebSocket):
async def build_command(self, json_message: dict[str, Any]) -> list[str]:
config_file = settings.rel_path(json_message["configuration"])
return [*DASHBOARD_COMMAND, "clean-platform", config_file]
class EsphomeCleanHandler(EsphomeCommandWebSocket): class EsphomeCleanHandler(EsphomeCommandWebSocket):
async def build_command(self, json_message: dict[str, Any]) -> list[str]: async def build_command(self, json_message: dict[str, Any]) -> list[str]:
config_file = settings.rel_path(json_message["configuration"]) config_file = settings.rel_path(json_message["configuration"])
@@ -1313,6 +1319,7 @@ def make_app(debug=get_bool_env(ENV_DEV)) -> tornado.web.Application:
(f"{rel}compile", EsphomeCompileHandler), (f"{rel}compile", EsphomeCompileHandler),
(f"{rel}validate", EsphomeValidateHandler), (f"{rel}validate", EsphomeValidateHandler),
(f"{rel}clean-mqtt", EsphomeCleanMqttHandler), (f"{rel}clean-mqtt", EsphomeCleanMqttHandler),
(f"{rel}clean-platform", EsphomeCleanPlatformHandler),
(f"{rel}clean", EsphomeCleanHandler), (f"{rel}clean", EsphomeCleanHandler),
(f"{rel}vscode", EsphomeVscodeHandler), (f"{rel}vscode", EsphomeVscodeHandler),
(f"{rel}ace", EsphomeAceEditorHandler), (f"{rel}ace", EsphomeAceEditorHandler),

View File

@@ -323,19 +323,41 @@ def clean_build():
# Clean PlatformIO cache to resolve CMake compiler detection issues # Clean PlatformIO cache to resolve CMake compiler detection issues
# This helps when toolchain paths change or get corrupted # This helps when toolchain paths change or get corrupted
try: try:
from platformio.project.helpers import get_project_cache_dir from platformio.project.config import ProjectConfig
except ImportError: except ImportError:
# PlatformIO is not available, skip cache cleaning # PlatformIO is not available, skip cache cleaning
pass pass
else: else:
cache_dir = get_project_cache_dir() config = ProjectConfig.get_instance()
if cache_dir and cache_dir.strip(): cache_dir = Path(config.get("platformio", "cache_dir"))
cache_path = Path(cache_dir) if cache_dir.is_dir():
if cache_path.is_dir():
_LOGGER.info("Deleting PlatformIO cache %s", cache_dir) _LOGGER.info("Deleting PlatformIO cache %s", cache_dir)
shutil.rmtree(cache_dir) shutil.rmtree(cache_dir)
def clean_platform():
import shutil
# Clean entire build dir
if CORE.build_path.is_dir():
_LOGGER.info("Deleting %s", CORE.build_path)
shutil.rmtree(CORE.build_path)
# Clean PlatformIO project files
try:
from platformio.project.config import ProjectConfig
except ImportError:
# PlatformIO is not available, skip cleaning
pass
else:
config = ProjectConfig.get_instance()
for pio_dir in ["cache_dir", "packages_dir", "platforms_dir", "core_dir"]:
path = Path(config.get("platformio", pio_dir))
if path.is_dir():
_LOGGER.info("Deleting PlatformIO %s %s", pio_dir, path)
shutil.rmtree(path)
GITIGNORE_CONTENT = """# Gitignore settings for ESPHome GITIGNORE_CONTENT = """# Gitignore settings for ESPHome
# This is an example and may include too much for your use-case. # This is an example and may include too much for your use-case.
# You can modify this file to suit your needs. # You can modify this file to suit your needs.

View File

@@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
esptool==5.1.0 esptool==5.1.0
click==8.1.7 click==8.1.7
esphome-dashboard==20250904.0 esphome-dashboard==20250904.0
aioesphomeapi==41.7.0 aioesphomeapi==41.9.1
zeroconf==0.147.2 zeroconf==0.147.2
puremagic==1.30 puremagic==1.30
ruamel.yaml==0.18.15 # dashboard_import ruamel.yaml==0.18.15 # dashboard_import

View File

@@ -353,13 +353,34 @@ def create_field_type_info(
return FixedArrayRepeatedType(field, size_define) return FixedArrayRepeatedType(field, size_define)
return RepeatedTypeInfo(field) return RepeatedTypeInfo(field)
# Check for fixed_array_size option on bytes fields # Check for mutually exclusive options on bytes fields
if ( if field.type == 12:
field.type == 12 has_pointer_to_buffer = get_field_opt(field, pb.pointer_to_buffer, False)
and (fixed_size := get_field_opt(field, pb.fixed_array_size)) is not None fixed_size = get_field_opt(field, pb.fixed_array_size, None)
):
if has_pointer_to_buffer and fixed_size is not None:
raise ValueError(
f"Field '{field.name}' has both pointer_to_buffer and fixed_array_size. "
"These options are mutually exclusive. Use pointer_to_buffer for zero-copy "
"or fixed_array_size for traditional array storage."
)
if has_pointer_to_buffer:
# Zero-copy pointer approach - no size needed, will use size_t for length
return PointerToBytesBufferType(field, None)
if fixed_size is not None:
# Traditional fixed array approach with copy
return FixedArrayBytesType(field, fixed_size) return FixedArrayBytesType(field, fixed_size)
# Check for pointer_to_buffer option on string fields
if field.type == 9:
has_pointer_to_buffer = get_field_opt(field, pb.pointer_to_buffer, False)
if has_pointer_to_buffer:
# Zero-copy pointer approach for strings
return PointerToBytesBufferType(field, None)
# Special handling for bytes fields # Special handling for bytes fields
if field.type == 12: if field.type == 12:
return BytesType(field, needs_decode, needs_encode) return BytesType(field, needs_decode, needs_encode)
@@ -818,6 +839,91 @@ class BytesType(TypeInfo):
return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical bytes return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical bytes
class PointerToBytesBufferType(TypeInfo):
"""Type for bytes fields that use pointer_to_buffer option for zero-copy."""
@classmethod
def can_use_dump_field(cls) -> bool:
return False
def __init__(
self, field: descriptor.FieldDescriptorProto, size: int | None = None
) -> None:
super().__init__(field)
# Size is not used for pointer_to_buffer - we always use size_t for length
self.array_size = 0
@property
def cpp_type(self) -> str:
return "const uint8_t*"
@property
def default_value(self) -> str:
return "nullptr"
@property
def reference_type(self) -> str:
return "const uint8_t*"
@property
def const_reference_type(self) -> str:
return "const uint8_t*"
@property
def public_content(self) -> list[str]:
# Use uint16_t for length - max packet size is well below 65535
# Add pointer and length fields
return [
f"const uint8_t* {self.field_name}{{nullptr}};",
f"uint16_t {self.field_name}_len{{0}};",
]
@property
def encode_content(self) -> str:
return f"buffer.encode_bytes({self.number}, this->{self.field_name}, this->{self.field_name}_len);"
@property
def decode_length_content(self) -> str | None:
# Decode directly stores the pointer to avoid allocation
return f"""case {self.number}: {{
// Use raw data directly to avoid allocation
this->{self.field_name} = value.data();
this->{self.field_name}_len = value.size();
break;
}}"""
@property
def decode_length(self) -> str | None:
# This is handled in decode_length_content
return None
@property
def wire_type(self) -> WireType:
"""Get the wire type for this bytes field."""
return WireType.LENGTH_DELIMITED # Uses wire type 2
def dump(self, name: str) -> str:
return (
f"format_hex_pretty(this->{self.field_name}, this->{self.field_name}_len)"
)
@property
def dump_content(self) -> str:
# Custom dump that doesn't use dump_field template
return (
f'out.append(" {self.name}: ");\n'
+ f"out.append({self.dump(self.field_name)});\n"
+ 'out.append("\\n");'
)
def get_size_calculation(self, name: str, force: bool = False) -> str:
return f"size.add_length({self.number}, this->{self.field_name}_len);"
def get_estimated_size(self) -> int:
# field ID + length varint + typical data (assume small for pointer fields)
return self.calculate_field_id_size() + 2 + 16
class FixedArrayBytesType(TypeInfo): class FixedArrayBytesType(TypeInfo):
"""Special type for fixed-size byte arrays.""" """Special type for fixed-size byte arrays."""

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
from collections.abc import Generator from collections.abc import Generator
from dataclasses import dataclass from dataclasses import dataclass
import logging
from pathlib import Path from pathlib import Path
import re import re
from typing import Any from typing import Any
@@ -16,6 +17,7 @@ from esphome import platformio_api
from esphome.__main__ import ( from esphome.__main__ import (
Purpose, Purpose,
choose_upload_log_host, choose_upload_log_host,
command_clean_platform,
command_rename, command_rename,
command_update_all, command_update_all,
command_wizard, command_wizard,
@@ -1853,3 +1855,101 @@ esp32:
# Should not have any Python error messages # Should not have any Python error messages
assert "TypeError" not in clean_output assert "TypeError" not in clean_output
assert "can only concatenate str" not in clean_output assert "can only concatenate str" not in clean_output
def test_command_clean_platform_success(
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test command_clean_platform when writer.clean_platform() succeeds."""
args = MockArgs()
config = {}
# Set logger level to capture INFO messages
with (
caplog.at_level(logging.INFO),
patch("esphome.writer.clean_platform") as mock_clean_platform,
):
result = command_clean_platform(args, config)
assert result == 0
mock_clean_platform.assert_called_once()
# Check that success message was logged
assert "Done!" in caplog.text
def test_command_clean_platform_oserror(
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test command_clean_platform when writer.clean_platform() raises OSError."""
args = MockArgs()
config = {}
# Create a mock OSError with a specific message
mock_error = OSError("Permission denied: cannot delete directory")
# Set logger level to capture ERROR and INFO messages
with (
caplog.at_level(logging.INFO),
patch(
"esphome.writer.clean_platform", side_effect=mock_error
) as mock_clean_platform,
):
result = command_clean_platform(args, config)
assert result == 1
mock_clean_platform.assert_called_once()
# Check that error message was logged
assert (
"Error deleting platform files: Permission denied: cannot delete directory"
in caplog.text
)
# Should not have success message
assert "Done!" not in caplog.text
def test_command_clean_platform_oserror_no_message(
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test command_clean_platform when writer.clean_platform() raises OSError without message."""
args = MockArgs()
config = {}
# Create a mock OSError without a message
mock_error = OSError()
# Set logger level to capture ERROR and INFO messages
with (
caplog.at_level(logging.INFO),
patch(
"esphome.writer.clean_platform", side_effect=mock_error
) as mock_clean_platform,
):
result = command_clean_platform(args, config)
assert result == 1
mock_clean_platform.assert_called_once()
# Check that error message was logged (should show empty string for OSError without message)
assert "Error deleting platform files:" in caplog.text
# Should not have success message
assert "Done!" not in caplog.text
def test_command_clean_platform_args_and_config_ignored() -> None:
"""Test that command_clean_platform ignores args and config parameters."""
# Test with various args and config to ensure they don't affect the function
args1 = MockArgs(name="test1", file="test.bin")
config1 = {"wifi": {"ssid": "test"}}
args2 = MockArgs(name="test2", dashboard=True)
config2 = {"api": {}, "ota": {}}
with patch("esphome.writer.clean_platform") as mock_clean_platform:
result1 = command_clean_platform(args1, config1)
result2 = command_clean_platform(args2, config2)
assert result1 == 0
assert result2 == 0
assert mock_clean_platform.call_count == 2

View File

@@ -362,11 +362,17 @@ def test_clean_build(
assert dependencies_lock.exists() assert dependencies_lock.exists()
assert platformio_cache_dir.exists() assert platformio_cache_dir.exists()
# Mock PlatformIO's get_project_cache_dir # Mock PlatformIO's ProjectConfig cache_dir
with patch( with patch(
"platformio.project.helpers.get_project_cache_dir" "platformio.project.config.ProjectConfig.get_instance"
) as mock_get_cache_dir: ) as mock_get_instance:
mock_get_cache_dir.return_value = str(platformio_cache_dir) mock_config = MagicMock()
mock_get_instance.return_value = mock_config
mock_config.get.side_effect = (
lambda section, option: str(platformio_cache_dir)
if (section, option) == ("platformio", "cache_dir")
else ""
)
# Call the function # Call the function
with caplog.at_level("INFO"): with caplog.at_level("INFO"):
@@ -486,7 +492,7 @@ def test_clean_build_platformio_not_available(
# Mock import error for platformio # Mock import error for platformio
with ( with (
patch.dict("sys.modules", {"platformio.project.helpers": None}), patch.dict("sys.modules", {"platformio.project.config": None}),
caplog.at_level("INFO"), caplog.at_level("INFO"),
): ):
# Call the function # Call the function
@@ -520,11 +526,17 @@ def test_clean_build_empty_cache_dir(
# Verify pioenvs exists before # Verify pioenvs exists before
assert pioenvs_dir.exists() assert pioenvs_dir.exists()
# Mock PlatformIO's get_project_cache_dir to return whitespace # Mock PlatformIO's ProjectConfig cache_dir to return whitespace
with patch( with patch(
"platformio.project.helpers.get_project_cache_dir" "platformio.project.config.ProjectConfig.get_instance"
) as mock_get_cache_dir: ) as mock_get_instance:
mock_get_cache_dir.return_value = " " # Whitespace only mock_config = MagicMock()
mock_get_instance.return_value = mock_config
mock_config.get.side_effect = (
lambda section, option: " " # Whitespace only
if (section, option) == ("platformio", "cache_dir")
else ""
)
# Call the function # Call the function
with caplog.at_level("INFO"): with caplog.at_level("INFO"):
@@ -723,3 +735,126 @@ def test_write_cpp_with_duplicate_markers(
# Call should raise an error # Call should raise an error
with pytest.raises(EsphomeError, match="Found multiple auto generate code begins"): with pytest.raises(EsphomeError, match="Found multiple auto generate code begins"):
write_cpp("// New code") write_cpp("// New code")
@patch("esphome.writer.CORE")
def test_clean_platform(
mock_core: MagicMock,
tmp_path: Path,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test clean_platform removes build and PlatformIO dirs."""
# Create build directory
build_dir = tmp_path / "build"
build_dir.mkdir()
(build_dir / "dummy.txt").write_text("x")
# Create PlatformIO directories
pio_cache = tmp_path / "pio_cache"
pio_packages = tmp_path / "pio_packages"
pio_platforms = tmp_path / "pio_platforms"
pio_core = tmp_path / "pio_core"
for d in (pio_cache, pio_packages, pio_platforms, pio_core):
d.mkdir()
(d / "keep").write_text("x")
# Setup CORE
mock_core.build_path = build_dir
# Mock ProjectConfig
with patch(
"platformio.project.config.ProjectConfig.get_instance"
) as mock_get_instance:
mock_config = MagicMock()
mock_get_instance.return_value = mock_config
def cfg_get(section: str, option: str) -> str:
mapping = {
("platformio", "cache_dir"): str(pio_cache),
("platformio", "packages_dir"): str(pio_packages),
("platformio", "platforms_dir"): str(pio_platforms),
("platformio", "core_dir"): str(pio_core),
}
return mapping.get((section, option), "")
mock_config.get.side_effect = cfg_get
# Call
from esphome.writer import clean_platform
with caplog.at_level("INFO"):
clean_platform()
# Verify deletions
assert not build_dir.exists()
assert not pio_cache.exists()
assert not pio_packages.exists()
assert not pio_platforms.exists()
assert not pio_core.exists()
# Verify logging mentions each
assert "Deleting" in caplog.text
assert str(build_dir) in caplog.text
assert "PlatformIO cache" in caplog.text
assert "PlatformIO packages" in caplog.text
assert "PlatformIO platforms" in caplog.text
assert "PlatformIO core" in caplog.text
@patch("esphome.writer.CORE")
def test_clean_platform_platformio_not_available(
mock_core: MagicMock,
tmp_path: Path,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test clean_platform when PlatformIO is not available."""
# Build dir
build_dir = tmp_path / "build"
build_dir.mkdir()
mock_core.build_path = build_dir
# PlatformIO dirs that should remain untouched
pio_cache = tmp_path / "pio_cache"
pio_cache.mkdir()
from esphome.writer import clean_platform
with (
patch.dict("sys.modules", {"platformio.project.config": None}),
caplog.at_level("INFO"),
):
clean_platform()
# Build dir removed, PlatformIO dirs remain
assert not build_dir.exists()
assert pio_cache.exists()
# No PlatformIO-specific logs
assert "PlatformIO" not in caplog.text
@patch("esphome.writer.CORE")
def test_clean_platform_partial_exists(
mock_core: MagicMock,
tmp_path: Path,
) -> None:
"""Test clean_platform when only build dir exists."""
build_dir = tmp_path / "build"
build_dir.mkdir()
mock_core.build_path = build_dir
with patch(
"platformio.project.config.ProjectConfig.get_instance"
) as mock_get_instance:
mock_config = MagicMock()
mock_get_instance.return_value = mock_config
# Return non-existent dirs
mock_config.get.side_effect = lambda *_args, **_kw: str(
tmp_path / "does_not_exist"
)
from esphome.writer import clean_platform
clean_platform()
assert not build_dir.exists()