#pragma once #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/optional.h" #include "esphome/core/preferences.h" #include "esphome/core/string_ref.h" #include "fan_traits.h" namespace esphome { namespace fan { #define LOG_FAN(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ (obj)->dump_traits_(TAG, prefix); \ } /// Simple enum to represent the direction of a fan. enum class FanDirection { FORWARD = 0, REVERSE = 1 }; /// Restore mode of a fan. enum class FanRestoreMode { NO_RESTORE, ALWAYS_OFF, ALWAYS_ON, RESTORE_DEFAULT_OFF, RESTORE_DEFAULT_ON, RESTORE_INVERTED_DEFAULT_OFF, RESTORE_INVERTED_DEFAULT_ON, }; const LogString *fan_direction_to_string(FanDirection direction); class Fan; class FanCall { public: explicit FanCall(Fan &parent) : parent_(parent) {} FanCall &set_state(bool binary_state) { this->binary_state_ = binary_state; return *this; } FanCall &set_state(optional binary_state) { this->binary_state_ = binary_state; return *this; } optional get_state() const { return this->binary_state_; } FanCall &set_oscillating(bool oscillating) { this->oscillating_ = oscillating; return *this; } FanCall &set_oscillating(optional oscillating) { this->oscillating_ = oscillating; return *this; } optional get_oscillating() const { return this->oscillating_; } FanCall &set_speed(int speed) { this->speed_ = speed; return *this; } optional get_speed() const { return this->speed_; } FanCall &set_direction(FanDirection direction) { this->direction_ = direction; return *this; } FanCall &set_direction(optional direction) { this->direction_ = direction; return *this; } optional get_direction() const { return this->direction_; } FanCall &set_preset_mode(const std::string &preset_mode); FanCall &set_preset_mode(const char *preset_mode); FanCall &set_preset_mode(const char *preset_mode, size_t len); const char *get_preset_mode() const { return this->preset_mode_; } bool has_preset_mode() const { return this->preset_mode_ != nullptr; } void perform(); protected: void validate_(); Fan &parent_; optional binary_state_; optional oscillating_; optional speed_; optional direction_{}; const char *preset_mode_{nullptr}; // Pointer to string in traits (after validation) }; struct FanRestoreState { static constexpr uint8_t NO_PRESET = UINT8_MAX; bool state; int speed; bool oscillating; FanDirection direction; uint8_t preset_mode{NO_PRESET}; /// Convert this struct to a fan call that can be performed. FanCall to_call(Fan &fan); /// Apply these settings to the fan. void apply(Fan &fan); } __attribute__((packed)); class Fan : public EntityBase { public: /// The current on/off state of the fan. bool state{false}; /// The current oscillation state of the fan. bool oscillating{false}; /// The current fan speed level int speed{0}; /// The current direction of the fan FanDirection direction{FanDirection::FORWARD}; FanCall turn_on(); FanCall turn_off(); FanCall toggle(); FanCall make_call(); /// Register a callback that will be called each time the state changes. template void add_on_state_callback(F &&callback) { this->state_callback_.add(std::forward(callback)); } void publish_state(); virtual FanTraits get_traits() = 0; /// Set the supported preset modes (stored on Fan, referenced by FanTraits via pointer). void set_supported_preset_modes(std::initializer_list preset_modes) { this->ensure_preset_modes_().assign(preset_modes.begin(), preset_modes.end()); } void set_supported_preset_modes(const std::vector &preset_modes) { this->ensure_preset_modes_() = preset_modes; } /// Set the restore mode of this fan. void set_restore_mode(FanRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } /// Get the current preset mode. /// Returns a StringRef of the string stored in traits, or empty ref if not set. /// The returned ref points to string literals from codegen (static storage). /// Traits are set once at startup and valid for the lifetime of the program. StringRef get_preset_mode() const { return StringRef::from_maybe_nullptr(this->preset_mode_); } /// Check if a preset mode is currently active bool has_preset_mode() const { return this->preset_mode_ != nullptr; } protected: friend FanCall; friend struct FanRestoreState; virtual void control(const FanCall &call) = 0; optional restore_state_(); void save_state_(); void dump_traits_(const char *tag, const char *prefix); /// Set the preset mode (finds and stores pointer from traits). Returns true if changed. /// Passing nullptr or empty string clears the preset mode. bool set_preset_mode_(const char *preset_mode, size_t len); bool set_preset_mode_(const char *preset_mode); bool set_preset_mode_(const std::string &preset_mode); bool set_preset_mode_(StringRef preset_mode); /// Clear the preset mode void clear_preset_mode_(); /// Apply preset mode from a FanCall (handles speed-clears-preset convention) void apply_preset_mode_(const FanCall &call); /// Find and return the matching preset mode pointer from traits, or nullptr if not found. const char *find_preset_mode_(const char *preset_mode); 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_); } } LazyCallbackManager state_callback_{}; ESPPreferenceObject rtc_; FanRestoreMode restore_mode_; private: /// Lazy-allocate preset modes vector (never freed — entity lives forever). std::vector &ensure_preset_modes_() { if (!this->supported_preset_modes_) { this->supported_preset_modes_ = new std::vector(); // NOLINT } return *this->supported_preset_modes_; } std::vector *supported_preset_modes_{nullptr}; const char *preset_mode_{nullptr}; }; } // namespace fan } // namespace esphome