ESPHome 2026.2.3
Loading...
Searching...
No Matches
fan.cpp
Go to the documentation of this file.
1#include "fan.h"
4#include "esphome/core/log.h"
6
7namespace esphome {
8namespace fan {
9
10static const char *const TAG = "fan";
11
12// Fan direction strings indexed by FanDirection enum (0-1): FORWARD, REVERSE, plus UNKNOWN
13PROGMEM_STRING_TABLE(FanDirectionStrings, "FORWARD", "REVERSE", "UNKNOWN");
14
16 return FanDirectionStrings::get_log_str(static_cast<uint8_t>(direction), FanDirectionStrings::LAST_INDEX);
17}
18
20 return this->set_preset_mode(preset_mode.data(), preset_mode.size());
21}
22
24 return this->set_preset_mode(preset_mode, preset_mode ? strlen(preset_mode) : 0);
25}
26
28 if (preset_mode == nullptr || len == 0) {
29 this->preset_mode_ = nullptr;
30 return *this;
31 }
32
33 // Find and validate pointer from traits immediately
34 auto traits = this->parent_.get_traits();
35 const char *validated_mode = traits.find_preset_mode(preset_mode, len);
36 if (validated_mode != nullptr) {
37 this->preset_mode_ = validated_mode; // Store pointer from traits
38 } else {
39 // Preset mode not found in traits - log warning and don't set
40 ESP_LOGW(TAG, "%s: Preset mode '%.*s' not supported", this->parent_.get_name().c_str(), (int) len, preset_mode);
41 this->preset_mode_ = nullptr;
42 }
43 return *this;
44}
45
47 ESP_LOGD(TAG, "'%s' - Setting:", this->parent_.get_name().c_str());
48 this->validate_();
49 if (this->binary_state_.has_value()) {
50 ESP_LOGD(TAG, " State: %s", ONOFF(*this->binary_state_));
51 }
52 if (this->oscillating_.has_value()) {
53 ESP_LOGD(TAG, " Oscillating: %s", YESNO(*this->oscillating_));
54 }
55 if (this->speed_.has_value()) {
56 ESP_LOGD(TAG, " Speed: %d", *this->speed_);
57 }
58 if (this->direction_.has_value()) {
59 ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(*this->direction_)));
60 }
61 if (this->preset_mode_ != nullptr) {
62 ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode_);
63 }
64 this->parent_.control(*this);
65}
66
68 auto traits = this->parent_.get_traits();
69
70 if (this->speed_.has_value()) {
71 this->speed_ = clamp(*this->speed_, 1, traits.supported_speed_count());
72
73 // https://developers.home-assistant.io/docs/core/entity/fan/#preset-modes
74 // "Manually setting a speed must disable any set preset mode"
75 this->preset_mode_ = nullptr;
76 }
77
78 // when turning on...
79 if (!this->parent_.state && this->binary_state_.has_value() &&
80 *this->binary_state_
81 // ..,and no preset mode will be active...
82 && !this->has_preset_mode() &&
83 !this->parent_.has_preset_mode()
84 // ...and neither current nor new speed is available...
85 && traits.supports_speed() && this->parent_.speed == 0 && !this->speed_.has_value()) {
86 // ...set speed to 100%
87 this->speed_ = traits.supported_speed_count();
88 }
89
90 if (this->oscillating_.has_value() && !traits.supports_oscillation()) {
91 ESP_LOGW(TAG, "%s: Oscillation not supported", this->parent_.get_name().c_str());
92 this->oscillating_.reset();
93 }
94
95 if (this->speed_.has_value() && !traits.supports_speed()) {
96 ESP_LOGW(TAG, "%s: Speed control not supported", this->parent_.get_name().c_str());
97 this->speed_.reset();
98 }
99
100 if (this->direction_.has_value() && !traits.supports_direction()) {
101 ESP_LOGW(TAG, "%s: Direction control not supported", this->parent_.get_name().c_str());
102 this->direction_.reset();
103 }
104}
105
107 auto call = fan.make_call();
108 call.set_state(this->state);
109 call.set_oscillating(this->oscillating);
110 call.set_speed(this->speed);
111 call.set_direction(this->direction);
112
113 auto traits = fan.get_traits();
114 if (traits.supports_preset_modes()) {
115 // Use stored preset index to get preset name
116 const auto &preset_modes = traits.supported_preset_modes();
117 if (this->preset_mode < preset_modes.size()) {
118 call.set_preset_mode(preset_modes[this->preset_mode]);
119 }
120 }
121 return call;
122}
124 fan.state = this->state;
125 fan.oscillating = this->oscillating;
126 fan.speed = this->speed;
127 fan.direction = this->direction;
128
129 auto traits = fan.get_traits();
130 if (traits.supports_preset_modes()) {
131 // Use stored preset index to get preset name from traits
132 const auto &preset_modes = traits.supported_preset_modes();
133 if (this->preset_mode < preset_modes.size()) {
134 fan.set_preset_mode_(preset_modes[this->preset_mode]);
135 }
136 }
137
138 fan.publish_state();
139}
140
141FanCall Fan::turn_on() { return this->make_call().set_state(true); }
142FanCall Fan::turn_off() { return this->make_call().set_state(false); }
143FanCall Fan::toggle() { return this->make_call().set_state(!this->state); }
144FanCall Fan::make_call() { return FanCall(*this); }
145
146const char *Fan::find_preset_mode_(const char *preset_mode) {
147 return this->find_preset_mode_(preset_mode, preset_mode ? strlen(preset_mode) : 0);
148}
149
150const char *Fan::find_preset_mode_(const char *preset_mode, size_t len) {
151 return this->get_traits().find_preset_mode(preset_mode, len);
152}
153
154bool Fan::set_preset_mode_(const char *preset_mode, size_t len) {
155 if (preset_mode == nullptr || len == 0) {
156 // Treat nullptr or empty string as clearing the preset mode (no valid preset is "")
157 if (this->preset_mode_ == nullptr) {
158 return false; // No change
159 }
160 this->clear_preset_mode_();
161 return true;
162 }
163 const char *validated = this->find_preset_mode_(preset_mode, len);
164 if (validated == nullptr || this->preset_mode_ == validated) {
165 return false; // Preset mode not supported or no change
166 }
167 this->preset_mode_ = validated;
168 return true;
169}
170
172 return this->set_preset_mode_(preset_mode, preset_mode ? strlen(preset_mode) : 0);
173}
174
175bool Fan::set_preset_mode_(const std::string &preset_mode) {
176 return this->set_preset_mode_(preset_mode.data(), preset_mode.size());
177}
178
180 // Safe: find_preset_mode_ only uses the input for comparison and returns
181 // a pointer from traits, so the input StringRef's lifetime doesn't matter.
182 return this->set_preset_mode_(preset_mode.c_str(), preset_mode.size());
183}
184
185void Fan::clear_preset_mode_() { this->preset_mode_ = nullptr; }
186
188 if (call.has_preset_mode()) {
189 this->set_preset_mode_(call.get_preset_mode());
190 } else if (call.get_speed().has_value()) {
191 // Manually setting speed clears preset (per Home Assistant convention)
192 this->clear_preset_mode_();
193 }
194}
195
196void Fan::add_on_state_callback(std::function<void()> &&callback) { this->state_callback_.add(std::move(callback)); }
198 auto traits = this->get_traits();
199
200 ESP_LOGD(TAG,
201 "'%s' >>\n"
202 " State: %s",
203 this->name_.c_str(), ONOFF(this->state));
204 if (traits.supports_speed()) {
205 ESP_LOGD(TAG, " Speed: %d", this->speed);
206 }
207 if (traits.supports_oscillation()) {
208 ESP_LOGD(TAG, " Oscillating: %s", YESNO(this->oscillating));
209 }
210 if (traits.supports_direction()) {
211 ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(this->direction)));
212 }
213 if (this->preset_mode_ != nullptr) {
214 ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode_);
215 }
216 this->state_callback_.call();
217#if defined(USE_FAN) && defined(USE_CONTROLLER_REGISTRY)
219#endif
220 this->save_state_();
221}
222
223// Random 32-bit value, change this every time the layout of the FanRestoreState struct changes.
224constexpr uint32_t RESTORE_STATE_VERSION = 0x71700ABB;
226 FanRestoreState recovered{};
227 this->rtc_ = this->make_entity_preference<FanRestoreState>(RESTORE_STATE_VERSION);
228 bool restored = this->rtc_.load(&recovered);
229
230 if (!restored) {
231 // No valid saved data; ensure preset_mode sentinel is set
232 recovered.preset_mode = FanRestoreState::NO_PRESET;
233 }
234
235 switch (this->restore_mode_) {
237 return {};
239 recovered.state = false;
240 return recovered;
242 recovered.state = true;
243 return recovered;
245 recovered.state = restored ? recovered.state : false;
246 return recovered;
248 recovered.state = restored ? recovered.state : true;
249 return recovered;
251 recovered.state = restored ? !recovered.state : false;
252 return recovered;
254 recovered.state = restored ? !recovered.state : true;
255 return recovered;
256 }
257
258 return {};
259}
262 return;
263 }
264
265 auto traits = this->get_traits();
266
268 state.state = this->state;
269 state.oscillating = this->oscillating;
270 state.speed = this->speed;
271 state.direction = this->direction;
272 state.preset_mode = FanRestoreState::NO_PRESET;
273
274 if (this->has_preset_mode()) {
275 const auto &preset_modes = traits.supported_preset_modes();
276 // Find index of current preset mode (pointer comparison is safe since preset is from traits)
277 for (size_t i = 0; i < preset_modes.size(); i++) {
278 if (preset_modes[i] == this->preset_mode_) {
279 state.preset_mode = i;
280 break;
281 }
282 }
283 }
284
285 this->rtc_.save(&state);
286}
287
288void Fan::dump_traits_(const char *tag, const char *prefix) {
289 auto traits = this->get_traits();
290
291 if (traits.supports_speed()) {
292 ESP_LOGCONFIG(tag,
293 "%s Speed: YES\n"
294 "%s Speed count: %d",
295 prefix, prefix, traits.supported_speed_count());
296 }
297 if (traits.supports_oscillation()) {
298 ESP_LOGCONFIG(tag, "%s Oscillation: YES", prefix);
299 }
300 if (traits.supports_direction()) {
301 ESP_LOGCONFIG(tag, "%s Direction: YES", prefix);
302 }
303 if (traits.supports_preset_modes()) {
304 ESP_LOGCONFIG(tag, "%s Supported presets:", prefix);
305 for (const char *s : traits.supported_preset_modes())
306 ESP_LOGCONFIG(tag, "%s - %s", prefix, s);
307 }
308}
309
310} // namespace fan
311} // namespace esphome
static void notify_fan_update(fan::Fan *obj)
bool save(const T *src)
Definition preferences.h:21
const StringRef & get_name() const
ESPPreferenceObject make_entity_preference(uint32_t version=0)
Create a preference object for storing this entity's state/settings.
StringRef is a reference to a string owned by something else.
Definition string_ref.h:26
constexpr const char * c_str() const
Definition string_ref.h:73
FanCall & set_oscillating(bool oscillating)
Definition fan.h:51
FanCall & set_direction(FanDirection direction)
Definition fan.h:65
optional< bool > binary_state_
Definition fan.h:86
optional< FanDirection > direction_
Definition fan.h:89
optional< int > speed_
Definition fan.h:88
const char * preset_mode_
Definition fan.h:90
const char * get_preset_mode() const
Definition fan.h:77
bool has_preset_mode() const
Definition fan.h:78
FanCall & set_speed(int speed)
Definition fan.h:60
FanCall & set_state(bool binary_state)
Definition fan.h:42
optional< int > get_speed() const
Definition fan.h:64
optional< bool > oscillating_
Definition fan.h:87
FanCall & set_preset_mode(const std::string &preset_mode)
Definition fan.cpp:19
friend FanCall
Definition fan.h:144
void publish_state()
Definition fan.cpp:197
FanCall turn_on()
Definition fan.cpp:141
FanCall turn_off()
Definition fan.cpp:142
FanCall make_call()
Definition fan.cpp:144
virtual FanTraits get_traits()=0
FanCall toggle()
Definition fan.cpp:143
LazyCallbackManager< void()> state_callback_
Definition fan.h:168
void apply_preset_mode_(const FanCall &call)
Apply preset mode from a FanCall (handles speed-clears-preset convention)
Definition fan.cpp:187
ESPPreferenceObject rtc_
Definition fan.h:169
void clear_preset_mode_()
Clear the preset mode.
Definition fan.cpp:185
bool set_preset_mode_(const char *preset_mode, size_t len)
Set the preset mode (finds and stores pointer from traits).
Definition fan.cpp:154
void add_on_state_callback(std::function< void()> &&callback)
Register a callback that will be called each time the state changes.
Definition fan.cpp:196
FanDirection direction
The current direction of the fan.
Definition fan.h:117
void save_state_()
Definition fan.cpp:260
FanRestoreMode restore_mode_
Definition fan.h:170
bool oscillating
The current oscillation state of the fan.
Definition fan.h:113
virtual void control(const FanCall &call)=0
bool state
The current on/off state of the fan.
Definition fan.h:111
const char * find_preset_mode_(const char *preset_mode)
Find and return the matching preset mode pointer from traits, or nullptr if not found.
Definition fan.cpp:146
bool has_preset_mode() const
Check if a preset mode is currently active.
Definition fan.h:141
int speed
The current fan speed level.
Definition fan.h:115
void dump_traits_(const char *tag, const char *prefix)
Definition fan.cpp:288
optional< FanRestoreState > restore_state_()
Definition fan.cpp:225
const char * find_preset_mode(const char *preset_mode) const
Find and return the matching preset mode pointer from supported modes, or nullptr if not found.
Definition fan_traits.h:49
bool has_value() const
Definition optional.h:92
FanDirection direction
Definition fan.h:5
uint8_t preset_mode
Definition fan.h:6
const LogString * fan_direction_to_string(FanDirection direction)
Definition fan.cpp:15
constexpr uint32_t RESTORE_STATE_VERSION
Definition fan.cpp:224
FanDirection
Simple enum to represent the direction of a fan.
Definition fan.h:21
PROGMEM_STRING_TABLE(FanDirectionStrings, "FORWARD", "REVERSE", "UNKNOWN")
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
std::string size_t len
Definition helpers.h:692
static constexpr uint8_t NO_PRESET
Definition fan.h:94
void apply(Fan &fan)
Apply these settings to the fan.
Definition fan.cpp:123
FanDirection direction
Definition fan.h:99
FanCall to_call(Fan &fan)
Convert this struct to a fan call that can be performed.
Definition fan.cpp:106