mirror of
https://github.com/esphome/esphome.git
synced 2025-11-18 15:55:46 +00:00
277 lines
9.0 KiB
C++
277 lines
9.0 KiB
C++
#include "fan.h"
|
|
#include "esphome/core/defines.h"
|
|
#include "esphome/core/controller_registry.h"
|
|
#include "esphome/core/log.h"
|
|
|
|
namespace esphome {
|
|
namespace fan {
|
|
|
|
static const char *const TAG = "fan";
|
|
|
|
const LogString *fan_direction_to_string(FanDirection direction) {
|
|
switch (direction) {
|
|
case FanDirection::FORWARD:
|
|
return LOG_STR("FORWARD");
|
|
case FanDirection::REVERSE:
|
|
return LOG_STR("REVERSE");
|
|
default:
|
|
return LOG_STR("UNKNOWN");
|
|
}
|
|
}
|
|
|
|
FanCall &FanCall::set_preset_mode(const std::string &preset_mode) { return this->set_preset_mode(preset_mode.c_str()); }
|
|
|
|
FanCall &FanCall::set_preset_mode(const char *preset_mode) {
|
|
if (preset_mode == nullptr || strlen(preset_mode) == 0) {
|
|
this->preset_mode_ = nullptr;
|
|
return *this;
|
|
}
|
|
|
|
// Find and validate pointer from traits immediately
|
|
auto traits = this->parent_.get_traits();
|
|
const char *validated_mode = traits.find_preset_mode(preset_mode);
|
|
if (validated_mode != nullptr) {
|
|
this->preset_mode_ = validated_mode; // Store pointer from traits
|
|
} else {
|
|
// Preset mode not found in traits - log warning and don't set
|
|
ESP_LOGW(TAG, "%s: Preset mode '%s' not supported", this->parent_.get_name().c_str(), preset_mode);
|
|
this->preset_mode_ = nullptr;
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
void FanCall::perform() {
|
|
ESP_LOGD(TAG, "'%s' - Setting:", this->parent_.get_name().c_str());
|
|
this->validate_();
|
|
if (this->binary_state_.has_value()) {
|
|
ESP_LOGD(TAG, " State: %s", ONOFF(*this->binary_state_));
|
|
}
|
|
if (this->oscillating_.has_value()) {
|
|
ESP_LOGD(TAG, " Oscillating: %s", YESNO(*this->oscillating_));
|
|
}
|
|
if (this->speed_.has_value()) {
|
|
ESP_LOGD(TAG, " Speed: %d", *this->speed_);
|
|
}
|
|
if (this->direction_.has_value()) {
|
|
ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(*this->direction_)));
|
|
}
|
|
if (this->has_preset_mode()) {
|
|
ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode_);
|
|
}
|
|
this->parent_.control(*this);
|
|
}
|
|
|
|
void FanCall::validate_() {
|
|
auto traits = this->parent_.get_traits();
|
|
|
|
if (this->speed_.has_value()) {
|
|
this->speed_ = clamp(*this->speed_, 1, traits.supported_speed_count());
|
|
|
|
// https://developers.home-assistant.io/docs/core/entity/fan/#preset-modes
|
|
// "Manually setting a speed must disable any set preset mode"
|
|
this->preset_mode_ = nullptr;
|
|
}
|
|
|
|
// when turning on...
|
|
if (!this->parent_.state && this->binary_state_.has_value() &&
|
|
*this->binary_state_
|
|
// ..,and no preset mode will be active...
|
|
&& !this->has_preset_mode() &&
|
|
this->parent_.get_preset_mode() == nullptr
|
|
// ...and neither current nor new speed is available...
|
|
&& traits.supports_speed() && this->parent_.speed == 0 && !this->speed_.has_value()) {
|
|
// ...set speed to 100%
|
|
this->speed_ = traits.supported_speed_count();
|
|
}
|
|
|
|
if (this->oscillating_.has_value() && !traits.supports_oscillation()) {
|
|
ESP_LOGW(TAG, "%s: Oscillation not supported", this->parent_.get_name().c_str());
|
|
this->oscillating_.reset();
|
|
}
|
|
|
|
if (this->speed_.has_value() && !traits.supports_speed()) {
|
|
ESP_LOGW(TAG, "%s: Speed control not supported", this->parent_.get_name().c_str());
|
|
this->speed_.reset();
|
|
}
|
|
|
|
if (this->direction_.has_value() && !traits.supports_direction()) {
|
|
ESP_LOGW(TAG, "%s: Direction control not supported", this->parent_.get_name().c_str());
|
|
this->direction_.reset();
|
|
}
|
|
}
|
|
|
|
FanCall FanRestoreState::to_call(Fan &fan) {
|
|
auto call = fan.make_call();
|
|
call.set_state(this->state);
|
|
call.set_oscillating(this->oscillating);
|
|
call.set_speed(this->speed);
|
|
call.set_direction(this->direction);
|
|
|
|
auto traits = fan.get_traits();
|
|
if (traits.supports_preset_modes()) {
|
|
// Use stored preset index to get preset name
|
|
const auto &preset_modes = traits.supported_preset_modes();
|
|
if (this->preset_mode < preset_modes.size()) {
|
|
call.set_preset_mode(preset_modes[this->preset_mode]);
|
|
}
|
|
}
|
|
return call;
|
|
}
|
|
void FanRestoreState::apply(Fan &fan) {
|
|
fan.state = this->state;
|
|
fan.oscillating = this->oscillating;
|
|
fan.speed = this->speed;
|
|
fan.direction = this->direction;
|
|
|
|
auto traits = fan.get_traits();
|
|
if (traits.supports_preset_modes()) {
|
|
// Use stored preset index to get preset name from traits
|
|
const auto &preset_modes = traits.supported_preset_modes();
|
|
if (this->preset_mode < preset_modes.size()) {
|
|
fan.set_preset_mode_(preset_modes[this->preset_mode]);
|
|
}
|
|
}
|
|
|
|
fan.publish_state();
|
|
}
|
|
|
|
FanCall Fan::turn_on() { return this->make_call().set_state(true); }
|
|
FanCall Fan::turn_off() { return this->make_call().set_state(false); }
|
|
FanCall Fan::toggle() { return this->make_call().set_state(!this->state); }
|
|
FanCall Fan::make_call() { return FanCall(*this); }
|
|
|
|
const char *Fan::find_preset_mode_(const char *preset_mode) { return this->get_traits().find_preset_mode(preset_mode); }
|
|
|
|
bool Fan::set_preset_mode_(const char *preset_mode) {
|
|
if (preset_mode == nullptr) {
|
|
// Treat nullptr as clearing the preset mode
|
|
if (this->preset_mode_ == nullptr) {
|
|
return false; // No change
|
|
}
|
|
this->clear_preset_mode_();
|
|
return true;
|
|
}
|
|
const char *validated = this->find_preset_mode_(preset_mode);
|
|
if (validated == nullptr || this->preset_mode_ == validated) {
|
|
return false; // Preset mode not supported or no change
|
|
}
|
|
this->preset_mode_ = validated;
|
|
return true;
|
|
}
|
|
|
|
bool Fan::set_preset_mode_(const std::string &preset_mode) { return this->set_preset_mode_(preset_mode.c_str()); }
|
|
|
|
void Fan::clear_preset_mode_() { this->preset_mode_ = nullptr; }
|
|
|
|
void Fan::add_on_state_callback(std::function<void()> &&callback) { this->state_callback_.add(std::move(callback)); }
|
|
void Fan::publish_state() {
|
|
auto traits = this->get_traits();
|
|
|
|
ESP_LOGD(TAG, "'%s' - Sending state:", this->name_.c_str());
|
|
ESP_LOGD(TAG, " State: %s", ONOFF(this->state));
|
|
if (traits.supports_speed()) {
|
|
ESP_LOGD(TAG, " Speed: %d", this->speed);
|
|
}
|
|
if (traits.supports_oscillation()) {
|
|
ESP_LOGD(TAG, " Oscillating: %s", YESNO(this->oscillating));
|
|
}
|
|
if (traits.supports_direction()) {
|
|
ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(this->direction)));
|
|
}
|
|
const char *preset = this->get_preset_mode();
|
|
if (preset != nullptr) {
|
|
ESP_LOGD(TAG, " Preset Mode: %s", preset);
|
|
}
|
|
this->state_callback_.call();
|
|
#if defined(USE_FAN) && defined(USE_CONTROLLER_REGISTRY)
|
|
ControllerRegistry::notify_fan_update(this);
|
|
#endif
|
|
this->save_state_();
|
|
}
|
|
|
|
// Random 32-bit value, change this every time the layout of the FanRestoreState struct changes.
|
|
constexpr uint32_t RESTORE_STATE_VERSION = 0x71700ABA;
|
|
optional<FanRestoreState> Fan::restore_state_() {
|
|
FanRestoreState recovered{};
|
|
this->rtc_ =
|
|
global_preferences->make_preference<FanRestoreState>(this->get_preference_hash() ^ RESTORE_STATE_VERSION);
|
|
bool restored = this->rtc_.load(&recovered);
|
|
|
|
switch (this->restore_mode_) {
|
|
case FanRestoreMode::NO_RESTORE:
|
|
return {};
|
|
case FanRestoreMode::ALWAYS_OFF:
|
|
recovered.state = false;
|
|
return recovered;
|
|
case FanRestoreMode::ALWAYS_ON:
|
|
recovered.state = true;
|
|
return recovered;
|
|
case FanRestoreMode::RESTORE_DEFAULT_OFF:
|
|
recovered.state = restored ? recovered.state : false;
|
|
return recovered;
|
|
case FanRestoreMode::RESTORE_DEFAULT_ON:
|
|
recovered.state = restored ? recovered.state : true;
|
|
return recovered;
|
|
case FanRestoreMode::RESTORE_INVERTED_DEFAULT_OFF:
|
|
recovered.state = restored ? !recovered.state : false;
|
|
return recovered;
|
|
case FanRestoreMode::RESTORE_INVERTED_DEFAULT_ON:
|
|
recovered.state = restored ? !recovered.state : true;
|
|
return recovered;
|
|
}
|
|
|
|
return {};
|
|
}
|
|
void Fan::save_state_() {
|
|
if (this->restore_mode_ == FanRestoreMode::NO_RESTORE) {
|
|
return;
|
|
}
|
|
|
|
auto traits = this->get_traits();
|
|
|
|
FanRestoreState state{};
|
|
state.state = this->state;
|
|
state.oscillating = this->oscillating;
|
|
state.speed = this->speed;
|
|
state.direction = this->direction;
|
|
|
|
const char *preset = this->get_preset_mode();
|
|
if (preset != nullptr) {
|
|
const auto &preset_modes = traits.supported_preset_modes();
|
|
// Find index of current preset mode (pointer comparison is safe since preset is from traits)
|
|
for (size_t i = 0; i < preset_modes.size(); i++) {
|
|
if (preset_modes[i] == preset) {
|
|
state.preset_mode = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
this->rtc_.save(&state);
|
|
}
|
|
|
|
void Fan::dump_traits_(const char *tag, const char *prefix) {
|
|
auto traits = this->get_traits();
|
|
|
|
if (traits.supports_speed()) {
|
|
ESP_LOGCONFIG(tag,
|
|
"%s Speed: YES\n"
|
|
"%s Speed count: %d",
|
|
prefix, prefix, traits.supported_speed_count());
|
|
}
|
|
if (traits.supports_oscillation()) {
|
|
ESP_LOGCONFIG(tag, "%s Oscillation: YES", prefix);
|
|
}
|
|
if (traits.supports_direction()) {
|
|
ESP_LOGCONFIG(tag, "%s Direction: YES", prefix);
|
|
}
|
|
if (traits.supports_preset_modes()) {
|
|
ESP_LOGCONFIG(tag, "%s Supported presets:", prefix);
|
|
for (const char *s : traits.supported_preset_modes())
|
|
ESP_LOGCONFIG(tag, "%s - %s", prefix, s);
|
|
}
|
|
}
|
|
|
|
} // namespace fan
|
|
} // namespace esphome
|