mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 18:22:48 +00:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff46f4f1d1 | ||
|
|
46276ef2ad | ||
|
|
899ba0580a | ||
|
|
de6d3a45fc | ||
|
|
beb4d803b1 | ||
|
|
efec456328 | ||
|
|
2033e6a60a | ||
|
|
726677fc0e | ||
|
|
fa8cf3a60d | ||
|
|
a617a8a29d |
@@ -680,7 +680,7 @@ message ListEntitiesFanResponse {
|
||||
bool disabled_by_default = 9;
|
||||
string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
|
||||
EntityCategory entity_category = 11;
|
||||
repeated string supported_preset_modes = 12 [(container_pointer_no_template) = "std::vector<const char *>"];
|
||||
repeated string supported_preset_modes = 12 [(container_pointer_no_template) = "ConstVector<const char *, true>"];
|
||||
uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"];
|
||||
}
|
||||
// Deprecated in API version 1.6 - only used in deprecated fields
|
||||
|
||||
@@ -983,7 +983,7 @@ class ListEntitiesFanResponse final : public InfoResponseProtoMessage {
|
||||
bool supports_speed{false};
|
||||
bool supports_direction{false};
|
||||
int32_t supported_speed_count{0};
|
||||
const std::vector<const char *> *supported_preset_modes{};
|
||||
const ConstVector<const char *, true> *supported_preset_modes{};
|
||||
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
|
||||
|
||||
@@ -37,6 +37,7 @@ from esphome.core.entity_helpers import (
|
||||
queue_entity_register,
|
||||
setup_entity,
|
||||
)
|
||||
from esphome.cpp_generator import MockObj
|
||||
|
||||
DOMAIN = "fan"
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
@@ -214,6 +215,15 @@ def validate_preset_modes(value):
|
||||
return value
|
||||
|
||||
|
||||
def set_preset_modes(var: MockObj, preset_modes: list[str]) -> None:
|
||||
"""Point the fan at a shared flash table of preset modes."""
|
||||
if preset_modes:
|
||||
table = cg.shared_progmem_array(
|
||||
"fan_preset_modes", cg.const_char_ptr, preset_modes
|
||||
)
|
||||
cg.add(var.set_supported_preset_modes_static(table, len(preset_modes)))
|
||||
|
||||
|
||||
# The edge and set triggers keep their classes; each tracks the previous value.
|
||||
_TRIGGER_AUTOMATIONS = (
|
||||
(CONF_ON_TURN_ON, []),
|
||||
|
||||
@@ -8,20 +8,10 @@ namespace esphome::fan {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "fan");
|
||||
|
||||
// Compat: shared empty vector for getter when no preset modes are set.
|
||||
// Remove in 2026.11.0 when deprecated FanTraits setters are removed
|
||||
// and getter can return const vector * instead of const vector &.
|
||||
static const std::vector<const char *> EMPTY_PRESET_MODES; // NOLINT
|
||||
static const FanPresetModes EMPTY_PRESET_MODES; // NOLINT
|
||||
|
||||
const std::vector<const char *> &FanTraits::supported_preset_modes() const {
|
||||
if (this->preset_modes_) {
|
||||
return *this->preset_modes_;
|
||||
}
|
||||
// Compat: fall back to owned vector from deprecated setters. Remove in 2026.11.0 (change return to const vector *).
|
||||
if (!this->compat_preset_modes_.empty()) {
|
||||
return this->compat_preset_modes_;
|
||||
}
|
||||
return EMPTY_PRESET_MODES;
|
||||
const FanPresetModes &FanTraits::supported_preset_modes() const {
|
||||
return this->preset_modes_ != nullptr ? *this->preset_modes_ : EMPTY_PRESET_MODES;
|
||||
}
|
||||
|
||||
// Fan direction strings indexed by FanDirection enum (0-1): FORWARD, REVERSE, plus UNKNOWN
|
||||
@@ -161,16 +151,12 @@ const char *Fan::find_preset_mode_(const char *preset_mode, size_t len) {
|
||||
if (preset_mode == nullptr || len == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
if (this->supported_preset_modes_) {
|
||||
for (const char *mode : *this->supported_preset_modes_) {
|
||||
if (strncmp(mode, preset_mode, len) == 0 && mode[len] == '\0') {
|
||||
return mode;
|
||||
}
|
||||
for (const char *mode : this->supported_preset_modes_) {
|
||||
if (strncmp(mode, preset_mode, len) == 0 && mode[len] == '\0') {
|
||||
return mode;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
// Fallback for deprecated path: external components may set modes on FanTraits directly
|
||||
return this->get_traits().find_preset_mode(preset_mode, len);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool Fan::set_preset_mode_(const char *preset_mode, size_t len) {
|
||||
@@ -291,25 +277,10 @@ void Fan::save_state_() {
|
||||
state.preset_mode = FanRestoreState::NO_PRESET;
|
||||
|
||||
if (this->has_preset_mode()) {
|
||||
if (this->supported_preset_modes_) {
|
||||
// New path: search Fan-owned vector directly
|
||||
for (size_t i = 0; i < this->supported_preset_modes_->size(); i++) {
|
||||
if ((*this->supported_preset_modes_)[i] == this->preset_mode_) {
|
||||
state.preset_mode = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Compat: fall back to traits for deprecated path. Remove in 2026.11.0.
|
||||
// Pointer comparison works because preset_mode_ and the compat vector both
|
||||
// hold pointers to string literals in .rodata (stable addresses).
|
||||
auto traits = this->get_traits();
|
||||
const auto &preset_modes = traits.supported_preset_modes();
|
||||
for (size_t i = 0; i < preset_modes.size(); i++) {
|
||||
if (preset_modes[i] == this->preset_mode_) {
|
||||
state.preset_mode = i;
|
||||
break;
|
||||
}
|
||||
for (size_t i = 0; i < this->supported_preset_modes_.size(); i++) {
|
||||
if (this->supported_preset_modes_[i] == this->preset_mode_) {
|
||||
state.preset_mode = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
@@ -129,12 +131,19 @@ class Fan : public EntityBase {
|
||||
|
||||
virtual FanTraits get_traits() = 0;
|
||||
|
||||
/// Set the supported preset modes (stored on Fan, referenced by FanTraits via pointer).
|
||||
/// Set the supported preset modes; the list is copied (stored on Fan, referenced by FanTraits).
|
||||
void set_supported_preset_modes(std::initializer_list<const char *> preset_modes) {
|
||||
this->ensure_preset_modes_().assign(preset_modes.begin(), preset_modes.end());
|
||||
this->supported_preset_modes_.assign_copy(preset_modes.begin(), preset_modes.size());
|
||||
}
|
||||
void set_supported_preset_modes(const std::vector<const char *> &preset_modes) {
|
||||
this->ensure_preset_modes_() = preset_modes;
|
||||
this->supported_preset_modes_.assign_copy(preset_modes.data(), preset_modes.size());
|
||||
}
|
||||
void set_supported_preset_modes(const FanPresetModes &preset_modes) {
|
||||
this->supported_preset_modes_.assign_copy(preset_modes.data(), preset_modes.size());
|
||||
}
|
||||
/// Points at a static table that outlives the fan (codegen); call before any runtime copy.
|
||||
void set_supported_preset_modes_static(const char *const *preset_modes, size_t count) {
|
||||
this->supported_preset_modes_.assign_static(preset_modes, count);
|
||||
}
|
||||
|
||||
/// Set the restore mode of this fan.
|
||||
@@ -175,26 +184,14 @@ class Fan : public EntityBase {
|
||||
const char *find_preset_mode_(const char *preset_mode, size_t len);
|
||||
|
||||
/// Wire the Fan-owned preset modes pointer into the given traits object.
|
||||
void wire_preset_modes_(FanTraits &traits) {
|
||||
if (this->supported_preset_modes_) {
|
||||
traits.set_supported_preset_modes_(this->supported_preset_modes_);
|
||||
}
|
||||
}
|
||||
void wire_preset_modes_(FanTraits &traits) { traits.set_supported_preset_modes_(&this->supported_preset_modes_); }
|
||||
|
||||
LazyCallbackManager<void()> state_callback_{};
|
||||
ESPPreferenceObject rtc_;
|
||||
FanRestoreMode restore_mode_{FanRestoreMode::NO_RESTORE};
|
||||
|
||||
private:
|
||||
/// Lazy-allocate preset modes vector (never freed — entity lives forever).
|
||||
std::vector<const char *> &ensure_preset_modes_() {
|
||||
if (!this->supported_preset_modes_) {
|
||||
this->supported_preset_modes_ = new std::vector<const char *>(); // NOLINT
|
||||
}
|
||||
return *this->supported_preset_modes_;
|
||||
}
|
||||
|
||||
std::vector<const char *> *supported_preset_modes_{nullptr};
|
||||
FanPresetModes supported_preset_modes_;
|
||||
const char *preset_mode_{nullptr};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
#include <initializer_list>
|
||||
#include <type_traits>
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::fan {
|
||||
|
||||
class Fan; // Forward declaration
|
||||
|
||||
/// Preset modes: a flash table from codegen, or an owned copy set at runtime.
|
||||
using FanPresetModes = ConstVector<const char *, true>;
|
||||
|
||||
class FanTraits {
|
||||
friend class Fan; // Allow Fan to access protected pointer setter
|
||||
|
||||
@@ -33,32 +35,11 @@ class FanTraits {
|
||||
bool supports_direction() const { return this->direction_; }
|
||||
/// Set whether this fan supports changing direction
|
||||
void set_direction(bool direction) { this->direction_ = direction; }
|
||||
// Compat: returns const ref with empty fallback. In 2026.11.0 change to return const vector *.
|
||||
const std::vector<const char *> &supported_preset_modes() const;
|
||||
// Remove before 2026.11.0
|
||||
ESPDEPRECATED("Call set_supported_preset_modes() on the Fan entity instead. Removed in 2026.11.0", "2026.5.0")
|
||||
void set_supported_preset_modes(std::initializer_list<const char *> preset_modes) {
|
||||
// Compat: store in owned vector. Copies copy the vector (deprecated path still copies this vector).
|
||||
this->compat_preset_modes_ = preset_modes;
|
||||
}
|
||||
// Remove before 2026.11.0
|
||||
ESPDEPRECATED("Call set_supported_preset_modes() on the Fan entity instead. Removed in 2026.11.0", "2026.5.0")
|
||||
void set_supported_preset_modes(const std::vector<const char *> &preset_modes) {
|
||||
this->compat_preset_modes_ = preset_modes;
|
||||
}
|
||||
|
||||
// Deleted overloads to catch incorrect std::string usage at compile time with clear error messages
|
||||
void set_supported_preset_modes(const std::vector<std::string> &preset_modes) = delete;
|
||||
void set_supported_preset_modes(std::initializer_list<std::string> preset_modes) = delete;
|
||||
/// Empty when the fan has no preset modes. Set them on the Fan entity.
|
||||
const FanPresetModes &supported_preset_modes() const;
|
||||
|
||||
/// Return if preset modes are supported
|
||||
bool supports_preset_modes() const {
|
||||
// Same precedence as supported_preset_modes() getter
|
||||
if (this->preset_modes_) {
|
||||
return !this->preset_modes_->empty();
|
||||
}
|
||||
return !this->compat_preset_modes_.empty();
|
||||
}
|
||||
bool supports_preset_modes() const { return this->preset_modes_ != nullptr && !this->preset_modes_->empty(); }
|
||||
/// Find and return the matching preset mode pointer from supported modes, or nullptr if not found.
|
||||
const char *find_preset_mode(const char *preset_mode) const {
|
||||
return this->find_preset_mode(preset_mode, preset_mode ? strlen(preset_mode) : 0);
|
||||
@@ -67,9 +48,7 @@ class FanTraits {
|
||||
if (preset_mode == nullptr || len == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
// Check pointer-based storage (new path) then compat owned vector (deprecated path)
|
||||
const auto &modes = this->preset_modes_ ? *this->preset_modes_ : this->compat_preset_modes_;
|
||||
for (const char *mode : modes) {
|
||||
for (const char *mode : this->supported_preset_modes()) {
|
||||
if (strncmp(mode, preset_mode, len) == 0 && mode[len] == '\0') {
|
||||
return mode;
|
||||
}
|
||||
@@ -79,18 +58,14 @@ class FanTraits {
|
||||
|
||||
protected:
|
||||
/// Set the preset modes pointer (only Fan::wire_preset_modes_() should call this).
|
||||
void set_supported_preset_modes_(const std::vector<const char *> *preset_modes) {
|
||||
this->preset_modes_ = preset_modes;
|
||||
}
|
||||
void set_supported_preset_modes_(const FanPresetModes *preset_modes) { this->preset_modes_ = preset_modes; }
|
||||
|
||||
bool oscillation_{false};
|
||||
bool speed_{false};
|
||||
bool direction_{false};
|
||||
int speed_count_{};
|
||||
const std::vector<const char *> *preset_modes_{nullptr};
|
||||
// Compat: owned storage for deprecated setters. Copies copy the vector (copies include this vector).
|
||||
// Remove in 2026.11.0.
|
||||
std::vector<const char *> compat_preset_modes_;
|
||||
const FanPresetModes *preset_modes_{nullptr}; // owned by the Fan entity
|
||||
};
|
||||
static_assert(std::is_trivially_copyable_v<FanTraits>, "FanTraits is returned by value from get_traits()");
|
||||
|
||||
} // namespace esphome::fan
|
||||
|
||||
@@ -68,5 +68,4 @@ async def to_code(config: ConfigType) -> None:
|
||||
enable_pin = await cg.get_variable(config[CONF_ENABLE_PIN])
|
||||
cg.add(var.set_enable_pin(enable_pin))
|
||||
|
||||
if CONF_PRESET_MODES in config:
|
||||
cg.add(var.set_preset_modes(config[CONF_PRESET_MODES]))
|
||||
fan.set_preset_modes(var, config.get(CONF_PRESET_MODES, []))
|
||||
|
||||
@@ -18,6 +18,8 @@ class HBridgeFan final : 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; }
|
||||
// Remove before 2027.5.0
|
||||
ESPDEPRECATED("Use set_supported_preset_modes() instead. Removed in 2027.5.0", "2026.11.0")
|
||||
void set_preset_modes(std::initializer_list<const char *> presets) { this->set_supported_preset_modes(presets); }
|
||||
|
||||
void setup() override;
|
||||
|
||||
@@ -49,5 +49,4 @@ async def to_code(config: ConfigType) -> None:
|
||||
direction_output = await cg.get_variable(config[CONF_DIRECTION_OUTPUT])
|
||||
cg.add(var.set_direction(direction_output))
|
||||
|
||||
if CONF_PRESET_MODES in config:
|
||||
cg.add(var.set_preset_modes(config[CONF_PRESET_MODES]))
|
||||
fan.set_preset_modes(var, config.get(CONF_PRESET_MODES, []))
|
||||
|
||||
@@ -15,6 +15,8 @@ class SpeedFan final : 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; }
|
||||
// Remove before 2027.5.0
|
||||
ESPDEPRECATED("Use set_supported_preset_modes() instead. Removed in 2027.5.0", "2026.11.0")
|
||||
void set_preset_modes(std::initializer_list<const char *> presets) { this->set_supported_preset_modes(presets); }
|
||||
fan::FanTraits get_traits() override {
|
||||
this->wire_preset_modes_(this->traits_);
|
||||
|
||||
@@ -37,5 +37,4 @@ async def to_code(config):
|
||||
if CONF_SPEED_COUNT in config:
|
||||
cg.add(var.set_speed_count(config[CONF_SPEED_COUNT]))
|
||||
|
||||
if CONF_PRESET_MODES in config:
|
||||
cg.add(var.set_preset_modes(config[CONF_PRESET_MODES]))
|
||||
fan.set_preset_modes(var, config.get(CONF_PRESET_MODES, []))
|
||||
|
||||
@@ -13,6 +13,8 @@ class TemplateFan final : 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; }
|
||||
// Remove before 2027.5.0
|
||||
ESPDEPRECATED("Use set_supported_preset_modes() instead. Removed in 2027.5.0", "2026.11.0")
|
||||
void set_preset_modes(std::initializer_list<const char *> presets) { this->set_supported_preset_modes(presets); }
|
||||
fan::FanTraits get_traits() override {
|
||||
this->wire_preset_modes_(this->traits_);
|
||||
|
||||
@@ -12,7 +12,11 @@ class BenchFan : public fan::Fan {
|
||||
public:
|
||||
void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); }
|
||||
|
||||
fan::FanTraits get_traits() override { return this->traits_; }
|
||||
fan::FanTraits get_traits() override {
|
||||
auto traits = this->traits_;
|
||||
this->wire_preset_modes_(traits);
|
||||
return traits;
|
||||
}
|
||||
|
||||
fan::FanTraits traits_;
|
||||
|
||||
@@ -30,7 +34,7 @@ static void setup_fan(BenchFan &fan) {
|
||||
fan.traits_.set_supported_speed_count(6);
|
||||
fan.traits_.set_direction(true);
|
||||
fan.set_restore_mode(fan::FanRestoreMode::NO_RESTORE);
|
||||
fan.traits_.set_supported_preset_modes({
|
||||
fan.set_supported_preset_modes({
|
||||
"auto",
|
||||
"sleep",
|
||||
"nature",
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp8266:
|
||||
board: esp01_1m
|
||||
|
||||
fan:
|
||||
- platform: template
|
||||
id: fan_a
|
||||
name: Fan A
|
||||
preset_modes: [Eco, Sleep]
|
||||
- platform: template
|
||||
id: fan_b
|
||||
name: Fan B
|
||||
preset_modes: [Eco, Sleep]
|
||||
- platform: template
|
||||
id: fan_c
|
||||
name: Fan C
|
||||
@@ -0,0 +1,19 @@
|
||||
"""Fan preset modes from YAML live in shared flash tables."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
|
||||
def test_preset_modes_use_shared_tables(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
main_cpp = generate_main(component_config_path("preset_modes.yaml"))
|
||||
|
||||
tables = re.findall(
|
||||
r"static constexpr const char \* (fan_preset_modes\w*)\[\] PROGMEM", main_cpp
|
||||
)
|
||||
assert len(tables) == 1 # Fan A and B share one table
|
||||
assert main_cpp.count(f"set_supported_preset_modes_static({tables[0]}, 2);") == 2
|
||||
assert "fan_c->set_supported_preset_modes" not in main_cpp
|
||||
@@ -79,6 +79,10 @@ button:
|
||||
ESP_LOGD("test", "%s", mode);
|
||||
for (const char *mode : traits.supported_preset_modes())
|
||||
ESP_LOGD("test", "%s", mode);
|
||||
if (modes.size() > 1) {
|
||||
const auto *it = std::find(modes.begin(), modes.end(), modes[1]);
|
||||
ESP_LOGD("test", "index %d", (int) (it - modes.begin()));
|
||||
}
|
||||
std::vector<const char *> runtime_modes{"Eco", "Turbo"};
|
||||
id(test_fan).set_supported_preset_modes(runtime_modes);
|
||||
id(test_fan).set_supported_preset_modes({"Eco"});
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/fan/fan.h"
|
||||
|
||||
namespace esphome::fan::testing {
|
||||
|
||||
class TestFan : public Fan {
|
||||
public:
|
||||
FanTraits get_traits() override {
|
||||
FanTraits traits(false, true, false, 3);
|
||||
this->wire_preset_modes_(traits);
|
||||
return traits;
|
||||
}
|
||||
|
||||
protected:
|
||||
void control(const FanCall &call) override {}
|
||||
};
|
||||
|
||||
static constexpr const char *const PRESET_TABLE[] = {"Eco", "Sleep"};
|
||||
|
||||
TEST(FanPresetModesTest, StaticTableIsViewedNotCopied) {
|
||||
TestFan fan;
|
||||
fan.set_supported_preset_modes_static(PRESET_TABLE, 2);
|
||||
auto traits = fan.get_traits();
|
||||
ASSERT_EQ(traits.supported_preset_modes().size(), 2u);
|
||||
EXPECT_EQ(traits.supported_preset_modes().data(), PRESET_TABLE);
|
||||
EXPECT_TRUE(traits.supports_preset_modes());
|
||||
EXPECT_EQ(traits.find_preset_mode("Sleep"), PRESET_TABLE[1]);
|
||||
EXPECT_EQ(traits.find_preset_mode("Turbo"), nullptr);
|
||||
}
|
||||
|
||||
TEST(FanPresetModesTest, RuntimeListsAreCopiedAndReplaced) {
|
||||
TestFan fan;
|
||||
std::vector<const char *> modes{"Low", "High"};
|
||||
fan.set_supported_preset_modes(modes);
|
||||
modes[0] = "Changed"; // the fan keeps its own copy
|
||||
EXPECT_STREQ(fan.get_traits().supported_preset_modes()[0], "Low");
|
||||
fan.set_supported_preset_modes({});
|
||||
EXPECT_FALSE(fan.get_traits().supports_preset_modes());
|
||||
}
|
||||
|
||||
TEST(FanPresetModesTest, CopyFromAnotherFanKeepsItsOwnList) {
|
||||
TestFan source;
|
||||
source.set_supported_preset_modes({"Auto"});
|
||||
TestFan copy;
|
||||
copy.set_supported_preset_modes(source.get_traits().supported_preset_modes());
|
||||
source.set_supported_preset_modes({"Night"});
|
||||
ASSERT_EQ(copy.get_traits().supported_preset_modes().size(), 1u);
|
||||
EXPECT_STREQ(copy.get_traits().supported_preset_modes()[0], "Auto");
|
||||
}
|
||||
|
||||
TEST(FanPresetModesTest, TraitsWithoutAFanHaveNoPresets) {
|
||||
FanTraits traits;
|
||||
EXPECT_TRUE(traits.supported_preset_modes().empty());
|
||||
EXPECT_FALSE(traits.supports_preset_modes());
|
||||
}
|
||||
|
||||
} // namespace esphome::fan::testing
|
||||
@@ -1,4 +0,0 @@
|
||||
"""Legacy fan component — tests deprecated FanTraits setters backward compat.
|
||||
|
||||
Remove this entire directory in 2026.11.0 when the deprecated setters are removed.
|
||||
"""
|
||||
@@ -1,16 +0,0 @@
|
||||
"""Legacy fan platform that uses deprecated FanTraits setters."""
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import fan
|
||||
import esphome.config_validation as cv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
legacy_fan_ns = cg.esphome_ns.namespace("legacy_fan_test")
|
||||
LegacyFan = legacy_fan_ns.class_("LegacyFan", fan.Fan, cg.Component)
|
||||
|
||||
CONFIG_SCHEMA = fan.fan_schema(LegacyFan).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await fan.new_fan(config)
|
||||
await cg.register_component(var, config)
|
||||
@@ -1,45 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/fan/fan.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome::legacy_fan_test {
|
||||
|
||||
/// Test fan that uses the DEPRECATED FanTraits setters for preset modes.
|
||||
/// This validates backward compatibility for external components that haven't migrated.
|
||||
class LegacyFan : public fan::Fan, public Component {
|
||||
public:
|
||||
void setup() override {
|
||||
auto restore = this->restore_state_();
|
||||
if (restore.has_value()) {
|
||||
restore->apply(*this);
|
||||
}
|
||||
this->publish_state();
|
||||
}
|
||||
|
||||
fan::FanTraits get_traits() override {
|
||||
auto traits = fan::FanTraits(false, true, false, 3);
|
||||
|
||||
// DEPRECATED API: setting preset modes directly on FanTraits.
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
traits.set_supported_preset_modes({"Turbo", "Silent", "Eco"});
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
return traits;
|
||||
}
|
||||
|
||||
protected:
|
||||
void control(const fan::FanCall &call) override {
|
||||
if (call.get_state().has_value()) {
|
||||
this->state = *call.get_state();
|
||||
}
|
||||
if (call.get_speed().has_value()) {
|
||||
this->speed = *call.get_speed();
|
||||
}
|
||||
this->apply_preset_mode_(call);
|
||||
this->publish_state();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::legacy_fan_test
|
||||
@@ -1,18 +0,0 @@
|
||||
esphome:
|
||||
name: legacy-fan-compat
|
||||
|
||||
host:
|
||||
api:
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: EXTERNAL_COMPONENT_PATH
|
||||
components: [legacy_fan_component]
|
||||
|
||||
fan:
|
||||
- platform: legacy_fan_component
|
||||
name: "Legacy Fan"
|
||||
id: legacy_fan
|
||||
@@ -67,7 +67,6 @@
|
||||
"tests/integration/test_large_message_batching.py": 13.94,
|
||||
"tests/integration/test_legacy_area.py": 13.34,
|
||||
"tests/integration/test_legacy_climate_compat.py": 13.19,
|
||||
"tests/integration/test_legacy_fan_compat.py": 9.31,
|
||||
"tests/integration/test_light_automations.py": 14.39,
|
||||
"tests/integration/test_light_binary_effect_off_phase.py": 14.75,
|
||||
"tests/integration/test_light_calls.py": 13.85,
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
"""Integration test for backward compatibility of deprecated FanTraits setters.
|
||||
|
||||
Verifies that external components using the old traits.set_supported_preset_modes()
|
||||
API still work correctly during the deprecation period.
|
||||
|
||||
Remove this entire test file and the legacy_fan_component external component
|
||||
in 2026.11.0 when the deprecated FanTraits setters are removed.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
from aioesphomeapi import FanInfo, FanState
|
||||
import pytest
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_legacy_fan_compat(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test that deprecated FanTraits preset mode setters still work end-to-end."""
|
||||
external_components_path = str(
|
||||
Path(__file__).parent / "fixtures" / "external_components"
|
||||
)
|
||||
yaml_config = yaml_config.replace(
|
||||
"EXTERNAL_COMPONENT_PATH", external_components_path
|
||||
)
|
||||
|
||||
async with run_compiled(yaml_config), api_client_connected() as client:
|
||||
entities, _ = await client.list_entities_services()
|
||||
|
||||
fan_infos = [e for e in entities if isinstance(e, FanInfo)]
|
||||
assert len(fan_infos) == 1, f"Expected 1 fan entity, got {len(fan_infos)}"
|
||||
|
||||
test_fan = fan_infos[0]
|
||||
|
||||
# Verify preset modes set via deprecated FanTraits setter are exposed
|
||||
assert set(test_fan.supported_preset_modes) == {
|
||||
"Turbo",
|
||||
"Silent",
|
||||
"Eco",
|
||||
}, (
|
||||
f"Expected preset modes {{Turbo, Silent, Eco}}, "
|
||||
f"got {test_fan.supported_preset_modes}"
|
||||
)
|
||||
|
||||
# Verify speed support
|
||||
assert test_fan.supports_speed is True
|
||||
assert test_fan.supported_speed_count == 3
|
||||
|
||||
# Subscribe and wait for initial states
|
||||
states: dict[int, FanState] = {}
|
||||
state_event = asyncio.Event()
|
||||
|
||||
def on_state(state: FanState) -> None:
|
||||
if isinstance(state, FanState):
|
||||
states[state.key] = state
|
||||
state_event.set()
|
||||
|
||||
client.subscribe_states(on_state)
|
||||
|
||||
# Wait for initial state
|
||||
await asyncio.wait_for(state_event.wait(), timeout=5.0)
|
||||
|
||||
# Turn on fan with preset mode (tests find_preset_mode_ compat path)
|
||||
state_event.clear()
|
||||
client.fan_command(
|
||||
key=test_fan.key,
|
||||
state=True,
|
||||
preset_mode="Turbo",
|
||||
)
|
||||
await asyncio.wait_for(state_event.wait(), timeout=5.0)
|
||||
|
||||
fan_state = states[test_fan.key]
|
||||
assert fan_state.state is True
|
||||
assert fan_state.preset_mode == "Turbo"
|
||||
Reference in New Issue
Block a user