mirror of
https://github.com/esphome/esphome.git
synced 2025-09-26 07:02:21 +01:00
Merge branch 'dev' into auth_connection_checks_dry
This commit is contained in:
@@ -731,6 +731,16 @@ def command_clean_mqtt(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
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:
|
||||
from esphome import mqtt
|
||||
|
||||
@@ -929,9 +939,10 @@ POST_CONFIG_ACTIONS = {
|
||||
"upload": command_upload,
|
||||
"logs": command_logs,
|
||||
"run": command_run,
|
||||
"clean-mqtt": command_clean_mqtt,
|
||||
"mqtt-fingerprint": command_mqtt_fingerprint,
|
||||
"clean": command_clean,
|
||||
"clean-mqtt": command_clean_mqtt,
|
||||
"clean-platform": command_clean_platform,
|
||||
"mqtt-fingerprint": command_mqtt_fingerprint,
|
||||
"idedata": command_idedata,
|
||||
"rename": command_rename,
|
||||
"discover": command_discover,
|
||||
@@ -940,6 +951,7 @@ POST_CONFIG_ACTIONS = {
|
||||
SIMPLE_CONFIG_ACTIONS = [
|
||||
"clean",
|
||||
"clean-mqtt",
|
||||
"clean-platform",
|
||||
"config",
|
||||
]
|
||||
|
||||
@@ -1144,6 +1156,13 @@ def parse_args(argv):
|
||||
"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(
|
||||
"dashboard", help="Create a simple web server for a dashboard."
|
||||
)
|
||||
|
@@ -102,7 +102,7 @@ message HelloRequest {
|
||||
// For example "Home Assistant"
|
||||
// Not strictly necessary to send but nice for debugging
|
||||
// purposes.
|
||||
string client_info = 1;
|
||||
string client_info = 1 [(pointer_to_buffer) = true];
|
||||
uint32 api_version_major = 2;
|
||||
uint32 api_version_minor = 3;
|
||||
}
|
||||
@@ -139,7 +139,7 @@ message AuthenticationRequest {
|
||||
option (ifdef) = "USE_API_PASSWORD";
|
||||
|
||||
// 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.
|
||||
@@ -769,7 +769,7 @@ message HomeassistantServiceMap {
|
||||
string value = 2 [(no_zero_copy) = true];
|
||||
}
|
||||
|
||||
message HomeassistantServiceResponse {
|
||||
message HomeassistantActionRequest {
|
||||
option (id) = 35;
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (no_delay) = true;
|
||||
@@ -824,7 +824,7 @@ message GetTimeResponse {
|
||||
option (no_delay) = true;
|
||||
|
||||
fixed32 epoch_seconds = 1;
|
||||
string timezone = 2;
|
||||
string timezone = 2 [(pointer_to_buffer) = true];
|
||||
}
|
||||
|
||||
// ==================== USER-DEFINES SERVICES ====================
|
||||
@@ -1465,7 +1465,7 @@ message BluetoothDeviceRequest {
|
||||
|
||||
uint64 address = 1;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1571,7 +1571,7 @@ message BluetoothGATTWriteRequest {
|
||||
uint32 handle = 2;
|
||||
bool response = 3;
|
||||
|
||||
bytes data = 4;
|
||||
bytes data = 4 [(pointer_to_buffer) = true];
|
||||
}
|
||||
|
||||
message BluetoothGATTReadDescriptorRequest {
|
||||
@@ -1591,7 +1591,7 @@ message BluetoothGATTWriteDescriptorRequest {
|
||||
uint64 address = 1;
|
||||
uint32 handle = 2;
|
||||
|
||||
bytes data = 3;
|
||||
bytes data = 3 [(pointer_to_buffer) = true];
|
||||
}
|
||||
|
||||
message BluetoothGATTNotifyRequest {
|
||||
@@ -2292,7 +2292,7 @@ message ZWaveProxyFrame {
|
||||
option (ifdef) = "USE_ZWAVE_PROXY";
|
||||
option (no_delay) = true;
|
||||
|
||||
bytes data = 1 [(fixed_array_size) = 257];
|
||||
bytes data = 1 [(pointer_to_buffer) = true];
|
||||
}
|
||||
|
||||
enum ZWaveProxyRequestType {
|
||||
|
@@ -1078,8 +1078,14 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) {
|
||||
if (homeassistant::global_homeassistant_time != nullptr) {
|
||||
homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds);
|
||||
#ifdef USE_TIME_TIMEZONE
|
||||
if (!value.timezone.empty() && value.timezone != homeassistant::global_homeassistant_time->get_timezone()) {
|
||||
homeassistant::global_homeassistant_time->set_timezone(value.timezone);
|
||||
if (value.timezone_len > 0) {
|
||||
const std::string ¤t_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
|
||||
}
|
||||
@@ -1374,7 +1380,7 @@ void APIConnection::complete_authentication_() {
|
||||
}
|
||||
|
||||
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_api_version_major_ = msg.api_version_major;
|
||||
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) {
|
||||
AuthenticationResponse resp;
|
||||
// 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) {
|
||||
this->complete_authentication_();
|
||||
}
|
||||
|
@@ -10,8 +10,8 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/entity_base.h"
|
||||
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
@@ -132,10 +132,10 @@ class APIConnection final : public APIServerConnection {
|
||||
#endif
|
||||
bool try_send_log_message(int level, const char *tag, const char *line, size_t message_len);
|
||||
#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)
|
||||
return;
|
||||
this->send_message(call, HomeassistantServiceResponse::MESSAGE_TYPE);
|
||||
this->send_message(call, HomeassistantActionRequest::MESSAGE_TYPE);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
|
@@ -32,6 +32,13 @@ extend google.protobuf.FieldOptions {
|
||||
optional string fixed_array_size_define = 50010;
|
||||
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.
|
||||
//
|
||||
// When container_pointer is set on a repeated field, the generated message will
|
||||
|
@@ -22,9 +22,12 @@ bool HelloRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||
}
|
||||
bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 1:
|
||||
this->client_info = value.as_string();
|
||||
case 1: {
|
||||
// Use raw data directly to avoid allocation
|
||||
this->client_info = value.data();
|
||||
this->client_info_len = value.size();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -45,9 +48,12 @@ void HelloResponse::calculate_size(ProtoSize &size) const {
|
||||
#ifdef USE_API_PASSWORD
|
||||
bool AuthenticationRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 1:
|
||||
this->password = value.as_string();
|
||||
case 1: {
|
||||
// Use raw data directly to avoid allocation
|
||||
this->password = value.data();
|
||||
this->password_len = value.size();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
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->value.size());
|
||||
}
|
||||
void HomeassistantServiceResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
void HomeassistantActionRequest::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_string(1, this->service_ref_);
|
||||
for (auto &it : this->data) {
|
||||
buffer.encode_message(2, it, true);
|
||||
@@ -879,7 +885,7 @@ void HomeassistantServiceResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
}
|
||||
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_repeated_message(1, this->data);
|
||||
size.add_repeated_message(1, this->data_template);
|
||||
@@ -917,9 +923,12 @@ bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDel
|
||||
#endif
|
||||
bool GetTimeResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 2:
|
||||
this->timezone = value.as_string();
|
||||
case 2: {
|
||||
// Use raw data directly to avoid allocation
|
||||
this->timezone = value.data();
|
||||
this->timezone_len = value.size();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
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) {
|
||||
switch (field_id) {
|
||||
case 4:
|
||||
this->data = value.as_string();
|
||||
case 4: {
|
||||
// Use raw data directly to avoid allocation
|
||||
this->data = value.data();
|
||||
this->data_len = value.size();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
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) {
|
||||
switch (field_id) {
|
||||
case 3:
|
||||
this->data = value.as_string();
|
||||
case 3: {
|
||||
// Use raw data directly to avoid allocation
|
||||
this->data = value.data();
|
||||
this->data_len = value.size();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
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) {
|
||||
switch (field_id) {
|
||||
case 1: {
|
||||
const std::string &data_str = value.as_string();
|
||||
this->data_len = data_str.size();
|
||||
if (this->data_len > 257) {
|
||||
this->data_len = 257;
|
||||
}
|
||||
memcpy(this->data, data_str.data(), this->data_len);
|
||||
// Use raw data directly to avoid allocation
|
||||
this->data = value.data();
|
||||
this->data_len = value.size();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
|
@@ -330,11 +330,12 @@ class CommandProtoMessage : public ProtoDecodableMessage {
|
||||
class HelloRequest final : public ProtoDecodableMessage {
|
||||
public:
|
||||
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
|
||||
const char *message_name() const override { return "hello_request"; }
|
||||
#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_minor{0};
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@@ -370,11 +371,12 @@ class HelloResponse final : public ProtoMessage {
|
||||
class AuthenticationRequest final : public ProtoDecodableMessage {
|
||||
public:
|
||||
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
|
||||
const char *message_name() const override { return "authentication_request"; }
|
||||
#endif
|
||||
std::string password{};
|
||||
const uint8_t *password{nullptr};
|
||||
uint16_t password_len{0};
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -1098,12 +1100,12 @@ class HomeassistantServiceMap final : public ProtoMessage {
|
||||
|
||||
protected:
|
||||
};
|
||||
class HomeassistantServiceResponse final : public ProtoMessage {
|
||||
class HomeassistantActionRequest final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 35;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 113;
|
||||
#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
|
||||
StringRef service_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 {
|
||||
public:
|
||||
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
|
||||
const char *message_name() const override { return "get_time_response"; }
|
||||
#endif
|
||||
uint32_t epoch_seconds{0};
|
||||
std::string timezone{};
|
||||
const uint8_t *timezone{nullptr};
|
||||
uint16_t timezone_len{0};
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -1985,14 +1988,15 @@ class BluetoothGATTReadResponse final : public ProtoMessage {
|
||||
class BluetoothGATTWriteRequest final : public ProtoDecodableMessage {
|
||||
public:
|
||||
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
|
||||
const char *message_name() const override { return "bluetooth_gatt_write_request"; }
|
||||
#endif
|
||||
uint64_t address{0};
|
||||
uint32_t handle{0};
|
||||
bool response{false};
|
||||
std::string data{};
|
||||
const uint8_t *data{nullptr};
|
||||
uint16_t data_len{0};
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -2020,13 +2024,14 @@ class BluetoothGATTReadDescriptorRequest final : public ProtoDecodableMessage {
|
||||
class BluetoothGATTWriteDescriptorRequest final : public ProtoDecodableMessage {
|
||||
public:
|
||||
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
|
||||
const char *message_name() const override { return "bluetooth_gatt_write_descriptor_request"; }
|
||||
#endif
|
||||
uint64_t address{0};
|
||||
uint32_t handle{0};
|
||||
std::string data{};
|
||||
const uint8_t *data{nullptr};
|
||||
uint16_t data_len{0};
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -2929,11 +2934,11 @@ class UpdateCommandRequest final : public CommandProtoMessage {
|
||||
class ZWaveProxyFrame final : public ProtoDecodableMessage {
|
||||
public:
|
||||
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
|
||||
const char *message_name() const override { return "z_wave_proxy_frame"; }
|
||||
#endif
|
||||
uint8_t data[257]{};
|
||||
const uint8_t *data{nullptr};
|
||||
uint16_t data_len{0};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
|
@@ -670,7 +670,9 @@ template<> const char *proto_enum_to_string<enums::ZWaveProxyRequestType>(enums:
|
||||
|
||||
void HelloRequest::dump_to(std::string &out) const {
|
||||
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_minor", this->api_version_minor);
|
||||
}
|
||||
@@ -682,7 +684,12 @@ void HelloResponse::dump_to(std::string &out) const {
|
||||
dump_field(out, "name", this->name_ref_);
|
||||
}
|
||||
#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 {
|
||||
MessageDumpHelper helper(out, "AuthenticationResponse");
|
||||
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, "value", this->value);
|
||||
}
|
||||
void HomeassistantServiceResponse::dump_to(std::string &out) const {
|
||||
MessageDumpHelper helper(out, "HomeassistantServiceResponse");
|
||||
void HomeassistantActionRequest::dump_to(std::string &out) const {
|
||||
MessageDumpHelper helper(out, "HomeassistantActionRequest");
|
||||
dump_field(out, "service", this->service_ref_);
|
||||
for (const auto &it : this->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 {
|
||||
MessageDumpHelper helper(out, "GetTimeResponse");
|
||||
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
|
||||
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, "response", this->response);
|
||||
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");
|
||||
}
|
||||
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, "handle", this->handle);
|
||||
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");
|
||||
}
|
||||
void BluetoothGATTNotifyRequest::dump_to(std::string &out) const {
|
||||
|
@@ -217,12 +217,12 @@ void APIServer::dump_config() {
|
||||
}
|
||||
|
||||
#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
|
||||
const char *a = this->password_.c_str();
|
||||
uint32_t len_a = this->password_.length();
|
||||
const char *b = password.c_str();
|
||||
uint32_t len_b = password.length();
|
||||
const char *b = reinterpret_cast<const char *>(password_data);
|
||||
uint32_t len_b = password_len;
|
||||
|
||||
// disable optimization with volatile
|
||||
volatile uint32_t length = len_b;
|
||||
@@ -245,6 +245,7 @@ bool APIServer::check_password(const std::string &password) const {
|
||||
|
||||
return result == 0;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
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; }
|
||||
|
||||
#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_) {
|
||||
client->send_homeassistant_service_call(call);
|
||||
client->send_homeassistant_action(call);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
@@ -37,7 +37,7 @@ class APIServer : public Component, public Controller {
|
||||
void on_shutdown() override;
|
||||
bool teardown() override;
|
||||
#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);
|
||||
#endif
|
||||
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;
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
void send_homeassistant_service_call(const HomeassistantServiceResponse &call);
|
||||
void send_homeassistant_action(const HomeassistantActionRequest &call);
|
||||
|
||||
#endif
|
||||
#ifdef USE_API_SERVICES
|
||||
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
|
||||
|
@@ -179,9 +179,9 @@ class CustomAPIDevice {
|
||||
* @param service_name The service to call.
|
||||
*/
|
||||
void call_homeassistant_service(const std::string &service_name) {
|
||||
HomeassistantServiceResponse resp;
|
||||
HomeassistantActionRequest resp;
|
||||
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.
|
||||
@@ -199,7 +199,7 @@ class CustomAPIDevice {
|
||||
* @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) {
|
||||
HomeassistantServiceResponse resp;
|
||||
HomeassistantActionRequest resp;
|
||||
resp.set_service(StringRef(service_name));
|
||||
for (auto &it : data) {
|
||||
resp.data.emplace_back();
|
||||
@@ -207,7 +207,7 @@ class CustomAPIDevice {
|
||||
kv.set_key(StringRef(it.first));
|
||||
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.
|
||||
@@ -221,10 +221,10 @@ class CustomAPIDevice {
|
||||
* @param event_name The event to fire.
|
||||
*/
|
||||
void fire_homeassistant_event(const std::string &event_name) {
|
||||
HomeassistantServiceResponse resp;
|
||||
HomeassistantActionRequest resp;
|
||||
resp.set_service(StringRef(event_name));
|
||||
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.
|
||||
@@ -241,7 +241,7 @@ class CustomAPIDevice {
|
||||
* @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) {
|
||||
HomeassistantServiceResponse resp;
|
||||
HomeassistantActionRequest resp;
|
||||
resp.set_service(StringRef(service_name));
|
||||
resp.is_event = true;
|
||||
for (auto &it : data) {
|
||||
@@ -250,7 +250,7 @@ class CustomAPIDevice {
|
||||
kv.set_key(StringRef(it.first));
|
||||
kv.value = it.second;
|
||||
}
|
||||
global_api_server->send_homeassistant_service_call(resp);
|
||||
global_api_server->send_homeassistant_action(resp);
|
||||
}
|
||||
#else
|
||||
template<typename T = void> void call_homeassistant_service(const std::string &service_name) {
|
||||
|
@@ -3,10 +3,10 @@
|
||||
#include "api_server.h"
|
||||
#ifdef USE_API
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
#include <vector>
|
||||
#include "api_pb2.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
@@ -62,7 +62,7 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
||||
}
|
||||
|
||||
void play(Ts... x) override {
|
||||
HomeassistantServiceResponse resp;
|
||||
HomeassistantActionRequest resp;
|
||||
std::string service_value = this->service_.value(x...);
|
||||
resp.set_service(StringRef(service_value));
|
||||
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.value = it.value.value(x...);
|
||||
}
|
||||
this->parent_->send_homeassistant_service_call(resp);
|
||||
this->parent_->send_homeassistant_action(resp);
|
||||
}
|
||||
|
||||
protected:
|
||||
|
@@ -182,6 +182,10 @@ class ProtoLengthDelimited {
|
||||
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_); }
|
||||
|
||||
// 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.
|
||||
*
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
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()) {
|
||||
this->log_gatt_not_connected_("write", "characteristic");
|
||||
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(),
|
||||
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(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);
|
||||
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);
|
||||
}
|
||||
|
||||
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()) {
|
||||
this->log_gatt_not_connected_("write", "descriptor");
|
||||
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(),
|
||||
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(
|
||||
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);
|
||||
return this->check_and_log_error_("esp_ble_gattc_write_char_descr", err);
|
||||
}
|
||||
|
@@ -18,9 +18,9 @@ class BluetoothConnection final : public esp32_ble_client::BLEClientBase {
|
||||
esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override;
|
||||
|
||||
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 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);
|
||||
|
||||
|
@@ -305,7 +305,7 @@ void BluetoothProxy::bluetooth_gatt_write(const api::BluetoothGATTWriteRequest &
|
||||
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) {
|
||||
this->send_gatt_error(msg.address, msg.handle, err);
|
||||
}
|
||||
@@ -331,7 +331,7 @@ void BluetoothProxy::bluetooth_gatt_write_descriptor(const api::BluetoothGATTWri
|
||||
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) {
|
||||
this->send_gatt_error(msg.address, msg.handle, err);
|
||||
}
|
||||
|
@@ -87,7 +87,7 @@ void HomeassistantNumber::control(float value) {
|
||||
static constexpr auto ENTITY_ID_KEY = StringRef::from_lit("entity_id");
|
||||
static constexpr auto VALUE_KEY = StringRef::from_lit("value");
|
||||
|
||||
api::HomeassistantServiceResponse resp;
|
||||
api::HomeassistantActionRequest resp;
|
||||
resp.set_service(SERVICE_NAME);
|
||||
|
||||
resp.data.emplace_back();
|
||||
@@ -100,7 +100,7 @@ void HomeassistantNumber::control(float value) {
|
||||
entity_value.set_key(VALUE_KEY);
|
||||
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
|
||||
|
@@ -44,7 +44,7 @@ void HomeassistantSwitch::write_state(bool state) {
|
||||
static constexpr auto SERVICE_OFF = StringRef::from_lit("homeassistant.turn_off");
|
||||
static constexpr auto ENTITY_ID_KEY = StringRef::from_lit("entity_id");
|
||||
|
||||
api::HomeassistantServiceResponse resp;
|
||||
api::HomeassistantActionRequest resp;
|
||||
if (state) {
|
||||
resp.set_service(SERVICE_ON);
|
||||
} else {
|
||||
@@ -56,7 +56,7 @@ void HomeassistantSwitch::write_state(bool state) {
|
||||
entity_id_kv.set_key(ENTITY_ID_KEY);
|
||||
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
|
||||
|
@@ -125,8 +125,8 @@ EAP_AUTH_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_USERNAME): cv.string_strict,
|
||||
cv.Optional(CONF_PASSWORD): cv.string_strict,
|
||||
cv.Optional(CONF_CERTIFICATE_AUTHORITY): wpa2_eap.validate_certificate,
|
||||
cv.SplitDefault(CONF_TTLS_PHASE_2, esp32_idf="mschapv2"): cv.All(
|
||||
cv.enum(TTLS_PHASE_2), cv.only_with_esp_idf
|
||||
cv.SplitDefault(CONF_TTLS_PHASE_2, esp32="mschapv2"): cv.All(
|
||||
cv.enum(TTLS_PHASE_2), cv.only_on_esp32
|
||||
),
|
||||
cv.Inclusive(
|
||||
CONF_CERTIFICATE, "certificate_and_key"
|
||||
@@ -280,11 +280,11 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.SplitDefault(CONF_OUTPUT_POWER, esp8266=20.0): cv.All(
|
||||
cv.decibel, cv.float_range(min=8.5, max=20.5)
|
||||
),
|
||||
cv.SplitDefault(CONF_ENABLE_BTM, esp32_idf=False): cv.All(
|
||||
cv.boolean, cv.only_with_esp_idf
|
||||
cv.SplitDefault(CONF_ENABLE_BTM, esp32=False): cv.All(
|
||||
cv.boolean, cv.only_on_esp32
|
||||
),
|
||||
cv.SplitDefault(CONF_ENABLE_RRM, esp32_idf=False): cv.All(
|
||||
cv.boolean, cv.only_with_esp_idf
|
||||
cv.SplitDefault(CONF_ENABLE_RRM, esp32=False): cv.All(
|
||||
cv.boolean, cv.only_on_esp32
|
||||
),
|
||||
cv.Optional(CONF_PASSIVE_SCAN, default=False): cv.boolean,
|
||||
cv.Optional("enable_mdns"): cv.invalid(
|
||||
@@ -416,10 +416,10 @@ async def to_code(config):
|
||||
|
||||
if CORE.is_esp8266:
|
||||
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)
|
||||
|
||||
if CORE.is_esp32 and CORE.using_esp_idf:
|
||||
if CORE.is_esp32:
|
||||
if config[CONF_ENABLE_BTM] or config[CONF_ENABLE_RRM]:
|
||||
add_idf_sdkconfig_option("CONFIG_WPA_11KV_SUPPORT", True)
|
||||
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(
|
||||
{
|
||||
"wifi_component_esp32_arduino.cpp": {PlatformFramework.ESP32_ARDUINO},
|
||||
"wifi_component_esp_idf.cpp": {PlatformFramework.ESP32_IDF},
|
||||
"wifi_component_esp_idf.cpp": {
|
||||
PlatformFramework.ESP32_IDF,
|
||||
PlatformFramework.ESP32_ARDUINO,
|
||||
},
|
||||
"wifi_component_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
|
||||
"wifi_component_libretiny.cpp": {
|
||||
PlatformFramework.BK72XX_ARDUINO,
|
||||
|
@@ -3,7 +3,7 @@
|
||||
#include <cinttypes>
|
||||
#include <map>
|
||||
|
||||
#ifdef USE_ESP_IDF
|
||||
#ifdef USE_ESP32
|
||||
#if (ESP_IDF_VERSION_MAJOR >= 5 && ESP_IDF_VERSION_MINOR >= 1)
|
||||
#include <esp_eap_client.h>
|
||||
#else
|
||||
@@ -11,7 +11,7 @@
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_ESP_IDF)
|
||||
#if defined(USE_ESP32)
|
||||
#include <esp_wifi.h>
|
||||
#endif
|
||||
#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, " Username: " LOG_SECRET("'%s'"), eap_config.username.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
|
||||
std::map<esp_eap_ttls_phase2_types, std::string> phase2types = {{ESP_EAP_TTLS_PHASE2_PAP, "pap"},
|
||||
{ESP_EAP_TTLS_PHASE2_CHAP, "chap"},
|
||||
|
@@ -20,7 +20,7 @@
|
||||
#include <WiFi.h>
|
||||
#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)
|
||||
#include <esp_eap_client.h>
|
||||
#else
|
||||
@@ -113,7 +113,7 @@ struct EAPAuth {
|
||||
const char *client_cert;
|
||||
const char *client_key;
|
||||
// used for EAP-TTLS
|
||||
#ifdef USE_ESP_IDF
|
||||
#ifdef USE_ESP32
|
||||
esp_eap_ttls_phase2_types ttls_phase_2;
|
||||
#endif
|
||||
};
|
||||
@@ -199,7 +199,7 @@ enum WiFiPowerSaveMode : uint8_t {
|
||||
WIFI_POWER_SAVE_HIGH,
|
||||
};
|
||||
|
||||
#ifdef USE_ESP_IDF
|
||||
#ifdef USE_ESP32
|
||||
struct IDFWiFiEvent;
|
||||
#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_scan_done_callback_();
|
||||
#endif
|
||||
#ifdef USE_ESP_IDF
|
||||
#ifdef USE_ESP32
|
||||
void wifi_process_event_(IDFWiFiEvent *data);
|
||||
#endif
|
||||
|
||||
|
@@ -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, ¤t_conf);
|
||||
if (err != ERR_OK) {
|
||||
ESP_LOGW(TAG, "esp_wifi_get_config failed: %s", esp_err_to_name(err));
|
||||
// can continue
|
||||
}
|
||||
|
||||
if (memcmp(¤t_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
|
@@ -1,7 +1,7 @@
|
||||
#include "wifi_component.h"
|
||||
|
||||
#ifdef USE_WIFI
|
||||
#ifdef USE_ESP_IDF
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include <esp_event.h>
|
||||
#include <esp_netif.h>
|
||||
@@ -1050,5 +1050,5 @@ network::IPAddress WiFiComponent::wifi_dns_ip_(int num) {
|
||||
} // namespace wifi
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP_IDF
|
||||
#endif // USE_ESP32
|
||||
#endif
|
||||
|
@@ -1,4 +1,5 @@
|
||||
#include "zwave_proxy.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.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][...]
|
||||
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 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) {
|
||||
// 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; }
|
||||
|
||||
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() {
|
||||
if (this->response_handler_()) {
|
||||
@@ -37,6 +76,11 @@ void ZWaveProxy::loop() {
|
||||
this->api_connection_ = nullptr; // Unsubscribe if disconnected
|
||||
}
|
||||
|
||||
this->process_uart_();
|
||||
this->status_clear_warning();
|
||||
}
|
||||
|
||||
void ZWaveProxy::process_uart_() {
|
||||
while (this->available()) {
|
||||
uint8_t byte;
|
||||
if (!this->read_byte(&byte)) {
|
||||
@@ -56,24 +100,24 @@ void ZWaveProxy::loop() {
|
||||
// Extract the 4-byte Home ID starting at offset 4
|
||||
// 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());
|
||||
this->home_id_ready_ = true;
|
||||
ESP_LOGI(TAG, "Home ID: %s",
|
||||
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));
|
||||
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_) {
|
||||
this->outgoing_proto_msg_.data_len = this->buffer_index_;
|
||||
} else {
|
||||
// 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;
|
||||
}
|
||||
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->status_clear_warning();
|
||||
}
|
||||
|
||||
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
|
||||
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->api_connection_->send_message(this->outgoing_proto_msg_, api::ZWaveProxyFrame::MESSAGE_TYPE);
|
||||
}
|
||||
|
@@ -11,6 +11,8 @@
|
||||
namespace esphome {
|
||||
namespace zwave_proxy {
|
||||
|
||||
static constexpr size_t MAX_ZWAVE_FRAME_SIZE = 257; // Maximum Z-Wave frame size
|
||||
|
||||
enum ZWaveResponseTypes : uint8_t {
|
||||
ZWAVE_FRAME_TYPE_ACK = 0x06,
|
||||
ZWAVE_FRAME_TYPE_CAN = 0x18,
|
||||
@@ -44,6 +46,8 @@ class ZWaveProxy : public uart::UARTDevice, public Component {
|
||||
void setup() override;
|
||||
void loop() 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);
|
||||
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)
|
||||
void parse_start_(uint8_t byte);
|
||||
bool response_handler_();
|
||||
|
||||
api::APIConnection *api_connection_{nullptr}; // Current subscribed client
|
||||
|
||||
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
|
||||
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 last_response_{0}; // Last response type sent
|
||||
ZWaveParsingState parsing_state_{ZWAVE_PARSING_STATE_WAIT_START};
|
||||
bool in_bootloader_{false}; // True if the device is detected to be in bootloader mode
|
||||
void process_uart_(); // Process all available UART data
|
||||
|
||||
// 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
|
||||
|
||||
// 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 end_frame_after_{0}; // Payload reception ends after this index
|
||||
uint8_t last_response_{0}; // Last response type sent
|
||||
ZWaveParsingState parsing_state_{ZWAVE_PARSING_STATE_WAIT_START};
|
||||
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
|
||||
};
|
||||
|
||||
extern ZWaveProxy *global_zwave_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
@@ -479,6 +479,12 @@ class EsphomeCleanMqttHandler(EsphomeCommandWebSocket):
|
||||
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):
|
||||
async def build_command(self, json_message: dict[str, Any]) -> list[str]:
|
||||
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}validate", EsphomeValidateHandler),
|
||||
(f"{rel}clean-mqtt", EsphomeCleanMqttHandler),
|
||||
(f"{rel}clean-platform", EsphomeCleanPlatformHandler),
|
||||
(f"{rel}clean", EsphomeCleanHandler),
|
||||
(f"{rel}vscode", EsphomeVscodeHandler),
|
||||
(f"{rel}ace", EsphomeAceEditorHandler),
|
||||
|
@@ -323,17 +323,39 @@ def clean_build():
|
||||
# Clean PlatformIO cache to resolve CMake compiler detection issues
|
||||
# This helps when toolchain paths change or get corrupted
|
||||
try:
|
||||
from platformio.project.helpers import get_project_cache_dir
|
||||
from platformio.project.config import ProjectConfig
|
||||
except ImportError:
|
||||
# PlatformIO is not available, skip cache cleaning
|
||||
pass
|
||||
else:
|
||||
cache_dir = get_project_cache_dir()
|
||||
if cache_dir and cache_dir.strip():
|
||||
cache_path = Path(cache_dir)
|
||||
if cache_path.is_dir():
|
||||
_LOGGER.info("Deleting PlatformIO cache %s", cache_dir)
|
||||
shutil.rmtree(cache_dir)
|
||||
config = ProjectConfig.get_instance()
|
||||
cache_dir = Path(config.get("platformio", "cache_dir"))
|
||||
if cache_dir.is_dir():
|
||||
_LOGGER.info("Deleting PlatformIO cache %s", 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
|
||||
|
@@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
|
||||
esptool==5.1.0
|
||||
click==8.1.7
|
||||
esphome-dashboard==20250904.0
|
||||
aioesphomeapi==41.7.0
|
||||
aioesphomeapi==41.9.1
|
||||
zeroconf==0.147.2
|
||||
puremagic==1.30
|
||||
ruamel.yaml==0.18.15 # dashboard_import
|
||||
|
@@ -353,12 +353,33 @@ def create_field_type_info(
|
||||
return FixedArrayRepeatedType(field, size_define)
|
||||
return RepeatedTypeInfo(field)
|
||||
|
||||
# Check for fixed_array_size option on bytes fields
|
||||
if (
|
||||
field.type == 12
|
||||
and (fixed_size := get_field_opt(field, pb.fixed_array_size)) is not None
|
||||
):
|
||||
return FixedArrayBytesType(field, fixed_size)
|
||||
# Check for mutually exclusive options on bytes fields
|
||||
if field.type == 12:
|
||||
has_pointer_to_buffer = get_field_opt(field, pb.pointer_to_buffer, False)
|
||||
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)
|
||||
|
||||
# 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
|
||||
if field.type == 12:
|
||||
@@ -818,6 +839,91 @@ class BytesType(TypeInfo):
|
||||
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):
|
||||
"""Special type for fixed-size byte arrays."""
|
||||
|
||||
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Generator
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import re
|
||||
from typing import Any
|
||||
@@ -16,6 +17,7 @@ from esphome import platformio_api
|
||||
from esphome.__main__ import (
|
||||
Purpose,
|
||||
choose_upload_log_host,
|
||||
command_clean_platform,
|
||||
command_rename,
|
||||
command_update_all,
|
||||
command_wizard,
|
||||
@@ -1853,3 +1855,101 @@ esp32:
|
||||
# Should not have any Python error messages
|
||||
assert "TypeError" 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
|
||||
|
@@ -362,11 +362,17 @@ def test_clean_build(
|
||||
assert dependencies_lock.exists()
|
||||
assert platformio_cache_dir.exists()
|
||||
|
||||
# Mock PlatformIO's get_project_cache_dir
|
||||
# Mock PlatformIO's ProjectConfig cache_dir
|
||||
with patch(
|
||||
"platformio.project.helpers.get_project_cache_dir"
|
||||
) as mock_get_cache_dir:
|
||||
mock_get_cache_dir.return_value = str(platformio_cache_dir)
|
||||
"platformio.project.config.ProjectConfig.get_instance"
|
||||
) as mock_get_instance:
|
||||
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
|
||||
with caplog.at_level("INFO"):
|
||||
@@ -486,7 +492,7 @@ def test_clean_build_platformio_not_available(
|
||||
|
||||
# Mock import error for platformio
|
||||
with (
|
||||
patch.dict("sys.modules", {"platformio.project.helpers": None}),
|
||||
patch.dict("sys.modules", {"platformio.project.config": None}),
|
||||
caplog.at_level("INFO"),
|
||||
):
|
||||
# Call the function
|
||||
@@ -520,11 +526,17 @@ def test_clean_build_empty_cache_dir(
|
||||
# Verify pioenvs exists before
|
||||
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(
|
||||
"platformio.project.helpers.get_project_cache_dir"
|
||||
) as mock_get_cache_dir:
|
||||
mock_get_cache_dir.return_value = " " # Whitespace only
|
||||
"platformio.project.config.ProjectConfig.get_instance"
|
||||
) as mock_get_instance:
|
||||
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
|
||||
with caplog.at_level("INFO"):
|
||||
@@ -723,3 +735,126 @@ def test_write_cpp_with_duplicate_markers(
|
||||
# Call should raise an error
|
||||
with pytest.raises(EsphomeError, match="Found multiple auto generate code begins"):
|
||||
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()
|
||||
|
Reference in New Issue
Block a user