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