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

Merge branch 'dev' into auth_connection_checks_dry

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

View File

@@ -731,6 +731,16 @@ def command_clean_mqtt(args: ArgsProtocol, config: ConfigType) -> int | None:
return clean_mqtt(config, args)
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."
)

View File

@@ -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 {

View File

@@ -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 &current_tz = homeassistant::global_homeassistant_time->get_timezone();
// Compare without allocating a string
if (current_tz.length() != value.timezone_len ||
memcmp(current_tz.c_str(), value.timezone, value.timezone_len) != 0) {
homeassistant::global_homeassistant_time->set_timezone(
std::string(reinterpret_cast<const char *>(value.timezone), value.timezone_len));
}
}
#endif
}
@@ -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_();
}

View File

@@ -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

View File

@@ -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

View File

@@ -22,9 +22,12 @@ bool HelloRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
}
bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
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:

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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

View File

@@ -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); }

View File

@@ -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) {

View File

@@ -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:

View File

@@ -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.
*

View File

@@ -514,7 +514,8 @@ esp_err_t BluetoothConnection::read_characteristic(uint16_t handle) {
return this->check_and_log_error_("esp_ble_gattc_read_char", err);
}
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);
}

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -87,7 +87,7 @@ void HomeassistantNumber::control(float value) {
static constexpr auto ENTITY_ID_KEY = StringRef::from_lit("entity_id");
static constexpr auto 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

View File

@@ -44,7 +44,7 @@ void HomeassistantSwitch::write_state(bool state) {
static constexpr auto SERVICE_OFF = StringRef::from_lit("homeassistant.turn_off");
static constexpr auto 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

View File

@@ -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,

View File

@@ -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"},

View File

@@ -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

View File

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

View File

@@ -1,7 +1,7 @@
#include "wifi_component.h"
#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

View File

@@ -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);
}

View File

@@ -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_();
void process_uart_(); // Process all available UART data
api::APIConnection *api_connection_{nullptr}; // Current subscribed client
// Pre-allocated message - always ready to send
api::ZWaveProxyFrame outgoing_proto_msg_;
std::array<uint8_t, MAX_ZWAVE_FRAME_SIZE> buffer_; // Fixed buffer for incoming data
std::array<uint8_t, 4> home_id_{0, 0, 0, 0}; // Fixed buffer for home ID
std::array<uint8_t, sizeof(api::ZWaveProxyFrame::data)> buffer_; // Fixed buffer for incoming data
// Pointers and 32-bit values (aligned together)
api::APIConnection *api_connection_{nullptr}; // Current subscribed client
uint32_t setup_time_{0}; // Time when setup() was called
// 8-bit values (grouped together to minimize padding)
uint8_t buffer_index_{0}; // Index for populating the data buffer
uint8_t 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
// Pre-allocated message - always ready to send
api::ZWaveProxyFrame outgoing_proto_msg_;
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)

View File

@@ -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),

View File

@@ -323,19 +323,41 @@ 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():
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
# This is an example and may include too much for your use-case.
# You can modify this file to suit your needs.

View File

@@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
esptool==5.1.0
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

View File

@@ -353,13 +353,34 @@ 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
):
# 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:
return BytesType(field, needs_decode, needs_encode)
@@ -818,6 +839,91 @@ class BytesType(TypeInfo):
return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical bytes
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."""

View File

@@ -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

View File

@@ -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()