Merge branch 'noise_api_zero_copy' into integration

This commit is contained in:
J. Nick Koston
2025-12-15 13:19:52 -06:00
13 changed files with 138 additions and 57 deletions

View File

@@ -477,7 +477,7 @@ message FanCommandRequest {
bool has_speed_level = 10;
int32 speed_level = 11;
bool has_preset_mode = 12;
string preset_mode = 13;
string preset_mode = 13 [(pointer_to_buffer) = true];
uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"];
}
@@ -747,7 +747,7 @@ message NoiseEncryptionSetKeyRequest {
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_API_NOISE";
bytes key = 1;
bytes key = 1 [(pointer_to_buffer) = true];
}
message NoiseEncryptionSetKeyResponse {
@@ -1091,11 +1091,11 @@ message ClimateCommandRequest {
bool has_swing_mode = 14;
ClimateSwingMode swing_mode = 15;
bool has_custom_fan_mode = 16;
string custom_fan_mode = 17;
string custom_fan_mode = 17 [(pointer_to_buffer) = true];
bool has_preset = 18;
ClimatePreset preset = 19;
bool has_custom_preset = 20;
string custom_preset = 21;
string custom_preset = 21 [(pointer_to_buffer) = true];
bool has_target_humidity = 22;
float target_humidity = 23;
uint32 device_id = 24 [(field_ifdef) = "USE_DEVICES"];

View File

@@ -447,7 +447,7 @@ void APIConnection::fan_command(const FanCommandRequest &msg) {
if (msg.has_direction)
call.set_direction(static_cast<fan::FanDirection>(msg.direction));
if (msg.has_preset_mode)
call.set_preset_mode(msg.preset_mode);
call.set_preset_mode(reinterpret_cast<const char *>(msg.preset_mode), msg.preset_mode_len);
call.perform();
}
#endif
@@ -712,11 +712,11 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) {
if (msg.has_fan_mode)
call.set_fan_mode(static_cast<climate::ClimateFanMode>(msg.fan_mode));
if (msg.has_custom_fan_mode)
call.set_fan_mode(msg.custom_fan_mode);
call.set_fan_mode(reinterpret_cast<const char *>(msg.custom_fan_mode), msg.custom_fan_mode_len);
if (msg.has_preset)
call.set_preset(static_cast<climate::ClimatePreset>(msg.preset));
if (msg.has_custom_preset)
call.set_preset(msg.custom_preset);
call.set_preset(reinterpret_cast<const char *>(msg.custom_preset), msg.custom_preset_len);
if (msg.has_swing_mode)
call.set_swing_mode(static_cast<climate::ClimateSwingMode>(msg.swing_mode));
call.perform();
@@ -1663,13 +1663,13 @@ bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryption
resp.success = false;
psk_t psk{};
if (msg.key.empty()) {
if (msg.key_len == 0) {
if (this->parent_->clear_noise_psk(true)) {
resp.success = true;
} else {
ESP_LOGW(TAG, "Failed to clear encryption key");
}
} else if (base64_decode(msg.key, psk.data(), psk.size()) != psk.size()) {
} else if (base64_decode(msg.key, msg.key_len, psk.data(), psk.size()) != psk.size()) {
ESP_LOGW(TAG, "Invalid encryption key length");
} else if (!this->parent_->save_noise_psk(psk, true)) {
ESP_LOGW(TAG, "Failed to save encryption key");

View File

@@ -447,9 +447,12 @@ bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
}
bool FanCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 13:
this->preset_mode = value.as_string();
case 13: {
// Use raw data directly to avoid allocation
this->preset_mode = value.data();
this->preset_mode_len = value.size();
break;
}
default:
return false;
}
@@ -855,9 +858,12 @@ void SubscribeLogsResponse::calculate_size(ProtoSize &size) const {
#ifdef USE_API_NOISE
bool NoiseEncryptionSetKeyRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 1:
this->key = value.as_string();
case 1: {
// Use raw data directly to avoid allocation
this->key = value.data();
this->key_len = value.size();
break;
}
default:
return false;
}
@@ -1392,12 +1398,18 @@ bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value)
}
bool ClimateCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 17:
this->custom_fan_mode = value.as_string();
case 17: {
// Use raw data directly to avoid allocation
this->custom_fan_mode = value.data();
this->custom_fan_mode_len = value.size();
break;
case 21:
this->custom_preset = value.as_string();
}
case 21: {
// Use raw data directly to avoid allocation
this->custom_preset = value.data();
this->custom_preset_len = value.size();
break;
}
default:
return false;
}

View File

