[thermostat] General clean-up, optimization, properly support "auto" mode (#10561)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
Keith Burzinski
2025-09-10 13:24:50 +12:00
committed by GitHub
co-authored by Jesse Hills
parent e218f16f0f
commit d9f625e5c8
4 changed files with 220 additions and 185 deletions
+11 -7
View File
@@ -32,6 +32,7 @@ from esphome.const import (
CONF_FAN_WITH_COOLING,
CONF_FAN_WITH_HEATING,
CONF_HEAT_ACTION,
CONF_HEAT_COOL_MODE,
CONF_HEAT_DEADBAND,
CONF_HEAT_MODE,
CONF_HEAT_OVERRUN,
@@ -150,7 +151,7 @@ def generate_comparable_preset(config, name):
def validate_thermostat(config):
# verify corresponding action(s) exist(s) for any defined climate mode or action
requirements = {
CONF_AUTO_MODE: [
CONF_HEAT_COOL_MODE: [
CONF_COOL_ACTION,
CONF_HEAT_ACTION,
CONF_MIN_COOLING_OFF_TIME,
@@ -540,6 +541,9 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_FAN_ONLY_MODE): automation.validate_automation(
single=True
),
cv.Optional(CONF_HEAT_COOL_MODE): automation.validate_automation(
single=True
),
cv.Optional(CONF_HEAT_MODE): automation.validate_automation(single=True),
cv.Optional(CONF_OFF_MODE): automation.validate_automation(single=True),
cv.Optional(CONF_FAN_MODE_ON_ACTION): automation.validate_automation(
@@ -644,7 +648,6 @@ async def to_code(config):
var = await climate.new_climate(config)
await cg.register_component(var, config)
heat_cool_mode_available = CONF_HEAT_ACTION in config and CONF_COOL_ACTION in config
two_points_available = CONF_HEAT_ACTION in config and (
CONF_COOL_ACTION in config
or (config[CONF_FAN_ONLY_COOLING] and CONF_FAN_ONLY_ACTION in config)
@@ -739,11 +742,6 @@ async def to_code(config):
var.get_idle_action_trigger(), [], config[CONF_IDLE_ACTION]
)
if heat_cool_mode_available is True:
cg.add(var.set_supports_heat_cool(True))
else:
cg.add(var.set_supports_heat_cool(False))
if CONF_COOL_ACTION in config:
await automation.build_automation(
var.get_cool_action_trigger(), [], config[CONF_COOL_ACTION]
@@ -780,6 +778,7 @@ async def to_code(config):
await automation.build_automation(
var.get_auto_mode_trigger(), [], config[CONF_AUTO_MODE]
)
cg.add(var.set_supports_auto(True))
if CONF_COOL_MODE in config:
await automation.build_automation(
var.get_cool_mode_trigger(), [], config[CONF_COOL_MODE]
@@ -800,6 +799,11 @@ async def to_code(config):
var.get_heat_mode_trigger(), [], config[CONF_HEAT_MODE]
)
cg.add(var.set_supports_heat(True))
if CONF_HEAT_COOL_MODE in config:
await automation.build_automation(
var.get_heat_cool_mode_trigger(), [], config[CONF_HEAT_COOL_MODE]
)
cg.add(var.set_supports_heat_cool(True))
if CONF_OFF_MODE in config:
await automation.build_automation(
var.get_off_mode_trigger(), [], config[CONF_OFF_MODE]
File diff suppressed because it is too large Load Diff
@@ -6,9 +6,9 @@
#include "esphome/components/climate/climate.h"
#include "esphome/components/sensor/sensor.h"
#include <array>
#include <cinttypes>
#include <map>
#include <vector>
namespace esphome {
namespace thermostat {
@@ -24,6 +24,7 @@ enum ThermostatClimateTimerIndex : uint8_t {
TIMER_HEATING_OFF = 7,
TIMER_HEATING_ON = 8,
TIMER_IDLE_ON = 9,
TIMER_COUNT = 10,
};
enum OnBootRestoreFrom : uint8_t {
@@ -131,6 +132,7 @@ class ThermostatClimate : public climate::Climate, public Component {
Trigger<> *get_dry_mode_trigger() const;
Trigger<> *get_fan_only_mode_trigger() const;
Trigger<> *get_heat_mode_trigger() const;
Trigger<> *get_heat_cool_mode_trigger() const;
Trigger<> *get_off_mode_trigger() const;
Trigger<> *get_fan_mode_on_trigger() const;
Trigger<> *get_fan_mode_off_trigger() const;
@@ -163,9 +165,10 @@ class ThermostatClimate : public climate::Climate, public Component {
/// Returns the fan mode that is locked in (check fan_mode_change_delayed(), first!)
climate::ClimateFanMode locked_fan_mode();
/// Set point and hysteresis validation
bool hysteresis_valid(); // returns true if valid
bool hysteresis_valid(); // returns true if valid
bool limit_setpoints_for_heat_cool(); // returns true if set points should be further limited within visual range
void validate_target_temperature();
void validate_target_temperatures();
void validate_target_temperatures(bool pin_target_temperature_high);
void validate_target_temperature_low();
void validate_target_temperature_high();
@@ -241,12 +244,28 @@ class ThermostatClimate : public climate::Climate, public Component {
bool supplemental_cooling_required_();
bool supplemental_heating_required_();
void dump_preset_config_(const char *preset_name, const ThermostatClimateTargetTempConfig &config,
bool is_default_preset);
void dump_preset_config_(const char *preset_name, const ThermostatClimateTargetTempConfig &config);
/// Minimum allowable duration in seconds for action timers
const uint8_t min_timer_duration_{1};
/// Store previously-known states
///
/// These are used to determine when a trigger/action needs to be called
climate::ClimateFanMode prev_fan_mode_{climate::CLIMATE_FAN_ON};
climate::ClimateMode prev_mode_{climate::CLIMATE_MODE_OFF};
climate::ClimateSwingMode prev_swing_mode_{climate::CLIMATE_SWING_OFF};
/// The current supplemental action
climate::ClimateAction supplemental_action_{climate::CLIMATE_ACTION_OFF};
/// Default standard preset to use on start up
climate::ClimatePreset default_preset_{};
/// If set to DEFAULT_PRESET then the default preset is always used. When MEMORY prior
/// state will attempt to be restored if possible
OnBootRestoreFrom on_boot_restore_from_{OnBootRestoreFrom::MEMORY};
/// Whether the controller supports auto/cooling/drying/fanning/heating.
///
/// A false value for any given attribute means that the controller has no such action
@@ -362,9 +381,15 @@ class ThermostatClimate : public climate::Climate, public Component {
Trigger<> *supplemental_heat_action_trigger_{nullptr};
Trigger<> *heat_mode_trigger_{nullptr};
/// The trigger to call when the controller should switch to heat/cool mode.
///
/// In heat/cool mode, the controller will enable heating/cooling as necessary and switch
/// to idle when the temperature is within the thresholds/set points.
Trigger<> *heat_cool_mode_trigger_{nullptr};
/// The trigger to call when the controller should switch to auto mode.
///
/// In auto mode, the controller will enable heating/cooling as necessary and switch
/// In auto mode, the controller will enable heating/cooling as supported/necessary and switch
/// to idle when the temperature is within the thresholds/set points.
Trigger<> *auto_mode_trigger_{nullptr};
@@ -438,35 +463,21 @@ class ThermostatClimate : public climate::Climate, public Component {
Trigger<> *prev_mode_trigger_{nullptr};
Trigger<> *prev_swing_mode_trigger_{nullptr};
/// If set to DEFAULT_PRESET then the default preset is always used. When MEMORY prior
/// state will attempt to be restored if possible
OnBootRestoreFrom on_boot_restore_from_{OnBootRestoreFrom::MEMORY};
/// Store previously-known states
///
/// These are used to determine when a trigger/action needs to be called
climate::ClimateAction supplemental_action_{climate::CLIMATE_ACTION_OFF};
climate::ClimateFanMode prev_fan_mode_{climate::CLIMATE_FAN_ON};
climate::ClimateMode prev_mode_{climate::CLIMATE_MODE_OFF};
climate::ClimateSwingMode prev_swing_mode_{climate::CLIMATE_SWING_OFF};
/// Default standard preset to use on start up
climate::ClimatePreset default_preset_{};
/// Default custom preset to use on start up
std::string default_custom_preset_{};
/// Climate action timers
std::vector<ThermostatClimateTimer> timer_{
{false, 0, 0, std::bind(&ThermostatClimate::cooling_max_run_time_timer_callback_, this)},
{false, 0, 0, std::bind(&ThermostatClimate::cooling_off_timer_callback_, this)},
{false, 0, 0, std::bind(&ThermostatClimate::cooling_on_timer_callback_, this)},
{false, 0, 0, std::bind(&ThermostatClimate::fan_mode_timer_callback_, this)},
{false, 0, 0, std::bind(&ThermostatClimate::fanning_off_timer_callback_, this)},
{false, 0, 0, std::bind(&ThermostatClimate::fanning_on_timer_callback_, this)},
{false, 0, 0, std::bind(&ThermostatClimate::heating_max_run_time_timer_callback_, this)},
{false, 0, 0, std::bind(&ThermostatClimate::heating_off_timer_callback_, this)},
{false, 0, 0, std::bind(&ThermostatClimate::heating_on_timer_callback_, this)},
{false, 0, 0, std::bind(&ThermostatClimate::idle_on_timer_callback_, this)},
std::array<ThermostatClimateTimer, TIMER_COUNT> timer_{
ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::cooling_max_run_time_timer_callback_, this)),
ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::cooling_off_timer_callback_, this)),
ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::cooling_on_timer_callback_, this)),
ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::fan_mode_timer_callback_, this)),
ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::fanning_off_timer_callback_, this)),
ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::fanning_on_timer_callback_, this)),
ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::heating_max_run_time_timer_callback_, this)),
ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::heating_off_timer_callback_, this)),
ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::heating_on_timer_callback_, this)),
ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::idle_on_timer_callback_, this)),
};
/// The set of standard preset configurations this thermostat supports (Eg. AWAY, ECO, etc)
+1
View File
@@ -424,6 +424,7 @@ CONF_HEAD = "head"
CONF_HEADING = "heading"
CONF_HEARTBEAT = "heartbeat"
CONF_HEAT_ACTION = "heat_action"
CONF_HEAT_COOL_MODE = "heat_cool_mode"
CONF_HEAT_DEADBAND = "heat_deadband"
CONF_HEAT_MODE = "heat_mode"
CONF_HEAT_OVERRUN = "heat_overrun"