[climate] Don't restore a saved mode the device no longer supports (#18296)

This commit is contained in:
Leonardo Rivera
2026-08-26 14:26:31 -04:00
committed by GitHub
parent 8a89f3075f
commit 34e7453677
2 changed files with 81 additions and 1 deletions
+8 -1
View File
@@ -551,7 +551,14 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) {
void ClimateDeviceRestoreState::apply(Climate *climate) {
auto traits = climate->get_traits();
climate->mode = this->mode;
// A saved mode the device no longer offers cannot be selected again, so skip it and leave the
// entity on the mode it already has. The other saved fields are still restored.
if (traits.supports_mode(this->mode)) {
climate->mode = this->mode;
} else {
ESP_LOGW(TAG, "'%s' - Saved mode %s is no longer supported, keeping %s", climate->get_name().c_str(),
LOG_STR_ARG(climate_mode_to_string(this->mode)), LOG_STR_ARG(climate_mode_to_string(climate->mode)));
}
if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
climate->target_temperature_low = this->target_temperature_low;
+73
View File
@@ -0,0 +1,73 @@
#include <gtest/gtest.h>
#include "esphome/components/climate/climate.h"
namespace esphome::climate::testing {
// Minimal concrete Climate that offers a fixed set of modes, so the restore path can be exercised
// without any hardware or platform component.
class TestClimate : public Climate {
public:
ClimateTraits traits() override {
auto traits = ClimateTraits();
traits.set_supported_modes({CLIMATE_MODE_OFF, CLIMATE_MODE_COOL});
traits.set_supported_fan_modes({CLIMATE_FAN_LOW, CLIMATE_FAN_HIGH});
return traits;
}
protected:
void control(const ClimateCall &call) override {}
};
TEST(ClimateRestoreStateTest, RestoresASupportedMode) {
TestClimate climate;
// Value-initialized: several members (mode, swing_mode, the temperature union) have no default
// member initializer, so leaving the {} off would read indeterminate values.
ClimateDeviceRestoreState state{};
state.mode = CLIMATE_MODE_COOL;
state.apply(&climate);
EXPECT_EQ(climate.mode, CLIMATE_MODE_COOL);
}
TEST(ClimateRestoreStateTest, DoesNotRestoreAnUnsupportedMode) {
TestClimate climate;
ClimateDeviceRestoreState state{};
state.mode = CLIMATE_MODE_HEAT;
state.apply(&climate);
// The device never advertised HEAT, so the mode stays where it was.
EXPECT_EQ(climate.mode, CLIMATE_MODE_OFF);
}
TEST(ClimateRestoreStateTest, LeavesTheCurrentModeAloneRatherThanForcingOff) {
TestClimate climate;
// apply() is public and nothing restricts it to setup(), so the entity is not necessarily off
// when an unsupported mode is dropped. It keeps what it had rather than being forced to OFF.
climate.mode = CLIMATE_MODE_COOL;
ClimateDeviceRestoreState state{};
state.mode = CLIMATE_MODE_HEAT;
state.apply(&climate);
EXPECT_EQ(climate.mode, CLIMATE_MODE_COOL);
}
TEST(ClimateRestoreStateTest, KeepsRestoringTheOtherFieldsWhenTheModeIsDropped) {
TestClimate climate;
ClimateDeviceRestoreState state{};
state.mode = CLIMATE_MODE_HEAT;
state.target_temperature = 21.0f;
state.uses_custom_fan_mode = false;
state.fan_mode = CLIMATE_FAN_HIGH;
state.apply(&climate);
EXPECT_EQ(climate.mode, CLIMATE_MODE_OFF);
EXPECT_FLOAT_EQ(climate.target_temperature, 21.0f);
// Compared as an optional: this asserts both that the fan mode was restored and what it holds.
EXPECT_EQ(climate.fan_mode, CLIMATE_FAN_HIGH);
}
} // namespace esphome::climate::testing