diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py index 55fb534682..7b5ee7c624 100644 --- a/esphome/components/thermostat/climate.py +++ b/esphome/components/thermostat/climate.py @@ -24,18 +24,35 @@ from esphome.const import ( CONF_FAN_MODE_FOCUS_ACTION, CONF_FAN_MODE_DIFFUSE_ACTION, CONF_FAN_ONLY_ACTION, + CONF_FAN_ONLY_ACTION_USES_FAN_MODE_TIMER, CONF_FAN_ONLY_COOLING, CONF_FAN_ONLY_MODE, + CONF_FAN_WITH_COOLING, + CONF_FAN_WITH_HEATING, CONF_HEAT_ACTION, CONF_HEAT_DEADBAND, CONF_HEAT_MODE, CONF_HEAT_OVERRUN, - CONF_HYSTERESIS, CONF_ID, CONF_IDLE_ACTION, + CONF_MAX_COOLING_RUN_TIME, + CONF_MAX_HEATING_RUN_TIME, + CONF_MIN_COOLING_OFF_TIME, + CONF_MIN_COOLING_RUN_TIME, + CONF_MIN_FAN_MODE_SWITCHING_TIME, + CONF_MIN_FANNING_OFF_TIME, + CONF_MIN_FANNING_RUN_TIME, + CONF_MIN_HEATING_OFF_TIME, + CONF_MIN_HEATING_RUN_TIME, + CONF_MIN_IDLE_TIME, CONF_OFF_MODE, CONF_SENSOR, CONF_SET_POINT_MINIMUM_DIFFERENTIAL, + CONF_STARTUP_DELAY, + CONF_SUPPLEMENTAL_COOLING_ACTION, + CONF_SUPPLEMENTAL_COOLING_DELTA, + CONF_SUPPLEMENTAL_HEATING_ACTION, + CONF_SUPPLEMENTAL_HEATING_DELTA, CONF_SWING_BOTH_ACTION, CONF_SWING_HORIZONTAL_ACTION, CONF_SWING_OFF_ACTION, @@ -67,18 +84,141 @@ validate_climate_mode = cv.enum(CLIMATE_MODES, upper=True) def validate_thermostat(config): - # verify corresponding climate action action exists for any defined climate mode action + # verify corresponding action(s) exist(s) for any defined climate mode or action requirements = { - CONF_AUTO_MODE: [CONF_COOL_ACTION, CONF_HEAT_ACTION], - CONF_COOL_MODE: [CONF_COOL_ACTION], - CONF_DRY_MODE: [CONF_DRY_ACTION], - CONF_FAN_ONLY_MODE: [CONF_FAN_ONLY_ACTION], - CONF_HEAT_MODE: [CONF_HEAT_ACTION], + CONF_AUTO_MODE: [ + CONF_COOL_ACTION, + CONF_HEAT_ACTION, + CONF_MIN_COOLING_OFF_TIME, + CONF_MIN_COOLING_RUN_TIME, + CONF_MIN_HEATING_OFF_TIME, + CONF_MIN_HEATING_RUN_TIME, + ], + CONF_COOL_MODE: [ + CONF_COOL_ACTION, + CONF_MIN_COOLING_OFF_TIME, + CONF_MIN_COOLING_RUN_TIME, + ], + CONF_DRY_MODE: [ + CONF_DRY_ACTION, + CONF_MIN_COOLING_OFF_TIME, + CONF_MIN_COOLING_RUN_TIME, + ], + CONF_FAN_ONLY_MODE: [ + CONF_FAN_ONLY_ACTION, + ], + CONF_HEAT_MODE: [ + CONF_HEAT_ACTION, + CONF_MIN_HEATING_OFF_TIME, + CONF_MIN_HEATING_RUN_TIME, + ], + CONF_COOL_ACTION: [ + CONF_MIN_COOLING_OFF_TIME, + CONF_MIN_COOLING_RUN_TIME, + ], + CONF_DRY_ACTION: [ + CONF_MIN_COOLING_OFF_TIME, + CONF_MIN_COOLING_RUN_TIME, + ], + CONF_HEAT_ACTION: [ + CONF_MIN_HEATING_OFF_TIME, + CONF_MIN_HEATING_RUN_TIME, + ], + CONF_SUPPLEMENTAL_COOLING_ACTION: [ + CONF_COOL_ACTION, + CONF_MAX_COOLING_RUN_TIME, + CONF_MIN_COOLING_OFF_TIME, + CONF_MIN_COOLING_RUN_TIME, + CONF_SUPPLEMENTAL_COOLING_DELTA, + ], + CONF_SUPPLEMENTAL_HEATING_ACTION: [ + CONF_HEAT_ACTION, + CONF_MAX_HEATING_RUN_TIME, + CONF_MIN_HEATING_OFF_TIME, + CONF_MIN_HEATING_RUN_TIME, + CONF_SUPPLEMENTAL_HEATING_DELTA, + ], + CONF_MAX_COOLING_RUN_TIME: [ + CONF_COOL_ACTION, + CONF_SUPPLEMENTAL_COOLING_ACTION, + CONF_SUPPLEMENTAL_COOLING_DELTA, + ], + CONF_MAX_HEATING_RUN_TIME: [ + CONF_HEAT_ACTION, + CONF_SUPPLEMENTAL_HEATING_ACTION, + CONF_SUPPLEMENTAL_HEATING_DELTA, + ], + CONF_MIN_COOLING_OFF_TIME: [ + CONF_COOL_ACTION, + ], + CONF_MIN_COOLING_RUN_TIME: [ + CONF_COOL_ACTION, + ], + CONF_MIN_FANNING_OFF_TIME: [ + CONF_FAN_ONLY_ACTION, + ], + CONF_MIN_FANNING_RUN_TIME: [ + CONF_FAN_ONLY_ACTION, + ], + CONF_MIN_HEATING_OFF_TIME: [ + CONF_HEAT_ACTION, + ], + CONF_MIN_HEATING_RUN_TIME: [ + CONF_HEAT_ACTION, + ], + CONF_SUPPLEMENTAL_COOLING_DELTA: [ + CONF_COOL_ACTION, + CONF_MAX_COOLING_RUN_TIME, + CONF_SUPPLEMENTAL_COOLING_ACTION, + ], + CONF_SUPPLEMENTAL_HEATING_DELTA: [ + CONF_HEAT_ACTION, + CONF_MAX_HEATING_RUN_TIME, + CONF_SUPPLEMENTAL_HEATING_ACTION, + ], } - for config_mode, req_actions in requirements.items(): - for req_action in req_actions: - if config_mode in config and req_action not in config: - raise cv.Invalid(f"{req_action} must be defined to use {config_mode}") + for config_trigger, req_triggers in requirements.items(): + for req_trigger in req_triggers: + if config_trigger in config and req_trigger not in config: + raise cv.Invalid( + f"{req_trigger} must be defined to use {config_trigger}" + ) + + if CONF_FAN_ONLY_ACTION in config: + # determine validation requirements based on fan_only_action_uses_fan_mode_timer setting + if config[CONF_FAN_ONLY_ACTION_USES_FAN_MODE_TIMER] is True: + requirements = [CONF_MIN_FAN_MODE_SWITCHING_TIME] + else: + requirements = [ + CONF_MIN_FANNING_OFF_TIME, + CONF_MIN_FANNING_RUN_TIME, + ] + for config_req_action in requirements: + if config_req_action not in config: + raise cv.Invalid( + f"{config_req_action} must be defined to use {CONF_FAN_ONLY_ACTION}" + ) + + # for any fan_mode action, confirm min_fan_mode_switching_time is defined + requirements = { + CONF_MIN_FAN_MODE_SWITCHING_TIME: [ + CONF_FAN_MODE_ON_ACTION, + CONF_FAN_MODE_OFF_ACTION, + CONF_FAN_MODE_AUTO_ACTION, + CONF_FAN_MODE_LOW_ACTION, + CONF_FAN_MODE_MEDIUM_ACTION, + CONF_FAN_MODE_HIGH_ACTION, + CONF_FAN_MODE_MIDDLE_ACTION, + CONF_FAN_MODE_FOCUS_ACTION, + CONF_FAN_MODE_DIFFUSE_ACTION, + ], + } + for req_config_item, config_triggers in requirements.items(): + for config_trigger in config_triggers: + if config_trigger in config and req_config_item not in config: + raise cv.Invalid( + f"{req_config_item} must be defined to use {config_trigger}" + ) # determine validation requirements based on fan_only_cooling setting if config[CONF_FAN_ONLY_COOLING] is True: @@ -137,6 +277,34 @@ def validate_thermostat(config): f"{CONF_DEFAULT_MODE} is set to {default_mode} but {req} is not present in the configuration" ) + if config[CONF_FAN_WITH_COOLING] is True and CONF_FAN_ONLY_ACTION not in config: + raise cv.Invalid( + f"{CONF_FAN_ONLY_ACTION} must be defined to use {CONF_FAN_WITH_COOLING}" + ) + if config[CONF_FAN_WITH_HEATING] is True and CONF_FAN_ONLY_ACTION not in config: + raise cv.Invalid( + f"{CONF_FAN_ONLY_ACTION} must be defined to use {CONF_FAN_WITH_HEATING}" + ) + + # if min_fan_mode_switching_time is defined, at least one fan_mode action should be defined + if CONF_MIN_FAN_MODE_SWITCHING_TIME in config: + requirements = [ + CONF_FAN_MODE_ON_ACTION, + CONF_FAN_MODE_OFF_ACTION, + CONF_FAN_MODE_AUTO_ACTION, + CONF_FAN_MODE_LOW_ACTION, + CONF_FAN_MODE_MEDIUM_ACTION, + CONF_FAN_MODE_HIGH_ACTION, + CONF_FAN_MODE_MIDDLE_ACTION, + CONF_FAN_MODE_FOCUS_ACTION, + CONF_FAN_MODE_DIFFUSE_ACTION, + ] + for config_req_action in requirements: + if config_req_action in config: + return config + raise cv.Invalid( + f"At least one of {CONF_FAN_MODE_ON_ACTION}, {CONF_FAN_MODE_OFF_ACTION}, {CONF_FAN_MODE_AUTO_ACTION}, {CONF_FAN_MODE_LOW_ACTION}, {CONF_FAN_MODE_MEDIUM_ACTION}, {CONF_FAN_MODE_HIGH_ACTION}, {CONF_FAN_MODE_MIDDLE_ACTION}, {CONF_FAN_MODE_FOCUS_ACTION}, {CONF_FAN_MODE_DIFFUSE_ACTION} must be defined to use {CONF_MIN_FAN_MODE_SWITCHING_TIME}" + ) return config @@ -147,11 +315,17 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), cv.Required(CONF_IDLE_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_COOL_ACTION): automation.validate_automation(single=True), + cv.Optional( + CONF_SUPPLEMENTAL_COOLING_ACTION + ): automation.validate_automation(single=True), cv.Optional(CONF_DRY_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_FAN_ONLY_ACTION): automation.validate_automation( single=True ), cv.Optional(CONF_HEAT_ACTION): automation.validate_automation(single=True), + cv.Optional( + CONF_SUPPLEMENTAL_HEATING_ACTION + ): automation.validate_automation(single=True), cv.Optional(CONF_AUTO_MODE): automation.validate_automation(single=True), cv.Optional(CONF_COOL_MODE): automation.validate_automation(single=True), cv.Optional(CONF_DRY_MODE): automation.validate_automation(single=True), @@ -210,12 +384,31 @@ CONFIG_SCHEMA = cv.All( cv.Optional( CONF_SET_POINT_MINIMUM_DIFFERENTIAL, default=0.5 ): cv.temperature, - cv.Optional(CONF_COOL_DEADBAND): cv.temperature, - cv.Optional(CONF_COOL_OVERRUN): cv.temperature, - cv.Optional(CONF_HEAT_DEADBAND): cv.temperature, - cv.Optional(CONF_HEAT_OVERRUN): cv.temperature, - cv.Optional(CONF_HYSTERESIS, default=0.5): cv.temperature, + cv.Optional(CONF_COOL_DEADBAND, default=0.5): cv.temperature, + cv.Optional(CONF_COOL_OVERRUN, default=0.5): cv.temperature, + cv.Optional(CONF_HEAT_DEADBAND, default=0.5): cv.temperature, + cv.Optional(CONF_HEAT_OVERRUN, default=0.5): cv.temperature, + cv.Optional(CONF_MAX_COOLING_RUN_TIME): cv.positive_time_period_seconds, + cv.Optional(CONF_MAX_HEATING_RUN_TIME): cv.positive_time_period_seconds, + cv.Optional(CONF_MIN_COOLING_OFF_TIME): cv.positive_time_period_seconds, + cv.Optional(CONF_MIN_COOLING_RUN_TIME): cv.positive_time_period_seconds, + cv.Optional( + CONF_MIN_FAN_MODE_SWITCHING_TIME + ): cv.positive_time_period_seconds, + cv.Optional(CONF_MIN_FANNING_OFF_TIME): cv.positive_time_period_seconds, + cv.Optional(CONF_MIN_FANNING_RUN_TIME): cv.positive_time_period_seconds, + cv.Optional(CONF_MIN_HEATING_OFF_TIME): cv.positive_time_period_seconds, + cv.Optional(CONF_MIN_HEATING_RUN_TIME): cv.positive_time_period_seconds, + cv.Required(CONF_MIN_IDLE_TIME): cv.positive_time_period_seconds, + cv.Optional(CONF_SUPPLEMENTAL_COOLING_DELTA): cv.temperature, + cv.Optional(CONF_SUPPLEMENTAL_HEATING_DELTA): cv.temperature, + cv.Optional( + CONF_FAN_ONLY_ACTION_USES_FAN_MODE_TIMER, default=False + ): cv.boolean, cv.Optional(CONF_FAN_ONLY_COOLING, default=False): cv.boolean, + cv.Optional(CONF_FAN_WITH_COOLING, default=False): cv.boolean, + cv.Optional(CONF_FAN_WITH_HEATING, default=False): cv.boolean, + cv.Optional(CONF_STARTUP_DELAY, default=False): cv.boolean, cv.Optional(CONF_AWAY_CONFIG): cv.Schema( { cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, @@ -250,25 +443,10 @@ async def to_code(config): ) cg.add(var.set_sensor(sens)) - if CONF_COOL_DEADBAND in config: - cg.add(var.set_cool_deadband(config[CONF_COOL_DEADBAND])) - else: - cg.add(var.set_cool_deadband(config[CONF_HYSTERESIS])) - - if CONF_COOL_OVERRUN in config: - cg.add(var.set_cool_overrun(config[CONF_COOL_OVERRUN])) - else: - cg.add(var.set_cool_overrun(config[CONF_HYSTERESIS])) - - if CONF_HEAT_DEADBAND in config: - cg.add(var.set_heat_deadband(config[CONF_HEAT_DEADBAND])) - else: - cg.add(var.set_heat_deadband(config[CONF_HYSTERESIS])) - - if CONF_HEAT_OVERRUN in config: - cg.add(var.set_heat_overrun(config[CONF_HEAT_OVERRUN])) - else: - cg.add(var.set_heat_overrun(config[CONF_HYSTERESIS])) + cg.add(var.set_cool_deadband(config[CONF_COOL_DEADBAND])) + cg.add(var.set_cool_overrun(config[CONF_COOL_OVERRUN])) + cg.add(var.set_heat_deadband(config[CONF_HEAT_DEADBAND])) + cg.add(var.set_heat_overrun(config[CONF_HEAT_OVERRUN])) if two_points_available is True: cg.add(var.set_supports_two_points(True)) @@ -286,7 +464,72 @@ async def to_code(config): normal_config = ThermostatClimateTargetTempConfig( config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW] ) + + if CONF_MAX_COOLING_RUN_TIME in config: + cg.add( + var.set_cooling_maximum_run_time_in_sec(config[CONF_MAX_COOLING_RUN_TIME]) + ) + + if CONF_MAX_HEATING_RUN_TIME in config: + cg.add( + var.set_heating_maximum_run_time_in_sec(config[CONF_MAX_HEATING_RUN_TIME]) + ) + + if CONF_MIN_COOLING_OFF_TIME in config: + cg.add( + var.set_cooling_minimum_off_time_in_sec(config[CONF_MIN_COOLING_OFF_TIME]) + ) + + if CONF_MIN_COOLING_RUN_TIME in config: + cg.add( + var.set_cooling_minimum_run_time_in_sec(config[CONF_MIN_COOLING_RUN_TIME]) + ) + + if CONF_MIN_FAN_MODE_SWITCHING_TIME in config: + cg.add( + var.set_fan_mode_minimum_switching_time_in_sec( + config[CONF_MIN_FAN_MODE_SWITCHING_TIME] + ) + ) + + if CONF_MIN_FANNING_OFF_TIME in config: + cg.add( + var.set_fanning_minimum_off_time_in_sec(config[CONF_MIN_FANNING_OFF_TIME]) + ) + + if CONF_MIN_FANNING_RUN_TIME in config: + cg.add( + var.set_fanning_minimum_run_time_in_sec(config[CONF_MIN_FANNING_RUN_TIME]) + ) + + if CONF_MIN_HEATING_OFF_TIME in config: + cg.add( + var.set_heating_minimum_off_time_in_sec(config[CONF_MIN_HEATING_OFF_TIME]) + ) + + if CONF_MIN_HEATING_RUN_TIME in config: + cg.add( + var.set_heating_minimum_run_time_in_sec(config[CONF_MIN_HEATING_RUN_TIME]) + ) + + if CONF_SUPPLEMENTAL_COOLING_DELTA in config: + cg.add(var.set_supplemental_cool_delta(config[CONF_SUPPLEMENTAL_COOLING_DELTA])) + + if CONF_SUPPLEMENTAL_HEATING_DELTA in config: + cg.add(var.set_supplemental_heat_delta(config[CONF_SUPPLEMENTAL_HEATING_DELTA])) + + cg.add(var.set_idle_minimum_time_in_sec(config[CONF_MIN_IDLE_TIME])) + + cg.add( + var.set_supports_fan_only_action_uses_fan_mode_timer( + config[CONF_FAN_ONLY_ACTION_USES_FAN_MODE_TIMER] + ) + ) cg.add(var.set_supports_fan_only_cooling(config[CONF_FAN_ONLY_COOLING])) + cg.add(var.set_supports_fan_with_cooling(config[CONF_FAN_WITH_COOLING])) + cg.add(var.set_supports_fan_with_heating(config[CONF_FAN_WITH_HEATING])) + + cg.add(var.set_use_startup_delay(config[CONF_STARTUP_DELAY])) cg.add(var.set_normal_config(normal_config)) await automation.build_automation( @@ -303,6 +546,12 @@ async def to_code(config): var.get_cool_action_trigger(), [], config[CONF_COOL_ACTION] ) cg.add(var.set_supports_cool(True)) + if CONF_SUPPLEMENTAL_COOLING_ACTION in config: + await automation.build_automation( + var.get_supplemental_cool_action_trigger(), + [], + config[CONF_SUPPLEMENTAL_COOLING_ACTION], + ) if CONF_DRY_ACTION in config: await automation.build_automation( var.get_dry_action_trigger(), [], config[CONF_DRY_ACTION] @@ -318,6 +567,12 @@ async def to_code(config): var.get_heat_action_trigger(), [], config[CONF_HEAT_ACTION] ) cg.add(var.set_supports_heat(True)) + if CONF_SUPPLEMENTAL_HEATING_ACTION in config: + await automation.build_automation( + var.get_supplemental_heat_action_trigger(), + [], + config[CONF_SUPPLEMENTAL_HEATING_ACTION], + ) if CONF_AUTO_MODE in config: await automation.build_automation( var.get_auto_mode_trigger(), [], config[CONF_AUTO_MODE] diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 1176a75514..d9d8b106ea 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -7,11 +7,20 @@ namespace thermostat { static const char *const TAG = "thermostat.climate"; void ThermostatClimate::setup() { + if (this->use_startup_delay_) { + // start timers so that no actions are called for a moment + this->start_timer_(thermostat::TIMER_COOLING_OFF); + this->start_timer_(thermostat::TIMER_FANNING_OFF); + this->start_timer_(thermostat::TIMER_HEATING_OFF); + if (this->supports_fan_only_action_uses_fan_mode_timer_) + this->start_timer_(thermostat::TIMER_FAN_MODE); + } // add a callback so that whenever the sensor state changes we can take action this->sensor_->add_on_state_callback([this](float state) { this->current_temperature = state; // required action may have changed, recompute, refresh - this->switch_to_action_(compute_action_()); + this->switch_to_action_(this->compute_action_()); + this->switch_to_supplemental_action_(this->compute_supplemental_action_()); // current temperature and possibly action changed, so publish the new state this->publish_state(); }); @@ -26,31 +35,58 @@ void ThermostatClimate::setup() { this->change_away_(false); } // refresh the climate action based on the restored settings - this->switch_to_action_(compute_action_()); + this->switch_to_action_(this->compute_action_()); + this->switch_to_supplemental_action_(this->compute_supplemental_action_()); this->setup_complete_ = true; this->publish_state(); } -float ThermostatClimate::cool_deadband() { return this->cool_deadband_; } -float ThermostatClimate::cool_overrun() { return this->cool_overrun_; } -float ThermostatClimate::heat_deadband() { return this->heat_deadband_; } -float ThermostatClimate::heat_overrun() { return this->heat_overrun_; } +float ThermostatClimate::cool_deadband() { return this->cooling_deadband_; } +float ThermostatClimate::cool_overrun() { return this->cooling_overrun_; } +float ThermostatClimate::heat_deadband() { return this->heating_deadband_; } +float ThermostatClimate::heat_overrun() { return this->heating_overrun_; } void ThermostatClimate::refresh() { this->switch_to_mode_(this->mode); - this->switch_to_action_(compute_action_()); + this->switch_to_action_(this->compute_action_()); + this->switch_to_supplemental_action_(this->compute_supplemental_action_()); this->switch_to_fan_mode_(this->fan_mode.value()); this->switch_to_swing_mode_(this->swing_mode); this->check_temperature_change_trigger_(); this->publish_state(); } +bool ThermostatClimate::climate_action_change_delayed() { + switch (this->compute_action_(true)) { + case climate::CLIMATE_ACTION_OFF: + case climate::CLIMATE_ACTION_IDLE: + return !this->idle_action_ready_(); + case climate::CLIMATE_ACTION_COOLING: + return !this->cooling_action_ready_(); + case climate::CLIMATE_ACTION_HEATING: + return !this->heating_action_ready_(); + case climate::CLIMATE_ACTION_FAN: + return !this->fanning_action_ready_(); + case climate::CLIMATE_ACTION_DRYING: + return !this->drying_action_ready_(); + default: + break; + } + return false; +} + +bool ThermostatClimate::fan_mode_change_delayed() { return !this->fan_mode_ready_(); } + +climate::ClimateAction ThermostatClimate::delayed_climate_action() { return this->compute_action_(true); } + +climate::ClimateFanMode ThermostatClimate::delayed_fan_mode() { return this->desired_fan_mode_; } + bool ThermostatClimate::hysteresis_valid() { if ((this->supports_cool_ || (this->supports_fan_only_ && this->supports_fan_only_cooling_)) && - (isnan(this->cool_deadband_) || isnan(this->cool_overrun_))) + (isnan(this->cooling_deadband_) || isnan(this->cooling_overrun_))) return false; - if (this->supports_heat_ && (isnan(this->heat_deadband_) || isnan(this->heat_overrun_))) + if (this->supports_heat_ && (isnan(this->heating_deadband_) || isnan(this->heating_overrun_))) return false; return true; @@ -72,10 +108,10 @@ void ThermostatClimate::validate_target_temperature() { void ThermostatClimate::validate_target_temperatures() { if (this->supports_two_points_) { - validate_target_temperature_low(); - validate_target_temperature_high(); + this->validate_target_temperature_low(); + this->validate_target_temperature_high(); } else { - validate_target_temperature(); + this->validate_target_temperature(); } } @@ -201,12 +237,19 @@ climate::ClimateTraits ThermostatClimate::traits() { return traits; } -climate::ClimateAction ThermostatClimate::compute_action_() { +climate::ClimateAction ThermostatClimate::compute_action_(const bool ignore_timers) { auto target_action = climate::CLIMATE_ACTION_IDLE; // if any hysteresis values or current_temperature is not valid, we go to OFF; if (isnan(this->current_temperature) || !this->hysteresis_valid()) { return climate::CLIMATE_ACTION_OFF; } + // do not change the action if an "ON" timer is running + if ((!ignore_timers) && + (timer_active_(thermostat::TIMER_IDLE_ON) || timer_active_(thermostat::TIMER_COOLING_ON) || + timer_active_(thermostat::TIMER_FANNING_ON) || timer_active_(thermostat::TIMER_HEATING_ON))) { + return this->action; + } + // ensure set point(s) is/are valid before computing the action this->validate_target_temperatures(); // everything has been validated so we can now safely compute the action @@ -245,6 +288,56 @@ climate::ClimateAction ThermostatClimate::compute_action_() { default: break; } + // do not abruptly switch actions. cycle through IDLE, first. we'll catch this at the next update. + if ((((this->action == climate::CLIMATE_ACTION_COOLING) || (this->action == climate::CLIMATE_ACTION_DRYING)) && + (target_action == climate::CLIMATE_ACTION_HEATING)) || + ((this->action == climate::CLIMATE_ACTION_HEATING) && + ((target_action == climate::CLIMATE_ACTION_COOLING) || (target_action == climate::CLIMATE_ACTION_DRYING)))) { + return climate::CLIMATE_ACTION_IDLE; + } + + return target_action; +} + +climate::ClimateAction ThermostatClimate::compute_supplemental_action_() { + auto target_action = climate::CLIMATE_ACTION_IDLE; + // if any hysteresis values or current_temperature is not valid, we go to OFF; + if (isnan(this->current_temperature) || !this->hysteresis_valid()) { + return climate::CLIMATE_ACTION_OFF; + } + + // ensure set point(s) is/are valid before computing the action + this->validate_target_temperatures(); + // everything has been validated so we can now safely compute the action + switch (this->mode) { + // if the climate mode is OFF then the climate action must be OFF + case climate::CLIMATE_MODE_OFF: + target_action = climate::CLIMATE_ACTION_OFF; + break; + case climate::CLIMATE_MODE_HEAT_COOL: + if (this->supplemental_cooling_required_() && this->supplemental_heating_required_()) { + // this is bad and should never happen, so just stop. + // target_action = climate::CLIMATE_ACTION_IDLE; + } else if (this->supplemental_cooling_required_()) { + target_action = climate::CLIMATE_ACTION_COOLING; + } else if (this->supplemental_heating_required_()) { + target_action = climate::CLIMATE_ACTION_HEATING; + } + break; + case climate::CLIMATE_MODE_COOL: + if (this->supplemental_cooling_required_()) { + target_action = climate::CLIMATE_ACTION_COOLING; + } + break; + case climate::CLIMATE_MODE_HEAT: + if (this->supplemental_heating_required_()) { + target_action = climate::CLIMATE_ACTION_HEATING; + } + break; + default: + break; + } + return target_action; } @@ -257,34 +350,81 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action) { if (((action == climate::CLIMATE_ACTION_OFF && this->action == climate::CLIMATE_ACTION_IDLE) || (action == climate::CLIMATE_ACTION_IDLE && this->action == climate::CLIMATE_ACTION_OFF)) && this->setup_complete_) { - // switching from OFF to IDLE or vice-versa - // these only have visual difference. OFF means user manually disabled, - // IDLE means it's in auto mode but value is in target range. + // switching from OFF to IDLE or vice-versa -- this is only a visual difference. + // OFF means user manually disabled, IDLE means the temperature is in target range. this->action = action; return; } - if (this->prev_action_trigger_ != nullptr) { - this->prev_action_trigger_->stop_action(); - this->prev_action_trigger_ = nullptr; - } - Trigger<> *trig = this->idle_action_trigger_; + bool action_ready = false; + Trigger<> *trig = this->idle_action_trigger_, *trig_fan = nullptr; switch (action) { case climate::CLIMATE_ACTION_OFF: case climate::CLIMATE_ACTION_IDLE: - // trig = this->idle_action_trigger_; + if (this->idle_action_ready_()) { + this->start_timer_(thermostat::TIMER_IDLE_ON); + if (this->action == climate::CLIMATE_ACTION_COOLING) + this->start_timer_(thermostat::TIMER_COOLING_OFF); + if (this->action == climate::CLIMATE_ACTION_FAN) { + if (this->supports_fan_only_action_uses_fan_mode_timer_) + this->start_timer_(thermostat::TIMER_FAN_MODE); + else + this->start_timer_(thermostat::TIMER_FANNING_OFF); + } + if (this->action == climate::CLIMATE_ACTION_HEATING) + this->start_timer_(thermostat::TIMER_HEATING_OFF); + // trig = this->idle_action_trigger_; + ESP_LOGVV(TAG, "Switching to IDLE/OFF action"); + this->cooling_max_runtime_exceeded_ = false; + this->heating_max_runtime_exceeded_ = false; + action_ready = true; + } break; case climate::CLIMATE_ACTION_COOLING: - trig = this->cool_action_trigger_; + if (this->cooling_action_ready_()) { + this->start_timer_(thermostat::TIMER_COOLING_ON); + this->start_timer_(thermostat::TIMER_COOLING_MAX_RUN_TIME); + if (this->supports_fan_with_cooling_) { + this->start_timer_(thermostat::TIMER_FANNING_ON); + trig_fan = this->fan_only_action_trigger_; + } + trig = this->cool_action_trigger_; + ESP_LOGVV(TAG, "Switching to COOLING action"); + action_ready = true; + } break; case climate::CLIMATE_ACTION_HEATING: - trig = this->heat_action_trigger_; + if (this->heating_action_ready_()) { + this->start_timer_(thermostat::TIMER_HEATING_ON); + this->start_timer_(thermostat::TIMER_HEATING_MAX_RUN_TIME); + if (this->supports_fan_with_heating_) { + this->start_timer_(thermostat::TIMER_FANNING_ON); + trig_fan = this->fan_only_action_trigger_; + } + trig = this->heat_action_trigger_; + ESP_LOGVV(TAG, "Switching to HEATING action"); + action_ready = true; + } break; case climate::CLIMATE_ACTION_FAN: - trig = this->fan_only_action_trigger_; + if (this->fanning_action_ready_()) { + if (this->supports_fan_only_action_uses_fan_mode_timer_) + this->start_timer_(thermostat::TIMER_FAN_MODE); + else + this->start_timer_(thermostat::TIMER_FANNING_ON); + trig = this->fan_only_action_trigger_; + ESP_LOGVV(TAG, "Switching to FAN_ONLY action"); + action_ready = true; + } break; case climate::CLIMATE_ACTION_DRYING: - trig = this->dry_action_trigger_; + if (this->drying_action_ready_()) { + this->start_timer_(thermostat::TIMER_COOLING_ON); + this->start_timer_(thermostat::TIMER_FANNING_ON); + trig = this->dry_action_trigger_; + ESP_LOGVV(TAG, "Switching to DRYING action"); + action_ready = true; + } break; default: // we cannot report an invalid mode back to HA (even if it asked for one) @@ -292,10 +432,76 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action) { action = climate::CLIMATE_ACTION_OFF; // trig = this->idle_action_trigger_; } - assert(trig != nullptr); - trig->trigger(); - this->action = action; - this->prev_action_trigger_ = trig; + + if (action_ready) { + if (this->prev_action_trigger_ != nullptr) { + this->prev_action_trigger_->stop_action(); + this->prev_action_trigger_ = nullptr; + } + this->action = action; + this->prev_action_trigger_ = trig; + assert(trig != nullptr); + trig->trigger(); + // if enabled, call the fan_only action with cooling/heating actions + if (trig_fan != nullptr) { + ESP_LOGVV(TAG, "Calling FAN_ONLY action with HEATING/COOLING action"); + trig_fan->trigger(); + } + } +} + +void ThermostatClimate::switch_to_supplemental_action_(climate::ClimateAction action) { + // setup_complete_ helps us ensure an action is called immediately after boot + if ((action == this->supplemental_action_) && this->setup_complete_) + // already in target mode + return; + + switch (action) { + case climate::CLIMATE_ACTION_OFF: + case climate::CLIMATE_ACTION_IDLE: + this->cancel_timer_(thermostat::TIMER_COOLING_MAX_RUN_TIME); + this->cancel_timer_(thermostat::TIMER_HEATING_MAX_RUN_TIME); + break; + case climate::CLIMATE_ACTION_COOLING: + this->cancel_timer_(thermostat::TIMER_COOLING_MAX_RUN_TIME); + break; + case climate::CLIMATE_ACTION_HEATING: + this->cancel_timer_(thermostat::TIMER_HEATING_MAX_RUN_TIME); + break; + default: + return; + } + ESP_LOGVV(TAG, "Updating supplemental action..."); + this->supplemental_action_ = action; + this->trigger_supplemental_action_(); +} + +void ThermostatClimate::trigger_supplemental_action_() { + Trigger<> *trig = nullptr; + + switch (this->supplemental_action_) { + case climate::CLIMATE_ACTION_COOLING: + if (!this->timer_active_(thermostat::TIMER_COOLING_MAX_RUN_TIME)) { + this->start_timer_(thermostat::TIMER_COOLING_MAX_RUN_TIME); + } + trig = this->supplemental_cool_action_trigger_; + ESP_LOGVV(TAG, "Calling supplemental COOLING action"); + break; + case climate::CLIMATE_ACTION_HEATING: + if (!this->timer_active_(thermostat::TIMER_HEATING_MAX_RUN_TIME)) { + this->start_timer_(thermostat::TIMER_HEATING_MAX_RUN_TIME); + } + trig = this->supplemental_heat_action_trigger_; + ESP_LOGVV(TAG, "Calling supplemental HEATING action"); + break; + default: + break; + } + + if (trig != nullptr) { + assert(trig != nullptr); + trig->trigger(); + } } void ThermostatClimate::switch_to_fan_mode_(climate::ClimateFanMode fan_mode) { @@ -304,50 +510,64 @@ void ThermostatClimate::switch_to_fan_mode_(climate::ClimateFanMode fan_mode) { // already in target mode return; - if (this->prev_fan_mode_trigger_ != nullptr) { - this->prev_fan_mode_trigger_->stop_action(); - this->prev_fan_mode_trigger_ = nullptr; + this->desired_fan_mode_ = fan_mode; // needed for timer callback + + if (this->fan_mode_ready_()) { + Trigger<> *trig = this->fan_mode_auto_trigger_; + switch (fan_mode) { + case climate::CLIMATE_FAN_ON: + trig = this->fan_mode_on_trigger_; + ESP_LOGVV(TAG, "Switching to FAN_ON mode"); + break; + case climate::CLIMATE_FAN_OFF: + trig = this->fan_mode_off_trigger_; + ESP_LOGVV(TAG, "Switching to FAN_OFF mode"); + break; + case climate::CLIMATE_FAN_AUTO: + // trig = this->fan_mode_auto_trigger_; + ESP_LOGVV(TAG, "Switching to FAN_AUTO mode"); + break; + case climate::CLIMATE_FAN_LOW: + trig = this->fan_mode_low_trigger_; + ESP_LOGVV(TAG, "Switching to FAN_LOW mode"); + break; + case climate::CLIMATE_FAN_MEDIUM: + trig = this->fan_mode_medium_trigger_; + ESP_LOGVV(TAG, "Switching to FAN_MEDIUM mode"); + break; + case climate::CLIMATE_FAN_HIGH: + trig = this->fan_mode_high_trigger_; + ESP_LOGVV(TAG, "Switching to FAN_HIGH mode"); + break; + case climate::CLIMATE_FAN_MIDDLE: + trig = this->fan_mode_middle_trigger_; + ESP_LOGVV(TAG, "Switching to FAN_MIDDLE mode"); + break; + case climate::CLIMATE_FAN_FOCUS: + trig = this->fan_mode_focus_trigger_; + ESP_LOGVV(TAG, "Switching to FAN_FOCUS mode"); + break; + case climate::CLIMATE_FAN_DIFFUSE: + trig = this->fan_mode_diffuse_trigger_; + ESP_LOGVV(TAG, "Switching to FAN_DIFFUSE mode"); + break; + default: + // we cannot report an invalid mode back to HA (even if it asked for one) + // and must assume some valid value + fan_mode = climate::CLIMATE_FAN_AUTO; + // trig = this->fan_mode_auto_trigger_; + } + if (this->prev_fan_mode_trigger_ != nullptr) { + this->prev_fan_mode_trigger_->stop_action(); + this->prev_fan_mode_trigger_ = nullptr; + } + this->start_timer_(thermostat::TIMER_FAN_MODE); + assert(trig != nullptr); + trig->trigger(); + this->fan_mode = fan_mode; + this->prev_fan_mode_ = fan_mode; + this->prev_fan_mode_trigger_ = trig; } - Trigger<> *trig = this->fan_mode_auto_trigger_; - switch (fan_mode) { - case climate::CLIMATE_FAN_ON: - trig = this->fan_mode_on_trigger_; - break; - case climate::CLIMATE_FAN_OFF: - trig = this->fan_mode_off_trigger_; - break; - case climate::CLIMATE_FAN_AUTO: - // trig = this->fan_mode_auto_trigger_; - break; - case climate::CLIMATE_FAN_LOW: - trig = this->fan_mode_low_trigger_; - break; - case climate::CLIMATE_FAN_MEDIUM: - trig = this->fan_mode_medium_trigger_; - break; - case climate::CLIMATE_FAN_HIGH: - trig = this->fan_mode_high_trigger_; - break; - case climate::CLIMATE_FAN_MIDDLE: - trig = this->fan_mode_middle_trigger_; - break; - case climate::CLIMATE_FAN_FOCUS: - trig = this->fan_mode_focus_trigger_; - break; - case climate::CLIMATE_FAN_DIFFUSE: - trig = this->fan_mode_diffuse_trigger_; - break; - default: - // we cannot report an invalid mode back to HA (even if it asked for one) - // and must assume some valid value - fan_mode = climate::CLIMATE_FAN_AUTO; - // trig = this->fan_mode_auto_trigger_; - } - assert(trig != nullptr); - trig->trigger(); - this->fan_mode = fan_mode; - this->prev_fan_mode_ = fan_mode; - this->prev_fan_mode_trigger_ = trig; } void ThermostatClimate::switch_to_mode_(climate::ClimateMode mode) { @@ -430,6 +650,135 @@ void ThermostatClimate::switch_to_swing_mode_(climate::ClimateSwingMode swing_mo this->prev_swing_mode_trigger_ = trig; } +bool ThermostatClimate::idle_action_ready_() { + if (this->supports_fan_only_action_uses_fan_mode_timer_) { + return !(this->timer_active_(thermostat::TIMER_COOLING_ON) || this->timer_active_(thermostat::TIMER_FAN_MODE) || + this->timer_active_(thermostat::TIMER_HEATING_ON)); + } + return !(this->timer_active_(thermostat::TIMER_COOLING_ON) || this->timer_active_(thermostat::TIMER_FANNING_ON) || + this->timer_active_(thermostat::TIMER_HEATING_ON)); +} + +bool ThermostatClimate::cooling_action_ready_() { + return !(this->timer_active_(thermostat::TIMER_IDLE_ON) || this->timer_active_(thermostat::TIMER_FANNING_OFF) || + this->timer_active_(thermostat::TIMER_COOLING_OFF) || this->timer_active_(thermostat::TIMER_HEATING_ON)); +} + +bool ThermostatClimate::drying_action_ready_() { + return !(this->timer_active_(thermostat::TIMER_IDLE_ON) || this->timer_active_(thermostat::TIMER_FANNING_OFF) || + this->timer_active_(thermostat::TIMER_COOLING_OFF) || this->timer_active_(thermostat::TIMER_HEATING_ON)); +} + +bool ThermostatClimate::fan_mode_ready_() { return !(this->timer_active_(thermostat::TIMER_FAN_MODE)); } + +bool ThermostatClimate::fanning_action_ready_() { + if (this->supports_fan_only_action_uses_fan_mode_timer_) { + return !(this->timer_active_(thermostat::TIMER_FAN_MODE)); + } + return !(this->timer_active_(thermostat::TIMER_IDLE_ON) || this->timer_active_(thermostat::TIMER_FANNING_OFF)); +} + +bool ThermostatClimate::heating_action_ready_() { + return !(this->timer_active_(thermostat::TIMER_IDLE_ON) || this->timer_active_(thermostat::TIMER_COOLING_ON) || + this->timer_active_(thermostat::TIMER_FANNING_OFF) || this->timer_active_(thermostat::TIMER_HEATING_OFF)); +} + +void ThermostatClimate::start_timer_(const ThermostatClimateTimerIndex timer_index) { + if (this->timer_duration_(timer_index) > 0) { + this->set_timeout(this->timer_[timer_index].name, this->timer_duration_(timer_index), + this->timer_cbf_(timer_index)); + this->timer_[timer_index].active = true; + } +} + +bool ThermostatClimate::cancel_timer_(ThermostatClimateTimerIndex timer_index) { + this->timer_[timer_index].active = false; + return this->cancel_timeout(this->timer_[timer_index].name); +} + +bool ThermostatClimate::timer_active_(ThermostatClimateTimerIndex timer_index) { + return this->timer_[timer_index].active; +} + +uint32_t ThermostatClimate::timer_duration_(ThermostatClimateTimerIndex timer_index) { + return this->timer_[timer_index].time; +} + +std::function ThermostatClimate::timer_cbf_(ThermostatClimateTimerIndex timer_index) { + return this->timer_[timer_index].func; +} + +void ThermostatClimate::cooling_max_run_time_timer_callback_() { + ESP_LOGVV(TAG, "cooling_max_run_time timer expired"); + this->timer_[thermostat::TIMER_COOLING_MAX_RUN_TIME].active = false; + this->cooling_max_runtime_exceeded_ = true; + this->trigger_supplemental_action_(); + this->switch_to_supplemental_action_(this->compute_supplemental_action_()); +} + +void ThermostatClimate::cooling_off_timer_callback_() { + ESP_LOGVV(TAG, "cooling_off timer expired"); + this->timer_[thermostat::TIMER_COOLING_OFF].active = false; + this->switch_to_action_(this->compute_action_()); + this->switch_to_supplemental_action_(this->compute_supplemental_action_()); +} + +void ThermostatClimate::cooling_on_timer_callback_() { + ESP_LOGVV(TAG, "cooling_on timer expired"); + this->timer_[thermostat::TIMER_COOLING_ON].active = false; + this->switch_to_action_(this->compute_action_()); + this->switch_to_supplemental_action_(this->compute_supplemental_action_()); +} + +void ThermostatClimate::fan_mode_timer_callback_() { + ESP_LOGVV(TAG, "fan_mode timer expired"); + this->timer_[thermostat::TIMER_FAN_MODE].active = false; + this->switch_to_fan_mode_(this->desired_fan_mode_); + if (this->supports_fan_only_action_uses_fan_mode_timer_) + this->switch_to_action_(this->compute_action_()); +} + +void ThermostatClimate::fanning_off_timer_callback_() { + ESP_LOGVV(TAG, "fanning_off timer expired"); + this->timer_[thermostat::TIMER_FANNING_OFF].active = false; + this->switch_to_action_(this->compute_action_()); +} + +void ThermostatClimate::fanning_on_timer_callback_() { + ESP_LOGVV(TAG, "fanning_on timer expired"); + this->timer_[thermostat::TIMER_FANNING_ON].active = false; + this->switch_to_action_(this->compute_action_()); +} + +void ThermostatClimate::heating_max_run_time_timer_callback_() { + ESP_LOGVV(TAG, "heating_max_run_time timer expired"); + this->timer_[thermostat::TIMER_HEATING_MAX_RUN_TIME].active = false; + this->heating_max_runtime_exceeded_ = true; + this->trigger_supplemental_action_(); + this->switch_to_supplemental_action_(this->compute_supplemental_action_()); +} + +void ThermostatClimate::heating_off_timer_callback_() { + ESP_LOGVV(TAG, "heating_off timer expired"); + this->timer_[thermostat::TIMER_HEATING_OFF].active = false; + this->switch_to_action_(this->compute_action_()); + this->switch_to_supplemental_action_(this->compute_supplemental_action_()); +} + +void ThermostatClimate::heating_on_timer_callback_() { + ESP_LOGVV(TAG, "heating_on timer expired"); + this->timer_[thermostat::TIMER_HEATING_ON].active = false; + this->switch_to_action_(this->compute_action_()); + this->switch_to_supplemental_action_(this->compute_supplemental_action_()); +} + +void ThermostatClimate::idle_on_timer_callback_() { + ESP_LOGVV(TAG, "idle_on timer expired"); + this->timer_[thermostat::TIMER_IDLE_ON].active = false; + this->switch_to_action_(this->compute_action_()); + this->switch_to_supplemental_action_(this->compute_supplemental_action_()); +} + void ThermostatClimate::check_temperature_change_trigger_() { if (this->supports_two_points_) { // setup_complete_ helps us ensure an action is called immediately after boot @@ -459,10 +808,10 @@ bool ThermostatClimate::cooling_required_() { auto temperature = this->supports_two_points_ ? this->target_temperature_high : this->target_temperature; if (this->supports_cool_) { - if (this->current_temperature > (temperature + this->cool_deadband_)) { + if (this->current_temperature > temperature + this->cooling_deadband_) { // if the current temperature exceeds the target + deadband, cooling is required return true; - } else if (this->current_temperature < (temperature - this->cool_overrun_)) { + } else if (this->current_temperature < temperature - this->cooling_overrun_) { // if the current temperature is less than the target - overrun, cooling should stop return false; } else { @@ -480,10 +829,10 @@ bool ThermostatClimate::fanning_required_() { if (this->supports_fan_only_) { if (this->supports_fan_only_cooling_) { - if (this->current_temperature > (temperature + this->cool_deadband_)) { + if (this->current_temperature > temperature + this->cooling_deadband_) { // if the current temperature exceeds the target + deadband, fanning is required return true; - } else if (this->current_temperature < (temperature - this->cool_overrun_)) { + } else if (this->current_temperature < temperature - this->cooling_overrun_) { // if the current temperature is less than the target - overrun, fanning should stop return false; } else { @@ -502,10 +851,10 @@ bool ThermostatClimate::heating_required_() { auto temperature = this->supports_two_points_ ? this->target_temperature_low : this->target_temperature; if (this->supports_heat_) { - if (this->current_temperature < temperature - this->heat_deadband_) { + if (this->current_temperature < temperature - this->heating_deadband_) { // if the current temperature is below the target - deadband, heating is required return true; - } else if (this->current_temperature > temperature + this->heat_overrun_) { + } else if (this->current_temperature > temperature + this->heating_overrun_) { // if the current temperature is above the target + overrun, heating should stop return false; } else { @@ -518,6 +867,26 @@ bool ThermostatClimate::heating_required_() { return false; } +bool ThermostatClimate::supplemental_cooling_required_() { + auto temperature = this->supports_two_points_ ? this->target_temperature_high : this->target_temperature; + // the component must supports_cool_ and the climate action must be climate::CLIMATE_ACTION_COOLING. then... + // supplemental cooling is required if the max delta or max runtime was exceeded or the action is already engaged + return this->supports_cool_ && (this->action == climate::CLIMATE_ACTION_COOLING) && + (this->cooling_max_runtime_exceeded_ || + (this->current_temperature > temperature + this->supplemental_cool_delta_) || + (this->supplemental_action_ == climate::CLIMATE_ACTION_COOLING)); +} + +bool ThermostatClimate::supplemental_heating_required_() { + auto temperature = this->supports_two_points_ ? this->target_temperature_low : this->target_temperature; + // the component must supports_heat_ and the climate action must be climate::CLIMATE_ACTION_HEATING. then... + // supplemental heating is required if the max delta or max runtime was exceeded or the action is already engaged + return this->supports_heat_ && (this->action == climate::CLIMATE_ACTION_HEATING) && + (this->heating_max_runtime_exceeded_ || + (this->current_temperature < temperature - this->supplemental_heat_delta_) || + (this->supplemental_action_ == climate::CLIMATE_ACTION_HEATING)); +} + void ThermostatClimate::change_away_(bool away) { if (!away) { if (this->supports_two_points_) { @@ -546,10 +915,12 @@ void ThermostatClimate::set_away_config(const ThermostatClimateTargetTempConfig ThermostatClimate::ThermostatClimate() : cool_action_trigger_(new Trigger<>()), + supplemental_cool_action_trigger_(new Trigger<>()), cool_mode_trigger_(new Trigger<>()), dry_action_trigger_(new Trigger<>()), dry_mode_trigger_(new Trigger<>()), heat_action_trigger_(new Trigger<>()), + supplemental_heat_action_trigger_(new Trigger<>()), heat_mode_trigger_(new Trigger<>()), auto_mode_trigger_(new Trigger<>()), idle_action_trigger_(new Trigger<>()), @@ -575,11 +946,54 @@ void ThermostatClimate::set_default_mode(climate::ClimateMode default_mode) { th void ThermostatClimate::set_set_point_minimum_differential(float differential) { this->set_point_minimum_differential_ = differential; } -void ThermostatClimate::set_cool_deadband(float deadband) { this->cool_deadband_ = deadband; } -void ThermostatClimate::set_cool_overrun(float overrun) { this->cool_overrun_ = overrun; } -void ThermostatClimate::set_heat_deadband(float deadband) { this->heat_deadband_ = deadband; } -void ThermostatClimate::set_heat_overrun(float overrun) { this->heat_overrun_ = overrun; } +void ThermostatClimate::set_cool_deadband(float deadband) { this->cooling_deadband_ = deadband; } +void ThermostatClimate::set_cool_overrun(float overrun) { this->cooling_overrun_ = overrun; } +void ThermostatClimate::set_heat_deadband(float deadband) { this->heating_deadband_ = deadband; } +void ThermostatClimate::set_heat_overrun(float overrun) { this->heating_overrun_ = overrun; } +void ThermostatClimate::set_supplemental_cool_delta(float delta) { this->supplemental_cool_delta_ = delta; } +void ThermostatClimate::set_supplemental_heat_delta(float delta) { this->supplemental_heat_delta_ = delta; } +void ThermostatClimate::set_cooling_maximum_run_time_in_sec(uint32_t time) { + this->timer_[thermostat::TIMER_COOLING_MAX_RUN_TIME].time = + 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); +} +void ThermostatClimate::set_cooling_minimum_off_time_in_sec(uint32_t time) { + this->timer_[thermostat::TIMER_COOLING_OFF].time = + 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); +} +void ThermostatClimate::set_cooling_minimum_run_time_in_sec(uint32_t time) { + this->timer_[thermostat::TIMER_COOLING_ON].time = + 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); +} +void ThermostatClimate::set_fan_mode_minimum_switching_time_in_sec(uint32_t time) { + this->timer_[thermostat::TIMER_FAN_MODE].time = + 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); +} +void ThermostatClimate::set_fanning_minimum_off_time_in_sec(uint32_t time) { + this->timer_[thermostat::TIMER_FANNING_OFF].time = + 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); +} +void ThermostatClimate::set_fanning_minimum_run_time_in_sec(uint32_t time) { + this->timer_[thermostat::TIMER_FANNING_ON].time = + 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); +} +void ThermostatClimate::set_heating_maximum_run_time_in_sec(uint32_t time) { + this->timer_[thermostat::TIMER_HEATING_MAX_RUN_TIME].time = + 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); +} +void ThermostatClimate::set_heating_minimum_off_time_in_sec(uint32_t time) { + this->timer_[thermostat::TIMER_HEATING_OFF].time = + 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); +} +void ThermostatClimate::set_heating_minimum_run_time_in_sec(uint32_t time) { + this->timer_[thermostat::TIMER_HEATING_ON].time = + 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); +} +void ThermostatClimate::set_idle_minimum_time_in_sec(uint32_t time) { + this->timer_[thermostat::TIMER_IDLE_ON].time = + 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); +} void ThermostatClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } +void ThermostatClimate::set_use_startup_delay(bool use_startup_delay) { this->use_startup_delay_ = use_startup_delay; } void ThermostatClimate::set_supports_heat_cool(bool supports_heat_cool) { this->supports_heat_cool_ = supports_heat_cool; } @@ -587,9 +1001,19 @@ void ThermostatClimate::set_supports_auto(bool supports_auto) { this->supports_a void ThermostatClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } void ThermostatClimate::set_supports_dry(bool supports_dry) { this->supports_dry_ = supports_dry; } void ThermostatClimate::set_supports_fan_only(bool supports_fan_only) { this->supports_fan_only_ = supports_fan_only; } +void ThermostatClimate::set_supports_fan_only_action_uses_fan_mode_timer( + bool supports_fan_only_action_uses_fan_mode_timer) { + this->supports_fan_only_action_uses_fan_mode_timer_ = supports_fan_only_action_uses_fan_mode_timer; +} void ThermostatClimate::set_supports_fan_only_cooling(bool supports_fan_only_cooling) { this->supports_fan_only_cooling_ = supports_fan_only_cooling; } +void ThermostatClimate::set_supports_fan_with_cooling(bool supports_fan_with_cooling) { + this->supports_fan_with_cooling_ = supports_fan_with_cooling; +} +void ThermostatClimate::set_supports_fan_with_heating(bool supports_fan_with_heating) { + this->supports_fan_with_heating_ = supports_fan_with_heating; +} void ThermostatClimate::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } void ThermostatClimate::set_supports_fan_mode_on(bool supports_fan_mode_on) { this->supports_fan_mode_on_ = supports_fan_mode_on; @@ -635,9 +1059,15 @@ void ThermostatClimate::set_supports_two_points(bool supports_two_points) { } Trigger<> *ThermostatClimate::get_cool_action_trigger() const { return this->cool_action_trigger_; } +Trigger<> *ThermostatClimate::get_supplemental_cool_action_trigger() const { + return this->supplemental_cool_action_trigger_; +} Trigger<> *ThermostatClimate::get_dry_action_trigger() const { return this->dry_action_trigger_; } Trigger<> *ThermostatClimate::get_fan_only_action_trigger() const { return this->fan_only_action_trigger_; } Trigger<> *ThermostatClimate::get_heat_action_trigger() const { return this->heat_action_trigger_; } +Trigger<> *ThermostatClimate::get_supplemental_heat_action_trigger() const { + return this->supplemental_heat_action_trigger_; +} Trigger<> *ThermostatClimate::get_idle_action_trigger() const { return this->idle_action_trigger_; } Trigger<> *ThermostatClimate::get_auto_mode_trigger() const { return this->auto_mode_trigger_; } Trigger<> *ThermostatClimate::get_cool_mode_trigger() const { return this->cool_mode_trigger_; } @@ -668,23 +1098,62 @@ void ThermostatClimate::dump_config() { else ESP_LOGCONFIG(TAG, " Default Target Temperature Low: %.1f°C", this->normal_config_.default_temperature); } - if ((this->supports_cool_) || (this->supports_fan_only_)) { + if ((this->supports_cool_) || (this->supports_fan_only_ && this->supports_fan_only_cooling_)) { if (this->supports_two_points_) ESP_LOGCONFIG(TAG, " Default Target Temperature High: %.1f°C", this->normal_config_.default_temperature_high); else ESP_LOGCONFIG(TAG, " Default Target Temperature High: %.1f°C", this->normal_config_.default_temperature); } - ESP_LOGCONFIG(TAG, " Minimum Set Point Differential: %.1f°C", this->set_point_minimum_differential_); - ESP_LOGCONFIG(TAG, " Cool Deadband: %.1f°C", this->cool_deadband_); - ESP_LOGCONFIG(TAG, " Cool Overrun: %.1f°C", this->cool_overrun_); - ESP_LOGCONFIG(TAG, " Heat Deadband: %.1f°C", this->heat_deadband_); - ESP_LOGCONFIG(TAG, " Heat Overrun: %.1f°C", this->heat_overrun_); + if (this->supports_two_points_) + ESP_LOGCONFIG(TAG, " Minimum Set Point Differential: %.1f°C", this->set_point_minimum_differential_); + ESP_LOGCONFIG(TAG, " Start-up Delay Enabled: %s", YESNO(this->use_startup_delay_)); + if (this->supports_cool_) { + ESP_LOGCONFIG(TAG, " Cooling Parameters:"); + ESP_LOGCONFIG(TAG, " Deadband: %.1f°C", this->cooling_deadband_); + ESP_LOGCONFIG(TAG, " Overrun: %.1f°C", this->cooling_overrun_); + if ((this->supplemental_cool_delta_ > 0) || (this->timer_duration_(thermostat::TIMER_COOLING_MAX_RUN_TIME) > 0)) { + ESP_LOGCONFIG(TAG, " Supplemental Delta: %.1f°C", this->supplemental_cool_delta_); + ESP_LOGCONFIG(TAG, " Maximum Run Time: %us", + this->timer_duration_(thermostat::TIMER_COOLING_MAX_RUN_TIME) / 1000); + } + ESP_LOGCONFIG(TAG, " Minimum Off Time: %us", this->timer_duration_(thermostat::TIMER_COOLING_OFF) / 1000); + ESP_LOGCONFIG(TAG, " Minimum Run Time: %us", this->timer_duration_(thermostat::TIMER_COOLING_ON) / 1000); + } + if (this->supports_heat_) { + ESP_LOGCONFIG(TAG, " Heating Parameters:"); + ESP_LOGCONFIG(TAG, " Deadband: %.1f°C", this->heating_deadband_); + ESP_LOGCONFIG(TAG, " Overrun: %.1f°C", this->heating_overrun_); + if ((this->supplemental_heat_delta_ > 0) || (this->timer_duration_(thermostat::TIMER_HEATING_MAX_RUN_TIME) > 0)) { + ESP_LOGCONFIG(TAG, " Supplemental Delta: %.1f°C", this->supplemental_heat_delta_); + ESP_LOGCONFIG(TAG, " Maximum Run Time: %us", + this->timer_duration_(thermostat::TIMER_HEATING_MAX_RUN_TIME) / 1000); + } + ESP_LOGCONFIG(TAG, " Minimum Off Time: %us", this->timer_duration_(thermostat::TIMER_HEATING_OFF) / 1000); + ESP_LOGCONFIG(TAG, " Minimum Run Time: %us", this->timer_duration_(thermostat::TIMER_HEATING_ON) / 1000); + } + if (this->supports_fan_only_) { + ESP_LOGCONFIG(TAG, " Fanning Minimum Off Time: %us", this->timer_duration_(thermostat::TIMER_FANNING_OFF) / 1000); + ESP_LOGCONFIG(TAG, " Fanning Minimum Run Time: %us", this->timer_duration_(thermostat::TIMER_FANNING_ON) / 1000); + } + if (this->supports_fan_mode_on_ || this->supports_fan_mode_off_ || this->supports_fan_mode_auto_ || + this->supports_fan_mode_low_ || this->supports_fan_mode_medium_ || this->supports_fan_mode_high_ || + this->supports_fan_mode_middle_ || this->supports_fan_mode_focus_ || this->supports_fan_mode_diffuse_) { + ESP_LOGCONFIG(TAG, " Minimum Fan Mode Switching Time: %us", + this->timer_duration_(thermostat::TIMER_FAN_MODE) / 1000); + } + ESP_LOGCONFIG(TAG, " Minimum Idle Time: %us", this->timer_[thermostat::TIMER_IDLE_ON].time / 1000); ESP_LOGCONFIG(TAG, " Supports AUTO: %s", YESNO(this->supports_auto_)); ESP_LOGCONFIG(TAG, " Supports HEAT/COOL: %s", YESNO(this->supports_heat_cool_)); ESP_LOGCONFIG(TAG, " Supports COOL: %s", YESNO(this->supports_cool_)); ESP_LOGCONFIG(TAG, " Supports DRY: %s", YESNO(this->supports_dry_)); ESP_LOGCONFIG(TAG, " Supports FAN_ONLY: %s", YESNO(this->supports_fan_only_)); + ESP_LOGCONFIG(TAG, " Supports FAN_ONLY_ACTION_USES_FAN_MODE_TIMER: %s", + YESNO(this->supports_fan_only_action_uses_fan_mode_timer_)); ESP_LOGCONFIG(TAG, " Supports FAN_ONLY_COOLING: %s", YESNO(this->supports_fan_only_cooling_)); + if (this->supports_cool_) + ESP_LOGCONFIG(TAG, " Supports FAN_WITH_COOLING: %s", YESNO(this->supports_fan_with_cooling_)); + if (this->supports_heat_) + ESP_LOGCONFIG(TAG, " Supports FAN_WITH_HEATING: %s", YESNO(this->supports_fan_with_heating_)); ESP_LOGCONFIG(TAG, " Supports HEAT: %s", YESNO(this->supports_heat_)); ESP_LOGCONFIG(TAG, " Supports FAN MODE ON: %s", YESNO(this->supports_fan_mode_on_)); ESP_LOGCONFIG(TAG, " Supports FAN MODE OFF: %s", YESNO(this->supports_fan_mode_off_)); diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index 67f002a22a..1a5fd82ac0 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -8,6 +8,26 @@ namespace esphome { namespace thermostat { +enum ThermostatClimateTimerIndex : size_t { + TIMER_COOLING_MAX_RUN_TIME = 0, + TIMER_COOLING_OFF = 1, + TIMER_COOLING_ON = 2, + TIMER_FAN_MODE = 3, + TIMER_FANNING_OFF = 4, + TIMER_FANNING_ON = 5, + TIMER_HEATING_MAX_RUN_TIME = 6, + TIMER_HEATING_OFF = 7, + TIMER_HEATING_ON = 8, + TIMER_IDLE_ON = 9, +}; + +struct ThermostatClimateTimer { + const std::string name; + bool active; + uint32_t time; + std::function func; +}; + struct ThermostatClimateTargetTempConfig { public: ThermostatClimateTargetTempConfig(); @@ -35,13 +55,29 @@ class ThermostatClimate : public climate::Climate, public Component { void set_cool_overrun(float overrun); void set_heat_deadband(float deadband); void set_heat_overrun(float overrun); + void set_supplemental_cool_delta(float delta); + void set_supplemental_heat_delta(float delta); + void set_cooling_maximum_run_time_in_sec(uint32_t time); + void set_heating_maximum_run_time_in_sec(uint32_t time); + void set_cooling_minimum_off_time_in_sec(uint32_t time); + void set_cooling_minimum_run_time_in_sec(uint32_t time); + void set_fan_mode_minimum_switching_time_in_sec(uint32_t time); + void set_fanning_minimum_off_time_in_sec(uint32_t time); + void set_fanning_minimum_run_time_in_sec(uint32_t time); + void set_heating_minimum_off_time_in_sec(uint32_t time); + void set_heating_minimum_run_time_in_sec(uint32_t time); + void set_idle_minimum_time_in_sec(uint32_t time); void set_sensor(sensor::Sensor *sensor); + void set_use_startup_delay(bool use_startup_delay); void set_supports_auto(bool supports_auto); void set_supports_heat_cool(bool supports_heat_cool); void set_supports_cool(bool supports_cool); void set_supports_dry(bool supports_dry); void set_supports_fan_only(bool supports_fan_only); + void set_supports_fan_only_action_uses_fan_mode_timer(bool fan_only_action_uses_fan_mode_timer); void set_supports_fan_only_cooling(bool supports_fan_only_cooling); + void set_supports_fan_with_cooling(bool supports_fan_with_cooling); + void set_supports_fan_with_heating(bool supports_fan_with_heating); void set_supports_heat(bool supports_heat); void set_supports_fan_mode_on(bool supports_fan_mode_on); void set_supports_fan_mode_off(bool supports_fan_mode_off); @@ -62,9 +98,11 @@ class ThermostatClimate : public climate::Climate, public Component { void set_away_config(const ThermostatClimateTargetTempConfig &away_config); Trigger<> *get_cool_action_trigger() const; + Trigger<> *get_supplemental_cool_action_trigger() const; Trigger<> *get_dry_action_trigger() const; Trigger<> *get_fan_only_action_trigger() const; Trigger<> *get_heat_action_trigger() const; + Trigger<> *get_supplemental_heat_action_trigger() const; Trigger<> *get_idle_action_trigger() const; Trigger<> *get_auto_mode_trigger() const; Trigger<> *get_cool_mode_trigger() const; @@ -93,6 +131,13 @@ class ThermostatClimate : public climate::Climate, public Component { float heat_overrun(); /// Call triggers based on updated climate states (modes/actions) void refresh(); + /// Returns true if a climate action/fan mode transition is being delayed + bool climate_action_change_delayed(); + bool fan_mode_change_delayed(); + /// Returns the climate action that is being delayed (check climate_action_change_delayed(), first!) + climate::ClimateAction delayed_climate_action(); + /// Returns the fan mode that is being delayed (check fan_mode_change_delayed(), first!) + climate::ClimateFanMode delayed_fan_mode(); /// Set point and hysteresis validation bool hysteresis_valid(); // returns true if valid void validate_target_temperature(); @@ -111,10 +156,13 @@ class ThermostatClimate : public climate::Climate, public Component { climate::ClimateTraits traits() override; /// Re-compute the required action of this climate controller. - climate::ClimateAction compute_action_(); + climate::ClimateAction compute_action_(bool ignore_timers = false); + climate::ClimateAction compute_supplemental_action_(); /// Switch the climate device to the given climate action. void switch_to_action_(climate::ClimateAction action); + void switch_to_supplemental_action_(climate::ClimateAction action); + void trigger_supplemental_action_(); /// Switch the climate device to the given climate fan mode. void switch_to_fan_mode_(climate::ClimateFanMode fan_mode); @@ -128,10 +176,39 @@ class ThermostatClimate : public climate::Climate, public Component { /// Check if the temperature change trigger should be called. void check_temperature_change_trigger_(); + /// Is the action ready to be called? Returns true if so + bool idle_action_ready_(); + bool cooling_action_ready_(); + bool drying_action_ready_(); + bool fan_mode_ready_(); + bool fanning_action_ready_(); + bool heating_action_ready_(); + + /// Start/cancel/get status of climate action timer + void start_timer_(ThermostatClimateTimerIndex timer_index); + bool cancel_timer_(ThermostatClimateTimerIndex timer_index); + bool timer_active_(ThermostatClimateTimerIndex timer_index); + uint32_t timer_duration_(ThermostatClimateTimerIndex timer_index); + std::function timer_cbf_(ThermostatClimateTimerIndex timer_index); + + /// set_timeout() callbacks for various actions (see above) + void cooling_max_run_time_timer_callback_(); + void cooling_off_timer_callback_(); + void cooling_on_timer_callback_(); + void fan_mode_timer_callback_(); + void fanning_off_timer_callback_(); + void fanning_on_timer_callback_(); + void heating_max_run_time_timer_callback_(); + void heating_off_timer_callback_(); + void heating_on_timer_callback_(); + void idle_on_timer_callback_(); + /// Check if cooling/fanning/heating actions are required; returns true if so bool cooling_required_(); bool fanning_required_(); bool heating_required_(); + bool supplemental_cooling_required_(); + bool supplemental_heating_required_(); /// The sensor used for getting the current temperature sensor::Sensor *sensor_{nullptr}; @@ -146,8 +223,13 @@ class ThermostatClimate : public climate::Climate, public Component { bool supports_dry_{false}; bool supports_fan_only_{false}; bool supports_heat_{false}; + /// Special flag -- enables fan_modes to share timer with fan_only climate action + bool supports_fan_only_action_uses_fan_mode_timer_{false}; /// Special flag -- enables fan to be switched based on target_temperature_high bool supports_fan_only_cooling_{false}; + /// Special flags -- enables fan_only action to be called with cooling/heating actions + bool supports_fan_with_cooling_{false}; + bool supports_fan_with_heating_{false}; /// Whether the controller supports turning on or off just the fan. /// @@ -190,12 +272,23 @@ class ThermostatClimate : public climate::Climate, public Component { /// A false value means that the controller has no such mode. bool supports_away_{false}; + /// Flags indicating if maximum allowable run time was exceeded + bool cooling_max_runtime_exceeded_{false}; + bool heating_max_runtime_exceeded_{false}; + + /// Used to start "off" delay timers at boot + bool use_startup_delay_{false}; + + /// setup_complete_ blocks modifying/resetting the temps immediately after boot + bool setup_complete_{false}; + /// The trigger to call when the controller should switch to cooling action/mode. /// /// A null value for this attribute means that the controller has no cooling action /// For example electric heat, where only heating (power on) and not-heating /// (power off) is possible. Trigger<> *cool_action_trigger_{nullptr}; + Trigger<> *supplemental_cool_action_trigger_{nullptr}; Trigger<> *cool_mode_trigger_{nullptr}; /// The trigger to call when the controller should switch to dry (dehumidification) mode. @@ -211,6 +304,7 @@ class ThermostatClimate : public climate::Climate, public Component { /// For example window blinds, where only cooling (blinds closed) and not-cooling /// (blinds open) is possible. Trigger<> *heat_action_trigger_{nullptr}; + Trigger<> *supplemental_heat_action_trigger_{nullptr}; Trigger<> *heat_mode_trigger_{nullptr}; /// The trigger to call when the controller should switch to auto mode. @@ -283,12 +377,16 @@ class ThermostatClimate : public climate::Climate, public Component { Trigger<> *prev_mode_trigger_{nullptr}; Trigger<> *prev_swing_mode_trigger_{nullptr}; + /// Desired fan_mode -- used to store desired mode for callback when switching is delayed + climate::ClimateFanMode desired_fan_mode_{climate::CLIMATE_FAN_ON}; + /// 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::ClimateMode default_mode_{climate::CLIMATE_MODE_OFF}; + climate::ClimateMode prev_mode_{climate::CLIMATE_MODE_OFF}; climate::ClimateSwingMode prev_swing_mode_{climate::CLIMATE_SWING_OFF}; /// Store previously-known temperatures @@ -298,21 +396,38 @@ class ThermostatClimate : public climate::Climate, public Component { float prev_target_temperature_low_{NAN}; float prev_target_temperature_high_{NAN}; - /// Temperature data for normal/home and away modes - ThermostatClimateTargetTempConfig normal_config_{}; - ThermostatClimateTargetTempConfig away_config_{}; - /// Minimum differential required between set points float set_point_minimum_differential_{0}; /// Hysteresis values used for computing climate actions - float cool_deadband_{0}; - float cool_overrun_{0}; - float heat_deadband_{0}; - float heat_overrun_{0}; + float cooling_deadband_{0}; + float cooling_overrun_{0}; + float heating_deadband_{0}; + float heating_overrun_{0}; - /// setup_complete_ blocks modifying/resetting the temps immediately after boot - bool setup_complete_{false}; + /// Maximum allowable temperature deltas before engauging supplemental cooling/heating actions + float supplemental_cool_delta_{0}; + float supplemental_heat_delta_{0}; + + /// Minimum allowable duration in seconds for action timers + const uint8_t min_timer_duration_{1}; + + /// Temperature data for normal/home and away modes + ThermostatClimateTargetTempConfig normal_config_{}; + ThermostatClimateTargetTempConfig away_config_{}; + + /// Climate action timers + std::vector timer_{ + {"cool_run", false, 0, std::bind(&ThermostatClimate::cooling_max_run_time_timer_callback_, this)}, + {"cool_off", false, 0, std::bind(&ThermostatClimate::cooling_off_timer_callback_, this)}, + {"cool_on", false, 0, std::bind(&ThermostatClimate::cooling_on_timer_callback_, this)}, + {"fan_mode", false, 0, std::bind(&ThermostatClimate::fan_mode_timer_callback_, this)}, + {"fan_off", false, 0, std::bind(&ThermostatClimate::fanning_off_timer_callback_, this)}, + {"fan_on", false, 0, std::bind(&ThermostatClimate::fanning_on_timer_callback_, this)}, + {"heat_run", false, 0, std::bind(&ThermostatClimate::heating_max_run_time_timer_callback_, this)}, + {"heat_off", false, 0, std::bind(&ThermostatClimate::heating_off_timer_callback_, this)}, + {"heat_on", false, 0, std::bind(&ThermostatClimate::heating_on_timer_callback_, this)}, + {"idle_on", false, 0, std::bind(&ThermostatClimate::idle_on_timer_callback_, this)}}; }; } // namespace thermostat diff --git a/esphome/const.py b/esphome/const.py index 117b9ccc3e..02fb92d305 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -223,8 +223,11 @@ CONF_FAN_MODE_MIDDLE_ACTION = "fan_mode_middle_action" CONF_FAN_MODE_OFF_ACTION = "fan_mode_off_action" CONF_FAN_MODE_ON_ACTION = "fan_mode_on_action" CONF_FAN_ONLY_ACTION = "fan_only_action" +CONF_FAN_ONLY_ACTION_USES_FAN_MODE_TIMER = "fan_only_action_uses_fan_mode_timer" CONF_FAN_ONLY_COOLING = "fan_only_cooling" CONF_FAN_ONLY_MODE = "fan_only_mode" +CONF_FAN_WITH_COOLING = "fan_with_cooling" +CONF_FAN_WITH_HEATING = "fan_with_heating" CONF_FAST_CONNECT = "fast_connect" CONF_FILE = "file" CONF_FILTER = "filter" @@ -330,8 +333,10 @@ CONF_MAKE_ID = "make_id" CONF_MANUAL_IP = "manual_ip" CONF_MANUFACTURER_ID = "manufacturer_id" CONF_MASK_DISTURBER = "mask_disturber" +CONF_MAX_COOLING_RUN_TIME = "max_cooling_run_time" CONF_MAX_CURRENT = "max_current" CONF_MAX_DURATION = "max_duration" +CONF_MAX_HEATING_RUN_TIME = "max_heating_run_time" CONF_MAX_LENGTH = "max_length" CONF_MAX_LEVEL = "max_level" CONF_MAX_POWER = "max_power" @@ -345,6 +350,14 @@ CONF_MEASUREMENT_SEQUENCE_NUMBER = "measurement_sequence_number" CONF_MEDIUM = "medium" CONF_MEMORY_BLOCKS = "memory_blocks" CONF_METHOD = "method" +CONF_MIN_COOLING_OFF_TIME = "min_cooling_off_time" +CONF_MIN_COOLING_RUN_TIME = "min_cooling_run_time" +CONF_MIN_FAN_MODE_SWITCHING_TIME = "min_fan_mode_switching_time" +CONF_MIN_FANNING_OFF_TIME = "min_fanning_off_time" +CONF_MIN_FANNING_RUN_TIME = "min_fanning_run_time" +CONF_MIN_HEATING_OFF_TIME = "min_heating_off_time" +CONF_MIN_HEATING_RUN_TIME = "min_heating_run_time" +CONF_MIN_IDLE_TIME = "min_idle_time" CONF_MIN_LENGTH = "min_length" CONF_MIN_LEVEL = "min_level" CONF_MIN_POWER = "min_power" @@ -568,6 +581,7 @@ CONF_SPI_ID = "spi_id" CONF_SPIKE_REJECTION = "spike_rejection" CONF_SSID = "ssid" CONF_SSL_FINGERPRINTS = "ssl_fingerprints" +CONF_STARTUP_DELAY = "startup_delay" CONF_STATE = "state" CONF_STATE_CLASS = "state_class" CONF_STATE_TOPIC = "state_topic" @@ -580,6 +594,10 @@ CONF_STOP = "stop" CONF_STOP_ACTION = "stop_action" CONF_SUBNET = "subnet" CONF_SUBSTITUTIONS = "substitutions" +CONF_SUPPLEMENTAL_COOLING_ACTION = "supplemental_cooling_action" +CONF_SUPPLEMENTAL_COOLING_DELTA = "supplemental_cooling_delta" +CONF_SUPPLEMENTAL_HEATING_ACTION = "supplemental_heating_action" +CONF_SUPPLEMENTAL_HEATING_DELTA = "supplemental_heating_delta" CONF_SUPPORTS_COOL = "supports_cool" CONF_SUPPORTS_HEAT = "supports_heat" CONF_SWING_BOTH_ACTION = "swing_both_action" diff --git a/tests/test3.yaml b/tests/test3.yaml index d4c8e526fe..6402684c5d 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -858,8 +858,12 @@ climate: - switch.turn_on: gpio_switch1 cool_action: - switch.turn_on: gpio_switch2 + supplemental_cooling_action: + - switch.turn_on: gpio_switch3 heat_action: - switch.turn_on: gpio_switch1 + supplemental_heating_action: + - switch.turn_on: gpio_switch3 dry_action: - switch.turn_on: gpio_switch2 fan_only_action: @@ -902,7 +906,28 @@ climate: - switch.turn_on: gpio_switch1 swing_both_action: - switch.turn_on: gpio_switch2 - hysteresis: 0.2 + startup_delay: true + supplemental_cooling_delta: 2.0 + cool_deadband: 0.5 + cool_overrun: 0.5 + min_cooling_off_time: 300s + min_cooling_run_time: 300s + max_cooling_run_time: 600s + supplemental_heating_delta: 2.0 + heat_deadband: 0.5 + heat_overrun: 0.5 + min_heating_off_time: 300s + min_heating_run_time: 300s + max_heating_run_time: 600s + min_fanning_off_time: 30s + min_fanning_run_time: 30s + min_fan_mode_switching_time: 15s + min_idle_time: 30s + set_point_minimum_differential: 0.5 + fan_only_action_uses_fan_mode_timer: true + fan_only_cooling: true + fan_with_cooling: true + fan_with_heating: true away_config: default_target_temperature_low: 16°C default_target_temperature_high: 20°C