From f559fad4fccb5928b7e041a8ecb97b7ef2f93289 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 11:03:32 -1000 Subject: [PATCH 01/22] [fan] Use FixedVector for preset modes, preserve config order (breaking) --- esphome/components/api/api.proto | 2 +- esphome/components/api/api_pb2.h | 2 +- esphome/components/fan/fan.cpp | 31 +++++++++++++------ esphome/components/fan/fan.h | 2 +- esphome/components/fan/fan_traits.h | 23 +++++++++----- .../components/hbridge/fan/hbridge_fan.cpp | 3 +- esphome/components/hbridge/fan/hbridge_fan.h | 9 +++--- esphome/components/speed/fan/speed_fan.cpp | 3 +- esphome/components/speed/fan/speed_fan.h | 9 +++--- .../components/template/fan/template_fan.cpp | 3 +- .../components/template/fan/template_fan.h | 9 +++--- tests/components/fan/common.yaml | 11 +++++++ tests/components/fan/test.esp8266-ard.yaml | 1 + 13 files changed, 70 insertions(+), 38 deletions(-) create mode 100644 tests/components/fan/common.yaml create mode 100644 tests/components/fan/test.esp8266-ard.yaml diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index d202486cfa..34be6e4aa2 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -425,7 +425,7 @@ message ListEntitiesFanResponse { bool disabled_by_default = 9; string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"]; EntityCategory entity_category = 11; - repeated string supported_preset_modes = 12 [(container_pointer) = "std::set"]; + repeated string supported_preset_modes = 12 [(container_pointer) = "FixedVector"]; uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"]; } // Deprecated in API version 1.6 - only used in deprecated fields diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index ed49498176..647dd47b89 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -725,7 +725,7 @@ class ListEntitiesFanResponse final : public InfoResponseProtoMessage { bool supports_speed{false}; bool supports_direction{false}; int32_t supported_speed_count{0}; - const std::set *supported_preset_modes{}; + const FixedVector *supported_preset_modes{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index 26065ed644..839b0d08cc 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -39,7 +39,7 @@ void FanCall::perform() { } void FanCall::validate_() { - auto traits = this->parent_.get_traits(); + const auto &traits = this->parent_.get_traits(); if (this->speed_.has_value()) { this->speed_ = clamp(*this->speed_, 1, traits.supported_speed_count()); @@ -51,7 +51,15 @@ void FanCall::validate_() { if (!this->preset_mode_.empty()) { const auto &preset_modes = traits.supported_preset_modes(); - if (preset_modes.find(this->preset_mode_) == preset_modes.end()) { + // Linear search is efficient for small preset mode lists (typically 2-5 items) + bool found = false; + for (const auto &mode : preset_modes) { + if (mode == this->preset_mode_) { + found = true; + break; + } + } + if (!found) { ESP_LOGW(TAG, "%s: Preset mode '%s' not supported", this->parent_.get_name().c_str(), this->preset_mode_.c_str()); this->preset_mode_.clear(); } @@ -96,7 +104,7 @@ FanCall FanRestoreState::to_call(Fan &fan) { // Use stored preset index to get preset name const auto &preset_modes = fan.get_traits().supported_preset_modes(); if (this->preset_mode < preset_modes.size()) { - call.set_preset_mode(*std::next(preset_modes.begin(), this->preset_mode)); + call.set_preset_mode(preset_modes[this->preset_mode]); } } return call; @@ -111,7 +119,7 @@ void FanRestoreState::apply(Fan &fan) { // Use stored preset index to get preset name const auto &preset_modes = fan.get_traits().supported_preset_modes(); if (this->preset_mode < preset_modes.size()) { - fan.preset_mode = *std::next(preset_modes.begin(), this->preset_mode); + fan.preset_mode = preset_modes[this->preset_mode]; } } fan.publish_state(); @@ -124,7 +132,7 @@ FanCall Fan::make_call() { return FanCall(*this); } void Fan::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } void Fan::publish_state() { - auto traits = this->get_traits(); + const auto &traits = this->get_traits(); ESP_LOGD(TAG, "'%s' - Sending state:", this->name_.c_str()); ESP_LOGD(TAG, " State: %s", ONOFF(this->state)); @@ -190,17 +198,20 @@ void Fan::save_state_() { if (this->get_traits().supports_preset_modes() && !this->preset_mode.empty()) { const auto &preset_modes = this->get_traits().supported_preset_modes(); - // Store index of current preset mode - auto preset_iterator = preset_modes.find(this->preset_mode); - if (preset_iterator != preset_modes.end()) - state.preset_mode = std::distance(preset_modes.begin(), preset_iterator); + // Store index of current preset mode - linear search is efficient for small lists + for (size_t i = 0; i < preset_modes.size(); i++) { + if (preset_modes[i] == this->preset_mode) { + state.preset_mode = i; + break; + } + } } this->rtc_.save(&state); } void Fan::dump_traits_(const char *tag, const char *prefix) { - auto traits = this->get_traits(); + const auto &traits = this->get_traits(); if (traits.supports_speed()) { ESP_LOGCONFIG(tag, diff --git a/esphome/components/fan/fan.h b/esphome/components/fan/fan.h index b74187eb4a..901181903a 100644 --- a/esphome/components/fan/fan.h +++ b/esphome/components/fan/fan.h @@ -127,7 +127,7 @@ class Fan : public EntityBase { void publish_state(); - virtual FanTraits get_traits() = 0; + virtual const FanTraits &get_traits() = 0; /// Set the restore mode of this fan. void set_restore_mode(FanRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } diff --git a/esphome/components/fan/fan_traits.h b/esphome/components/fan/fan_traits.h index 48509e5705..e0b64aa0fa 100644 --- a/esphome/components/fan/fan_traits.h +++ b/esphome/components/fan/fan_traits.h @@ -1,5 +1,5 @@ -#include #include +#include "esphome/core/helpers.h" #pragma once @@ -36,9 +36,18 @@ class FanTraits { /// Set whether this fan supports changing direction void set_direction(bool direction) { this->direction_ = direction; } /// Return the preset modes supported by the fan. - std::set supported_preset_modes() const { return this->preset_modes_; } - /// Set the preset modes supported by the fan. - void set_supported_preset_modes(const std::set &preset_modes) { this->preset_modes_ = preset_modes; } + const FixedVector &supported_preset_modes() const { return this->preset_modes_; } + /// Set the preset modes supported by the fan (from initializer list). + void set_supported_preset_modes(const std::initializer_list &preset_modes) { + this->preset_modes_ = preset_modes; + } + /// Set the preset modes supported by the fan (from FixedVector). + template void set_supported_preset_modes(const T &preset_modes) { + this->preset_modes_.init(preset_modes.size()); + for (const auto &mode : preset_modes) { + this->preset_modes_.push_back(mode); + } + } /// Return if preset modes are supported bool supports_preset_modes() const { return !this->preset_modes_.empty(); } @@ -46,17 +55,17 @@ class FanTraits { #ifdef USE_API // The API connection is a friend class to access internal methods friend class api::APIConnection; - // This method returns a reference to the internal preset modes set. + // This method returns a reference to the internal preset modes. // It is used by the API to avoid copying data when encoding messages. // Warning: Do not use this method outside of the API connection code. // It returns a reference to internal data that can be invalidated. - const std::set &supported_preset_modes_for_api_() const { return this->preset_modes_; } + const FixedVector &supported_preset_modes_for_api_() const { return this->preset_modes_; } #endif bool oscillation_{false}; bool speed_{false}; bool direction_{false}; int speed_count_{}; - std::set preset_modes_{}; + FixedVector preset_modes_{}; }; } // namespace fan diff --git a/esphome/components/hbridge/fan/hbridge_fan.cpp b/esphome/components/hbridge/fan/hbridge_fan.cpp index 605a9d4ef3..c059783b1e 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.cpp +++ b/esphome/components/hbridge/fan/hbridge_fan.cpp @@ -36,7 +36,8 @@ void HBridgeFan::setup() { // Construct traits this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_); - this->traits_.set_supported_preset_modes(this->preset_modes_); + if (!this->preset_modes_.empty()) + this->traits_.set_supported_preset_modes(this->preset_modes_); } void HBridgeFan::dump_config() { diff --git a/esphome/components/hbridge/fan/hbridge_fan.h b/esphome/components/hbridge/fan/hbridge_fan.h index 4234fccae3..68458d7922 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.h +++ b/esphome/components/hbridge/fan/hbridge_fan.h @@ -1,8 +1,7 @@ #pragma once -#include - #include "esphome/core/automation.h" +#include "esphome/core/helpers.h" #include "esphome/components/output/binary_output.h" #include "esphome/components/output/float_output.h" #include "esphome/components/fan/fan.h" @@ -22,11 +21,11 @@ class HBridgeFan : public Component, public fan::Fan { void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; } void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; } void set_enable_pin(output::FloatOutput *enable) { enable_ = enable; } - void set_preset_modes(const std::set &presets) { preset_modes_ = presets; } + void set_preset_modes(const std::initializer_list &presets) { preset_modes_ = presets; } void setup() override; void dump_config() override; - fan::FanTraits get_traits() override { return this->traits_; } + const fan::FanTraits &get_traits() override { return this->traits_; } fan::FanCall brake(); @@ -38,7 +37,7 @@ class HBridgeFan : public Component, public fan::Fan { int speed_count_{}; DecayMode decay_mode_{DECAY_MODE_SLOW}; fan::FanTraits traits_; - std::set preset_modes_{}; + FixedVector preset_modes_{}; void control(const fan::FanCall &call) override; void write_state_(); diff --git a/esphome/components/speed/fan/speed_fan.cpp b/esphome/components/speed/fan/speed_fan.cpp index 57bd795416..9205d3592b 100644 --- a/esphome/components/speed/fan/speed_fan.cpp +++ b/esphome/components/speed/fan/speed_fan.cpp @@ -15,7 +15,8 @@ void SpeedFan::setup() { // Construct traits this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, this->direction_ != nullptr, this->speed_count_); - this->traits_.set_supported_preset_modes(this->preset_modes_); + if (!this->preset_modes_.empty()) + this->traits_.set_supported_preset_modes(this->preset_modes_); } void SpeedFan::dump_config() { LOG_FAN("", "Speed Fan", this); } diff --git a/esphome/components/speed/fan/speed_fan.h b/esphome/components/speed/fan/speed_fan.h index 6537bce3f6..60c2267b04 100644 --- a/esphome/components/speed/fan/speed_fan.h +++ b/esphome/components/speed/fan/speed_fan.h @@ -1,8 +1,7 @@ #pragma once -#include - #include "esphome/core/component.h" +#include "esphome/core/helpers.h" #include "esphome/components/output/binary_output.h" #include "esphome/components/output/float_output.h" #include "esphome/components/fan/fan.h" @@ -18,8 +17,8 @@ class SpeedFan : public Component, public fan::Fan { void set_output(output::FloatOutput *output) { this->output_ = output; } void set_oscillating(output::BinaryOutput *oscillating) { this->oscillating_ = oscillating; } void set_direction(output::BinaryOutput *direction) { this->direction_ = direction; } - void set_preset_modes(const std::set &presets) { this->preset_modes_ = presets; } - fan::FanTraits get_traits() override { return this->traits_; } + void set_preset_modes(const std::initializer_list &presets) { this->preset_modes_ = presets; } + const fan::FanTraits &get_traits() override { return this->traits_; } protected: void control(const fan::FanCall &call) override; @@ -30,7 +29,7 @@ class SpeedFan : public Component, public fan::Fan { output::BinaryOutput *direction_{nullptr}; int speed_count_{}; fan::FanTraits traits_; - std::set preset_modes_{}; + FixedVector preset_modes_{}; }; } // namespace speed diff --git a/esphome/components/template/fan/template_fan.cpp b/esphome/components/template/fan/template_fan.cpp index 5f4a2ae8f7..477e2c4981 100644 --- a/esphome/components/template/fan/template_fan.cpp +++ b/esphome/components/template/fan/template_fan.cpp @@ -15,7 +15,8 @@ void TemplateFan::setup() { // Construct traits this->traits_ = fan::FanTraits(this->has_oscillating_, this->speed_count_ > 0, this->has_direction_, this->speed_count_); - this->traits_.set_supported_preset_modes(this->preset_modes_); + if (!this->preset_modes_.empty()) + this->traits_.set_supported_preset_modes(this->preset_modes_); } void TemplateFan::dump_config() { LOG_FAN("", "Template Fan", this); } diff --git a/esphome/components/template/fan/template_fan.h b/esphome/components/template/fan/template_fan.h index 7f5305ca48..5b175b21a4 100644 --- a/esphome/components/template/fan/template_fan.h +++ b/esphome/components/template/fan/template_fan.h @@ -1,8 +1,7 @@ #pragma once -#include - #include "esphome/core/component.h" +#include "esphome/core/helpers.h" #include "esphome/components/fan/fan.h" namespace esphome { @@ -16,8 +15,8 @@ class TemplateFan : public Component, public fan::Fan { void set_has_direction(bool has_direction) { this->has_direction_ = has_direction; } void set_has_oscillating(bool has_oscillating) { this->has_oscillating_ = has_oscillating; } void set_speed_count(int count) { this->speed_count_ = count; } - void set_preset_modes(const std::set &presets) { this->preset_modes_ = presets; } - fan::FanTraits get_traits() override { return this->traits_; } + void set_preset_modes(const std::initializer_list &presets) { this->preset_modes_ = presets; } + const fan::FanTraits &get_traits() override { return this->traits_; } protected: void control(const fan::FanCall &call) override; @@ -26,7 +25,7 @@ class TemplateFan : public Component, public fan::Fan { bool has_direction_{false}; int speed_count_{0}; fan::FanTraits traits_; - std::set preset_modes_{}; + FixedVector preset_modes_{}; }; } // namespace template_ diff --git a/tests/components/fan/common.yaml b/tests/components/fan/common.yaml new file mode 100644 index 0000000000..55c2a656fd --- /dev/null +++ b/tests/components/fan/common.yaml @@ -0,0 +1,11 @@ +fan: + - platform: template + id: test_fan + name: "Test Fan" + preset_modes: + - Eco + - Sleep + - Turbo + has_oscillating: true + has_direction: true + speed_count: 3 diff --git a/tests/components/fan/test.esp8266-ard.yaml b/tests/components/fan/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/fan/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml From 04d127015c1b7e1741fb080f3bd4d983c4bd242b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 11:04:38 -1000 Subject: [PATCH 02/22] Add basic fan compile tests baseline for https://github.com/esphome/esphome/pull/11483 --- tests/components/fan/common.yaml | 11 +++++++++++ tests/components/fan/test.esp8266-ard.yaml | 1 + 2 files changed, 12 insertions(+) create mode 100644 tests/components/fan/common.yaml create mode 100644 tests/components/fan/test.esp8266-ard.yaml diff --git a/tests/components/fan/common.yaml b/tests/components/fan/common.yaml new file mode 100644 index 0000000000..55c2a656fd --- /dev/null +++ b/tests/components/fan/common.yaml @@ -0,0 +1,11 @@ +fan: + - platform: template + id: test_fan + name: "Test Fan" + preset_modes: + - Eco + - Sleep + - Turbo + has_oscillating: true + has_direction: true + speed_count: 3 diff --git a/tests/components/fan/test.esp8266-ard.yaml b/tests/components/fan/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/fan/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml From f11e8e36b5412ea09c3d69d904d4dbd3c6be8f0f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 11:09:10 -1000 Subject: [PATCH 03/22] missed --- esphome/components/api/api_connection.cpp | 4 ++-- esphome/components/binary/fan/binary_fan.h | 2 +- esphome/components/copy/fan/copy_fan.h | 2 +- esphome/components/demo/demo_fan.h | 24 ++++++++++++---------- esphome/components/tuya/fan/tuya_fan.h | 2 +- 5 files changed, 18 insertions(+), 16 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 7c135946f8..05a4f9e63e 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -401,7 +401,7 @@ uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *co bool is_single) { auto *fan = static_cast(entity); FanStateResponse msg; - auto traits = fan->get_traits(); + const auto &traits = fan->get_traits(); msg.state = fan->state; if (traits.supports_oscillation()) msg.oscillating = fan->oscillating; @@ -418,7 +418,7 @@ uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *con bool is_single) { auto *fan = static_cast(entity); ListEntitiesFanResponse msg; - auto traits = fan->get_traits(); + const auto &traits = fan->get_traits(); msg.supports_oscillation = traits.supports_oscillation(); msg.supports_speed = traits.supports_speed(); msg.supports_direction = traits.supports_direction(); diff --git a/esphome/components/binary/fan/binary_fan.h b/esphome/components/binary/fan/binary_fan.h index 16bce2e6af..b87e1c5d9d 100644 --- a/esphome/components/binary/fan/binary_fan.h +++ b/esphome/components/binary/fan/binary_fan.h @@ -16,7 +16,7 @@ class BinaryFan : public Component, public fan::Fan { void set_oscillating(output::BinaryOutput *oscillating) { this->oscillating_ = oscillating; } void set_direction(output::BinaryOutput *direction) { this->direction_ = direction; } - fan::FanTraits get_traits() override; + const fan::FanTraits &get_traits() override; protected: void control(const fan::FanCall &call) override; diff --git a/esphome/components/copy/fan/copy_fan.h b/esphome/components/copy/fan/copy_fan.h index b474975bc4..194827b9f8 100644 --- a/esphome/components/copy/fan/copy_fan.h +++ b/esphome/components/copy/fan/copy_fan.h @@ -12,7 +12,7 @@ class CopyFan : public fan::Fan, public Component { void setup() override; void dump_config() override; - fan::FanTraits get_traits() override; + const fan::FanTraits &get_traits() override; protected: void control(const fan::FanCall &call) override; diff --git a/esphome/components/demo/demo_fan.h b/esphome/components/demo/demo_fan.h index 09edc4e0b7..568e90b826 100644 --- a/esphome/components/demo/demo_fan.h +++ b/esphome/components/demo/demo_fan.h @@ -16,8 +16,9 @@ enum class DemoFanType { class DemoFan : public fan::Fan, public Component { public: void set_type(DemoFanType type) { type_ = type; } - fan::FanTraits get_traits() override { - fan::FanTraits traits{}; + const fan::FanTraits &get_traits() override { + // Note: Demo fan builds traits dynamically, so we store it as a member + this->traits_ = fan::FanTraits{}; // oscillation // speed @@ -27,22 +28,22 @@ class DemoFan : public fan::Fan, public Component { case DemoFanType::TYPE_1: break; case DemoFanType::TYPE_2: - traits.set_oscillation(true); + this->traits_.set_oscillation(true); break; case DemoFanType::TYPE_3: - traits.set_direction(true); - traits.set_speed(true); - traits.set_supported_speed_count(5); + this->traits_.set_direction(true); + this->traits_.set_speed(true); + this->traits_.set_supported_speed_count(5); break; case DemoFanType::TYPE_4: - traits.set_direction(true); - traits.set_speed(true); - traits.set_supported_speed_count(100); - traits.set_oscillation(true); + this->traits_.set_direction(true); + this->traits_.set_speed(true); + this->traits_.set_supported_speed_count(100); + this->traits_.set_oscillation(true); break; } - return traits; + return this->traits_; } protected: @@ -60,6 +61,7 @@ class DemoFan : public fan::Fan, public Component { } DemoFanType type_; + fan::FanTraits traits_; }; } // namespace demo diff --git a/esphome/components/tuya/fan/tuya_fan.h b/esphome/components/tuya/fan/tuya_fan.h index 527efa8246..100579ea9f 100644 --- a/esphome/components/tuya/fan/tuya_fan.h +++ b/esphome/components/tuya/fan/tuya_fan.h @@ -17,7 +17,7 @@ class TuyaFan : public Component, public fan::Fan { void set_oscillation_id(uint8_t oscillation_id) { this->oscillation_id_ = oscillation_id; } void set_direction_id(uint8_t direction_id) { this->direction_id_ = direction_id; } - fan::FanTraits get_traits() override; + const fan::FanTraits &get_traits() override; protected: void control(const fan::FanCall &call) override; From ac36b97262bbfd60f8d67746fcae0bd93f7af3f6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 11:16:13 -1000 Subject: [PATCH 04/22] reduce scope --- esphome/components/api/api.proto | 2 +- esphome/components/api/api_connection.cpp | 4 ++-- esphome/components/api/api_pb2.h | 2 +- esphome/components/binary/fan/binary_fan.h | 2 +- esphome/components/copy/fan/copy_fan.h | 3 +-- esphome/components/demo/demo_fan.h | 24 +++++++++---------- esphome/components/fan/fan.cpp | 4 ++-- esphome/components/fan/fan.h | 2 +- esphome/components/fan/fan_traits.h | 21 +++++----------- esphome/components/hbridge/fan/hbridge_fan.h | 6 ++--- esphome/components/speed/fan/speed_fan.h | 6 ++--- .../components/template/fan/template_fan.cpp | 3 +-- .../components/template/fan/template_fan.h | 6 ++--- esphome/components/tuya/fan/tuya_fan.h | 2 +- 14 files changed, 37 insertions(+), 50 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 34be6e4aa2..a4c2557ffe 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -425,7 +425,7 @@ message ListEntitiesFanResponse { bool disabled_by_default = 9; string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"]; EntityCategory entity_category = 11; - repeated string supported_preset_modes = 12 [(container_pointer) = "FixedVector"]; + repeated string supported_preset_modes = 12 [(container_pointer) = "std::vector"]; uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"]; } // Deprecated in API version 1.6 - only used in deprecated fields diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 05a4f9e63e..7c135946f8 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -401,7 +401,7 @@ uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *co bool is_single) { auto *fan = static_cast(entity); FanStateResponse msg; - const auto &traits = fan->get_traits(); + auto traits = fan->get_traits(); msg.state = fan->state; if (traits.supports_oscillation()) msg.oscillating = fan->oscillating; @@ -418,7 +418,7 @@ uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *con bool is_single) { auto *fan = static_cast(entity); ListEntitiesFanResponse msg; - const auto &traits = fan->get_traits(); + auto traits = fan->get_traits(); msg.supports_oscillation = traits.supports_oscillation(); msg.supports_speed = traits.supports_speed(); msg.supports_direction = traits.supports_direction(); diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 647dd47b89..e71ad2c64e 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -725,7 +725,7 @@ class ListEntitiesFanResponse final : public InfoResponseProtoMessage { bool supports_speed{false}; bool supports_direction{false}; int32_t supported_speed_count{0}; - const FixedVector *supported_preset_modes{}; + const std::vector *supported_preset_modes{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP diff --git a/esphome/components/binary/fan/binary_fan.h b/esphome/components/binary/fan/binary_fan.h index b87e1c5d9d..16bce2e6af 100644 --- a/esphome/components/binary/fan/binary_fan.h +++ b/esphome/components/binary/fan/binary_fan.h @@ -16,7 +16,7 @@ class BinaryFan : public Component, public fan::Fan { void set_oscillating(output::BinaryOutput *oscillating) { this->oscillating_ = oscillating; } void set_direction(output::BinaryOutput *direction) { this->direction_ = direction; } - const fan::FanTraits &get_traits() override; + fan::FanTraits get_traits() override; protected: void control(const fan::FanCall &call) override; diff --git a/esphome/components/copy/fan/copy_fan.h b/esphome/components/copy/fan/copy_fan.h index 194827b9f8..e1212537f1 100644 --- a/esphome/components/copy/fan/copy_fan.h +++ b/esphome/components/copy/fan/copy_fan.h @@ -12,11 +12,10 @@ class CopyFan : public fan::Fan, public Component { void setup() override; void dump_config() override; - const fan::FanTraits &get_traits() override; + fan::FanTraits get_traits() override; protected: void control(const fan::FanCall &call) override; - ; fan::Fan *source_; }; diff --git a/esphome/components/demo/demo_fan.h b/esphome/components/demo/demo_fan.h index 568e90b826..09edc4e0b7 100644 --- a/esphome/components/demo/demo_fan.h +++ b/esphome/components/demo/demo_fan.h @@ -16,9 +16,8 @@ enum class DemoFanType { class DemoFan : public fan::Fan, public Component { public: void set_type(DemoFanType type) { type_ = type; } - const fan::FanTraits &get_traits() override { - // Note: Demo fan builds traits dynamically, so we store it as a member - this->traits_ = fan::FanTraits{}; + fan::FanTraits get_traits() override { + fan::FanTraits traits{}; // oscillation // speed @@ -28,22 +27,22 @@ class DemoFan : public fan::Fan, public Component { case DemoFanType::TYPE_1: break; case DemoFanType::TYPE_2: - this->traits_.set_oscillation(true); + traits.set_oscillation(true); break; case DemoFanType::TYPE_3: - this->traits_.set_direction(true); - this->traits_.set_speed(true); - this->traits_.set_supported_speed_count(5); + traits.set_direction(true); + traits.set_speed(true); + traits.set_supported_speed_count(5); break; case DemoFanType::TYPE_4: - this->traits_.set_direction(true); - this->traits_.set_speed(true); - this->traits_.set_supported_speed_count(100); - this->traits_.set_oscillation(true); + traits.set_direction(true); + traits.set_speed(true); + traits.set_supported_speed_count(100); + traits.set_oscillation(true); break; } - return this->traits_; + return traits; } protected: @@ -61,7 +60,6 @@ class DemoFan : public fan::Fan, public Component { } DemoFanType type_; - fan::FanTraits traits_; }; } // namespace demo diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index 839b0d08cc..ea9cfd0c37 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -132,7 +132,7 @@ FanCall Fan::make_call() { return FanCall(*this); } void Fan::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } void Fan::publish_state() { - const auto &traits = this->get_traits(); + auto traits = this->get_traits(); ESP_LOGD(TAG, "'%s' - Sending state:", this->name_.c_str()); ESP_LOGD(TAG, " State: %s", ONOFF(this->state)); @@ -211,7 +211,7 @@ void Fan::save_state_() { } void Fan::dump_traits_(const char *tag, const char *prefix) { - const auto &traits = this->get_traits(); + auto traits = this->get_traits(); if (traits.supports_speed()) { ESP_LOGCONFIG(tag, diff --git a/esphome/components/fan/fan.h b/esphome/components/fan/fan.h index 901181903a..b74187eb4a 100644 --- a/esphome/components/fan/fan.h +++ b/esphome/components/fan/fan.h @@ -127,7 +127,7 @@ class Fan : public EntityBase { void publish_state(); - virtual const FanTraits &get_traits() = 0; + virtual FanTraits get_traits() = 0; /// Set the restore mode of this fan. void set_restore_mode(FanRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } diff --git a/esphome/components/fan/fan_traits.h b/esphome/components/fan/fan_traits.h index e0b64aa0fa..9e1b669a2b 100644 --- a/esphome/components/fan/fan_traits.h +++ b/esphome/components/fan/fan_traits.h @@ -1,5 +1,5 @@ #include -#include "esphome/core/helpers.h" +#include #pragma once @@ -36,18 +36,9 @@ class FanTraits { /// Set whether this fan supports changing direction void set_direction(bool direction) { this->direction_ = direction; } /// Return the preset modes supported by the fan. - const FixedVector &supported_preset_modes() const { return this->preset_modes_; } - /// Set the preset modes supported by the fan (from initializer list). - void set_supported_preset_modes(const std::initializer_list &preset_modes) { - this->preset_modes_ = preset_modes; - } - /// Set the preset modes supported by the fan (from FixedVector). - template void set_supported_preset_modes(const T &preset_modes) { - this->preset_modes_.init(preset_modes.size()); - for (const auto &mode : preset_modes) { - this->preset_modes_.push_back(mode); - } - } + const std::vector &supported_preset_modes() const { return this->preset_modes_; } + /// Set the preset modes supported by the fan. + void set_supported_preset_modes(const std::vector &preset_modes) { this->preset_modes_ = preset_modes; } /// Return if preset modes are supported bool supports_preset_modes() const { return !this->preset_modes_.empty(); } @@ -59,13 +50,13 @@ class FanTraits { // It is used by the API to avoid copying data when encoding messages. // Warning: Do not use this method outside of the API connection code. // It returns a reference to internal data that can be invalidated. - const FixedVector &supported_preset_modes_for_api_() const { return this->preset_modes_; } + const std::vector &supported_preset_modes_for_api_() const { return this->preset_modes_; } #endif bool oscillation_{false}; bool speed_{false}; bool direction_{false}; int speed_count_{}; - FixedVector preset_modes_{}; + std::vector preset_modes_{}; }; } // namespace fan diff --git a/esphome/components/hbridge/fan/hbridge_fan.h b/esphome/components/hbridge/fan/hbridge_fan.h index 68458d7922..8562fd20be 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.h +++ b/esphome/components/hbridge/fan/hbridge_fan.h @@ -21,11 +21,11 @@ class HBridgeFan : public Component, public fan::Fan { void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; } void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; } void set_enable_pin(output::FloatOutput *enable) { enable_ = enable; } - void set_preset_modes(const std::initializer_list &presets) { preset_modes_ = presets; } + void set_preset_modes(const std::vector &presets) { preset_modes_ = presets; } void setup() override; void dump_config() override; - const fan::FanTraits &get_traits() override { return this->traits_; } + fan::FanTraits get_traits() override { return this->traits_; } fan::FanCall brake(); @@ -37,7 +37,7 @@ class HBridgeFan : public Component, public fan::Fan { int speed_count_{}; DecayMode decay_mode_{DECAY_MODE_SLOW}; fan::FanTraits traits_; - FixedVector preset_modes_{}; + std::vector preset_modes_{}; void control(const fan::FanCall &call) override; void write_state_(); diff --git a/esphome/components/speed/fan/speed_fan.h b/esphome/components/speed/fan/speed_fan.h index 60c2267b04..d994ddd15e 100644 --- a/esphome/components/speed/fan/speed_fan.h +++ b/esphome/components/speed/fan/speed_fan.h @@ -17,8 +17,8 @@ class SpeedFan : public Component, public fan::Fan { void set_output(output::FloatOutput *output) { this->output_ = output; } void set_oscillating(output::BinaryOutput *oscillating) { this->oscillating_ = oscillating; } void set_direction(output::BinaryOutput *direction) { this->direction_ = direction; } - void set_preset_modes(const std::initializer_list &presets) { this->preset_modes_ = presets; } - const fan::FanTraits &get_traits() override { return this->traits_; } + void set_preset_modes(const std::vector &presets) { this->preset_modes_ = presets; } + fan::FanTraits get_traits() override { return this->traits_; } protected: void control(const fan::FanCall &call) override; @@ -29,7 +29,7 @@ class SpeedFan : public Component, public fan::Fan { output::BinaryOutput *direction_{nullptr}; int speed_count_{}; fan::FanTraits traits_; - FixedVector preset_modes_{}; + std::vector preset_modes_{}; }; } // namespace speed diff --git a/esphome/components/template/fan/template_fan.cpp b/esphome/components/template/fan/template_fan.cpp index 477e2c4981..5f4a2ae8f7 100644 --- a/esphome/components/template/fan/template_fan.cpp +++ b/esphome/components/template/fan/template_fan.cpp @@ -15,8 +15,7 @@ void TemplateFan::setup() { // Construct traits this->traits_ = fan::FanTraits(this->has_oscillating_, this->speed_count_ > 0, this->has_direction_, this->speed_count_); - if (!this->preset_modes_.empty()) - this->traits_.set_supported_preset_modes(this->preset_modes_); + this->traits_.set_supported_preset_modes(this->preset_modes_); } void TemplateFan::dump_config() { LOG_FAN("", "Template Fan", this); } diff --git a/esphome/components/template/fan/template_fan.h b/esphome/components/template/fan/template_fan.h index 5b175b21a4..4a32c912fc 100644 --- a/esphome/components/template/fan/template_fan.h +++ b/esphome/components/template/fan/template_fan.h @@ -15,8 +15,8 @@ class TemplateFan : public Component, public fan::Fan { void set_has_direction(bool has_direction) { this->has_direction_ = has_direction; } void set_has_oscillating(bool has_oscillating) { this->has_oscillating_ = has_oscillating; } void set_speed_count(int count) { this->speed_count_ = count; } - void set_preset_modes(const std::initializer_list &presets) { this->preset_modes_ = presets; } - const fan::FanTraits &get_traits() override { return this->traits_; } + void set_preset_modes(const std::vector &presets) { this->preset_modes_ = presets; } + fan::FanTraits get_traits() override { return this->traits_; } protected: void control(const fan::FanCall &call) override; @@ -25,7 +25,7 @@ class TemplateFan : public Component, public fan::Fan { bool has_direction_{false}; int speed_count_{0}; fan::FanTraits traits_; - FixedVector preset_modes_{}; + std::vector preset_modes_{}; }; } // namespace template_ diff --git a/esphome/components/tuya/fan/tuya_fan.h b/esphome/components/tuya/fan/tuya_fan.h index 100579ea9f..527efa8246 100644 --- a/esphome/components/tuya/fan/tuya_fan.h +++ b/esphome/components/tuya/fan/tuya_fan.h @@ -17,7 +17,7 @@ class TuyaFan : public Component, public fan::Fan { void set_oscillation_id(uint8_t oscillation_id) { this->oscillation_id_ = oscillation_id; } void set_direction_id(uint8_t direction_id) { this->direction_id_ = direction_id; } - const fan::FanTraits &get_traits() override; + fan::FanTraits get_traits() override; protected: void control(const fan::FanCall &call) override; From acd24402ddc7e7c086ed20d5e88baecb7d826824 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 11:16:28 -1000 Subject: [PATCH 05/22] reduce scope --- esphome/components/hbridge/fan/hbridge_fan.cpp | 3 +-- esphome/components/speed/fan/speed_fan.cpp | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/components/hbridge/fan/hbridge_fan.cpp b/esphome/components/hbridge/fan/hbridge_fan.cpp index c059783b1e..605a9d4ef3 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.cpp +++ b/esphome/components/hbridge/fan/hbridge_fan.cpp @@ -36,8 +36,7 @@ void HBridgeFan::setup() { // Construct traits this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_); - if (!this->preset_modes_.empty()) - this->traits_.set_supported_preset_modes(this->preset_modes_); + this->traits_.set_supported_preset_modes(this->preset_modes_); } void HBridgeFan::dump_config() { diff --git a/esphome/components/speed/fan/speed_fan.cpp b/esphome/components/speed/fan/speed_fan.cpp index 9205d3592b..57bd795416 100644 --- a/esphome/components/speed/fan/speed_fan.cpp +++ b/esphome/components/speed/fan/speed_fan.cpp @@ -15,8 +15,7 @@ void SpeedFan::setup() { // Construct traits this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, this->direction_ != nullptr, this->speed_count_); - if (!this->preset_modes_.empty()) - this->traits_.set_supported_preset_modes(this->preset_modes_); + this->traits_.set_supported_preset_modes(this->preset_modes_); } void SpeedFan::dump_config() { LOG_FAN("", "Speed Fan", this); } From 935acc7d5e0ac279f71fbd1f76211bb9fd421385 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 11:24:12 -1000 Subject: [PATCH 06/22] fixed --- esphome/components/api/api.proto | 2 +- esphome/components/api/api_pb2.h | 2 +- esphome/components/fan/fan_traits.h | 15 ++++++++++----- esphome/components/hbridge/fan/hbridge_fan.h | 4 ++-- esphome/components/speed/fan/speed_fan.h | 4 ++-- esphome/components/template/fan/template_fan.h | 4 ++-- 6 files changed, 18 insertions(+), 13 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index a4c2557ffe..34be6e4aa2 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -425,7 +425,7 @@ message ListEntitiesFanResponse { bool disabled_by_default = 9; string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"]; EntityCategory entity_category = 11; - repeated string supported_preset_modes = 12 [(container_pointer) = "std::vector"]; + repeated string supported_preset_modes = 12 [(container_pointer) = "FixedVector"]; uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"]; } // Deprecated in API version 1.6 - only used in deprecated fields diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index e71ad2c64e..647dd47b89 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -725,7 +725,7 @@ class ListEntitiesFanResponse final : public InfoResponseProtoMessage { bool supports_speed{false}; bool supports_direction{false}; int32_t supported_speed_count{0}; - const std::vector *supported_preset_modes{}; + const FixedVector *supported_preset_modes{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP diff --git a/esphome/components/fan/fan_traits.h b/esphome/components/fan/fan_traits.h index 9e1b669a2b..c37acfa67d 100644 --- a/esphome/components/fan/fan_traits.h +++ b/esphome/components/fan/fan_traits.h @@ -1,5 +1,5 @@ #include -#include +#include "esphome/core/helpers.h" #pragma once @@ -36,9 +36,14 @@ class FanTraits { /// Set whether this fan supports changing direction void set_direction(bool direction) { this->direction_ = direction; } /// Return the preset modes supported by the fan. - const std::vector &supported_preset_modes() const { return this->preset_modes_; } + const FixedVector &supported_preset_modes() const { return this->preset_modes_; } /// Set the preset modes supported by the fan. - void set_supported_preset_modes(const std::vector &preset_modes) { this->preset_modes_ = preset_modes; } + template void set_supported_preset_modes(const T &preset_modes) { + this->preset_modes_.init(preset_modes.size()); + for (const auto &mode : preset_modes) { + this->preset_modes_.push_back(mode); + } + } /// Return if preset modes are supported bool supports_preset_modes() const { return !this->preset_modes_.empty(); } @@ -50,13 +55,13 @@ class FanTraits { // It is used by the API to avoid copying data when encoding messages. // Warning: Do not use this method outside of the API connection code. // It returns a reference to internal data that can be invalidated. - const std::vector &supported_preset_modes_for_api_() const { return this->preset_modes_; } + const FixedVector &supported_preset_modes_for_api_() const { return this->preset_modes_; } #endif bool oscillation_{false}; bool speed_{false}; bool direction_{false}; int speed_count_{}; - std::vector preset_modes_{}; + FixedVector preset_modes_{}; }; } // namespace fan diff --git a/esphome/components/hbridge/fan/hbridge_fan.h b/esphome/components/hbridge/fan/hbridge_fan.h index 8562fd20be..cea4f81fe5 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.h +++ b/esphome/components/hbridge/fan/hbridge_fan.h @@ -21,7 +21,7 @@ class HBridgeFan : public Component, public fan::Fan { void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; } void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; } void set_enable_pin(output::FloatOutput *enable) { enable_ = enable; } - void set_preset_modes(const std::vector &presets) { preset_modes_ = presets; } + void set_preset_modes(const FixedVector &presets) { preset_modes_ = presets; } void setup() override; void dump_config() override; @@ -37,7 +37,7 @@ class HBridgeFan : public Component, public fan::Fan { int speed_count_{}; DecayMode decay_mode_{DECAY_MODE_SLOW}; fan::FanTraits traits_; - std::vector preset_modes_{}; + FixedVector preset_modes_{}; void control(const fan::FanCall &call) override; void write_state_(); diff --git a/esphome/components/speed/fan/speed_fan.h b/esphome/components/speed/fan/speed_fan.h index d994ddd15e..3ffffac231 100644 --- a/esphome/components/speed/fan/speed_fan.h +++ b/esphome/components/speed/fan/speed_fan.h @@ -17,7 +17,7 @@ class SpeedFan : public Component, public fan::Fan { void set_output(output::FloatOutput *output) { this->output_ = output; } void set_oscillating(output::BinaryOutput *oscillating) { this->oscillating_ = oscillating; } void set_direction(output::BinaryOutput *direction) { this->direction_ = direction; } - void set_preset_modes(const std::vector &presets) { this->preset_modes_ = presets; } + void set_preset_modes(const FixedVector &presets) { this->preset_modes_ = presets; } fan::FanTraits get_traits() override { return this->traits_; } protected: @@ -29,7 +29,7 @@ class SpeedFan : public Component, public fan::Fan { output::BinaryOutput *direction_{nullptr}; int speed_count_{}; fan::FanTraits traits_; - std::vector preset_modes_{}; + FixedVector preset_modes_{}; }; } // namespace speed diff --git a/esphome/components/template/fan/template_fan.h b/esphome/components/template/fan/template_fan.h index 4a32c912fc..330f8f2565 100644 --- a/esphome/components/template/fan/template_fan.h +++ b/esphome/components/template/fan/template_fan.h @@ -15,7 +15,7 @@ class TemplateFan : public Component, public fan::Fan { void set_has_direction(bool has_direction) { this->has_direction_ = has_direction; } void set_has_oscillating(bool has_oscillating) { this->has_oscillating_ = has_oscillating; } void set_speed_count(int count) { this->speed_count_ = count; } - void set_preset_modes(const std::vector &presets) { this->preset_modes_ = presets; } + void set_preset_modes(const FixedVector &presets) { this->preset_modes_ = presets; } fan::FanTraits get_traits() override { return this->traits_; } protected: @@ -25,7 +25,7 @@ class TemplateFan : public Component, public fan::Fan { bool has_direction_{false}; int speed_count_{0}; fan::FanTraits traits_; - std::vector preset_modes_{}; + FixedVector preset_modes_{}; }; } // namespace template_ From 657e6f0bce67b70dc6c6567bf63c82e526c2ba4d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 11:28:53 -1000 Subject: [PATCH 07/22] fixed --- esphome/components/fan/fan.cpp | 2 +- esphome/components/fan/fan.h | 16 ++++++++++++++++ esphome/components/fan/fan_traits.h | 2 ++ esphome/components/hbridge/fan/hbridge_fan.cpp | 8 +++++--- esphome/components/hbridge/fan/hbridge_fan.h | 5 +---- esphome/components/speed/fan/speed_fan.cpp | 8 +++++--- esphome/components/speed/fan/speed_fan.h | 5 +---- esphome/components/template/fan/template_fan.cpp | 8 +++++--- esphome/components/template/fan/template_fan.h | 5 +---- 9 files changed, 37 insertions(+), 22 deletions(-) diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index ea9cfd0c37..26a61de0b1 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -39,7 +39,7 @@ void FanCall::perform() { } void FanCall::validate_() { - const auto &traits = this->parent_.get_traits(); + auto traits = this->parent_.get_traits(); if (this->speed_.has_value()) { this->speed_ = clamp(*this->speed_, 1, traits.supported_speed_count()); diff --git a/esphome/components/fan/fan.h b/esphome/components/fan/fan.h index b74187eb4a..9b11a214d6 100644 --- a/esphome/components/fan/fan.h +++ b/esphome/components/fan/fan.h @@ -1,5 +1,6 @@ #pragma once +#include #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -132,6 +133,20 @@ class Fan : public EntityBase { /// Set the restore mode of this fan. void set_restore_mode(FanRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } + /// Set preset modes - helper for components + void set_preset_modes(const std::initializer_list &presets) { + this->preset_modes_.init(presets.size()); + for (const auto &mode : presets) { + this->preset_modes_.push_back(mode); + } + } + template void set_preset_modes(const T &presets) { + this->preset_modes_.init(presets.size()); + for (const auto &mode : presets) { + this->preset_modes_.push_back(mode); + } + } + protected: friend FanCall; @@ -145,6 +160,7 @@ class Fan : public EntityBase { CallbackManager state_callback_{}; ESPPreferenceObject rtc_; FanRestoreMode restore_mode_; + FixedVector preset_modes_{}; }; } // namespace fan diff --git a/esphome/components/fan/fan_traits.h b/esphome/components/fan/fan_traits.h index c37acfa67d..50090f9621 100644 --- a/esphome/components/fan/fan_traits.h +++ b/esphome/components/fan/fan_traits.h @@ -18,6 +18,8 @@ class FanTraits { FanTraits() = default; FanTraits(bool oscillation, bool speed, bool direction, int speed_count) : oscillation_(oscillation), speed_(speed), direction_(direction), speed_count_(speed_count) {} + FanTraits(FanTraits &&) = default; + FanTraits &operator=(FanTraits &&) = default; /// Return if this fan supports oscillation. bool supports_oscillation() const { return this->oscillation_; } diff --git a/esphome/components/hbridge/fan/hbridge_fan.cpp b/esphome/components/hbridge/fan/hbridge_fan.cpp index 605a9d4ef3..56df053d57 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.cpp +++ b/esphome/components/hbridge/fan/hbridge_fan.cpp @@ -33,10 +33,12 @@ void HBridgeFan::setup() { restore->apply(*this); this->write_state_(); } +} - // Construct traits - this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_); - this->traits_.set_supported_preset_modes(this->preset_modes_); +fan::FanTraits HBridgeFan::get_traits() { + auto traits = fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_); + traits.set_supported_preset_modes(this->preset_modes_); + return traits; } void HBridgeFan::dump_config() { diff --git a/esphome/components/hbridge/fan/hbridge_fan.h b/esphome/components/hbridge/fan/hbridge_fan.h index cea4f81fe5..d8fa0f99cb 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.h +++ b/esphome/components/hbridge/fan/hbridge_fan.h @@ -21,11 +21,10 @@ class HBridgeFan : public Component, public fan::Fan { void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; } void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; } void set_enable_pin(output::FloatOutput *enable) { enable_ = enable; } - void set_preset_modes(const FixedVector &presets) { preset_modes_ = presets; } void setup() override; void dump_config() override; - fan::FanTraits get_traits() override { return this->traits_; } + fan::FanTraits get_traits() override; fan::FanCall brake(); @@ -36,8 +35,6 @@ class HBridgeFan : public Component, public fan::Fan { output::BinaryOutput *oscillating_{nullptr}; int speed_count_{}; DecayMode decay_mode_{DECAY_MODE_SLOW}; - fan::FanTraits traits_; - FixedVector preset_modes_{}; void control(const fan::FanCall &call) override; void write_state_(); diff --git a/esphome/components/speed/fan/speed_fan.cpp b/esphome/components/speed/fan/speed_fan.cpp index 57bd795416..03d242178f 100644 --- a/esphome/components/speed/fan/speed_fan.cpp +++ b/esphome/components/speed/fan/speed_fan.cpp @@ -12,10 +12,12 @@ void SpeedFan::setup() { restore->apply(*this); this->write_state_(); } +} - // Construct traits - this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, this->direction_ != nullptr, this->speed_count_); - this->traits_.set_supported_preset_modes(this->preset_modes_); +fan::FanTraits SpeedFan::get_traits() { + auto traits = fan::FanTraits(this->oscillating_ != nullptr, true, this->direction_ != nullptr, this->speed_count_); + traits.set_supported_preset_modes(this->preset_modes_); + return traits; } void SpeedFan::dump_config() { LOG_FAN("", "Speed Fan", this); } diff --git a/esphome/components/speed/fan/speed_fan.h b/esphome/components/speed/fan/speed_fan.h index 3ffffac231..f29a42190e 100644 --- a/esphome/components/speed/fan/speed_fan.h +++ b/esphome/components/speed/fan/speed_fan.h @@ -17,8 +17,7 @@ class SpeedFan : public Component, public fan::Fan { void set_output(output::FloatOutput *output) { this->output_ = output; } void set_oscillating(output::BinaryOutput *oscillating) { this->oscillating_ = oscillating; } void set_direction(output::BinaryOutput *direction) { this->direction_ = direction; } - void set_preset_modes(const FixedVector &presets) { this->preset_modes_ = presets; } - fan::FanTraits get_traits() override { return this->traits_; } + fan::FanTraits get_traits() override; protected: void control(const fan::FanCall &call) override; @@ -28,8 +27,6 @@ class SpeedFan : public Component, public fan::Fan { output::BinaryOutput *oscillating_{nullptr}; output::BinaryOutput *direction_{nullptr}; int speed_count_{}; - fan::FanTraits traits_; - FixedVector preset_modes_{}; }; } // namespace speed diff --git a/esphome/components/template/fan/template_fan.cpp b/esphome/components/template/fan/template_fan.cpp index 5f4a2ae8f7..39e853fdb6 100644 --- a/esphome/components/template/fan/template_fan.cpp +++ b/esphome/components/template/fan/template_fan.cpp @@ -11,11 +11,13 @@ void TemplateFan::setup() { if (restore.has_value()) { restore->apply(*this); } +} - // Construct traits - this->traits_ = +fan::FanTraits TemplateFan::get_traits() { + auto traits = fan::FanTraits(this->has_oscillating_, this->speed_count_ > 0, this->has_direction_, this->speed_count_); - this->traits_.set_supported_preset_modes(this->preset_modes_); + traits.set_supported_preset_modes(this->preset_modes_); + return traits; } void TemplateFan::dump_config() { LOG_FAN("", "Template Fan", this); } diff --git a/esphome/components/template/fan/template_fan.h b/esphome/components/template/fan/template_fan.h index 330f8f2565..561c2de756 100644 --- a/esphome/components/template/fan/template_fan.h +++ b/esphome/components/template/fan/template_fan.h @@ -15,8 +15,7 @@ class TemplateFan : public Component, public fan::Fan { void set_has_direction(bool has_direction) { this->has_direction_ = has_direction; } void set_has_oscillating(bool has_oscillating) { this->has_oscillating_ = has_oscillating; } void set_speed_count(int count) { this->speed_count_ = count; } - void set_preset_modes(const FixedVector &presets) { this->preset_modes_ = presets; } - fan::FanTraits get_traits() override { return this->traits_; } + fan::FanTraits get_traits() override; protected: void control(const fan::FanCall &call) override; @@ -24,8 +23,6 @@ class TemplateFan : public Component, public fan::Fan { bool has_oscillating_{false}; bool has_direction_{false}; int speed_count_{0}; - fan::FanTraits traits_; - FixedVector preset_modes_{}; }; } // namespace template_ From eaf0a367b4278d01a139b026d4ea886a6d6a3d06 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 11:37:19 -1000 Subject: [PATCH 08/22] fixed --- esphome/components/copy/fan/copy_fan.cpp | 2 +- esphome/components/fan/fan.h | 16 ------------ esphome/components/fan/fan_traits.h | 25 +++++++++---------- .../components/hbridge/fan/hbridge_fan.cpp | 4 +-- esphome/components/hbridge/fan/hbridge_fan.h | 3 +++ esphome/components/speed/fan/speed_fan.cpp | 5 ++-- esphome/components/speed/fan/speed_fan.h | 2 ++ .../components/template/fan/template_fan.cpp | 6 ++--- .../components/template/fan/template_fan.h | 2 ++ 9 files changed, 25 insertions(+), 40 deletions(-) diff --git a/esphome/components/copy/fan/copy_fan.cpp b/esphome/components/copy/fan/copy_fan.cpp index 15a7f5e025..e2b4c24dd8 100644 --- a/esphome/components/copy/fan/copy_fan.cpp +++ b/esphome/components/copy/fan/copy_fan.cpp @@ -35,7 +35,7 @@ fan::FanTraits CopyFan::get_traits() { traits.set_speed(base.supports_speed()); traits.set_supported_speed_count(base.supported_speed_count()); traits.set_direction(base.supports_direction()); - traits.set_supported_preset_modes(base.supported_preset_modes()); + traits.set_supported_preset_modes(&source_->preset_modes_); return traits; } diff --git a/esphome/components/fan/fan.h b/esphome/components/fan/fan.h index 9b11a214d6..b74187eb4a 100644 --- a/esphome/components/fan/fan.h +++ b/esphome/components/fan/fan.h @@ -1,6 +1,5 @@ #pragma once -#include #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -133,20 +132,6 @@ class Fan : public EntityBase { /// Set the restore mode of this fan. void set_restore_mode(FanRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } - /// Set preset modes - helper for components - void set_preset_modes(const std::initializer_list &presets) { - this->preset_modes_.init(presets.size()); - for (const auto &mode : presets) { - this->preset_modes_.push_back(mode); - } - } - template void set_preset_modes(const T &presets) { - this->preset_modes_.init(presets.size()); - for (const auto &mode : presets) { - this->preset_modes_.push_back(mode); - } - } - protected: friend FanCall; @@ -160,7 +145,6 @@ class Fan : public EntityBase { CallbackManager state_callback_{}; ESPPreferenceObject rtc_; FanRestoreMode restore_mode_; - FixedVector preset_modes_{}; }; } // namespace fan diff --git a/esphome/components/fan/fan_traits.h b/esphome/components/fan/fan_traits.h index 50090f9621..4b0113c451 100644 --- a/esphome/components/fan/fan_traits.h +++ b/esphome/components/fan/fan_traits.h @@ -18,8 +18,12 @@ class FanTraits { FanTraits() = default; FanTraits(bool oscillation, bool speed, bool direction, int speed_count) : oscillation_(oscillation), speed_(speed), direction_(direction), speed_count_(speed_count) {} - FanTraits(FanTraits &&) = default; - FanTraits &operator=(FanTraits &&) = default; + FanTraits(bool oscillation, bool speed, bool direction, int speed_count, const FixedVector *preset_modes) + : oscillation_(oscillation), + speed_(speed), + direction_(direction), + speed_count_(speed_count), + preset_modes_(preset_modes) {} /// Return if this fan supports oscillation. bool supports_oscillation() const { return this->oscillation_; } @@ -38,16 +42,11 @@ class FanTraits { /// Set whether this fan supports changing direction void set_direction(bool direction) { this->direction_ = direction; } /// Return the preset modes supported by the fan. - const FixedVector &supported_preset_modes() const { return this->preset_modes_; } - /// Set the preset modes supported by the fan. - template void set_supported_preset_modes(const T &preset_modes) { - this->preset_modes_.init(preset_modes.size()); - for (const auto &mode : preset_modes) { - this->preset_modes_.push_back(mode); - } - } + const FixedVector &supported_preset_modes() const { return *this->preset_modes_; } + /// Set the preset modes pointer (points to parent Fan's preset_modes_) + void set_supported_preset_modes(const FixedVector *preset_modes) { this->preset_modes_ = preset_modes; } /// Return if preset modes are supported - bool supports_preset_modes() const { return !this->preset_modes_.empty(); } + bool supports_preset_modes() const { return !this->preset_modes_->empty(); } protected: #ifdef USE_API @@ -57,13 +56,13 @@ class FanTraits { // It is used by the API to avoid copying data when encoding messages. // Warning: Do not use this method outside of the API connection code. // It returns a reference to internal data that can be invalidated. - const FixedVector &supported_preset_modes_for_api_() const { return this->preset_modes_; } + const FixedVector &supported_preset_modes_for_api_() const { return *this->preset_modes_; } #endif bool oscillation_{false}; bool speed_{false}; bool direction_{false}; int speed_count_{}; - FixedVector preset_modes_{}; + const FixedVector *preset_modes_{nullptr}; }; } // namespace fan diff --git a/esphome/components/hbridge/fan/hbridge_fan.cpp b/esphome/components/hbridge/fan/hbridge_fan.cpp index 56df053d57..6971e11cf6 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.cpp +++ b/esphome/components/hbridge/fan/hbridge_fan.cpp @@ -36,9 +36,7 @@ void HBridgeFan::setup() { } fan::FanTraits HBridgeFan::get_traits() { - auto traits = fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_); - traits.set_supported_preset_modes(this->preset_modes_); - return traits; + return fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_, &this->preset_modes_); } void HBridgeFan::dump_config() { diff --git a/esphome/components/hbridge/fan/hbridge_fan.h b/esphome/components/hbridge/fan/hbridge_fan.h index d8fa0f99cb..847eca6166 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.h +++ b/esphome/components/hbridge/fan/hbridge_fan.h @@ -21,6 +21,8 @@ class HBridgeFan : public Component, public fan::Fan { void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; } void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; } void set_enable_pin(output::FloatOutput *enable) { enable_ = enable; } + void set_oscillating(output::BinaryOutput *oscillating) { this->oscillating_ = oscillating; } + void set_preset_modes(const std::initializer_list &presets) { this->preset_modes_ = presets; } void setup() override; void dump_config() override; @@ -35,6 +37,7 @@ class HBridgeFan : public Component, public fan::Fan { output::BinaryOutput *oscillating_{nullptr}; int speed_count_{}; DecayMode decay_mode_{DECAY_MODE_SLOW}; + FixedVector preset_modes_{}; void control(const fan::FanCall &call) override; void write_state_(); diff --git a/esphome/components/speed/fan/speed_fan.cpp b/esphome/components/speed/fan/speed_fan.cpp index 03d242178f..081588286f 100644 --- a/esphome/components/speed/fan/speed_fan.cpp +++ b/esphome/components/speed/fan/speed_fan.cpp @@ -15,9 +15,8 @@ void SpeedFan::setup() { } fan::FanTraits SpeedFan::get_traits() { - auto traits = fan::FanTraits(this->oscillating_ != nullptr, true, this->direction_ != nullptr, this->speed_count_); - traits.set_supported_preset_modes(this->preset_modes_); - return traits; + return fan::FanTraits(this->oscillating_ != nullptr, true, this->direction_ != nullptr, this->speed_count_, + &this->preset_modes_); } void SpeedFan::dump_config() { LOG_FAN("", "Speed Fan", this); } diff --git a/esphome/components/speed/fan/speed_fan.h b/esphome/components/speed/fan/speed_fan.h index f29a42190e..baf0fe30f0 100644 --- a/esphome/components/speed/fan/speed_fan.h +++ b/esphome/components/speed/fan/speed_fan.h @@ -17,6 +17,7 @@ class SpeedFan : public Component, public fan::Fan { void set_output(output::FloatOutput *output) { this->output_ = output; } void set_oscillating(output::BinaryOutput *oscillating) { this->oscillating_ = oscillating; } void set_direction(output::BinaryOutput *direction) { this->direction_ = direction; } + void set_preset_modes(const std::initializer_list &presets) { this->preset_modes_ = presets; } fan::FanTraits get_traits() override; protected: @@ -27,6 +28,7 @@ class SpeedFan : public Component, public fan::Fan { output::BinaryOutput *oscillating_{nullptr}; output::BinaryOutput *direction_{nullptr}; int speed_count_{}; + FixedVector preset_modes_{}; }; } // namespace speed diff --git a/esphome/components/template/fan/template_fan.cpp b/esphome/components/template/fan/template_fan.cpp index 39e853fdb6..94891e6a72 100644 --- a/esphome/components/template/fan/template_fan.cpp +++ b/esphome/components/template/fan/template_fan.cpp @@ -14,10 +14,8 @@ void TemplateFan::setup() { } fan::FanTraits TemplateFan::get_traits() { - auto traits = - fan::FanTraits(this->has_oscillating_, this->speed_count_ > 0, this->has_direction_, this->speed_count_); - traits.set_supported_preset_modes(this->preset_modes_); - return traits; + return fan::FanTraits(this->has_oscillating_, this->speed_count_ > 0, this->has_direction_, this->speed_count_, + &this->preset_modes_); } void TemplateFan::dump_config() { LOG_FAN("", "Template Fan", this); } diff --git a/esphome/components/template/fan/template_fan.h b/esphome/components/template/fan/template_fan.h index 561c2de756..affb313a2e 100644 --- a/esphome/components/template/fan/template_fan.h +++ b/esphome/components/template/fan/template_fan.h @@ -15,6 +15,7 @@ class TemplateFan : public Component, public fan::Fan { void set_has_direction(bool has_direction) { this->has_direction_ = has_direction; } void set_has_oscillating(bool has_oscillating) { this->has_oscillating_ = has_oscillating; } void set_speed_count(int count) { this->speed_count_ = count; } + void set_preset_modes(const std::initializer_list &presets) { this->preset_modes_ = presets; } fan::FanTraits get_traits() override; protected: @@ -23,6 +24,7 @@ class TemplateFan : public Component, public fan::Fan { bool has_oscillating_{false}; bool has_direction_{false}; int speed_count_{0}; + FixedVector preset_modes_{}; }; } // namespace template_ From 274c0505f7753a4baa7cfc4c84fc187e4b1e3f55 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 11:38:52 -1000 Subject: [PATCH 09/22] fixed --- esphome/components/fan/fan_traits.h | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/esphome/components/fan/fan_traits.h b/esphome/components/fan/fan_traits.h index 4b0113c451..8a25c287ab 100644 --- a/esphome/components/fan/fan_traits.h +++ b/esphome/components/fan/fan_traits.h @@ -1,8 +1,7 @@ -#include -#include "esphome/core/helpers.h" - #pragma once +#include "esphome/core/helpers.h" + namespace esphome { #ifdef USE_API @@ -43,14 +42,11 @@ class FanTraits { void set_direction(bool direction) { this->direction_ = direction; } /// Return the preset modes supported by the fan. const FixedVector &supported_preset_modes() const { return *this->preset_modes_; } - /// Set the preset modes pointer (points to parent Fan's preset_modes_) - void set_supported_preset_modes(const FixedVector *preset_modes) { this->preset_modes_ = preset_modes; } /// Return if preset modes are supported bool supports_preset_modes() const { return !this->preset_modes_->empty(); } protected: #ifdef USE_API - // The API connection is a friend class to access internal methods friend class api::APIConnection; // This method returns a reference to the internal preset modes. // It is used by the API to avoid copying data when encoding messages. From 43bcd98649efc09fc8b70d469ea58f1065699384 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 11:41:15 -1000 Subject: [PATCH 10/22] fixed --- esphome/components/fan/fan_traits.h | 2 ++ esphome/components/hbridge/fan/hbridge_fan.h | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/fan/fan_traits.h b/esphome/components/fan/fan_traits.h index 8a25c287ab..4c10ccd10a 100644 --- a/esphome/components/fan/fan_traits.h +++ b/esphome/components/fan/fan_traits.h @@ -42,6 +42,8 @@ class FanTraits { void set_direction(bool direction) { this->direction_ = direction; } /// Return the preset modes supported by the fan. const FixedVector &supported_preset_modes() const { return *this->preset_modes_; } + /// Set the preset modes supported by the fan. + void set_supported_preset_modes(const FixedVector *preset_modes) { this->preset_modes_ = preset_modes; } /// Return if preset modes are supported bool supports_preset_modes() const { return !this->preset_modes_->empty(); } diff --git a/esphome/components/hbridge/fan/hbridge_fan.h b/esphome/components/hbridge/fan/hbridge_fan.h index 847eca6166..e4b075f759 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.h +++ b/esphome/components/hbridge/fan/hbridge_fan.h @@ -21,7 +21,6 @@ class HBridgeFan : public Component, public fan::Fan { void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; } void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; } void set_enable_pin(output::FloatOutput *enable) { enable_ = enable; } - void set_oscillating(output::BinaryOutput *oscillating) { this->oscillating_ = oscillating; } void set_preset_modes(const std::initializer_list &presets) { this->preset_modes_ = presets; } void setup() override; From fdb23a2c1371bd0c5ca93605d9a0d5f6e4fa3d04 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 11:42:31 -1000 Subject: [PATCH 11/22] fixed --- esphome/components/copy/fan/copy_fan.cpp | 2 +- esphome/components/copy/fan/copy_fan.h | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/copy/fan/copy_fan.cpp b/esphome/components/copy/fan/copy_fan.cpp index e2b4c24dd8..cf5341531a 100644 --- a/esphome/components/copy/fan/copy_fan.cpp +++ b/esphome/components/copy/fan/copy_fan.cpp @@ -35,7 +35,7 @@ fan::FanTraits CopyFan::get_traits() { traits.set_speed(base.supports_speed()); traits.set_supported_speed_count(base.supported_speed_count()); traits.set_direction(base.supports_direction()); - traits.set_supported_preset_modes(&source_->preset_modes_); + traits.set_supported_preset_modes(&base.supported_preset_modes()); return traits; } diff --git a/esphome/components/copy/fan/copy_fan.h b/esphome/components/copy/fan/copy_fan.h index e1212537f1..b474975bc4 100644 --- a/esphome/components/copy/fan/copy_fan.h +++ b/esphome/components/copy/fan/copy_fan.h @@ -16,6 +16,7 @@ class CopyFan : public fan::Fan, public Component { protected: void control(const fan::FanCall &call) override; + ; fan::Fan *source_; }; From 5c7029623e6d728441bf655609ff0394ef7209e1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 11:44:42 -1000 Subject: [PATCH 12/22] fixed --- esphome/components/fan/fan.cpp | 14 ++++++++------ esphome/components/fan/fan_traits.h | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index 26a61de0b1..856152de63 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -50,13 +50,15 @@ void FanCall::validate_() { } if (!this->preset_mode_.empty()) { - const auto &preset_modes = traits.supported_preset_modes(); - // Linear search is efficient for small preset mode lists (typically 2-5 items) bool found = false; - for (const auto &mode : preset_modes) { - if (mode == this->preset_mode_) { - found = true; - break; + if (traits.supports_preset_modes()) { + const auto &preset_modes = traits.supported_preset_modes(); + // Linear search is efficient for small preset mode lists (typically 2-5 items) + for (const auto &mode : preset_modes) { + if (mode == this->preset_mode_) { + found = true; + break; + } } } if (!found) { diff --git a/esphome/components/fan/fan_traits.h b/esphome/components/fan/fan_traits.h index 4c10ccd10a..138d39bb65 100644 --- a/esphome/components/fan/fan_traits.h +++ b/esphome/components/fan/fan_traits.h @@ -45,7 +45,7 @@ class FanTraits { /// Set the preset modes supported by the fan. void set_supported_preset_modes(const FixedVector *preset_modes) { this->preset_modes_ = preset_modes; } /// Return if preset modes are supported - bool supports_preset_modes() const { return !this->preset_modes_->empty(); } + bool supports_preset_modes() const { return this->preset_modes_ != nullptr && !this->preset_modes_->empty(); } protected: #ifdef USE_API From b0f764a37e15fc5f5ddbd973ee16b5166edce466 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 11:52:15 -1000 Subject: [PATCH 13/22] fixed --- esphome/components/api/api_connection.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 7c135946f8..cb480ce51a 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -423,7 +423,8 @@ uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *con msg.supports_speed = traits.supports_speed(); msg.supports_direction = traits.supports_direction(); msg.supported_speed_count = traits.supported_speed_count(); - msg.supported_preset_modes = &traits.supported_preset_modes_for_api_(); + if (traits.supports_preset_modes()) + msg.supported_preset_modes = &traits.supported_preset_modes_for_api_(); return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::fan_command(const FanCommandRequest &msg) { From 26e47546737b78287359b1ebbbd8c23529792b71 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 12:02:20 -1000 Subject: [PATCH 14/22] fixed --- esphome/components/api/api_connection.cpp | 2 +- esphome/components/fan/fan_traits.h | 15 --------------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index cb480ce51a..970b6d29f4 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -424,7 +424,7 @@ uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *con msg.supports_direction = traits.supports_direction(); msg.supported_speed_count = traits.supported_speed_count(); if (traits.supports_preset_modes()) - msg.supported_preset_modes = &traits.supported_preset_modes_for_api_(); + msg.supported_preset_modes = &traits.supported_preset_modes(); return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::fan_command(const FanCommandRequest &msg) { diff --git a/esphome/components/fan/fan_traits.h b/esphome/components/fan/fan_traits.h index 138d39bb65..5c2a0eb355 100644 --- a/esphome/components/fan/fan_traits.h +++ b/esphome/components/fan/fan_traits.h @@ -3,13 +3,6 @@ #include "esphome/core/helpers.h" namespace esphome { - -#ifdef USE_API -namespace api { -class APIConnection; -} // namespace api -#endif - namespace fan { class FanTraits { @@ -48,14 +41,6 @@ class FanTraits { bool supports_preset_modes() const { return this->preset_modes_ != nullptr && !this->preset_modes_->empty(); } protected: -#ifdef USE_API - friend class api::APIConnection; - // This method returns a reference to the internal preset modes. - // It is used by the API to avoid copying data when encoding messages. - // Warning: Do not use this method outside of the API connection code. - // It returns a reference to internal data that can be invalidated. - const FixedVector &supported_preset_modes_for_api_() const { return *this->preset_modes_; } -#endif bool oscillation_{false}; bool speed_{false}; bool direction_{false}; From 6d1ee107426a38206e118a6818d1d937524f9725 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 12:24:47 -1000 Subject: [PATCH 15/22] manual copy --- esphome/components/copy/fan/copy_fan.cpp | 12 +++++++++++- esphome/components/copy/fan/copy_fan.h | 2 ++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/esphome/components/copy/fan/copy_fan.cpp b/esphome/components/copy/fan/copy_fan.cpp index cf5341531a..b939338b24 100644 --- a/esphome/components/copy/fan/copy_fan.cpp +++ b/esphome/components/copy/fan/copy_fan.cpp @@ -35,7 +35,17 @@ fan::FanTraits CopyFan::get_traits() { traits.set_speed(base.supports_speed()); traits.set_supported_speed_count(base.supported_speed_count()); traits.set_direction(base.supports_direction()); - traits.set_supported_preset_modes(&base.supported_preset_modes()); + + // Copy preset modes from source to avoid dangling pointer to temporary + if (base.supports_preset_modes()) { + const auto &source_modes = base.supported_preset_modes(); + this->preset_modes_.clear(); + for (const auto &mode : source_modes) { + this->preset_modes_.push_back(mode); + } + traits.set_supported_preset_modes(&this->preset_modes_); + } + return traits; } diff --git a/esphome/components/copy/fan/copy_fan.h b/esphome/components/copy/fan/copy_fan.h index b474975bc4..78134c6890 100644 --- a/esphome/components/copy/fan/copy_fan.h +++ b/esphome/components/copy/fan/copy_fan.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/helpers.h" #include "esphome/components/fan/fan.h" namespace esphome { @@ -19,6 +20,7 @@ class CopyFan : public fan::Fan, public Component { ; fan::Fan *source_; + FixedVector preset_modes_{}; }; } // namespace copy From c69e7f4e78655112278f1108aacafbff5288c440 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 12:25:35 -1000 Subject: [PATCH 16/22] init --- esphome/components/copy/fan/copy_fan.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/copy/fan/copy_fan.cpp b/esphome/components/copy/fan/copy_fan.cpp index b939338b24..9ec4d8f973 100644 --- a/esphome/components/copy/fan/copy_fan.cpp +++ b/esphome/components/copy/fan/copy_fan.cpp @@ -39,7 +39,7 @@ fan::FanTraits CopyFan::get_traits() { // Copy preset modes from source to avoid dangling pointer to temporary if (base.supports_preset_modes()) { const auto &source_modes = base.supported_preset_modes(); - this->preset_modes_.clear(); + this->preset_modes_.init(source_modes.size()); for (const auto &mode : source_modes) { this->preset_modes_.push_back(mode); } From c7aef0016a279d0e7f3b838f560baf1c9c53e1c5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 12:27:29 -1000 Subject: [PATCH 17/22] manual copy --- esphome/components/fan/fan.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index 856152de63..774cf59e23 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -102,9 +102,10 @@ FanCall FanRestoreState::to_call(Fan &fan) { call.set_speed(this->speed); call.set_direction(this->direction); - if (fan.get_traits().supports_preset_modes()) { + auto traits = fan.get_traits(); + if (traits.supports_preset_modes()) { // Use stored preset index to get preset name - const auto &preset_modes = fan.get_traits().supported_preset_modes(); + const auto &preset_modes = traits.supported_preset_modes(); if (this->preset_mode < preset_modes.size()) { call.set_preset_mode(preset_modes[this->preset_mode]); } @@ -117,9 +118,10 @@ void FanRestoreState::apply(Fan &fan) { fan.speed = this->speed; fan.direction = this->direction; - if (fan.get_traits().supports_preset_modes()) { + auto traits = fan.get_traits(); + if (traits.supports_preset_modes()) { // Use stored preset index to get preset name - const auto &preset_modes = fan.get_traits().supported_preset_modes(); + const auto &preset_modes = traits.supported_preset_modes(); if (this->preset_mode < preset_modes.size()) { fan.preset_mode = preset_modes[this->preset_mode]; } @@ -198,8 +200,9 @@ void Fan::save_state_() { state.speed = this->speed; state.direction = this->direction; - if (this->get_traits().supports_preset_modes() && !this->preset_mode.empty()) { - const auto &preset_modes = this->get_traits().supported_preset_modes(); + auto traits = this->get_traits(); + if (traits.supports_preset_modes() && !this->preset_mode.empty()) { + const auto &preset_modes = traits.supported_preset_modes(); // Store index of current preset mode - linear search is efficient for small lists for (size_t i = 0; i < preset_modes.size(); i++) { if (preset_modes[i] == this->preset_mode) { From fe6f87718581a78f2608c0cd82e071e9b7a0fe94 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 12:28:51 -1000 Subject: [PATCH 18/22] manual copy --- esphome/components/fan/automation.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/fan/automation.h b/esphome/components/fan/automation.h index 90661c307c..cf04362477 100644 --- a/esphome/components/fan/automation.h +++ b/esphome/components/fan/automation.h @@ -58,10 +58,11 @@ template class CycleSpeedAction : public Action { void play(Ts... x) override { // check to see if fan supports speeds and is on - if (this->state_->get_traits().supported_speed_count()) { + auto traits = this->state_->get_traits(); + if (traits.supported_speed_count()) { if (this->state_->state) { int speed = this->state_->speed + 1; - int supported_speed_count = this->state_->get_traits().supported_speed_count(); + int supported_speed_count = traits.supported_speed_count(); bool off_speed_cycle = no_off_cycle_.value(x...); if (speed > supported_speed_count && off_speed_cycle) { // was running at max speed, off speed cycle enabled, so turn off From 977dd9dd340bcc71166a5d4f691354fdaaf3584e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 12:29:23 -1000 Subject: [PATCH 19/22] manual copy --- esphome/components/web_server/web_server.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 1d08ef5a35..e84bb67aba 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -723,7 +723,7 @@ std::string WebServer::fan_json(fan::Fan *obj, JsonDetail start_config) { root["speed_level"] = obj->speed; root["speed_count"] = traits.supported_speed_count(); } - if (obj->get_traits().supports_oscillation()) + if (traits.supports_oscillation()) root["oscillation"] = obj->oscillating; if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); From 93c555ae873dbd29ac29f18ace4615fef6c482eb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 13:18:14 -1000 Subject: [PATCH 20/22] reset --- esphome/components/api/api.proto | 2 +- esphome/components/api/api_connection.cpp | 3 +- esphome/components/api/api_pb2.h | 2 +- esphome/components/copy/fan/copy_fan.cpp | 12 +---- esphome/components/copy/fan/copy_fan.h | 2 - esphome/components/fan/automation.h | 5 +-- esphome/components/fan/fan.cpp | 44 ++++++------------- esphome/components/fan/fan_traits.h | 35 ++++++++++----- .../components/hbridge/fan/hbridge_fan.cpp | 6 +-- esphome/components/hbridge/fan/hbridge_fan.h | 10 +++-- esphome/components/speed/fan/speed_fan.cpp | 7 ++- esphome/components/speed/fan/speed_fan.h | 10 +++-- .../components/template/fan/template_fan.cpp | 8 ++-- .../components/template/fan/template_fan.h | 10 +++-- esphome/components/web_server/web_server.cpp | 2 +- 15 files changed, 72 insertions(+), 86 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 34be6e4aa2..d202486cfa 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -425,7 +425,7 @@ message ListEntitiesFanResponse { bool disabled_by_default = 9; string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"]; EntityCategory entity_category = 11; - repeated string supported_preset_modes = 12 [(container_pointer) = "FixedVector"]; + repeated string supported_preset_modes = 12 [(container_pointer) = "std::set"]; uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"]; } // Deprecated in API version 1.6 - only used in deprecated fields diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 970b6d29f4..7c135946f8 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -423,8 +423,7 @@ uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *con msg.supports_speed = traits.supports_speed(); msg.supports_direction = traits.supports_direction(); msg.supported_speed_count = traits.supported_speed_count(); - if (traits.supports_preset_modes()) - msg.supported_preset_modes = &traits.supported_preset_modes(); + msg.supported_preset_modes = &traits.supported_preset_modes_for_api_(); return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::fan_command(const FanCommandRequest &msg) { diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 647dd47b89..ed49498176 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -725,7 +725,7 @@ class ListEntitiesFanResponse final : public InfoResponseProtoMessage { bool supports_speed{false}; bool supports_direction{false}; int32_t supported_speed_count{0}; - const FixedVector *supported_preset_modes{}; + const std::set *supported_preset_modes{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP diff --git a/esphome/components/copy/fan/copy_fan.cpp b/esphome/components/copy/fan/copy_fan.cpp index 9ec4d8f973..15a7f5e025 100644 --- a/esphome/components/copy/fan/copy_fan.cpp +++ b/esphome/components/copy/fan/copy_fan.cpp @@ -35,17 +35,7 @@ fan::FanTraits CopyFan::get_traits() { traits.set_speed(base.supports_speed()); traits.set_supported_speed_count(base.supported_speed_count()); traits.set_direction(base.supports_direction()); - - // Copy preset modes from source to avoid dangling pointer to temporary - if (base.supports_preset_modes()) { - const auto &source_modes = base.supported_preset_modes(); - this->preset_modes_.init(source_modes.size()); - for (const auto &mode : source_modes) { - this->preset_modes_.push_back(mode); - } - traits.set_supported_preset_modes(&this->preset_modes_); - } - + traits.set_supported_preset_modes(base.supported_preset_modes()); return traits; } diff --git a/esphome/components/copy/fan/copy_fan.h b/esphome/components/copy/fan/copy_fan.h index 78134c6890..b474975bc4 100644 --- a/esphome/components/copy/fan/copy_fan.h +++ b/esphome/components/copy/fan/copy_fan.h @@ -1,7 +1,6 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/core/helpers.h" #include "esphome/components/fan/fan.h" namespace esphome { @@ -20,7 +19,6 @@ class CopyFan : public fan::Fan, public Component { ; fan::Fan *source_; - FixedVector preset_modes_{}; }; } // namespace copy diff --git a/esphome/components/fan/automation.h b/esphome/components/fan/automation.h index cf04362477..90661c307c 100644 --- a/esphome/components/fan/automation.h +++ b/esphome/components/fan/automation.h @@ -58,11 +58,10 @@ template class CycleSpeedAction : public Action { void play(Ts... x) override { // check to see if fan supports speeds and is on - auto traits = this->state_->get_traits(); - if (traits.supported_speed_count()) { + if (this->state_->get_traits().supported_speed_count()) { if (this->state_->state) { int speed = this->state_->speed + 1; - int supported_speed_count = traits.supported_speed_count(); + int supported_speed_count = this->state_->get_traits().supported_speed_count(); bool off_speed_cycle = no_off_cycle_.value(x...); if (speed > supported_speed_count && off_speed_cycle) { // was running at max speed, off speed cycle enabled, so turn off diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index 774cf59e23..26065ed644 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -50,18 +50,8 @@ void FanCall::validate_() { } if (!this->preset_mode_.empty()) { - bool found = false; - if (traits.supports_preset_modes()) { - const auto &preset_modes = traits.supported_preset_modes(); - // Linear search is efficient for small preset mode lists (typically 2-5 items) - for (const auto &mode : preset_modes) { - if (mode == this->preset_mode_) { - found = true; - break; - } - } - } - if (!found) { + const auto &preset_modes = traits.supported_preset_modes(); + if (preset_modes.find(this->preset_mode_) == preset_modes.end()) { ESP_LOGW(TAG, "%s: Preset mode '%s' not supported", this->parent_.get_name().c_str(), this->preset_mode_.c_str()); this->preset_mode_.clear(); } @@ -102,12 +92,11 @@ FanCall FanRestoreState::to_call(Fan &fan) { call.set_speed(this->speed); call.set_direction(this->direction); - auto traits = fan.get_traits(); - if (traits.supports_preset_modes()) { + if (fan.get_traits().supports_preset_modes()) { // Use stored preset index to get preset name - const auto &preset_modes = traits.supported_preset_modes(); + const auto &preset_modes = fan.get_traits().supported_preset_modes(); if (this->preset_mode < preset_modes.size()) { - call.set_preset_mode(preset_modes[this->preset_mode]); + call.set_preset_mode(*std::next(preset_modes.begin(), this->preset_mode)); } } return call; @@ -118,12 +107,11 @@ void FanRestoreState::apply(Fan &fan) { fan.speed = this->speed; fan.direction = this->direction; - auto traits = fan.get_traits(); - if (traits.supports_preset_modes()) { + if (fan.get_traits().supports_preset_modes()) { // Use stored preset index to get preset name - const auto &preset_modes = traits.supported_preset_modes(); + const auto &preset_modes = fan.get_traits().supported_preset_modes(); if (this->preset_mode < preset_modes.size()) { - fan.preset_mode = preset_modes[this->preset_mode]; + fan.preset_mode = *std::next(preset_modes.begin(), this->preset_mode); } } fan.publish_state(); @@ -200,16 +188,12 @@ void Fan::save_state_() { state.speed = this->speed; state.direction = this->direction; - auto traits = this->get_traits(); - if (traits.supports_preset_modes() && !this->preset_mode.empty()) { - const auto &preset_modes = traits.supported_preset_modes(); - // Store index of current preset mode - linear search is efficient for small lists - for (size_t i = 0; i < preset_modes.size(); i++) { - if (preset_modes[i] == this->preset_mode) { - state.preset_mode = i; - break; - } - } + if (this->get_traits().supports_preset_modes() && !this->preset_mode.empty()) { + const auto &preset_modes = this->get_traits().supported_preset_modes(); + // Store index of current preset mode + auto preset_iterator = preset_modes.find(this->preset_mode); + if (preset_iterator != preset_modes.end()) + state.preset_mode = std::distance(preset_modes.begin(), preset_iterator); } this->rtc_.save(&state); diff --git a/esphome/components/fan/fan_traits.h b/esphome/components/fan/fan_traits.h index 5c2a0eb355..48509e5705 100644 --- a/esphome/components/fan/fan_traits.h +++ b/esphome/components/fan/fan_traits.h @@ -1,8 +1,16 @@ +#include +#include + #pragma once -#include "esphome/core/helpers.h" - namespace esphome { + +#ifdef USE_API +namespace api { +class APIConnection; +} // namespace api +#endif + namespace fan { class FanTraits { @@ -10,12 +18,6 @@ class FanTraits { FanTraits() = default; FanTraits(bool oscillation, bool speed, bool direction, int speed_count) : oscillation_(oscillation), speed_(speed), direction_(direction), speed_count_(speed_count) {} - FanTraits(bool oscillation, bool speed, bool direction, int speed_count, const FixedVector *preset_modes) - : oscillation_(oscillation), - speed_(speed), - direction_(direction), - speed_count_(speed_count), - preset_modes_(preset_modes) {} /// Return if this fan supports oscillation. bool supports_oscillation() const { return this->oscillation_; } @@ -34,18 +36,27 @@ class FanTraits { /// Set whether this fan supports changing direction void set_direction(bool direction) { this->direction_ = direction; } /// Return the preset modes supported by the fan. - const FixedVector &supported_preset_modes() const { return *this->preset_modes_; } + std::set supported_preset_modes() const { return this->preset_modes_; } /// Set the preset modes supported by the fan. - void set_supported_preset_modes(const FixedVector *preset_modes) { this->preset_modes_ = preset_modes; } + void set_supported_preset_modes(const std::set &preset_modes) { this->preset_modes_ = preset_modes; } /// Return if preset modes are supported - bool supports_preset_modes() const { return this->preset_modes_ != nullptr && !this->preset_modes_->empty(); } + bool supports_preset_modes() const { return !this->preset_modes_.empty(); } protected: +#ifdef USE_API + // The API connection is a friend class to access internal methods + friend class api::APIConnection; + // This method returns a reference to the internal preset modes set. + // It is used by the API to avoid copying data when encoding messages. + // Warning: Do not use this method outside of the API connection code. + // It returns a reference to internal data that can be invalidated. + const std::set &supported_preset_modes_for_api_() const { return this->preset_modes_; } +#endif bool oscillation_{false}; bool speed_{false}; bool direction_{false}; int speed_count_{}; - const FixedVector *preset_modes_{nullptr}; + std::set preset_modes_{}; }; } // namespace fan diff --git a/esphome/components/hbridge/fan/hbridge_fan.cpp b/esphome/components/hbridge/fan/hbridge_fan.cpp index 6971e11cf6..605a9d4ef3 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.cpp +++ b/esphome/components/hbridge/fan/hbridge_fan.cpp @@ -33,10 +33,10 @@ void HBridgeFan::setup() { restore->apply(*this); this->write_state_(); } -} -fan::FanTraits HBridgeFan::get_traits() { - return fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_, &this->preset_modes_); + // Construct traits + this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, true, this->speed_count_); + this->traits_.set_supported_preset_modes(this->preset_modes_); } void HBridgeFan::dump_config() { diff --git a/esphome/components/hbridge/fan/hbridge_fan.h b/esphome/components/hbridge/fan/hbridge_fan.h index e4b075f759..4234fccae3 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.h +++ b/esphome/components/hbridge/fan/hbridge_fan.h @@ -1,7 +1,8 @@ #pragma once +#include + #include "esphome/core/automation.h" -#include "esphome/core/helpers.h" #include "esphome/components/output/binary_output.h" #include "esphome/components/output/float_output.h" #include "esphome/components/fan/fan.h" @@ -21,11 +22,11 @@ class HBridgeFan : public Component, public fan::Fan { void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; } void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; } void set_enable_pin(output::FloatOutput *enable) { enable_ = enable; } - void set_preset_modes(const std::initializer_list &presets) { this->preset_modes_ = presets; } + void set_preset_modes(const std::set &presets) { preset_modes_ = presets; } void setup() override; void dump_config() override; - fan::FanTraits get_traits() override; + fan::FanTraits get_traits() override { return this->traits_; } fan::FanCall brake(); @@ -36,7 +37,8 @@ class HBridgeFan : public Component, public fan::Fan { output::BinaryOutput *oscillating_{nullptr}; int speed_count_{}; DecayMode decay_mode_{DECAY_MODE_SLOW}; - FixedVector preset_modes_{}; + fan::FanTraits traits_; + std::set preset_modes_{}; void control(const fan::FanCall &call) override; void write_state_(); diff --git a/esphome/components/speed/fan/speed_fan.cpp b/esphome/components/speed/fan/speed_fan.cpp index 081588286f..57bd795416 100644 --- a/esphome/components/speed/fan/speed_fan.cpp +++ b/esphome/components/speed/fan/speed_fan.cpp @@ -12,11 +12,10 @@ void SpeedFan::setup() { restore->apply(*this); this->write_state_(); } -} -fan::FanTraits SpeedFan::get_traits() { - return fan::FanTraits(this->oscillating_ != nullptr, true, this->direction_ != nullptr, this->speed_count_, - &this->preset_modes_); + // Construct traits + this->traits_ = fan::FanTraits(this->oscillating_ != nullptr, true, this->direction_ != nullptr, this->speed_count_); + this->traits_.set_supported_preset_modes(this->preset_modes_); } void SpeedFan::dump_config() { LOG_FAN("", "Speed Fan", this); } diff --git a/esphome/components/speed/fan/speed_fan.h b/esphome/components/speed/fan/speed_fan.h index baf0fe30f0..6537bce3f6 100644 --- a/esphome/components/speed/fan/speed_fan.h +++ b/esphome/components/speed/fan/speed_fan.h @@ -1,7 +1,8 @@ #pragma once +#include + #include "esphome/core/component.h" -#include "esphome/core/helpers.h" #include "esphome/components/output/binary_output.h" #include "esphome/components/output/float_output.h" #include "esphome/components/fan/fan.h" @@ -17,8 +18,8 @@ class SpeedFan : public Component, public fan::Fan { void set_output(output::FloatOutput *output) { this->output_ = output; } void set_oscillating(output::BinaryOutput *oscillating) { this->oscillating_ = oscillating; } void set_direction(output::BinaryOutput *direction) { this->direction_ = direction; } - void set_preset_modes(const std::initializer_list &presets) { this->preset_modes_ = presets; } - fan::FanTraits get_traits() override; + void set_preset_modes(const std::set &presets) { this->preset_modes_ = presets; } + fan::FanTraits get_traits() override { return this->traits_; } protected: void control(const fan::FanCall &call) override; @@ -28,7 +29,8 @@ class SpeedFan : public Component, public fan::Fan { output::BinaryOutput *oscillating_{nullptr}; output::BinaryOutput *direction_{nullptr}; int speed_count_{}; - FixedVector preset_modes_{}; + fan::FanTraits traits_; + std::set preset_modes_{}; }; } // namespace speed diff --git a/esphome/components/template/fan/template_fan.cpp b/esphome/components/template/fan/template_fan.cpp index 94891e6a72..5f4a2ae8f7 100644 --- a/esphome/components/template/fan/template_fan.cpp +++ b/esphome/components/template/fan/template_fan.cpp @@ -11,11 +11,11 @@ void TemplateFan::setup() { if (restore.has_value()) { restore->apply(*this); } -} -fan::FanTraits TemplateFan::get_traits() { - return fan::FanTraits(this->has_oscillating_, this->speed_count_ > 0, this->has_direction_, this->speed_count_, - &this->preset_modes_); + // Construct traits + this->traits_ = + fan::FanTraits(this->has_oscillating_, this->speed_count_ > 0, this->has_direction_, this->speed_count_); + this->traits_.set_supported_preset_modes(this->preset_modes_); } void TemplateFan::dump_config() { LOG_FAN("", "Template Fan", this); } diff --git a/esphome/components/template/fan/template_fan.h b/esphome/components/template/fan/template_fan.h index affb313a2e..7f5305ca48 100644 --- a/esphome/components/template/fan/template_fan.h +++ b/esphome/components/template/fan/template_fan.h @@ -1,7 +1,8 @@ #pragma once +#include + #include "esphome/core/component.h" -#include "esphome/core/helpers.h" #include "esphome/components/fan/fan.h" namespace esphome { @@ -15,8 +16,8 @@ class TemplateFan : public Component, public fan::Fan { void set_has_direction(bool has_direction) { this->has_direction_ = has_direction; } void set_has_oscillating(bool has_oscillating) { this->has_oscillating_ = has_oscillating; } void set_speed_count(int count) { this->speed_count_ = count; } - void set_preset_modes(const std::initializer_list &presets) { this->preset_modes_ = presets; } - fan::FanTraits get_traits() override; + void set_preset_modes(const std::set &presets) { this->preset_modes_ = presets; } + fan::FanTraits get_traits() override { return this->traits_; } protected: void control(const fan::FanCall &call) override; @@ -24,7 +25,8 @@ class TemplateFan : public Component, public fan::Fan { bool has_oscillating_{false}; bool has_direction_{false}; int speed_count_{0}; - FixedVector preset_modes_{}; + fan::FanTraits traits_; + std::set preset_modes_{}; }; } // namespace template_ diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index e84bb67aba..1d08ef5a35 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -723,7 +723,7 @@ std::string WebServer::fan_json(fan::Fan *obj, JsonDetail start_config) { root["speed_level"] = obj->speed; root["speed_count"] = traits.supported_speed_count(); } - if (traits.supports_oscillation()) + if (obj->get_traits().supports_oscillation()) root["oscillation"] = obj->oscillating; if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); From 39b93079e506fac4bfef9ae0f8da19bee7a360f0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 13:26:53 -1000 Subject: [PATCH 21/22] simp --- esphome/components/api/api.proto | 2 +- esphome/components/api/api_pb2.h | 2 +- esphome/components/fan/fan.cpp | 5 +++-- esphome/components/fan/fan_traits.h | 15 +++++++-------- esphome/components/hbridge/fan/hbridge_fan.h | 4 ++-- esphome/components/speed/fan/speed_fan.h | 4 ++-- esphome/components/template/fan/template_fan.h | 6 +++--- 7 files changed, 19 insertions(+), 19 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index d202486cfa..a4c2557ffe 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -425,7 +425,7 @@ message ListEntitiesFanResponse { bool disabled_by_default = 9; string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"]; EntityCategory entity_category = 11; - repeated string supported_preset_modes = 12 [(container_pointer) = "std::set"]; + repeated string supported_preset_modes = 12 [(container_pointer) = "std::vector"]; uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"]; } // Deprecated in API version 1.6 - only used in deprecated fields diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index ed49498176..e71ad2c64e 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -725,7 +725,7 @@ class ListEntitiesFanResponse final : public InfoResponseProtoMessage { bool supports_speed{false}; bool supports_direction{false}; int32_t supported_speed_count{0}; - const std::set *supported_preset_modes{}; + const std::vector *supported_preset_modes{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index 26065ed644..7fb19f242a 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -1,5 +1,6 @@ #include "fan.h" #include "esphome/core/log.h" +#include namespace esphome { namespace fan { @@ -51,7 +52,7 @@ void FanCall::validate_() { if (!this->preset_mode_.empty()) { const auto &preset_modes = traits.supported_preset_modes(); - if (preset_modes.find(this->preset_mode_) == preset_modes.end()) { + if (std::find(preset_modes.begin(), preset_modes.end(), this->preset_mode_) == preset_modes.end()) { ESP_LOGW(TAG, "%s: Preset mode '%s' not supported", this->parent_.get_name().c_str(), this->preset_mode_.c_str()); this->preset_mode_.clear(); } @@ -191,7 +192,7 @@ void Fan::save_state_() { if (this->get_traits().supports_preset_modes() && !this->preset_mode.empty()) { const auto &preset_modes = this->get_traits().supported_preset_modes(); // Store index of current preset mode - auto preset_iterator = preset_modes.find(this->preset_mode); + auto preset_iterator = std::find(preset_modes.begin(), preset_modes.end(), this->preset_mode); if (preset_iterator != preset_modes.end()) state.preset_mode = std::distance(preset_modes.begin(), preset_iterator); } diff --git a/esphome/components/fan/fan_traits.h b/esphome/components/fan/fan_traits.h index 48509e5705..15c951b045 100644 --- a/esphome/components/fan/fan_traits.h +++ b/esphome/components/fan/fan_traits.h @@ -1,8 +1,7 @@ -#include -#include - #pragma once +#include + namespace esphome { #ifdef USE_API @@ -36,9 +35,9 @@ class FanTraits { /// Set whether this fan supports changing direction void set_direction(bool direction) { this->direction_ = direction; } /// Return the preset modes supported by the fan. - std::set supported_preset_modes() const { return this->preset_modes_; } + const std::vector &supported_preset_modes() const { return this->preset_modes_; } /// Set the preset modes supported by the fan. - void set_supported_preset_modes(const std::set &preset_modes) { this->preset_modes_ = preset_modes; } + void set_supported_preset_modes(const std::vector &preset_modes) { this->preset_modes_ = preset_modes; } /// Return if preset modes are supported bool supports_preset_modes() const { return !this->preset_modes_.empty(); } @@ -46,17 +45,17 @@ class FanTraits { #ifdef USE_API // The API connection is a friend class to access internal methods friend class api::APIConnection; - // This method returns a reference to the internal preset modes set. + // This method returns a reference to the internal preset modes. // It is used by the API to avoid copying data when encoding messages. // Warning: Do not use this method outside of the API connection code. // It returns a reference to internal data that can be invalidated. - const std::set &supported_preset_modes_for_api_() const { return this->preset_modes_; } + const std::vector &supported_preset_modes_for_api_() const { return this->preset_modes_; } #endif bool oscillation_{false}; bool speed_{false}; bool direction_{false}; int speed_count_{}; - std::set preset_modes_{}; + std::vector preset_modes_{}; }; } // namespace fan diff --git a/esphome/components/hbridge/fan/hbridge_fan.h b/esphome/components/hbridge/fan/hbridge_fan.h index 4234fccae3..b5fb7f5daa 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.h +++ b/esphome/components/hbridge/fan/hbridge_fan.h @@ -22,7 +22,7 @@ class HBridgeFan : public Component, public fan::Fan { void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; } void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; } void set_enable_pin(output::FloatOutput *enable) { enable_ = enable; } - void set_preset_modes(const std::set &presets) { preset_modes_ = presets; } + void set_preset_modes(const std::vector &presets) { preset_modes_ = presets; } void setup() override; void dump_config() override; @@ -38,7 +38,7 @@ class HBridgeFan : public Component, public fan::Fan { int speed_count_{}; DecayMode decay_mode_{DECAY_MODE_SLOW}; fan::FanTraits traits_; - std::set preset_modes_{}; + std::vector preset_modes_{}; void control(const fan::FanCall &call) override; void write_state_(); diff --git a/esphome/components/speed/fan/speed_fan.h b/esphome/components/speed/fan/speed_fan.h index 6537bce3f6..454b7fc136 100644 --- a/esphome/components/speed/fan/speed_fan.h +++ b/esphome/components/speed/fan/speed_fan.h @@ -18,7 +18,7 @@ class SpeedFan : public Component, public fan::Fan { void set_output(output::FloatOutput *output) { this->output_ = output; } void set_oscillating(output::BinaryOutput *oscillating) { this->oscillating_ = oscillating; } void set_direction(output::BinaryOutput *direction) { this->direction_ = direction; } - void set_preset_modes(const std::set &presets) { this->preset_modes_ = presets; } + void set_preset_modes(const std::vector &presets) { this->preset_modes_ = presets; } fan::FanTraits get_traits() override { return this->traits_; } protected: @@ -30,7 +30,7 @@ class SpeedFan : public Component, public fan::Fan { output::BinaryOutput *direction_{nullptr}; int speed_count_{}; fan::FanTraits traits_; - std::set preset_modes_{}; + std::vector preset_modes_{}; }; } // namespace speed diff --git a/esphome/components/template/fan/template_fan.h b/esphome/components/template/fan/template_fan.h index 7f5305ca48..5d780f61f0 100644 --- a/esphome/components/template/fan/template_fan.h +++ b/esphome/components/template/fan/template_fan.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include "esphome/core/component.h" #include "esphome/components/fan/fan.h" @@ -16,7 +16,7 @@ class TemplateFan : public Component, public fan::Fan { void set_has_direction(bool has_direction) { this->has_direction_ = has_direction; } void set_has_oscillating(bool has_oscillating) { this->has_oscillating_ = has_oscillating; } void set_speed_count(int count) { this->speed_count_ = count; } - void set_preset_modes(const std::set &presets) { this->preset_modes_ = presets; } + void set_preset_modes(const std::initializer_list &presets) { this->preset_modes_ = presets; } fan::FanTraits get_traits() override { return this->traits_; } protected: @@ -26,7 +26,7 @@ class TemplateFan : public Component, public fan::Fan { bool has_direction_{false}; int speed_count_{0}; fan::FanTraits traits_; - std::set preset_modes_{}; + std::vector preset_modes_{}; }; } // namespace template_ From 091c12cb489f8df7aa4fd53f52f86f01891d73b5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 13:29:14 -1000 Subject: [PATCH 22/22] preen --- esphome/components/fan/fan.cpp | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index 7fb19f242a..cf1ec3d6ae 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -1,6 +1,5 @@ #include "fan.h" #include "esphome/core/log.h" -#include namespace esphome { namespace fan { @@ -52,7 +51,14 @@ void FanCall::validate_() { if (!this->preset_mode_.empty()) { const auto &preset_modes = traits.supported_preset_modes(); - if (std::find(preset_modes.begin(), preset_modes.end(), this->preset_mode_) == preset_modes.end()) { + bool found = false; + for (const auto &mode : preset_modes) { + if (mode == this->preset_mode_) { + found = true; + break; + } + } + if (!found) { ESP_LOGW(TAG, "%s: Preset mode '%s' not supported", this->parent_.get_name().c_str(), this->preset_mode_.c_str()); this->preset_mode_.clear(); } @@ -192,9 +198,14 @@ void Fan::save_state_() { if (this->get_traits().supports_preset_modes() && !this->preset_mode.empty()) { const auto &preset_modes = this->get_traits().supported_preset_modes(); // Store index of current preset mode - auto preset_iterator = std::find(preset_modes.begin(), preset_modes.end(), this->preset_mode); - if (preset_iterator != preset_modes.end()) - state.preset_mode = std::distance(preset_modes.begin(), preset_iterator); + size_t i = 0; + for (const auto &mode : preset_modes) { + if (mode == this->preset_mode) { + state.preset_mode = i; + break; + } + i++; + } } this->rtc_.save(&state);