[climate] Add zero-copy support for API custom fan mode and preset commands (#12402)

This commit is contained in:
J. Nick Koston
2025-12-19 11:18:50 -10:00
committed by GitHub
parent 81e91c2a8f
commit 940afdbb12
8 changed files with 80 additions and 31 deletions
+2 -2
View File
@@ -1091,11 +1091,11 @@ message ClimateCommandRequest {
bool has_swing_mode = 14; bool has_swing_mode = 14;
ClimateSwingMode swing_mode = 15; ClimateSwingMode swing_mode = 15;
bool has_custom_fan_mode = 16; bool has_custom_fan_mode = 16;
string custom_fan_mode = 17; string custom_fan_mode = 17 [(pointer_to_buffer) = true];
bool has_preset = 18; bool has_preset = 18;
ClimatePreset preset = 19; ClimatePreset preset = 19;
bool has_custom_preset = 20; bool has_custom_preset = 20;
string custom_preset = 21; string custom_preset = 21 [(pointer_to_buffer) = true];
bool has_target_humidity = 22; bool has_target_humidity = 22;
float target_humidity = 23; float target_humidity = 23;
uint32 device_id = 24 [(field_ifdef) = "USE_DEVICES"]; uint32 device_id = 24 [(field_ifdef) = "USE_DEVICES"];
+2 -2
View File
@@ -712,11 +712,11 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) {
if (msg.has_fan_mode) if (msg.has_fan_mode)
call.set_fan_mode(static_cast<climate::ClimateFanMode>(msg.fan_mode)); call.set_fan_mode(static_cast<climate::ClimateFanMode>(msg.fan_mode));
if (msg.has_custom_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) if (msg.has_preset)
call.set_preset(static_cast<climate::ClimatePreset>(msg.preset)); call.set_preset(static_cast<climate::ClimatePreset>(msg.preset));
if (msg.has_custom_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) if (msg.has_swing_mode)
call.set_swing_mode(static_cast<climate::ClimateSwingMode>(msg.swing_mode)); call.set_swing_mode(static_cast<climate::ClimateSwingMode>(msg.swing_mode));
call.perform(); call.perform();
+10 -4
View File
@@ -1392,12 +1392,18 @@ bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value)
} }
bool ClimateCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { bool ClimateCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) { switch (field_id) {
case 17: case 17: {
this->custom_fan_mode = value.as_string(); // Use raw data directly to avoid allocation
this->custom_fan_mode = value.data();
this->custom_fan_mode_len = value.size();
break; 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; break;
}
default: default:
return false; return false;
} }
+5 -3
View File
@@ -1475,7 +1475,7 @@ class ClimateStateResponse final : public StateResponseProtoMessage {
class ClimateCommandRequest final : public CommandProtoMessage { class ClimateCommandRequest final : public CommandProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 48; 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 #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "climate_command_request"; } const char *message_name() const override { return "climate_command_request"; }
#endif #endif
@@ -1492,11 +1492,13 @@ class ClimateCommandRequest final : public CommandProtoMessage {
bool has_swing_mode{false}; bool has_swing_mode{false};
enums::ClimateSwingMode swing_mode{}; enums::ClimateSwingMode swing_mode{};
bool has_custom_fan_mode{false}; 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}; bool has_preset{false};
enums::ClimatePreset preset{}; enums::ClimatePreset preset{};
bool has_custom_preset{false}; 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}; bool has_target_humidity{false};
float target_humidity{0.0f}; float target_humidity{0.0f};
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
+6 -2
View File
@@ -1374,11 +1374,15 @@ void ClimateCommandRequest::dump_to(std::string &out) const {
dump_field(out, "has_swing_mode", this->has_swing_mode); 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, "swing_mode", static_cast<enums::ClimateSwingMode>(this->swing_mode));
dump_field(out, "has_custom_fan_mode", this->has_custom_fan_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, "has_preset", this->has_preset);
dump_field(out, "preset", static_cast<enums::ClimatePreset>(this->preset)); dump_field(out, "preset", static_cast<enums::ClimatePreset>(this->preset));
dump_field(out, "has_custom_preset", this->has_custom_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, "has_target_humidity", this->has_target_humidity);
dump_field(out, "target_humidity", this->target_humidity); dump_field(out, "target_humidity", this->target_humidity);
#ifdef USE_DEVICES #ifdef USE_DEVICES
+33 -12
View File
@@ -2,6 +2,7 @@
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h" #include "esphome/core/controller_registry.h"
#include "esphome/core/macros.h" #include "esphome/core/macros.h"
#include <strings.h>
namespace esphome::climate { 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) { 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 // Check if it's a standard enum mode first
for (const auto &mode_entry : CLIMATE_FAN_MODES_BY_STR) { 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)); return this->set_fan_mode(static_cast<ClimateFanMode>(mode_entry.value));
} }
} }
// Find the matching pointer from parent climate device // 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->custom_fan_mode_ = mode_ptr;
this->fan_mode_.reset(); this->fan_mode_.reset();
return *this; 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; 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) { ClimateCall &ClimateCall::set_fan_mode(optional<std::string> fan_mode) {
if (fan_mode.has_value()) { if (fan_mode.has_value()) {
this->set_fan_mode(fan_mode.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) { 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 // Check if it's a standard enum preset first
for (const auto &preset_entry : CLIMATE_PRESETS_BY_STR) { 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)); return this->set_preset(static_cast<ClimatePreset>(preset_entry.value));
} }
} }
// Find the matching pointer from parent climate device // 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->custom_preset_ = preset_ptr;
this->preset_.reset(); this->preset_.reset();
return *this; 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; 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) { ClimateCall &ClimateCall::set_preset(optional<std::string> preset) {
if (preset.has_value()) { if (preset.has_value()) {
this->set_preset(preset.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; } void Climate::clear_custom_preset_() { this->custom_preset_ = nullptr; }
const char *Climate::find_custom_fan_mode_(const char *custom_fan_mode) { 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) { 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) { void Climate::dump_traits_(const char *tag) {
+6
View File
@@ -78,6 +78,8 @@ class ClimateCall {
ClimateCall &set_fan_mode(optional<std::string> fan_mode); ClimateCall &set_fan_mode(optional<std::string> fan_mode);
/// Set the custom fan mode of the climate device. /// Set the custom fan mode of the climate device.
ClimateCall &set_fan_mode(const char *custom_fan_mode); 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. /// Set the swing mode of the climate device.
ClimateCall &set_swing_mode(ClimateSwingMode swing_mode); ClimateCall &set_swing_mode(ClimateSwingMode swing_mode);
/// Set the swing mode of the climate device. /// Set the swing mode of the climate device.
@@ -94,6 +96,8 @@ class ClimateCall {
ClimateCall &set_preset(optional<std::string> preset); ClimateCall &set_preset(optional<std::string> preset);
/// Set the custom preset of the climate device. /// Set the custom preset of the climate device.
ClimateCall &set_preset(const char *custom_preset); 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(); 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. /// 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);
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. /// 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);
const char *find_custom_preset_(const char *custom_preset, size_t len);
/** Get the default traits of this climate device. /** Get the default traits of this climate device.
* *
+16 -6
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 // Lightweight linear search for small vectors (1-20 items) of const char* pointers
// Avoids std::find template overhead // 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) { for (const char *item : vec) {
if (strcmp(item, value) == 0) if (strncmp(item, value, len) == 0 && item[len] == '\0')
return true; return true;
} }
return false; 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 // 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) { for (const char *item : vec) {
if (strcmp(item, value) == 0) if (strncmp(item, value, len) == 0 && item[len] == '\0')
return item; return item;
} }
return nullptr; 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 /// 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 /// 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 { 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 /// 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 /// 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 { 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}; uint32_t feature_flags_{0};