ESPHome 2026.5.0-dev
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// Compat: shared empty vector for getter when no preset modes are set.
13// Remove in 2026.11.0 when deprecated FanTraits setters are removed
14// and getter can return const vector * instead of const vector &.
15static const std::vector<const char *> EMPTY_PRESET_MODES; // NOLINT
16
17const std::vector<const char *> &FanTraits::supported_preset_modes() const {
18 if (this->preset_modes_) {
19 return *this->preset_modes_;
20 }
21 // Compat: fall back to owned vector from deprecated setters. Remove in 2026.11.0 (change return to const vector *).
22 if (!this->compat_preset_modes_.empty()) {
23 return this->compat_preset_modes_;
24 }
25 return EMPTY_PRESET_MODES;
26}
27
28// Fan direction strings indexed by FanDirection enum (0-1): FORWARD, REVERSE, plus UNKNOWN
29PROGMEM_STRING_TABLE(FanDirectionStrings, "FORWARD", "REVERSE", "UNKNOWN");
30
32 return FanDirectionStrings::get_log_str(static_cast<uint8_t>(direction), FanDirectionStrings::LAST_INDEX);
33}
34
36 return this->set_preset_mode(preset_mode.data(), preset_mode.size());
37}
38
40 return this->set_preset_mode(preset_mode, preset_mode ? strlen(preset_mode) : 0);
41}
42
44 if (preset_mode == nullptr || len == 0) {
45 this->preset_mode_ = nullptr;
46 return *this;
47 }
48
49 // Find and validate pointer from traits immediately
50 auto traits = this->parent_.get_traits();
51 const char *validated_mode = traits.find_preset_mode(preset_mode, len);
52 if (validated_mode != nullptr) {
53 this->preset_mode_ = validated_mode; // Store pointer from traits
54 } else {
55 // Preset mode not found in traits - log warning and don't set
56 ESP_LOGW(TAG, "%s: Preset mode '%.*s' not supported", this->parent_.get_name().c_str(), (int) len, preset_mode);
57 this->preset_mode_ = nullptr;
58 }
59 return *this;
60}
61
63 ESP_LOGV(TAG, "'%s' - Setting:", this->parent_.get_name().c_str());
64 this->validate_();
65 if (this->binary_state_.has_value()) {
66 ESP_LOGV(TAG, " State: %s", ONOFF(*this->binary_state_));
67 }
68 if (this->oscillating_.has_value()) {
69 ESP_LOGV(TAG, " Oscillating: %s", YESNO(*this->oscillating_));
70 }
71 if (this->speed_.has_value()) {
72 ESP_LOGV(TAG, " Speed: %d", *this->speed_);
73 }
74 if (this->direction_.has_value()) {
75 ESP_LOGV(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(*this->direction_)));
76 }
77 if (this->preset_mode_ != nullptr) {
78 ESP_LOGV(TAG, " Preset Mode: %s", this->preset_mode_);
79 }
80 this->parent_.control(*this);
81}
82
84 auto traits = this->parent_.get_traits();
85
86 if (this->speed_.has_value()) {
87 this->speed_ = clamp(*this->speed_, 1, traits.supported_speed_count());
88
89 // https://developers.home-assistant.io/docs/core/entity/fan/#preset-modes
90 // "Manually setting a speed must disable any set preset mode"
91 this->preset_mode_ = nullptr;
92 }
93
94 // when turning on...
95 if (!this->parent_.state && this->binary_state_.has_value() &&
96 *this->binary_state_
97 // ..,and no preset mode will be active...
98 && !this->has_preset_mode() &&
99 !this->parent_.has_preset_mode()
100 // ...and neither current nor new speed is available...
101 && traits.supports_speed() && this->parent_.speed == 0 && !this->speed_.has_value()) {
102 // ...set speed to 100%
103 this->speed_ = traits.supported_speed_count();
104 }
105
106 if (this->oscillating_.has_value() && !traits.supports_oscillation()) {
107 ESP_LOGW(TAG, "%s: Oscillation not supported", this->parent_.get_name().c_str());
108 this->oscillating_.reset();
109 }
110
111 if (this->speed_.has_value() && !traits.supports_speed()) {
112 ESP_LOGW(TAG, "%s: Speed control not supported", this->parent_.get_name().c_str());
113 this->speed_.reset();
114 }
115
116 if (this->direction_.has_value() && !traits.supports_direction()) {
117 ESP_LOGW(TAG, "%s: Direction control not supported", this->parent_.get_name().c_str());
118 this->direction_.reset();
119 }
120}
121
123 auto call = fan.make_call();
124 call.set_state(this->state);
125 call.set_oscillating(this->oscillating);
126 call.set_speed(this->speed);
127 call.set_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
132 const auto &preset_modes = traits.supported_preset_modes();
133 if (this->preset_mode < preset_modes.size()) {
134 call.set_preset_mode(preset_modes[this->preset_mode]);
135 }
136 }
137 return call;
138}
140 fan.state = this->state;
141 fan.oscillating = this->oscillating;
142 fan.speed = this->speed;
143 fan.direction = this->direction;
144
145 auto traits = fan.get_traits();
146 if (traits.supports_preset_modes()) {
147 // Use stored preset index to get preset name from traits
148 const auto &preset_modes = traits.supported_preset_modes();
149 if (this->preset_mode < preset_modes.size()) {
150 fan.set_preset_mode_(preset_modes[this->preset_mode]);
151 }
152 }
153
154 fan.publish_state();
155}
156
157FanCall Fan::turn_on() { return this->make_call().set_state(true); }
158FanCall Fan::turn_off() { return this->make_call().set_state(false); }
159FanCall Fan::toggle() { return this->make_call().set_state(!this->state); }
160FanCall Fan::make_call() { return FanCall(*this); }
161
162const char *Fan::find_preset_mode_(const char *preset_mode) {
163 return this->find_preset_mode_(preset_mode, preset_mode ? strlen(preset_mode) : 0);
164}
165
166const char *Fan::find_preset_mode_(const char *preset_mode, size_t len) {
167 if (preset_mode == nullptr || len == 0) {
168 return nullptr;
169 }
170 if (this->supported_preset_modes_) {
171 for (const char *mode : *this->supported_preset_modes_) {
172 if (strncmp(mode, preset_mode, len) == 0 && mode[len] == '\0') {
173 return mode;
174 }
175 }
176 return nullptr;
177 }
178 // Fallback for deprecated path: external components may set modes on FanTraits directly
179 return this->get_traits().find_preset_mode(preset_mode, len);
180}
181
182bool Fan::set_preset_mode_(const char *preset_mode, size_t len) {
183 if (preset_mode == nullptr || len == 0) {
184 // Treat nullptr or empty string as clearing the preset mode (no valid preset is "")
185 if (this->preset_mode_ == nullptr) {
186 return false; // No change
187 }
188 this->clear_preset_mode_();
189 return true;
190 }
191 const char *validated = this->find_preset_mode_(preset_mode, len);
192 if (validated == nullptr || this->preset_mode_ == validated) {
193 return false; // Preset mode not supported or no change
194 }
195 this->preset_mode_ = validated;
196 return true;
197}
198
200 return this->set_preset_mode_(preset_mode, preset_mode ? strlen(preset_mode) : 0);
201}
202
203bool Fan::set_preset_mode_(const std::string &preset_mode) {
204 return this->set_preset_mode_(preset_mode.data(), preset_mode.size());
205}
206
208 // Safe: find_preset_mode_ only uses the input for comparison and returns
209 // a pointer from traits, so the input StringRef's lifetime doesn't matter.
210 return this->set_preset_mode_(preset_mode.c_str(), preset_mode.size());
211}
212
213void Fan::clear_preset_mode_() { this->preset_mode_ = nullptr; }
214
216 if (call.has_preset_mode()) {
217 this->set_preset_mode_(call.get_preset_mode());
218 } else if (call.get_speed().has_value()) {
219 // Manually setting speed clears preset (per Home Assistant convention)
220 this->clear_preset_mode_();
221 }
222}
223
225 auto traits = this->get_traits();
226
227 ESP_LOGV(TAG,
228 "'%s' >>\n"
229 " State: %s",
230 this->name_.c_str(), ONOFF(this->state));
231 if (traits.supports_speed()) {
232 ESP_LOGV(TAG, " Speed: %d", this->speed);
233 }
234 if (traits.supports_oscillation()) {
235 ESP_LOGV(TAG, " Oscillating: %s", YESNO(this->oscillating));
236 }
237 if (traits.supports_direction()) {
238 ESP_LOGV(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(this->direction)));
239 }
240 if (this->preset_mode_ != nullptr) {
241 ESP_LOGV(TAG, " Preset Mode: %s", this->preset_mode_);
242 }
243 this->state_callback_.call();
244#if defined(USE_FAN) && defined(USE_CONTROLLER_REGISTRY)
245 ControllerRegistry::notify_fan_update(this);
246#endif
247 this->save_state_();
248}
249
250// Random 32-bit value, change this every time the layout of the FanRestoreState struct changes.
251constexpr uint32_t RESTORE_STATE_VERSION = 0x71700ABB;
252optional<FanRestoreState> Fan::restore_state_() {
253 FanRestoreState recovered{};
254 this->rtc_ = this->make_entity_preference<FanRestoreState>(RESTORE_STATE_VERSION);
255 bool restored = this->rtc_.load(&recovered);
256
257 if (!restored) {
258 // No valid saved data; ensure preset_mode sentinel is set
259 recovered.preset_mode = FanRestoreState::NO_PRESET;
260 }
261
262 switch (this->restore_mode_) {
264 return {};
266 recovered.state = false;
267 return recovered;
269 recovered.state = true;
270 return recovered;
272 recovered.state = restored ? recovered.state : false;
273 return recovered;
275 recovered.state = restored ? recovered.state : true;
276 return recovered;
278 recovered.state = restored ? !recovered.state : false;
279 return recovered;
281 recovered.state = restored ? !recovered.state : true;
282 return recovered;
283 }
284
285 return {};
286}
289 return;
290 }
291
293 state.state = this->state;
294 state.oscillating = this->oscillating;
295 state.speed = this->speed;
296 state.direction = this->direction;
297 state.preset_mode = FanRestoreState::NO_PRESET;
298
299 if (this->has_preset_mode()) {
300 if (this->supported_preset_modes_) {
301 // New path: search Fan-owned vector directly
302 for (size_t i = 0; i < this->supported_preset_modes_->size(); i++) {
303 if ((*this->supported_preset_modes_)[i] == this->preset_mode_) {
304 state.preset_mode = i;
305 break;
306 }
307 }
308 } else {
309 // Compat: fall back to traits for deprecated path. Remove in 2026.11.0.
310 // Pointer comparison works because preset_mode_ and the compat vector both
311 // hold pointers to string literals in .rodata (stable addresses).
312 auto traits = this->get_traits();
313 const auto &preset_modes = traits.supported_preset_modes();
314 for (size_t i = 0; i < preset_modes.size(); i++) {
315 if (preset_modes[i] == this->preset_mode_) {
316 state.preset_mode = i;
317 break;
318 }
319 }
320 }
321 }
322
323 this->rtc_.save(&state);
324}
325
326void Fan::dump_traits_(const char *tag, const char *prefix) {
327 auto traits = this->get_traits();
328
329 if (traits.supports_speed()) {
330 ESP_LOGCONFIG(tag,
331 "%s Speed: YES\n"
332 "%s Speed count: %d",
333 prefix, prefix, traits.supported_speed_count());
334 }
335 if (traits.supports_oscillation()) {
336 ESP_LOGCONFIG(tag, "%s Oscillation: YES", prefix);
337 }
338 if (traits.supports_direction()) {
339 ESP_LOGCONFIG(tag, "%s Direction: YES", prefix);
340 }
341 if (traits.supports_preset_modes()) {
342 ESP_LOGCONFIG(tag, "%s Supported presets:", prefix);
343 for (const char *s : traits.supported_preset_modes())
344 ESP_LOGCONFIG(tag, "%s - %s", prefix, s);
345 }
346}
347
348} // namespace fan
349} // namespace esphome
BedjetMode mode
BedJet operating mode.
const StringRef & get_name() const
Definition entity_base.h:71
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:35
friend FanCall
Definition fan.h:154
void publish_state()
Definition fan.cpp:224
FanCall turn_on()
Definition fan.cpp:157
FanCall turn_off()
Definition fan.cpp:158
FanCall make_call()
Definition fan.cpp:160
virtual FanTraits get_traits()=0
FanCall toggle()
Definition fan.cpp:159
LazyCallbackManager< void()> state_callback_
Definition fan.h:185
void apply_preset_mode_(const FanCall &call)
Apply preset mode from a FanCall (handles speed-clears-preset convention)
Definition fan.cpp:215
ESPPreferenceObject rtc_
Definition fan.h:186
void clear_preset_mode_()
Clear the preset mode.
Definition fan.cpp:213
bool set_preset_mode_(const char *preset_mode, size_t len)
Set the preset mode (finds and stores pointer from traits).
Definition fan.cpp:182
FanDirection direction
The current direction of the fan.
Definition fan.h:117
void save_state_()
Definition fan.cpp:287
FanRestoreMode restore_mode_
Definition fan.h:187
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:162
bool has_preset_mode() const
Check if a preset mode is currently active.
Definition fan.h:151
int speed
The current fan speed level.
Definition fan.h:115
void dump_traits_(const char *tag, const char *prefix)
Definition fan.cpp:326
optional< FanRestoreState > restore_state_()
Definition fan.cpp:252
std::vector< const char * > compat_preset_modes_
Definition fan_traits.h:95
const std::vector< const char * > * preset_modes_
Definition fan_traits.h:92
const std::vector< const char * > & supported_preset_modes() const
Definition fan.cpp:17
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:65
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:31
constexpr uint32_t RESTORE_STATE_VERSION
Definition fan.cpp:251
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
const char * tag
Definition log.h:74
std::string size_t len
Definition helpers.h:1045
static void uint32_t
static constexpr uint8_t NO_PRESET
Definition fan.h:94
void apply(Fan &fan)
Apply these settings to the fan.
Definition fan.cpp:139
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:122