From d4e76185bdb34f31a1be8efcbf04226b7a06596b Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 1 Jul 2020 20:38:51 -0500 Subject: [PATCH] Thermostat component (#1105) * Add thermostat component * Add hysteresis accessor * Linted * Fixed up const.py and test * Fix test...again...oops --- esphome/components/thermostat/__init__.py | 0 esphome/components/thermostat/climate.py | 254 ++++++++ .../thermostat/thermostat_climate.cpp | 552 ++++++++++++++++++ .../thermostat/thermostat_climate.h | 271 +++++++++ esphome/const.py | 22 + tests/test3.yaml | 57 ++ 6 files changed, 1156 insertions(+) create mode 100644 esphome/components/thermostat/__init__.py create mode 100644 esphome/components/thermostat/climate.py create mode 100644 esphome/components/thermostat/thermostat_climate.cpp create mode 100644 esphome/components/thermostat/thermostat_climate.h diff --git a/esphome/components/thermostat/__init__.py b/esphome/components/thermostat/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py new file mode 100644 index 0000000000..c9cb81194c --- /dev/null +++ b/esphome/components/thermostat/climate.py @@ -0,0 +1,254 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components import climate, sensor +from esphome.const import CONF_AUTO_MODE, CONF_AWAY_CONFIG, CONF_COOL_ACTION, CONF_COOL_MODE, \ + CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_DRY_ACTION, \ + CONF_DRY_MODE, 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, \ + CONF_FAN_ONLY_ACTION, CONF_FAN_ONLY_MODE, CONF_HEAT_ACTION, CONF_HEAT_MODE, CONF_HYSTERESIS, \ + CONF_ID, CONF_IDLE_ACTION, CONF_OFF_MODE, CONF_SENSOR, CONF_SWING_BOTH_ACTION, \ + CONF_SWING_HORIZONTAL_ACTION, CONF_SWING_OFF_ACTION, CONF_SWING_VERTICAL_ACTION + +thermostat_ns = cg.esphome_ns.namespace('thermostat') +ThermostatClimate = thermostat_ns.class_('ThermostatClimate', climate.Climate, cg.Component) +ThermostatClimateTargetTempConfig = thermostat_ns.struct('ThermostatClimateTargetTempConfig') + + +def validate_thermostat(config): + # verify corresponding climate action action exists for any defined climate mode action + if CONF_COOL_MODE in config and CONF_COOL_ACTION not in config: + raise cv.Invalid("{} must be defined to use {}".format(CONF_COOL_ACTION, CONF_COOL_MODE)) + if CONF_DRY_MODE in config and CONF_DRY_ACTION not in config: + raise cv.Invalid("{} must be defined to use {}".format(CONF_DRY_ACTION, CONF_DRY_MODE)) + if CONF_FAN_ONLY_MODE in config and CONF_FAN_ONLY_ACTION not in config: + raise cv.Invalid("{} must be defined to use {}".format(CONF_FAN_ONLY_ACTION, + CONF_FAN_ONLY_MODE)) + if CONF_HEAT_MODE in config and CONF_HEAT_ACTION not in config: + raise cv.Invalid("{} must be defined to use {}".format(CONF_HEAT_ACTION, CONF_HEAT_MODE)) + # verify corresponding default target temperature exists when a given climate action exists + if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH not in config and (CONF_COOL_ACTION in config + or CONF_FAN_ONLY_ACTION in config): + raise cv.Invalid("{} must be defined when using {} or {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_COOL_ACTION, CONF_FAN_ONLY_ACTION)) + if CONF_DEFAULT_TARGET_TEMPERATURE_LOW not in config and CONF_HEAT_ACTION in config: + raise cv.Invalid("{} must be defined when using {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION)) + # if a given climate action is NOT defined, it should not have a default target temperature + if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in config and (CONF_COOL_ACTION not in config + and CONF_FAN_ONLY_ACTION not in config): + raise cv.Invalid("{} is defined with no {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_COOL_ACTION)) + if CONF_DEFAULT_TARGET_TEMPERATURE_LOW in config and CONF_HEAT_ACTION not in config: + raise cv.Invalid("{} is defined with no {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION)) + + if CONF_AWAY_CONFIG in config: + away = config[CONF_AWAY_CONFIG] + # verify corresponding default target temperature exists when a given climate action exists + if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH not in away and (CONF_COOL_ACTION in config or + CONF_FAN_ONLY_ACTION in config): + raise cv.Invalid("{} must be defined in away configuration when using {} or {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_COOL_ACTION, CONF_FAN_ONLY_ACTION)) + if CONF_DEFAULT_TARGET_TEMPERATURE_LOW not in away and CONF_HEAT_ACTION in config: + raise cv.Invalid("{} must be defined in away configuration when using {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION)) + # if a given climate action is NOT defined, it should not have a default target temperature + if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in away and (CONF_COOL_ACTION not in config and + CONF_FAN_ONLY_ACTION not in config): + raise cv.Invalid("{} is defined in away configuration with no {} or {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_COOL_ACTION, CONF_FAN_ONLY_ACTION)) + if CONF_DEFAULT_TARGET_TEMPERATURE_LOW in away and CONF_HEAT_ACTION not in config: + raise cv.Invalid("{} is defined in away configuration with no {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION)) + + return config + + +CONFIG_SCHEMA = cv.All(climate.CLIMATE_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(ThermostatClimate), + 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_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_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), + cv.Optional(CONF_FAN_ONLY_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(single=True), + cv.Optional(CONF_FAN_MODE_OFF_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_AUTO_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_LOW_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_MEDIUM_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_HIGH_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_MIDDLE_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_FOCUS_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_DIFFUSE_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_SWING_BOTH_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_SWING_HORIZONTAL_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_SWING_OFF_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_SWING_VERTICAL_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, + cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, + cv.Optional(CONF_HYSTERESIS, default=0.5): cv.temperature, + cv.Optional(CONF_AWAY_CONFIG): cv.Schema({ + cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, + cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, + }), +}).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(CONF_COOL_ACTION, CONF_DRY_ACTION, + CONF_FAN_ONLY_ACTION, CONF_HEAT_ACTION), + validate_thermostat) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield climate.register_climate(var, config) + + auto_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 + CONF_FAN_ONLY_ACTION in config) + + sens = yield cg.get_variable(config[CONF_SENSOR]) + cg.add(var.set_sensor(sens)) + cg.add(var.set_hysteresis(config[CONF_HYSTERESIS])) + + if two_points_available is True: + cg.add(var.set_supports_two_points(True)) + normal_config = ThermostatClimateTargetTempConfig( + config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW], + config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] + ) + elif CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in config: + cg.add(var.set_supports_two_points(False)) + normal_config = ThermostatClimateTargetTempConfig( + config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] + ) + elif CONF_DEFAULT_TARGET_TEMPERATURE_LOW in config: + cg.add(var.set_supports_two_points(False)) + normal_config = ThermostatClimateTargetTempConfig( + config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW] + ) + cg.add(var.set_normal_config(normal_config)) + + yield automation.build_automation(var.get_idle_action_trigger(), [], + config[CONF_IDLE_ACTION]) + + if auto_mode_available is True: + cg.add(var.set_supports_auto(True)) + else: + cg.add(var.set_supports_auto(False)) + + if CONF_COOL_ACTION in config: + yield automation.build_automation(var.get_cool_action_trigger(), [], + config[CONF_COOL_ACTION]) + cg.add(var.set_supports_cool(True)) + if CONF_DRY_ACTION in config: + yield automation.build_automation(var.get_dry_action_trigger(), [], + config[CONF_DRY_ACTION]) + cg.add(var.set_supports_dry(True)) + if CONF_FAN_ONLY_ACTION in config: + yield automation.build_automation(var.get_fan_only_action_trigger(), [], + config[CONF_FAN_ONLY_ACTION]) + cg.add(var.set_supports_fan_only(True)) + if CONF_HEAT_ACTION in config: + yield automation.build_automation(var.get_heat_action_trigger(), [], + config[CONF_HEAT_ACTION]) + cg.add(var.set_supports_heat(True)) + if CONF_AUTO_MODE in config: + yield automation.build_automation(var.get_auto_mode_trigger(), [], + config[CONF_AUTO_MODE]) + if CONF_COOL_MODE in config: + yield automation.build_automation(var.get_cool_mode_trigger(), [], + config[CONF_COOL_MODE]) + cg.add(var.set_supports_cool(True)) + if CONF_DRY_MODE in config: + yield automation.build_automation(var.get_dry_mode_trigger(), [], + config[CONF_DRY_MODE]) + cg.add(var.set_supports_dry(True)) + if CONF_FAN_ONLY_MODE in config: + yield automation.build_automation(var.get_fan_only_mode_trigger(), [], + config[CONF_FAN_ONLY_MODE]) + cg.add(var.set_supports_fan_only(True)) + if CONF_HEAT_MODE in config: + yield automation.build_automation(var.get_heat_mode_trigger(), [], + config[CONF_HEAT_MODE]) + cg.add(var.set_supports_heat(True)) + if CONF_OFF_MODE in config: + yield automation.build_automation(var.get_off_mode_trigger(), [], + config[CONF_OFF_MODE]) + if CONF_FAN_MODE_ON_ACTION in config: + yield automation.build_automation(var.get_fan_mode_on_trigger(), [], + config[CONF_FAN_MODE_ON_ACTION]) + cg.add(var.set_supports_fan_mode_on(True)) + if CONF_FAN_MODE_OFF_ACTION in config: + yield automation.build_automation(var.get_fan_mode_off_trigger(), [], + config[CONF_FAN_MODE_OFF_ACTION]) + cg.add(var.set_supports_fan_mode_off(True)) + if CONF_FAN_MODE_AUTO_ACTION in config: + yield automation.build_automation(var.get_fan_mode_auto_trigger(), [], + config[CONF_FAN_MODE_AUTO_ACTION]) + cg.add(var.set_supports_fan_mode_auto(True)) + if CONF_FAN_MODE_LOW_ACTION in config: + yield automation.build_automation(var.get_fan_mode_low_trigger(), [], + config[CONF_FAN_MODE_LOW_ACTION]) + cg.add(var.set_supports_fan_mode_low(True)) + if CONF_FAN_MODE_MEDIUM_ACTION in config: + yield automation.build_automation(var.get_fan_mode_medium_trigger(), [], + config[CONF_FAN_MODE_MEDIUM_ACTION]) + cg.add(var.set_supports_fan_mode_medium(True)) + if CONF_FAN_MODE_HIGH_ACTION in config: + yield automation.build_automation(var.get_fan_mode_high_trigger(), [], + config[CONF_FAN_MODE_HIGH_ACTION]) + cg.add(var.set_supports_fan_mode_high(True)) + if CONF_FAN_MODE_MIDDLE_ACTION in config: + yield automation.build_automation(var.get_fan_mode_middle_trigger(), [], + config[CONF_FAN_MODE_MIDDLE_ACTION]) + cg.add(var.set_supports_fan_mode_middle(True)) + if CONF_FAN_MODE_FOCUS_ACTION in config: + yield automation.build_automation(var.get_fan_mode_focus_trigger(), [], + config[CONF_FAN_MODE_FOCUS_ACTION]) + cg.add(var.set_supports_fan_mode_focus(True)) + if CONF_FAN_MODE_DIFFUSE_ACTION in config: + yield automation.build_automation(var.get_fan_mode_diffuse_trigger(), [], + config[CONF_FAN_MODE_DIFFUSE_ACTION]) + cg.add(var.set_supports_fan_mode_diffuse(True)) + if CONF_SWING_BOTH_ACTION in config: + yield automation.build_automation(var.get_swing_mode_both_trigger(), [], + config[CONF_SWING_BOTH_ACTION]) + cg.add(var.set_supports_swing_mode_both(True)) + if CONF_SWING_HORIZONTAL_ACTION in config: + yield automation.build_automation(var.get_swing_mode_horizontal_trigger(), [], + config[CONF_SWING_HORIZONTAL_ACTION]) + cg.add(var.set_supports_swing_mode_horizontal(True)) + if CONF_SWING_OFF_ACTION in config: + yield automation.build_automation(var.get_swing_mode_off_trigger(), [], + config[CONF_SWING_OFF_ACTION]) + cg.add(var.set_supports_swing_mode_off(True)) + if CONF_SWING_VERTICAL_ACTION in config: + yield automation.build_automation(var.get_swing_mode_vertical_trigger(), [], + config[CONF_SWING_VERTICAL_ACTION]) + cg.add(var.set_supports_swing_mode_vertical(True)) + + if CONF_AWAY_CONFIG in config: + away = config[CONF_AWAY_CONFIG] + + if two_points_available is True: + away_config = ThermostatClimateTargetTempConfig( + away[CONF_DEFAULT_TARGET_TEMPERATURE_LOW], + away[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] + ) + elif CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in away: + away_config = ThermostatClimateTargetTempConfig( + away[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] + ) + elif CONF_DEFAULT_TARGET_TEMPERATURE_LOW in away: + away_config = ThermostatClimateTargetTempConfig( + away[CONF_DEFAULT_TARGET_TEMPERATURE_LOW] + ) + cg.add(var.set_away_config(away_config)) diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp new file mode 100644 index 0000000000..5d97156e2f --- /dev/null +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -0,0 +1,552 @@ +#include "thermostat_climate.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace thermostat { + +static const char *TAG = "thermostat.climate"; + +void ThermostatClimate::setup() { + 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_()); + // current temperature and possibly action changed, so publish the new state + this->publish_state(); + }); + this->current_temperature = this->sensor_->state; + // restore all climate data, if possible + auto restore = this->restore_state_(); + if (restore.has_value()) { + restore->to_call(this).perform(); + } else { + // restore from defaults, change_away handles temps for us + this->mode = climate::CLIMATE_MODE_AUTO; + this->change_away_(false); + } + // refresh the climate action based on the restored settings + this->switch_to_action_(compute_action_()); + this->setup_complete_ = true; + this->publish_state(); +} +float ThermostatClimate::hysteresis() { return this->hysteresis_; } +void ThermostatClimate::refresh() { + this->switch_to_mode_(this->mode); + this->switch_to_action_(compute_action_()); + this->switch_to_fan_mode_(this->fan_mode); + this->switch_to_swing_mode_(this->swing_mode); + this->publish_state(); +} +void ThermostatClimate::control(const climate::ClimateCall &call) { + if (call.get_mode().has_value()) + this->mode = *call.get_mode(); + if (call.get_fan_mode().has_value()) + this->fan_mode = *call.get_fan_mode(); + if (call.get_swing_mode().has_value()) + this->swing_mode = *call.get_swing_mode(); + if (call.get_target_temperature().has_value()) + this->target_temperature = *call.get_target_temperature(); + if (call.get_target_temperature_low().has_value()) + this->target_temperature_low = *call.get_target_temperature_low(); + if (call.get_target_temperature_high().has_value()) + this->target_temperature_high = *call.get_target_temperature_high(); + if (call.get_away().has_value()) { + // setup_complete_ blocks modifying/resetting the temps immediately after boot + if (this->setup_complete_) { + this->change_away_(*call.get_away()); + } else { + this->away = *call.get_away(); + } + } + // set point validation + if (this->supports_two_points_) { + if (this->target_temperature_low < this->get_traits().get_visual_min_temperature()) + this->target_temperature_low = this->get_traits().get_visual_min_temperature(); + if (this->target_temperature_high > this->get_traits().get_visual_max_temperature()) + this->target_temperature_high = this->get_traits().get_visual_max_temperature(); + if (this->target_temperature_high < this->target_temperature_low) + this->target_temperature_high = this->target_temperature_low; + } else { + if (this->target_temperature < this->get_traits().get_visual_min_temperature()) + this->target_temperature = this->get_traits().get_visual_min_temperature(); + if (this->target_temperature > this->get_traits().get_visual_max_temperature()) + this->target_temperature = this->get_traits().get_visual_max_temperature(); + } + // make any changes happen + refresh(); +} +climate::ClimateTraits ThermostatClimate::traits() { + auto traits = climate::ClimateTraits(); + traits.set_supports_current_temperature(true); + traits.set_supports_auto_mode(this->supports_auto_); + traits.set_supports_cool_mode(this->supports_cool_); + traits.set_supports_dry_mode(this->supports_dry_); + traits.set_supports_fan_only_mode(this->supports_fan_only_); + traits.set_supports_heat_mode(this->supports_heat_); + traits.set_supports_fan_mode_on(this->supports_fan_mode_on_); + traits.set_supports_fan_mode_off(this->supports_fan_mode_off_); + traits.set_supports_fan_mode_auto(this->supports_fan_mode_auto_); + traits.set_supports_fan_mode_low(this->supports_fan_mode_low_); + traits.set_supports_fan_mode_medium(this->supports_fan_mode_medium_); + traits.set_supports_fan_mode_high(this->supports_fan_mode_high_); + traits.set_supports_fan_mode_middle(this->supports_fan_mode_middle_); + traits.set_supports_fan_mode_focus(this->supports_fan_mode_focus_); + traits.set_supports_fan_mode_diffuse(this->supports_fan_mode_diffuse_); + traits.set_supports_swing_mode_both(this->supports_swing_mode_both_); + traits.set_supports_swing_mode_horizontal(this->supports_swing_mode_horizontal_); + traits.set_supports_swing_mode_off(this->supports_swing_mode_off_); + traits.set_supports_swing_mode_vertical(this->supports_swing_mode_vertical_); + traits.set_supports_two_point_target_temperature(this->supports_two_points_); + traits.set_supports_away(this->supports_away_); + traits.set_supports_action(true); + return traits; +} +climate::ClimateAction ThermostatClimate::compute_action_() { + climate::ClimateAction target_action = this->action; + if (this->supports_two_points_) { + if (isnan(this->current_temperature) || isnan(this->target_temperature_low) || + isnan(this->target_temperature_high) || isnan(this->hysteresis_)) + // if any control parameters are nan, go to OFF action (not IDLE!) + return climate::CLIMATE_ACTION_OFF; + + if (((this->action == climate::CLIMATE_ACTION_FAN) && (this->mode != climate::CLIMATE_MODE_FAN_ONLY)) || + ((this->action == climate::CLIMATE_ACTION_DRYING) && (this->mode != climate::CLIMATE_MODE_DRY))) { + target_action = climate::CLIMATE_ACTION_IDLE; + } + + switch (this->mode) { + case climate::CLIMATE_MODE_FAN_ONLY: + if (this->supports_fan_only_) { + if (this->current_temperature > this->target_temperature_high + this->hysteresis_) + target_action = climate::CLIMATE_ACTION_FAN; + else if (this->current_temperature < this->target_temperature_high - this->hysteresis_) + if (this->action == climate::CLIMATE_ACTION_FAN) + target_action = climate::CLIMATE_ACTION_IDLE; + } + break; + case climate::CLIMATE_MODE_DRY: + target_action = climate::CLIMATE_ACTION_DRYING; + break; + case climate::CLIMATE_MODE_OFF: + target_action = climate::CLIMATE_ACTION_OFF; + break; + case climate::CLIMATE_MODE_AUTO: + case climate::CLIMATE_MODE_COOL: + case climate::CLIMATE_MODE_HEAT: + if (this->supports_cool_) { + if (this->current_temperature > this->target_temperature_high + this->hysteresis_) + target_action = climate::CLIMATE_ACTION_COOLING; + else if (this->current_temperature < this->target_temperature_high - this->hysteresis_) + if (this->action == climate::CLIMATE_ACTION_COOLING) + target_action = climate::CLIMATE_ACTION_IDLE; + } + if (this->supports_heat_) { + if (this->current_temperature < this->target_temperature_low - this->hysteresis_) + target_action = climate::CLIMATE_ACTION_HEATING; + else if (this->current_temperature > this->target_temperature_low + this->hysteresis_) + if (this->action == climate::CLIMATE_ACTION_HEATING) + target_action = climate::CLIMATE_ACTION_IDLE; + } + break; + default: + break; + } + } else { + if (isnan(this->current_temperature) || isnan(this->target_temperature) || isnan(this->hysteresis_)) + // if any control parameters are nan, go to OFF action (not IDLE!) + return climate::CLIMATE_ACTION_OFF; + + if (((this->action == climate::CLIMATE_ACTION_FAN) && (this->mode != climate::CLIMATE_MODE_FAN_ONLY)) || + ((this->action == climate::CLIMATE_ACTION_DRYING) && (this->mode != climate::CLIMATE_MODE_DRY))) { + target_action = climate::CLIMATE_ACTION_IDLE; + } + + switch (this->mode) { + case climate::CLIMATE_MODE_FAN_ONLY: + if (this->supports_fan_only_) { + if (this->current_temperature > this->target_temperature + this->hysteresis_) + target_action = climate::CLIMATE_ACTION_FAN; + else if (this->current_temperature < this->target_temperature - this->hysteresis_) + if (this->action == climate::CLIMATE_ACTION_FAN) + target_action = climate::CLIMATE_ACTION_IDLE; + } + break; + case climate::CLIMATE_MODE_DRY: + target_action = climate::CLIMATE_ACTION_DRYING; + break; + case climate::CLIMATE_MODE_OFF: + target_action = climate::CLIMATE_ACTION_OFF; + break; + case climate::CLIMATE_MODE_COOL: + if (this->supports_cool_) { + if (this->current_temperature > this->target_temperature + this->hysteresis_) + target_action = climate::CLIMATE_ACTION_COOLING; + else if (this->current_temperature < this->target_temperature - this->hysteresis_) + if (this->action == climate::CLIMATE_ACTION_COOLING) + target_action = climate::CLIMATE_ACTION_IDLE; + } + case climate::CLIMATE_MODE_HEAT: + if (this->supports_heat_) { + if (this->current_temperature < this->target_temperature - this->hysteresis_) + target_action = climate::CLIMATE_ACTION_HEATING; + else if (this->current_temperature > this->target_temperature + this->hysteresis_) + if (this->action == climate::CLIMATE_ACTION_HEATING) + target_action = climate::CLIMATE_ACTION_IDLE; + } + break; + default: + break; + } + } + // do not switch to an action that isn't enabled per the active climate mode + if ((this->mode == climate::CLIMATE_MODE_COOL) && (target_action == climate::CLIMATE_ACTION_HEATING)) + target_action = climate::CLIMATE_ACTION_IDLE; + if ((this->mode == climate::CLIMATE_MODE_HEAT) && (target_action == climate::CLIMATE_ACTION_COOLING)) + target_action = climate::CLIMATE_ACTION_IDLE; + + return target_action; +} +void ThermostatClimate::switch_to_action_(climate::ClimateAction action) { + // setup_complete_ helps us ensure an action is called immediately after boot + if ((action == this->action) && this->setup_complete_) + // already in target mode + return; + + 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. + this->action = action; + return; + } + + if (this->prev_action_trigger_ != nullptr) { + this->prev_action_trigger_->stop(); + this->prev_action_trigger_ = nullptr; + } + Trigger<> *trig = this->idle_action_trigger_; + switch (action) { + case climate::CLIMATE_ACTION_OFF: + case climate::CLIMATE_ACTION_IDLE: + // trig = this->idle_action_trigger_; + break; + case climate::CLIMATE_ACTION_COOLING: + trig = this->cool_action_trigger_; + break; + case climate::CLIMATE_ACTION_HEATING: + trig = this->heat_action_trigger_; + break; + case climate::CLIMATE_ACTION_FAN: + trig = this->fan_only_action_trigger_; + break; + case climate::CLIMATE_ACTION_DRYING: + trig = this->dry_action_trigger_; + break; + default: + // we cannot report an invalid mode back to HA (even if it asked for one) + // and must assume some valid value + action = climate::CLIMATE_ACTION_OFF; + // trig = this->idle_action_trigger_; + } + assert(trig != nullptr); + trig->trigger(); + this->action = action; + this->prev_action_trigger_ = trig; +} +void ThermostatClimate::switch_to_fan_mode_(climate::ClimateFanMode fan_mode) { + // setup_complete_ helps us ensure an action is called immediately after boot + if ((fan_mode == this->prev_fan_mode_) && this->setup_complete_) + // already in target mode + return; + + if (this->prev_fan_mode_trigger_ != nullptr) { + this->prev_fan_mode_trigger_->stop(); + this->prev_fan_mode_trigger_ = nullptr; + } + 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) { + // setup_complete_ helps us ensure an action is called immediately after boot + if ((mode == this->prev_mode_) && this->setup_complete_) + // already in target mode + return; + + if (this->prev_mode_trigger_ != nullptr) { + this->prev_mode_trigger_->stop(); + this->prev_mode_trigger_ = nullptr; + } + Trigger<> *trig = this->auto_mode_trigger_; + switch (mode) { + case climate::CLIMATE_MODE_OFF: + trig = this->off_mode_trigger_; + break; + case climate::CLIMATE_MODE_AUTO: + // trig = this->auto_mode_trigger_; + break; + case climate::CLIMATE_MODE_COOL: + trig = this->cool_mode_trigger_; + break; + case climate::CLIMATE_MODE_HEAT: + trig = this->heat_mode_trigger_; + break; + case climate::CLIMATE_MODE_FAN_ONLY: + trig = this->fan_only_mode_trigger_; + break; + case climate::CLIMATE_MODE_DRY: + trig = this->dry_mode_trigger_; + break; + default: + // we cannot report an invalid mode back to HA (even if it asked for one) + // and must assume some valid value + mode = climate::CLIMATE_MODE_AUTO; + // trig = this->auto_mode_trigger_; + } + assert(trig != nullptr); + trig->trigger(); + this->mode = mode; + this->prev_mode_ = mode; + this->prev_mode_trigger_ = trig; +} +void ThermostatClimate::switch_to_swing_mode_(climate::ClimateSwingMode swing_mode) { + // setup_complete_ helps us ensure an action is called immediately after boot + if ((swing_mode == this->prev_swing_mode_) && this->setup_complete_) + // already in target mode + return; + + if (this->prev_swing_mode_trigger_ != nullptr) { + this->prev_swing_mode_trigger_->stop(); + this->prev_swing_mode_trigger_ = nullptr; + } + Trigger<> *trig = this->swing_mode_off_trigger_; + switch (swing_mode) { + case climate::CLIMATE_SWING_BOTH: + trig = this->swing_mode_both_trigger_; + break; + case climate::CLIMATE_SWING_HORIZONTAL: + trig = this->swing_mode_horizontal_trigger_; + break; + case climate::CLIMATE_SWING_OFF: + // trig = this->swing_mode_off_trigger_; + break; + case climate::CLIMATE_SWING_VERTICAL: + trig = this->swing_mode_vertical_trigger_; + break; + default: + // we cannot report an invalid mode back to HA (even if it asked for one) + // and must assume some valid value + swing_mode = climate::CLIMATE_SWING_OFF; + // trig = this->swing_mode_off_trigger_; + } + assert(trig != nullptr); + trig->trigger(); + this->swing_mode = swing_mode; + this->prev_swing_mode_ = swing_mode; + this->prev_swing_mode_trigger_ = trig; +} +void ThermostatClimate::change_away_(bool away) { + if (!away) { + if (this->supports_two_points_) { + this->target_temperature_low = this->normal_config_.default_temperature_low; + this->target_temperature_high = this->normal_config_.default_temperature_high; + } else + this->target_temperature = this->normal_config_.default_temperature; + } else { + if (this->supports_two_points_) { + this->target_temperature_low = this->away_config_.default_temperature_low; + this->target_temperature_high = this->away_config_.default_temperature_high; + } else + this->target_temperature = this->away_config_.default_temperature; + } + this->away = away; +} +void ThermostatClimate::set_normal_config(const ThermostatClimateTargetTempConfig &normal_config) { + this->normal_config_ = normal_config; +} +void ThermostatClimate::set_away_config(const ThermostatClimateTargetTempConfig &away_config) { + this->supports_away_ = true; + this->away_config_ = away_config; +} +ThermostatClimate::ThermostatClimate() + : 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<>()), + heat_mode_trigger_(new Trigger<>()), + auto_mode_trigger_(new Trigger<>()), + idle_action_trigger_(new Trigger<>()), + off_mode_trigger_(new Trigger<>()), + fan_only_action_trigger_(new Trigger<>()), + fan_only_mode_trigger_(new Trigger<>()), + fan_mode_on_trigger_(new Trigger<>()), + fan_mode_off_trigger_(new Trigger<>()), + fan_mode_auto_trigger_(new Trigger<>()), + fan_mode_low_trigger_(new Trigger<>()), + fan_mode_medium_trigger_(new Trigger<>()), + fan_mode_high_trigger_(new Trigger<>()), + fan_mode_middle_trigger_(new Trigger<>()), + fan_mode_focus_trigger_(new Trigger<>()), + fan_mode_diffuse_trigger_(new Trigger<>()), + swing_mode_both_trigger_(new Trigger<>()), + swing_mode_off_trigger_(new Trigger<>()), + swing_mode_horizontal_trigger_(new Trigger<>()), + swing_mode_vertical_trigger_(new Trigger<>()) {} +void ThermostatClimate::set_hysteresis(float hysteresis) { this->hysteresis_ = hysteresis; } +void ThermostatClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } +void ThermostatClimate::set_supports_auto(bool supports_auto) { this->supports_auto_ = supports_auto; } +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_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; +} +void ThermostatClimate::set_supports_fan_mode_off(bool supports_fan_mode_off) { + this->supports_fan_mode_off_ = supports_fan_mode_off; +} +void ThermostatClimate::set_supports_fan_mode_auto(bool supports_fan_mode_auto) { + this->supports_fan_mode_auto_ = supports_fan_mode_auto; +} +void ThermostatClimate::set_supports_fan_mode_low(bool supports_fan_mode_low) { + this->supports_fan_mode_low_ = supports_fan_mode_low; +} +void ThermostatClimate::set_supports_fan_mode_medium(bool supports_fan_mode_medium) { + this->supports_fan_mode_medium_ = supports_fan_mode_medium; +} +void ThermostatClimate::set_supports_fan_mode_high(bool supports_fan_mode_high) { + this->supports_fan_mode_high_ = supports_fan_mode_high; +} +void ThermostatClimate::set_supports_fan_mode_middle(bool supports_fan_mode_middle) { + this->supports_fan_mode_middle_ = supports_fan_mode_middle; +} +void ThermostatClimate::set_supports_fan_mode_focus(bool supports_fan_mode_focus) { + this->supports_fan_mode_focus_ = supports_fan_mode_focus; +} +void ThermostatClimate::set_supports_fan_mode_diffuse(bool supports_fan_mode_diffuse) { + this->supports_fan_mode_diffuse_ = supports_fan_mode_diffuse; +} +void ThermostatClimate::set_supports_swing_mode_both(bool supports_swing_mode_both) { + this->supports_swing_mode_both_ = supports_swing_mode_both; +} +void ThermostatClimate::set_supports_swing_mode_off(bool supports_swing_mode_off) { + this->supports_swing_mode_off_ = supports_swing_mode_off; +} +void ThermostatClimate::set_supports_swing_mode_horizontal(bool supports_swing_mode_horizontal) { + this->supports_swing_mode_horizontal_ = supports_swing_mode_horizontal; +} +void ThermostatClimate::set_supports_swing_mode_vertical(bool supports_swing_mode_vertical) { + this->supports_swing_mode_vertical_ = supports_swing_mode_vertical; +} +void ThermostatClimate::set_supports_two_points(bool supports_two_points) { + this->supports_two_points_ = supports_two_points; +} +Trigger<> *ThermostatClimate::get_cool_action_trigger() const { return this->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_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_; } +Trigger<> *ThermostatClimate::get_dry_mode_trigger() const { return this->dry_mode_trigger_; } +Trigger<> *ThermostatClimate::get_fan_only_mode_trigger() const { return this->fan_only_mode_trigger_; } +Trigger<> *ThermostatClimate::get_heat_mode_trigger() const { return this->heat_mode_trigger_; } +Trigger<> *ThermostatClimate::get_off_mode_trigger() const { return this->off_mode_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_on_trigger() const { return this->fan_mode_on_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_off_trigger() const { return this->fan_mode_off_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_auto_trigger() const { return this->fan_mode_auto_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_low_trigger() const { return this->fan_mode_low_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_medium_trigger() const { return this->fan_mode_medium_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_high_trigger() const { return this->fan_mode_high_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_middle_trigger() const { return this->fan_mode_middle_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_focus_trigger() const { return this->fan_mode_focus_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_diffuse_trigger() const { return this->fan_mode_diffuse_trigger_; } +Trigger<> *ThermostatClimate::get_swing_mode_both_trigger() const { return this->swing_mode_both_trigger_; } +Trigger<> *ThermostatClimate::get_swing_mode_off_trigger() const { return this->swing_mode_off_trigger_; } +Trigger<> *ThermostatClimate::get_swing_mode_horizontal_trigger() const { return this->swing_mode_horizontal_trigger_; } +Trigger<> *ThermostatClimate::get_swing_mode_vertical_trigger() const { return this->swing_mode_vertical_trigger_; } +void ThermostatClimate::dump_config() { + LOG_CLIMATE("", "Thermostat", this); + if (this->supports_heat_) + ESP_LOGCONFIG(TAG, " Default Target Temperature Low: %.1f°C", this->normal_config_.default_temperature_low); + if ((this->supports_cool_) || (this->supports_fan_only_)) + ESP_LOGCONFIG(TAG, " Default Target Temperature High: %.1f°C", this->normal_config_.default_temperature_high); + ESP_LOGCONFIG(TAG, " Hysteresis: %.1f°C", this->hysteresis_); + ESP_LOGCONFIG(TAG, " Supports AUTO: %s", YESNO(this->supports_auto_)); + 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 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_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE AUTO: %s", YESNO(this->supports_fan_mode_auto_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE LOW: %s", YESNO(this->supports_fan_mode_low_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE MEDIUM: %s", YESNO(this->supports_fan_mode_medium_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE HIGH: %s", YESNO(this->supports_fan_mode_high_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE MIDDLE: %s", YESNO(this->supports_fan_mode_middle_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE FOCUS: %s", YESNO(this->supports_fan_mode_focus_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE DIFFUSE: %s", YESNO(this->supports_fan_mode_diffuse_)); + ESP_LOGCONFIG(TAG, " Supports SWING MODE BOTH: %s", YESNO(this->supports_swing_mode_both_)); + ESP_LOGCONFIG(TAG, " Supports SWING MODE OFF: %s", YESNO(this->supports_swing_mode_off_)); + ESP_LOGCONFIG(TAG, " Supports SWING MODE HORIZONTAL: %s", YESNO(this->supports_swing_mode_horizontal_)); + ESP_LOGCONFIG(TAG, " Supports SWING MODE VERTICAL: %s", YESNO(this->supports_swing_mode_vertical_)); + ESP_LOGCONFIG(TAG, " Supports TWO SET POINTS: %s", YESNO(this->supports_two_points_)); + ESP_LOGCONFIG(TAG, " Supports AWAY mode: %s", YESNO(this->supports_away_)); + if (this->supports_away_) { + if (this->supports_heat_) + ESP_LOGCONFIG(TAG, " Away Default Target Temperature Low: %.1f°C", this->away_config_.default_temperature_low); + if ((this->supports_cool_) || (this->supports_fan_only_)) + ESP_LOGCONFIG(TAG, " Away Default Target Temperature High: %.1f°C", + this->away_config_.default_temperature_high); + } +} + +ThermostatClimateTargetTempConfig::ThermostatClimateTargetTempConfig() = default; +ThermostatClimateTargetTempConfig::ThermostatClimateTargetTempConfig(float default_temperature) + : default_temperature(default_temperature) {} +ThermostatClimateTargetTempConfig::ThermostatClimateTargetTempConfig(float default_temperature_low, + float default_temperature_high) + : default_temperature_low(default_temperature_low), default_temperature_high(default_temperature_high) {} + +} // namespace thermostat +} // namespace esphome diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h new file mode 100644 index 0000000000..86a1007efa --- /dev/null +++ b/esphome/components/thermostat/thermostat_climate.h @@ -0,0 +1,271 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/climate/climate.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace thermostat { + +struct ThermostatClimateTargetTempConfig { + public: + ThermostatClimateTargetTempConfig(); + ThermostatClimateTargetTempConfig(float default_temperature); + ThermostatClimateTargetTempConfig(float default_temperature_low, float default_temperature_high); + + float default_temperature{NAN}; + float default_temperature_low{NAN}; + float default_temperature_high{NAN}; + float hysteresis{NAN}; +}; + +class ThermostatClimate : public climate::Climate, public Component { + public: + ThermostatClimate(); + void setup() override; + void dump_config() override; + + void set_hysteresis(float hysteresis); + void set_sensor(sensor::Sensor *sensor); + void set_supports_auto(bool supports_auto); + 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_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); + void set_supports_fan_mode_auto(bool supports_fan_mode_auto); + void set_supports_fan_mode_low(bool supports_fan_mode_low); + void set_supports_fan_mode_medium(bool supports_fan_mode_medium); + void set_supports_fan_mode_high(bool supports_fan_mode_high); + void set_supports_fan_mode_middle(bool supports_fan_mode_middle); + void set_supports_fan_mode_focus(bool supports_fan_mode_focus); + void set_supports_fan_mode_diffuse(bool supports_fan_mode_diffuse); + void set_supports_swing_mode_both(bool supports_swing_mode_both); + void set_supports_swing_mode_horizontal(bool supports_swing_mode_horizontal); + void set_supports_swing_mode_off(bool supports_swing_mode_off); + void set_supports_swing_mode_vertical(bool supports_swing_mode_vertical); + void set_supports_two_points(bool supports_two_points); + + void set_normal_config(const ThermostatClimateTargetTempConfig &normal_config); + void set_away_config(const ThermostatClimateTargetTempConfig &away_config); + + Trigger<> *get_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_idle_action_trigger() const; + Trigger<> *get_auto_mode_trigger() const; + Trigger<> *get_cool_mode_trigger() const; + Trigger<> *get_dry_mode_trigger() const; + Trigger<> *get_fan_only_mode_trigger() const; + Trigger<> *get_heat_mode_trigger() const; + Trigger<> *get_off_mode_trigger() const; + Trigger<> *get_fan_mode_on_trigger() const; + Trigger<> *get_fan_mode_off_trigger() const; + Trigger<> *get_fan_mode_auto_trigger() const; + Trigger<> *get_fan_mode_low_trigger() const; + Trigger<> *get_fan_mode_medium_trigger() const; + Trigger<> *get_fan_mode_high_trigger() const; + Trigger<> *get_fan_mode_middle_trigger() const; + Trigger<> *get_fan_mode_focus_trigger() const; + Trigger<> *get_fan_mode_diffuse_trigger() const; + Trigger<> *get_swing_mode_both_trigger() const; + Trigger<> *get_swing_mode_horizontal_trigger() const; + Trigger<> *get_swing_mode_off_trigger() const; + Trigger<> *get_swing_mode_vertical_trigger() const; + /// Get current hysteresis value + float hysteresis(); + /// Call triggers based on updated climate states (modes/actions) + void refresh(); + + protected: + /// Override control to change settings of the climate device. + void control(const climate::ClimateCall &call) override; + + /// Change the away setting, will reset target temperatures to defaults. + void change_away_(bool away); + + /// Return the traits of this controller. + climate::ClimateTraits traits() override; + + /// Re-compute the required action of this climate controller. + climate::ClimateAction compute_action_(); + + /// Switch the climate device to the given climate action. + void switch_to_action_(climate::ClimateAction action); + + /// Switch the climate device to the given climate fan mode. + void switch_to_fan_mode_(climate::ClimateFanMode fan_mode); + + /// Switch the climate device to the given climate mode. + void switch_to_mode_(climate::ClimateMode mode); + + /// Switch the climate device to the given climate swing mode. + void switch_to_swing_mode_(climate::ClimateSwingMode swing_mode); + + /// The sensor used for getting the current temperature + sensor::Sensor *sensor_{nullptr}; + + /// Whether the controller supports auto/cooling/drying/fanning/heating. + /// + /// A false value for any given attribute means that the controller has no such action + /// (for example a thermostat, where only heating and not-heating is possible). + bool supports_auto_{false}; + bool supports_cool_{false}; + bool supports_dry_{false}; + bool supports_fan_only_{false}; + bool supports_heat_{false}; + + /// Whether the controller supports turning on or off just the fan. + /// + /// A false value for either attribute means that the controller has no fan on/off action + /// (for example a thermostat, where independent control of the fan is not possible). + bool supports_fan_mode_on_{false}; + bool supports_fan_mode_off_{false}; + + /// Whether the controller supports fan auto mode. + /// + /// A false value for this attribute means that the controller has no fan-auto action + /// (for example a thermostat, where independent control of the fan is not possible). + bool supports_fan_mode_auto_{false}; + + /// Whether the controller supports various fan speeds and/or positions. + /// + /// A false value for any given attribute means that the controller has no such fan action. + bool supports_fan_mode_low_{false}; + bool supports_fan_mode_medium_{false}; + bool supports_fan_mode_high_{false}; + bool supports_fan_mode_middle_{false}; + bool supports_fan_mode_focus_{false}; + bool supports_fan_mode_diffuse_{false}; + + /// Whether the controller supports various swing modes. + /// + /// A false value for any given attribute means that the controller has no such swing mode. + bool supports_swing_mode_both_{false}; + bool supports_swing_mode_off_{false}; + bool supports_swing_mode_horizontal_{false}; + bool supports_swing_mode_vertical_{false}; + + /// Whether the controller supports two set points + /// + /// A false value means that the controller has no such support. + bool supports_two_points_{false}; + + /// Whether the controller supports an "away" mode + /// + /// A false value means that the controller has no such mode. + bool supports_away_{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<> *cool_mode_trigger_{nullptr}; + + /// The trigger to call when the controller should switch to dry (dehumidification) mode. + /// + /// In dry mode, the controller is assumed to have both heating and cooling disabled, + /// although the system may use its cooling mechanism to achieve drying. + Trigger<> *dry_action_trigger_{nullptr}; + Trigger<> *dry_mode_trigger_{nullptr}; + + /// The trigger to call when the controller should switch to heating action/mode. + /// + /// A null value for this attribute means that the controller has no heating action + /// For example window blinds, where only cooling (blinds closed) and not-cooling + /// (blinds open) is possible. + Trigger<> *heat_action_trigger_{nullptr}; + Trigger<> *heat_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 + /// to idle when the temperature is within the thresholds/set points. + Trigger<> *auto_mode_trigger_{nullptr}; + + /// The trigger to call when the controller should switch to idle action/off mode. + /// + /// In these actions/modes, the controller is assumed to have both heating and cooling disabled. + Trigger<> *idle_action_trigger_{nullptr}; + Trigger<> *off_mode_trigger_{nullptr}; + + /// The trigger to call when the controller should switch to fan-only action/mode. + /// + /// In fan-only mode, the controller is assumed to have both heating and cooling disabled. + /// The system should activate the fan only. + Trigger<> *fan_only_action_trigger_{nullptr}; + Trigger<> *fan_only_mode_trigger_{nullptr}; + + /// The trigger to call when the controller should switch on the fan. + Trigger<> *fan_mode_on_trigger_{nullptr}; + + /// The trigger to call when the controller should switch off the fan. + Trigger<> *fan_mode_off_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "auto" mode. + Trigger<> *fan_mode_auto_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "low" speed. + Trigger<> *fan_mode_low_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "medium" speed. + Trigger<> *fan_mode_medium_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "high" speed. + Trigger<> *fan_mode_high_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "middle" position. + Trigger<> *fan_mode_middle_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "focus" position. + Trigger<> *fan_mode_focus_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "diffuse" position. + Trigger<> *fan_mode_diffuse_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the swing mode to "both". + Trigger<> *swing_mode_both_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the swing mode to "off". + Trigger<> *swing_mode_off_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the swing mode to "horizontal". + Trigger<> *swing_mode_horizontal_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the swing mode to "vertical". + Trigger<> *swing_mode_vertical_trigger_{nullptr}; + + /// A reference to the trigger that was previously active. + /// + /// This is so that the previous trigger can be stopped before enabling a new one + /// for each climate category (mode, action, fan_mode, swing_mode). + Trigger<> *prev_action_trigger_{nullptr}; + Trigger<> *prev_fan_mode_trigger_{nullptr}; + Trigger<> *prev_mode_trigger_{nullptr}; + Trigger<> *prev_swing_mode_trigger_{nullptr}; + + /// 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}; + + /// Temperature data for normal/home and away modes + ThermostatClimateTargetTempConfig normal_config_{}; + ThermostatClimateTargetTempConfig away_config_{}; + + /// Hysteresis value used for computing climate actions + float hysteresis_{0}; + + /// setup_complete_ blocks modifying/resetting the temps immediately after boot + bool setup_complete_{false}; +}; + +} // namespace thermostat +} // namespace esphome diff --git a/esphome/const.py b/esphome/const.py index 4cdf48fc76..8b727b615c 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -44,6 +44,7 @@ CONF_ASSUMED_STATE = 'assumed_state' CONF_AT = 'at' CONF_ATTENUATION = 'attenuation' CONF_AUTH = 'auth' +CONF_AUTO_MODE = 'auto_mode' CONF_AUTOMATION_ID = 'automation_id' CONF_AVAILABILITY = 'availability' CONF_AWAY = 'away' @@ -102,6 +103,7 @@ CONF_CONDITION = 'condition' CONF_CONDITION_ID = 'condition_id' CONF_CONDUCTIVITY = 'conductivity' CONF_COOL_ACTION = 'cool_action' +CONF_COOL_MODE = 'cool_mode' CONF_COUNT_MODE = 'count_mode' CONF_CRON = 'cron' CONF_CS_PIN = 'cs_pin' @@ -140,6 +142,8 @@ CONF_DIV_RATIO = 'div_ratio' CONF_DNS1 = 'dns1' CONF_DNS2 = 'dns2' CONF_DOMAIN = 'domain' +CONF_DRY_ACTION = 'dry_action' +CONF_DRY_MODE = 'dry_mode' CONF_DUMP = 'dump' CONF_DURATION = 'duration' CONF_ECHO_PIN = 'echo_pin' @@ -159,6 +163,17 @@ CONF_EXTERNAL_VCC = 'external_vcc' CONF_FALLING_EDGE = 'falling_edge' CONF_FAMILY = 'family' CONF_FAN_MODE = 'fan_mode' +CONF_FAN_MODE_AUTO_ACTION = 'fan_mode_auto_action' +CONF_FAN_MODE_DIFFUSE_ACTION = 'fan_mode_diffuse_action' +CONF_FAN_MODE_FOCUS_ACTION = 'fan_mode_focus_action' +CONF_FAN_MODE_HIGH_ACTION = 'fan_mode_high_action' +CONF_FAN_MODE_LOW_ACTION = 'fan_mode_low_action' +CONF_FAN_MODE_MEDIUM_ACTION = 'fan_mode_medium_action' +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_MODE = 'fan_only_mode' CONF_FAST_CONNECT = 'fast_connect' CONF_FILE = 'file' CONF_FILTER = 'filter' @@ -183,6 +198,7 @@ CONF_GROUP = 'group' CONF_HARDWARE_UART = 'hardware_uart' CONF_HEARTBEAT = 'heartbeat' CONF_HEAT_ACTION = 'heat_action' +CONF_HEAT_MODE = 'heat_mode' CONF_HEATER = 'heater' CONF_HIDDEN = 'hidden' CONF_HIGH = 'high' @@ -190,6 +206,7 @@ CONF_HIGH_VOLTAGE_REFERENCE = 'high_voltage_reference' CONF_HOUR = 'hour' CONF_HOURS = 'hours' CONF_HUMIDITY = 'humidity' +CONF_HYSTERESIS = "hysteresis" CONF_I2C = 'i2c' CONF_I2C_ID = 'i2c_id' CONF_ICON = 'icon' @@ -285,6 +302,7 @@ CONF_NUM_CHANNELS = 'num_channels' CONF_NUM_CHIPS = 'num_chips' CONF_NUM_LEDS = 'num_leds' CONF_NUMBER = 'number' +CONF_OFF_MODE = 'off_mode' CONF_OFFSET = 'offset' CONF_ON = 'on' CONF_ON_BLE_ADVERTISE = 'on_ble_advertise' @@ -449,7 +467,11 @@ CONF_STOP_ACTION = 'stop_action' CONF_SUBNET = 'subnet' CONF_SUPPORTS_COOL = 'supports_cool' CONF_SUPPORTS_HEAT = 'supports_heat' +CONF_SWING_BOTH_ACTION = 'swing_both_action' +CONF_SWING_HORIZONTAL_ACTION = 'swing_horizontal_action' CONF_SWING_MODE = 'swing_mode' +CONF_SWING_OFF_ACTION = 'swing_off_action' +CONF_SWING_VERTICAL_ACTION = 'swing_vertical_action' CONF_SWITCHES = 'switches' CONF_SYNC = 'sync' CONF_TABLET = 'tablet' diff --git a/tests/test3.yaml b/tests/test3.yaml index 72dde13055..ef464fd5b7 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -624,6 +624,63 @@ climate: away_config: default_target_temperature_low: 16°C default_target_temperature_high: 20°C + - platform: thermostat + name: Thermostat Climate + sensor: ha_hello_world + default_target_temperature_low: 18°C + default_target_temperature_high: 24°C + idle_action: + - switch.turn_on: gpio_switch1 + cool_action: + - switch.turn_on: gpio_switch2 + heat_action: + - switch.turn_on: gpio_switch1 + dry_action: + - switch.turn_on: gpio_switch2 + fan_only_action: + - switch.turn_on: gpio_switch1 + auto_mode: + - switch.turn_on: gpio_switch2 + off_mode: + - switch.turn_on: gpio_switch1 + heat_mode: + - switch.turn_on: gpio_switch2 + cool_mode: + - switch.turn_on: gpio_switch1 + dry_mode: + - switch.turn_on: gpio_switch2 + fan_only_mode: + - switch.turn_on: gpio_switch1 + fan_mode_auto_action: + - switch.turn_on: gpio_switch2 + fan_mode_on_action: + - switch.turn_on: gpio_switch1 + fan_mode_off_action: + - switch.turn_on: gpio_switch2 + fan_mode_low_action: + - switch.turn_on: gpio_switch1 + fan_mode_medium_action: + - switch.turn_on: gpio_switch2 + fan_mode_high_action: + - switch.turn_on: gpio_switch1 + fan_mode_middle_action: + - switch.turn_on: gpio_switch2 + fan_mode_focus_action: + - switch.turn_on: gpio_switch1 + fan_mode_diffuse_action: + - switch.turn_on: gpio_switch2 + swing_off_action: + - switch.turn_on: gpio_switch1 + swing_horizontal_action: + - switch.turn_on: gpio_switch2 + swing_vertical_action: + - switch.turn_on: gpio_switch1 + swing_both_action: + - switch.turn_on: gpio_switch2 + hysteresis: 0.2 + away_config: + default_target_temperature_low: 16°C + default_target_temperature_high: 20°C cover: - platform: endstop