[fan] Make get_preset_mode() return empty string instead of nullptr for safety

This commit is contained in:
J. Nick Koston
2026-01-08 21:35:18 -10:00
parent cd43b4114e
commit ff0b1a24c7
8 changed files with 35 additions and 23 deletions
+11 -7
View File
@@ -8,20 +8,24 @@ static const char *const TAG = "copy.fan";
void CopyFan::setup() {
source_->add_on_state_callback([this]() {
this->state = source_->state;
this->oscillating = source_->oscillating;
this->speed = source_->speed;
this->direction = source_->direction;
this->set_preset_mode_(source_->get_preset_mode());
this->copy_state_from_source_();
this->publish_state();
});
this->copy_state_from_source_();
this->publish_state();
}
void CopyFan::copy_state_from_source_() {
this->state = source_->state;
this->oscillating = source_->oscillating;
this->speed = source_->speed;
this->direction = source_->direction;
this->set_preset_mode_(source_->get_preset_mode());
this->publish_state();
if (source_->has_preset_mode()) {
this->set_preset_mode_(source_->get_preset_mode());
} else {
this->clear_preset_mode_();
}
}
void CopyFan::dump_config() { LOG_FAN("", "Copy Fan", this); }
+1 -1
View File
@@ -16,7 +16,7 @@ class CopyFan : public fan::Fan, public Component {
protected:
void control(const fan::FanCall &call) override;
;
void copy_state_from_source_();
fan::Fan *source_;
};
+2 -3
View File
@@ -216,15 +216,14 @@ class FanPresetSetTrigger : public Trigger<std::string> {
auto should_trigger = preset_mode != this->last_preset_mode_;
this->last_preset_mode_ = preset_mode;
if (should_trigger) {
// Trigger with empty string when nullptr to maintain backward compatibility
this->trigger(preset_mode != nullptr ? preset_mode : "");
this->trigger(preset_mode);
}
});
this->last_preset_mode_ = state->get_preset_mode();
}
protected:
const char *last_preset_mode_{nullptr};
const char *last_preset_mode_{""};
};
} // namespace fan
+14 -7
View File
@@ -83,7 +83,7 @@ void FanCall::validate_() {
*this->binary_state_
// ..,and no preset mode will be active...
&& !this->has_preset_mode() &&
this->parent_.get_preset_mode() == nullptr
!this->parent_.has_preset_mode()
// ...and neither current nor new speed is available...
&& traits.supports_speed() && this->parent_.speed == 0 && !this->speed_.has_value()) {
// ...set speed to 100%
@@ -175,6 +175,15 @@ bool Fan::set_preset_mode_(const std::string &preset_mode) { return this->set_pr
void Fan::clear_preset_mode_() { this->preset_mode_ = nullptr; }
void Fan::apply_preset_mode_(const FanCall &call) {
if (call.has_preset_mode()) {
this->set_preset_mode_(call.get_preset_mode());
} else if (call.get_speed().has_value()) {
// Manually setting speed clears preset (per Home Assistant convention)
this->clear_preset_mode_();
}
}
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();
@@ -192,9 +201,8 @@ void Fan::publish_state() {
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);
if (this->has_preset_mode()) {
ESP_LOGD(TAG, " Preset Mode: %s", this->get_preset_mode());
}
this->state_callback_.call();
#if defined(USE_FAN) && defined(USE_CONTROLLER_REGISTRY)
@@ -249,12 +257,11 @@ void Fan::save_state_() {
state.speed = this->speed;
state.direction = this->direction;
const char *preset = this->get_preset_mode();
if (preset != nullptr) {
if (this->has_preset_mode()) {
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) {
if (preset_modes[i] == this->preset_mode_) {
state.preset_mode = i;
break;
}
+4 -2
View File
@@ -128,8 +128,8 @@ class Fan : public EntityBase {
/// 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 pointer to string stored in traits, or nullptr if not set)
const char *get_preset_mode() const { return this->preset_mode_; }
/// Get the current preset mode (returns pointer to string stored in traits, or empty string if not set)
const char *get_preset_mode() const { return this->preset_mode_ != nullptr ? this->preset_mode_ : ""; }
/// Check if a preset mode is currently active
bool has_preset_mode() const { return this->preset_mode_ != nullptr; }
@@ -151,6 +151,8 @@ class Fan : public EntityBase {
bool set_preset_mode_(const std::string &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);
@@ -57,7 +57,7 @@ void HBridgeFan::control(const fan::FanCall &call) {
this->oscillating = *call.get_oscillating();
if (call.get_direction().has_value())
this->direction = *call.get_direction();
this->set_preset_mode_(call.get_preset_mode());
this->apply_preset_mode_(call);
this->write_state_();
this->publish_state();
+1 -1
View File
@@ -29,7 +29,7 @@ void SpeedFan::control(const fan::FanCall &call) {
this->oscillating = *call.get_oscillating();
if (call.get_direction().has_value())
this->direction = *call.get_direction();
this->set_preset_mode_(call.get_preset_mode());
this->apply_preset_mode_(call);
this->write_state_();
this->publish_state();
@@ -28,7 +28,7 @@ void TemplateFan::control(const fan::FanCall &call) {
this->oscillating = *call.get_oscillating();
if (call.get_direction().has_value() && this->has_direction_)
this->direction = *call.get_direction();
this->set_preset_mode_(call.get_preset_mode());
this->apply_preset_mode_(call);
this->publish_state();
}