@@ -765,7 +765,7 @@ class FanStateResponse final : public StateResponseProtoMessage {
class FanCommandRequest final : public CommandProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 31;
static constexpr uint8_t ESTIMATED_SIZE = 38;
static constexpr uint8_t ESTIMATED_SIZE = 48;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "fan_command_request"; }
#endif
@@ -778,7 +778,8 @@ class FanCommandRequest final : public CommandProtoMessage {
bool has_speed_level{false};
int32_t speed_level{0};
bool has_preset_mode{false};
std::string preset_mode{};
const uint8_t *preset_mode{nullptr};
uint16_t preset_mode_len{0};
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1053,11 +1054,12 @@ class SubscribeLogsResponse final : public ProtoMessage {
class NoiseEncryptionSetKeyRequest final : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 124;
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 "noise_encryption_set_key_request"; }
#endif
std::string key{};
const uint8_t *key{nullptr};
uint16_t key_len{0};
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1475,7 +1477,7 @@ class ClimateStateResponse final : public StateResponseProtoMessage {
class ClimateCommandRequest final : public CommandProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 48;
static constexpr uint8_t ESTIMATED_SIZE = 84;
static constexpr uint8_t ESTIMATED_SIZE = 104;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "climate_command_request"; }
#endif
@@ -1492,11 +1494,13 @@ class ClimateCommandRequest final : public CommandProtoMessage {
bool has_swing_mode{false};
enums::ClimateSwingMode swing_mode{};
bool has_custom_fan_mode{false};
std::string custom_fan_mode{};
const uint8_t *custom_fan_mode{nullptr};
uint16_t custom_fan_mode_len{0};
bool has_preset{false};
enums::ClimatePreset preset{};
bool has_custom_preset{false};
std::string custom_preset{};
const uint8_t *custom_preset{nullptr};
uint16_t custom_preset_len{0};
bool has_target_humidity{false};
float target_humidity{0.0f};
#ifdef HAS_PROTO_MESSAGE_DUMP

View File

@@ -923,7 +923,9 @@ void FanCommandRequest::dump_to(std::string &out) const {
dump_field(out, "has_speed_level", this->has_speed_level);
dump_field(out, "speed_level", this->speed_level);
dump_field(out, "has_preset_mode", this->has_preset_mode);
dump_field(out, "preset_mode", this->preset_mode);
out.append(" preset_mode: ");
out.append(format_hex_pretty(this->preset_mode, this->preset_mode_len));
out.append("\n");
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
#endif
@@ -1113,7 +1115,7 @@ void SubscribeLogsResponse::dump_to(std::string &out) const {
void NoiseEncryptionSetKeyRequest::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "NoiseEncryptionSetKeyRequest");
out.append(" key: ");
out.append(format_hex_pretty(reinterpret_cast<const uint8_t *>(this->key.data()), this->key.size()));
out.append(format_hex_pretty(this->key, this->key_len));
out.append("\n");
}
void NoiseEncryptionSetKeyResponse::dump_to(std::string &out) const { dump_field(out, "success", this->success); }
@@ -1374,11 +1376,15 @@ void ClimateCommandRequest::dump_to(std::string &out) const {
dump_field(out, "has_swing_mode", this->has_swing_mode);
dump_field(out, "swing_mode", static_cast<enums::ClimateSwingMode>(this->swing_mode));
dump_field(out, "has_custom_fan_mode", this->has_custom_fan_mode);
dump_field(out, "custom_fan_mode", this->custom_fan_mode);
out.append(" custom_fan_mode: ");
out.append(format_hex_pretty(this->custom_fan_mode, this->custom_fan_mode_len));
out.append("\n");
dump_field(out, "has_preset", this->has_preset);
dump_field(out, "preset", static_cast<enums::ClimatePreset>(this->preset));
dump_field(out, "has_custom_preset", this->has_custom_preset);
dump_field(out, "custom_preset", this->custom_preset);
out.append(" custom_preset: ");
out.append(format_hex_pretty(this->custom_preset, this->custom_preset_len));
out.append("\n");
dump_field(out, "has_target_humidity", this->has_target_humidity);
dump_field(out, "target_humidity", this->target_humidity);
#ifdef USE_DEVICES

View File

@@ -2,6 +2,7 @@
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/macros.h"
#include <strings.h>
namespace esphome::climate {
@@ -190,24 +191,30 @@ ClimateCall &ClimateCall::set_fan_mode(ClimateFanMode fan_mode) {
}
ClimateCall &ClimateCall::set_fan_mode(const char *custom_fan_mode) {
return this->set_fan_mode(custom_fan_mode, strlen(custom_fan_mode));
}
ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) {
return this->set_fan_mode(fan_mode.data(), fan_mode.size());
}
ClimateCall &ClimateCall::set_fan_mode(const char *custom_fan_mode, size_t len) {
// Check if it's a standard enum mode first
for (const auto &mode_entry : CLIMATE_FAN_MODES_BY_STR) {
if (str_equals_case_insensitive(custom_fan_mode, mode_entry.str)) {
if (strncasecmp(custom_fan_mode, mode_entry.str, len) == 0 && mode_entry.str[len] == '\0') {
return this->set_fan_mode(static_cast<ClimateFanMode>(mode_entry.value));
}
}
// Find the matching pointer from parent climate device
if (const char *mode_ptr = this->parent_->find_custom_fan_mode_(custom_fan_mode)) {
if (const char *mode_ptr = this->parent_->find_custom_fan_mode_(custom_fan_mode, len)) {
this->custom_fan_mode_ = mode_ptr;
this->fan_mode_.reset();
return *this;
}
ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), custom_fan_mode);
ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %.*s", this->parent_->get_name().c_str(), (int) len, custom_fan_mode);
return *this;
}
ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) { return this->set_fan_mode(fan_mode.c_str()); }
ClimateCall &ClimateCall::set_fan_mode(optional<std::string> fan_mode) {
if (fan_mode.has_value()) {
this->set_fan_mode(fan_mode.value());
@@ -222,24 +229,30 @@ ClimateCall &ClimateCall::set_preset(ClimatePreset preset) {
}
ClimateCall &ClimateCall::set_preset(const char *custom_preset) {
return this->set_preset(custom_preset, strlen(custom_preset));
}
ClimateCall &ClimateCall::set_preset(const std::string &preset) {
return this->set_preset(preset.data(), preset.size());
}
ClimateCall &ClimateCall::set_preset(const char *custom_preset, size_t len) {
// Check if it's a standard enum preset first
for (const auto &preset_entry : CLIMATE_PRESETS_BY_STR) {
if (str_equals_case_insensitive(custom_preset, preset_entry.str)) {
if (strncasecmp(custom_preset, preset_entry.str, len) == 0 && preset_entry.str[len] == '\0') {
return this->set_preset(static_cast<ClimatePreset>(preset_entry.value));
}
}
// Find the matching pointer from parent climate device
if (const char *preset_ptr = this->parent_->find_custom_preset_(custom_preset)) {
if (const char *preset_ptr = this->parent_->find_custom_preset_(custom_preset, len)) {
this->custom_preset_ = preset_ptr;
this->preset_.reset();
return *this;
}
ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), custom_preset);
ESP_LOGW(TAG, "'%s' - Unrecognized preset %.*s", this->parent_->get_name().c_str(), (int) len, custom_preset);
return *this;
}
ClimateCall &ClimateCall::set_preset(const std::string &preset) { return this->set_preset(preset.c_str()); }
ClimateCall &ClimateCall::set_preset(optional<std::string> preset) {
if (preset.has_value()) {
this->set_preset(preset.value());
@@ -688,11 +701,19 @@ bool Climate::set_custom_preset_(const char *preset) {
void Climate::clear_custom_preset_() { this->custom_preset_ = nullptr; }
const char *Climate::find_custom_fan_mode_(const char *custom_fan_mode) {
return this->get_traits().find_custom_fan_mode_(custom_fan_mode);
return this->find_custom_fan_mode_(custom_fan_mode, strlen(custom_fan_mode));
}
const char *Climate::find_custom_fan_mode_(const char *custom_fan_mode, size_t len) {
return this->get_traits().find_custom_fan_mode_(custom_fan_mode, len);
}
const char *Climate::find_custom_preset_(const char *custom_preset) {
return this->get_traits().find_custom_preset_(custom_preset);
return this->find_custom_preset_(custom_preset, strlen(custom_preset));
}
const char *Climate::find_custom_preset_(const char *custom_preset, size_t len) {
return this->get_traits().find_custom_preset_(custom_preset, len);
}
void Climate::dump_traits_(const char *tag) {

View File

@@ -78,6 +78,8 @@ class ClimateCall {
ClimateCall &set_fan_mode(optional<std::string> fan_mode);
/// Set the custom fan mode of the climate device.
ClimateCall &set_fan_mode(const char *custom_fan_mode);
/// Set the custom fan mode of the climate device (zero-copy API path).
ClimateCall &set_fan_mode(const char *custom_fan_mode, size_t len);
/// Set the swing mode of the climate device.
ClimateCall &set_swing_mode(ClimateSwingMode swing_mode);
/// Set the swing mode of the climate device.
@@ -94,6 +96,8 @@ class ClimateCall {
ClimateCall &set_preset(optional<std::string> preset);
/// Set the custom preset of the climate device.
ClimateCall &set_preset(const char *custom_preset);
/// Set the custom preset of the climate device (zero-copy API path).
ClimateCall &set_preset(const char *custom_preset, size_t len);
void perform();
@@ -290,9 +294,11 @@ class Climate : public EntityBase {
/// Find and return the matching custom fan mode pointer from traits, or nullptr if not found.
const char *find_custom_fan_mode_(const char *custom_fan_mode);
const char *find_custom_fan_mode_(const char *custom_fan_mode, size_t len);
/// Find and return the matching custom preset pointer from traits, or nullptr if not found.
const char *find_custom_preset_(const char *custom_preset);
const char *find_custom_preset_(const char *custom_preset, size_t len);
/** Get the default traits of this climate device.
*

View File

@@ -20,18 +20,22 @@ using ClimatePresetMask = FiniteSetMask<ClimatePreset, DefaultBitPolicy<ClimateP
// Lightweight linear search for small vectors (1-20 items) of const char* pointers
// Avoids std::find template overhead
inline bool vector_contains(const std::vector<const char *> &vec, const char *value) {
inline bool vector_contains(const std::vector<const char *> &vec, const char *value, size_t len) {
for (const char *item : vec) {
if (strcmp(item, value) == 0)
if (strncmp(item, value, len) == 0 && item[len] == '\0')
return true;
}
return false;
}
inline bool vector_contains(const std::vector<const char *> &vec, const char *value) {
return vector_contains(vec, value, strlen(value));
}
// Find and return matching pointer from vector, or nullptr if not found
inline const char *vector_find(const std::vector<const char *> &vec, const char *value) {
inline const char *vector_find(const std::vector<const char *> &vec, const char *value, size_t len) {
for (const char *item : vec) {
if (strcmp(item, value) == 0)
if (strncmp(item, value, len) == 0 && item[len] == '\0')
return item;
}
return nullptr;
@@ -257,13 +261,19 @@ class ClimateTraits {
/// Find and return the matching custom fan mode pointer from supported modes, or nullptr if not found
/// This is protected as it's an implementation detail - use Climate::find_custom_fan_mode_() instead
const char *find_custom_fan_mode_(const char *custom_fan_mode) const {
return vector_find(this->supported_custom_fan_modes_, custom_fan_mode);
return this->find_custom_fan_mode_(custom_fan_mode, strlen(custom_fan_mode));
}
const char *find_custom_fan_mode_(const char *custom_fan_mode, size_t len) const {
return vector_find(this->supported_custom_fan_modes_, custom_fan_mode, len);
}
/// Find and return the matching custom preset pointer from supported presets, or nullptr if not found
/// This is protected as it's an implementation detail - use Climate::find_custom_preset_() instead
const char *find_custom_preset_(const char *custom_preset) const {
return vector_find(this->supported_custom_presets_, custom_preset);
return this->find_custom_preset_(custom_preset, strlen(custom_preset));
}
const char *find_custom_preset_(const char *custom_preset, size_t len) const {
return vector_find(this->supported_custom_presets_, custom_preset, len);
}
uint32_t feature_flags_{0};

View File

@@ -19,22 +19,28 @@ const LogString *fan_direction_to_string(FanDirection direction) {
}
}
FanCall &FanCall::set_preset_mode(const std::string &preset_mode) { return this->set_preset_mode(preset_mode.c_str()); }
FanCall &FanCall::set_preset_mode(const std::string &preset_mode) {
return this->set_preset_mode(preset_mode.data(), preset_mode.size());
}
FanCall &FanCall::set_preset_mode(const char *preset_mode) {
if (preset_mode == nullptr || strlen(preset_mode) == 0) {
return this->set_preset_mode(preset_mode, preset_mode ? strlen(preset_mode) : 0);
}
FanCall &FanCall::set_preset_mode(const char *preset_mode, size_t len) {
if (preset_mode == nullptr || len == 0) {
this->preset_mode_ = nullptr;
return *this;
}
// Find and validate pointer from traits immediately
auto traits = this->parent_.get_traits();
const char *validated_mode = traits.find_preset_mode(preset_mode);
const char *validated_mode = traits.find_preset_mode(preset_mode, len);
if (validated_mode != nullptr) {
this->preset_mode_ = validated_mode; // Store pointer from traits
} else {
// Preset mode not found in traits - log warning and don't set
ESP_LOGW(TAG, "%s: Preset mode '%s' not supported", this->parent_.get_name().c_str(), preset_mode);
ESP_LOGW(TAG, "%s: Preset mode '%.*s' not supported", this->parent_.get_name().c_str(), (int) len, preset_mode);
this->preset_mode_ = nullptr;
}
return *this;
@@ -140,7 +146,13 @@ FanCall Fan::turn_off() { return this->make_call().set_state(false); }
FanCall Fan::toggle() { return this->make_call().set_state(!this->state); }
FanCall Fan::make_call() { return FanCall(*this); }
const char *Fan::find_preset_mode_(const char *preset_mode) { return this->get_traits().find_preset_mode(preset_mode); }
const char *Fan::find_preset_mode_(const char *preset_mode) {
return this->find_preset_mode_(preset_mode, preset_mode ? strlen(preset_mode) : 0);
}
const char *Fan::find_preset_mode_(const char *preset_mode, size_t len) {
return this->get_traits().find_preset_mode(preset_mode, len);
}
bool Fan::set_preset_mode_(const char *preset_mode) {
if (preset_mode == nullptr) {

View File

@@ -72,6 +72,7 @@ class FanCall {
optional<FanDirection> get_direction() const { return this->direction_; }
FanCall &set_preset_mode(const std::string &preset_mode);
FanCall &set_preset_mode(const char *preset_mode);
FanCall &set_preset_mode(const char *preset_mode, size_t len);
const char *get_preset_mode() const { return this->preset_mode_; }
bool has_preset_mode() const { return this->preset_mode_ != nullptr; }
@@ -152,6 +153,7 @@ class Fan : public EntityBase {
void clear_preset_mode_();
/// Find and return the matching preset mode pointer from traits, or nullptr if not found.
const char *find_preset_mode_(const char *preset_mode);
const char *find_preset_mode_(const char *preset_mode, size_t len);
CallbackManager<void()> state_callback_{};
ESPPreferenceObject rtc_;

View File

@@ -47,10 +47,13 @@ class FanTraits {
bool supports_preset_modes() const { return !this->preset_modes_.empty(); }
/// Find and return the matching preset mode pointer from supported modes, or nullptr if not found.
const char *find_preset_mode(const char *preset_mode) const {
if (preset_mode == nullptr)
return this->find_preset_mode(preset_mode, strlen(preset_mode));
}
const char *find_preset_mode(const char *preset_mode, size_t len) const {
if (preset_mode == nullptr || len == 0)
return nullptr;
for (const char *mode : this->preset_modes_) {
if (strcmp(mode, preset_mode) == 0) {
if (strncmp(mode, preset_mode, len) == 0 && mode[len] == '\0') {
return mode; // Return pointer from traits
}
}

View File

@@ -476,10 +476,14 @@ std::string base64_encode(const uint8_t *buf, size_t buf_len) {
}
size_t base64_decode(const std::string &encoded_string, uint8_t *buf, size_t buf_len) {
int in_len = encoded_string.size();
return base64_decode(reinterpret_cast<const uint8_t *>(encoded_string.data()), encoded_string.size(), buf, buf_len);
}
size_t base64_decode(const uint8_t *encoded_data, size_t encoded_len, uint8_t *buf, size_t buf_len) {
size_t in_len = encoded_len;
int i = 0;
int j = 0;
int in = 0;
size_t in = 0;
size_t out = 0;
uint8_t char_array_4[4], char_array_3[3];
bool truncated = false;
@@ -487,8 +491,8 @@ size_t base64_decode(const std::string &encoded_string, uint8_t *buf, size_t buf
// SAFETY: The loop condition checks is_base64() before processing each character.
// This ensures base64_find_char() is only called on valid base64 characters,
// preventing the edge case where invalid chars would return 0 (same as 'A').
while (in_len-- && (encoded_string[in] != '=') && is_base64(encoded_string[in])) {
char_array_4[i++] = encoded_string[in];
while (in_len-- && (encoded_data[in] != '=') && is_base64(encoded_data[in])) {
char_array_4[i++] = encoded_data[in];
in++;
if (i == 4) {
for (i = 0; i < 4; i++)

View File

@@ -858,6 +858,7 @@ std::string base64_encode(const std::vector<uint8_t> &buf);
std::vector<uint8_t> base64_decode(const std::string &encoded_string);
size_t base64_decode(std::string const &encoded_string, uint8_t *buf, size_t buf_len);
size_t base64_decode(const uint8_t *encoded_data, size_t encoded_len, uint8_t *buf, size_t buf_len);
///@}